Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dedupe-studio-oauth-sync-providers.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -229,13 +242,13 @@ const SyncSettings = () => {
/>
))}
<LocalStorageItem />
{apiProviders.length > 0 && (
{visibleApiProviders.length > 0 && (
<Box css={{
width: '100%', height: '1px', backgroundColor: '$borderSubtle', margin: '$2 0',
}}
/>
)}
{apiProviders.length > 0 && apiProviders.map((item) => (
{visibleApiProviders.length > 0 && visibleApiProviders.map((item) => (
<StorageItem
key={item?.internalId || `${item.provider}-${item.id}`}
onEdit={handleEditClick(item)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { StorageProviderType } from '@/constants/StorageProviderType';
import { StorageTypeCredentials } from '@/types/StorageType';
import { tryParseJson } from '@/utils/tryParseJson';
import { FigmaStorageProperty, FigmaStorageType } from './FigmaStorageProperty';
Expand All @@ -9,9 +10,15 @@ export const ApiProvidersProperty = new FigmaStorageProperty<StorageTypeCredenti
(outgoing) => {
type PossibleIncomingType = StorageTypeCredentials[] | Record<string, StorageTypeCredentials>;
const result = tryParseJson<PossibleIncomingType>(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);
},
);
14 changes: 14 additions & 0 deletions packages/tokens-studio-for-figma/src/utils/credentials.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
8 changes: 8 additions & 0 deletions packages/tokens-studio-for-figma/src/utils/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof data> = [];
Expand Down
Loading