diff --git a/.changeset/dedupe-studio-oauth-sync-providers.md b/.changeset/dedupe-studio-oauth-sync-providers.md new file mode 100644 index 000000000..5f0d502a1 --- /dev/null +++ b/.changeset/dedupe-studio-oauth-sync-providers.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/figma-plugin": patch +--- + +Fix duplicate Tokens Studio sync providers appearing in Sync Settings. Tokens Studio OAuth providers are derived live from the user's organizations and are no longer persisted to client storage. Any legacy persisted OAuth entries are stripped on read, and the Sync Settings list now defensively dedupes against live OAuth providers. diff --git a/packages/tokens-studio-for-figma/src/app/components/SyncSettings.tsx b/packages/tokens-studio-for-figma/src/app/components/SyncSettings.tsx index 394c08177..40f0643e2 100644 --- a/packages/tokens-studio-for-figma/src/app/components/SyncSettings.tsx +++ b/packages/tokens-studio-for-figma/src/app/components/SyncSettings.tsx @@ -83,6 +83,19 @@ const SyncSettings = () => { return []; }, [isAuthenticated, organizations]); + // Defensive: never render an apiProvider that is a Tokens Studio OAuth entry + // (those are live-derived above and should not be persisted) or one whose + // internalId already matches a live studioProvider — both would create + // duplicate rows in Sync Settings. + const visibleApiProviders = React.useMemo(() => { + const studioInternalIds = new Set(studioProviders.map((p) => p.internalId).filter(Boolean)); + return apiProviders.filter((item) => { + if (item?.provider === StorageProviderType.TOKENS_STUDIO_OAUTH) return false; + if (item?.internalId && studioInternalIds.has(item.internalId)) return false; + return true; + }); + }, [apiProviders, studioProviders]); + const [open, setOpen] = React.useState(false); // Track when user opens the create new sync provider dialog @@ -229,13 +242,13 @@ const SyncSettings = () => { /> ))} - {apiProviders.length > 0 && ( + {visibleApiProviders.length > 0 && ( )} - {apiProviders.length > 0 && apiProviders.map((item) => ( + {visibleApiProviders.length > 0 && visibleApiProviders.map((item) => ( { type PossibleIncomingType = StorageTypeCredentials[] | Record; const result = tryParseJson(outgoing) ?? []; - if (Array.isArray(result)) return result; - return Object.values(result).filter((value) => ( - typeof value === 'object' - )); + const asArray = Array.isArray(result) + ? result + : Object.values(result).filter((value) => ( + typeof value === 'object' + )); + // Tokens Studio OAuth providers are derived live from the user's organizations + // and must never be persisted. Strip any legacy entries that were saved by + // older builds (e.g. when switching branches in an OAuth sync) so we don't + // render duplicate rows in Sync Settings. + return asArray.filter((value) => value?.provider !== StorageProviderType.TOKENS_STUDIO_OAUTH); }, ); diff --git a/packages/tokens-studio-for-figma/src/utils/credentials.test.ts b/packages/tokens-studio-for-figma/src/utils/credentials.test.ts index 4b2cb93c0..1c9682fa7 100644 --- a/packages/tokens-studio-for-figma/src/utils/credentials.test.ts +++ b/packages/tokens-studio-for-figma/src/utils/credentials.test.ts @@ -100,6 +100,20 @@ describe('updateCredentials', () => { await updateCredentials(newObject); expect(figma.clientStorage.setAsync).toHaveBeenCalledWith('apiProviders', JSON.stringify(newArray)); }); + + it('does not persist Tokens Studio OAuth providers', async () => { + const oauthCredential = { + id: 'project-1', + orgId: 'org-1', + name: "admin's workspace", + provider: StorageProviderType.TOKENS_STUDIO_OAUTH, + internalId: 'tokens-studio-org-1', + }; + await updateCredentials(oauthCredential); + + expect(figma.clientStorage.getAsync).not.toHaveBeenCalled(); + expect(figma.clientStorage.setAsync).not.toHaveBeenCalled(); + }); }); describe('removeSingleCredential', () => { diff --git a/packages/tokens-studio-for-figma/src/utils/credentials.ts b/packages/tokens-studio-for-figma/src/utils/credentials.ts index f935510db..a1bc7ab17 100644 --- a/packages/tokens-studio-for-figma/src/utils/credentials.ts +++ b/packages/tokens-studio-for-figma/src/utils/credentials.ts @@ -3,9 +3,17 @@ import { notifyAPIProviders, notifyUI } from '@/plugin/notifiers'; import isSameCredentials from './isSameCredentials'; import { ApiProvidersProperty } from '@/figmaStorage'; import { StorageTypeCredentials } from '@/types/StorageType'; +import { StorageProviderType } from '@/constants/StorageProviderType'; import { generateId } from './generateId'; export async function updateCredentials(context: StorageTypeCredentials) { + // Tokens Studio OAuth providers are derived live from the user's organizations + // on every load and must never be persisted to clientStorage. Persisting them + // creates duplicate rows in Sync Settings (a live OAuth-derived row + a stale + // persisted row) and can pin sync to a stale token/branch context. + if (context.provider === StorageProviderType.TOKENS_STUDIO_OAUTH) { + return; + } try { const data = await ApiProvidersProperty.read(); let existingProviders: NonNullable = [];