move fcm token storage (#1175)

* move fcm token from proving store to setting store for better separation of concerns.

* use analytics on adapter

* Revert "use analytics on adapter"

This reverts commit 4854d6fa87.

* prettier

* please be good

* ik heb oopsie wooopsie

* remove
This commit is contained in:
Aaron DeRuvo
2025-10-01 12:59:00 +02:00
committed by GitHub
parent 9131cd3649
commit 1856c9b325
14 changed files with 68 additions and 106 deletions

View File

@@ -20,6 +20,7 @@ import { navigationRef } from '@/navigation';
import { unsafe_getPrivateKey } from '@/providers/authProvider';
import { selfClientDocumentsAdapter } from '@/providers/passportDataProvider';
import { logNFCEvent, logProofEvent } from '@/Sentry';
import { useSettingStore } from '@/stores/settingStore';
import analytics from '@/utils/analytics';
type GlobalCrypto = { crypto?: { subtle?: Crypto['subtle'] } };
@@ -95,15 +96,6 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
auth: {
getPrivateKey: () => unsafe_getPrivateKey(),
},
notification: {
registerDeviceToken: async (sessionId, deviceToken, isMock) => {
// Forward to our app-level function which handles staging vs production
// and also fetches the token if not provided
const { registerDeviceToken: registerFirebaseDeviceToken } =
await import('@/utils/notifications/notificationService');
return registerFirebaseDeviceToken(sessionId, deviceToken, isMock);
},
},
}),
[],
);
@@ -158,6 +150,35 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => {
}
});
addListener(
SdkEvents.PROVING_BEGIN_GENERATION,
async ({ uuid, isMock, context }) => {
const { fcmToken } = useSettingStore.getState();
if (fcmToken) {
try {
analytics().trackEvent('DEVICE_TOKEN_REG_STARTED');
logProofEvent('info', 'Device token registration started', context);
const { registerDeviceToken: registerFirebaseDeviceToken } =
await import('@/utils/notifications/notificationService');
await registerFirebaseDeviceToken(uuid, fcmToken, isMock);
analytics().trackEvent('DEVICE_TOKEN_REG_SUCCESS');
logProofEvent('info', 'Device token registration success', context);
} catch (error) {
logProofEvent('warn', 'Device token registration failed', context, {
error: error instanceof Error ? error.message : String(error),
});
console.error('Error registering device token:', error);
analytics().trackEvent('DEVICE_TOKEN_REG_FAILED', {
message: error instanceof Error ? error.message : String(error),
});
}
}
},
);
addListener(SdkEvents.PROOF_EVENT, ({ level, context, event, details }) => {
// Log proof events for monitoring/debugging
logProofEvent(level, event, context, details);

View File

@@ -21,6 +21,7 @@ import { Title } from '@/components/typography/Title';
import useHapticNavigation from '@/hooks/useHapticNavigation';
import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout';
import { styles } from '@/screens/prove/ProofRequestStatusScreen';
import { useSettingStore } from '@/stores/settingStore';
import { flushAllAnalytics, trackNfcEvent } from '@/utils/analytics';
import { black, white } from '@/utils/colors';
import { notificationSuccess } from '@/utils/haptic';
@@ -40,8 +41,8 @@ const ConfirmBelongingScreen: React.FC<ConfirmBelongingScreenProps> = () => {
const [_requestingPermission, setRequestingPermission] = useState(false);
const currentState = useProvingStore(state => state.currentState);
const init = useProvingStore(state => state.init);
const setFcmToken = useProvingStore(state => state.setFcmToken);
const setUserConfirmed = useProvingStore(state => state.setUserConfirmed);
const setFcmToken = useSettingStore(state => state.setFcmToken);
const isReadyToProve = currentState === 'ready_to_prove';
useEffect(() => {
notificationSuccess();
@@ -74,7 +75,7 @@ const ConfirmBelongingScreen: React.FC<ConfirmBelongingScreenProps> = () => {
if (permissionGranted) {
const token = await getFCMToken();
if (token) {
setFcmToken(token, selfClient);
setFcmToken(token);
trackEvent(ProofEvents.FCM_TOKEN_STORED);
}
}

View File

@@ -20,6 +20,8 @@ interface PersistedSettingsState {
isDevMode: boolean;
setDevModeOn: () => void;
setDevModeOff: () => void;
fcmToken: string | null;
setFcmToken: (token: string | null) => void;
}
interface NonPersistedSettingsState {
@@ -69,6 +71,9 @@ export const useSettingStore = create<SettingsState>()(
setDevModeOn: () => set({ isDevMode: true }),
setDevModeOff: () => set({ isDevMode: false }),
fcmToken: null,
setFcmToken: (token: string | null) => set({ fcmToken: token }),
// Non-persisted state (will not be saved to storage)
hideNetworkModal: false,
setHideNetworkModal: (hideNetworkModal: boolean) => {

View File

@@ -76,9 +76,6 @@ function createTestClient() {
})),
},
},
notification: {
registerDeviceToken: async () => Promise.resolve(),
},
crypto: {
hash: jest.fn(),
sign: jest.fn(),

View File

@@ -62,7 +62,4 @@ export const mockAdapters = {
network: mockNetwork,
crypto: mockCrypto,
documents: mockDocuments,
notification: {
registerDeviceToken: async () => Promise.resolve(),
},
};

View File

@@ -40,7 +40,7 @@ const optionalDefaults: Required<Pick<Adapters, 'clock' | 'logger'>> = {
},
};
const REQUIRED_ADAPTERS = ['auth', 'scanner', 'network', 'crypto', 'documents', 'notification'] as const;
const REQUIRED_ADAPTERS = ['auth', 'scanner', 'network', 'crypto', 'documents'] as const;
export const createListenersMap = (): {
map: Map<SDKEvent, Set<(p: any) => void>>;
@@ -132,13 +132,6 @@ export function createSelfClient({
return adapters.auth.getPrivateKey();
}
async function registerNotificationsToken(sessionId: string, deviceToken?: string, isMock?: boolean): Promise<void> {
if (!_adapters.notification) {
throw notImplemented('notification');
}
return _adapters.notification.registerDeviceToken(sessionId, deviceToken, isMock);
}
async function hasPrivateKey(): Promise<boolean> {
if (!adapters.auth) return false;
try {
@@ -160,7 +153,6 @@ export function createSelfClient({
logProofEvent: (level: LogLevel, message: string, context: ProofContext, details?: Record<string, any>) => {
emit(SdkEvents.PROOF_EVENT, { context, event: message, details, level });
},
registerNotificationsToken,
// TODO: inline for now
loadDocumentCatalog: async () => {
return _adapters.documents.loadDocumentCatalog();

View File

@@ -214,9 +214,7 @@ export interface ProvingState {
error_code: string | null;
reason: string | null;
endpointType: EndpointType | null;
fcmToken: string | null;
env: 'prod' | 'stg' | null;
setFcmToken: (token: string, selfClient: SelfClient) => void;
init: (
selfClient: SelfClient,
circuitType: 'dsc' | 'disclose' | 'register',
@@ -487,11 +485,6 @@ export const useProvingStore = create<ProvingState>((set, get) => {
error_code: null,
reason: null,
endpointType: null,
fcmToken: null,
setFcmToken: (token: string, selfClient: SelfClient) => {
set({ fcmToken: token });
selfClient.trackEvent(ProofEvents.FCM_TOKEN_STORED);
},
_handleWebSocketMessage: async (event: MessageEvent, selfClient: SelfClient) => {
if (!actor) {
console.error('Cannot process message: State machine not initialized.');
@@ -1201,7 +1194,7 @@ export const useProvingStore = create<ProvingState>((set, get) => {
startProving: async (selfClient: SelfClient) => {
_checkActorInitialized(actor);
const startTime = Date.now();
const { wsConnection, sharedKey, passportData, secret, uuid, fcmToken } = get();
const { wsConnection, sharedKey, passportData, secret, uuid } = get();
const context = createProofContext(selfClient, 'startProving', {
sessionId: uuid || get().uuid || 'unknown-session',
});
@@ -1223,24 +1216,12 @@ export const useProvingStore = create<ProvingState>((set, get) => {
}
try {
if (fcmToken) {
try {
const isMockPassport = passportData?.mock;
selfClient.trackEvent(ProofEvents.DEVICE_TOKEN_REG_STARTED);
selfClient.logProofEvent('info', 'Device token registration started', context);
await selfClient.registerNotificationsToken(uuid, fcmToken, isMockPassport);
selfClient.trackEvent(ProofEvents.DEVICE_TOKEN_REG_SUCCESS);
selfClient.logProofEvent('info', 'Device token registration success', context);
} catch (error) {
selfClient.logProofEvent('warn', 'Device token registration failed', context, {
error: error instanceof Error ? error.message : String(error),
});
console.error('Error registering device token:', error);
selfClient.trackEvent(ProofEvents.DEVICE_TOKEN_REG_FAILED, {
message: error instanceof Error ? error.message : String(error),
});
}
}
// Emit event for FCM token registration
selfClient.emit(SdkEvents.PROVING_BEGIN_GENERATION, {
uuid,
isMock: passportData?.mock ?? false,
context,
});
selfClient.trackEvent(ProofEvents.PAYLOAD_GEN_STARTED);
selfClient.logProofEvent('info', 'Payload generation started', context);

View File

@@ -68,6 +68,14 @@ export enum SdkEvents {
* and guide them through the recovery process to regain access.
*/
PROVING_ACCOUNT_RECOVERY_REQUIRED = 'PROVING_ACCOUNT_RECOVERY_REQUIRED',
/**
* Emitted when the proving generation process begins.
*
* **Recommended:** Use this to handle notification token registration and other setup tasks
* that need to occur when proof generation starts.
*/
PROVING_BEGIN_GENERATION = 'PROVING_BEGIN_GENERATION',
/**
* Emitted for various proof-related events during the proving process.
*
@@ -97,6 +105,11 @@ export interface SDKEventMap {
documentCategory: DocumentCategory | null;
};
[SdkEvents.PROVING_ACCOUNT_RECOVERY_REQUIRED]: undefined;
[SdkEvents.PROVING_BEGIN_GENERATION]: {
uuid: string;
isMock: boolean;
context: ProofContext;
};
[SdkEvents.PROGRESS]: Progress;
[SdkEvents.ERROR]: Error;

View File

@@ -90,10 +90,6 @@ export interface MRZValidation {
overall: boolean;
}
export interface NotificationAdapter {
registerDeviceToken(sessionId: string, deviceToken?: string, isMock?: boolean): Promise<void>;
}
export type LogLevel = 'info' | 'warn' | 'error';
export interface Progress {
@@ -110,7 +106,6 @@ export interface Adapters {
analytics?: AnalyticsAdapter;
auth: AuthAdapter;
documents: DocumentsAdapter;
notification: NotificationAdapter;
}
export interface LoggerAdapter {
@@ -181,7 +176,6 @@ export interface SelfClient {
logProofEvent(level: LogLevel, message: string, context: ProofContext, details?: Record<string, any>): void;
loadDocumentCatalog(): Promise<DocumentCatalog>;
saveDocumentCatalog(catalog: DocumentCatalog): Promise<void>;
registerNotificationsToken(sessionId: string, deviceToken?: string, isMock?: boolean): Promise<void>;
loadDocumentById(id: string): Promise<IDDocument | null>;
saveDocument(id: string, passportData: IDDocument): Promise<void>;
deleteDocument(id: string): Promise<void>;

View File

@@ -6,7 +6,7 @@ import { describe, expect, it, vi } from 'vitest';
import type { CryptoAdapter, DocumentsAdapter, NetworkAdapter, ScannerAdapter } from '../src';
import { createListenersMap, createSelfClient, SdkEvents } from '../src/index';
import { AuthAdapter, NotificationAdapter } from '../src/types/public';
import { AuthAdapter } from '../src/types/public';
describe('createSelfClient', () => {
// Test eager validation during client creation
@@ -20,7 +20,6 @@ describe('createSelfClient', () => {
auth,
network,
crypto,
notification,
},
}),
).toThrow('scanner adapter not provided');
@@ -50,7 +49,7 @@ describe('createSelfClient', () => {
it('creates client successfully with all required adapters', () => {
const client = createSelfClient({
config: {},
adapters: { scanner, network, crypto, documents, auth, notification },
adapters: { scanner, network, crypto, documents, auth },
listeners: new Map(),
});
expect(client).toBeTruthy();
@@ -60,7 +59,7 @@ describe('createSelfClient', () => {
const scanMock = vi.fn().mockResolvedValue({ mode: 'qr', data: 'self://ok' });
const client = createSelfClient({
config: {},
adapters: { scanner: { scan: scanMock }, network, crypto, documents, auth, notification },
adapters: { scanner: { scan: scanMock }, network, crypto, documents, auth },
listeners: new Map(),
});
const result = await client.scanDocument({ mode: 'qr' });
@@ -78,7 +77,7 @@ describe('createSelfClient', () => {
const scanMock = vi.fn().mockRejectedValue(err);
const client = createSelfClient({
config: {},
adapters: { scanner: { scan: scanMock }, network, crypto, documents, auth, notification },
adapters: { scanner: { scan: scanMock }, network, crypto, documents, auth },
listeners: new Map(),
});
await expect(client.scanDocument({ mode: 'qr' })).rejects.toBe(err);
@@ -97,7 +96,7 @@ describe('createSelfClient', () => {
const client = createSelfClient({
config: {},
adapters: { scanner, network, crypto, documents, auth, notification },
adapters: { scanner, network, crypto, documents, auth },
listeners: listeners.map,
});
@@ -125,7 +124,7 @@ describe('createSelfClient', () => {
it('parses MRZ via client', () => {
const client = createSelfClient({
config: {},
adapters: { scanner, network, crypto, documents, auth, notification },
adapters: { scanner, network, crypto, documents, auth },
listeners: new Map(),
});
const sample = `P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<\nL898902C36UTO7408122F1204159ZE184226B<<<<<10`;
@@ -134,33 +133,12 @@ describe('createSelfClient', () => {
expect(info.validation?.overall).toBe(true);
});
it('registers device token when notification adapter is given', async () => {
const registerDeviceToken = vi.fn(() => Promise.resolve());
const client = createSelfClient({
config: {},
adapters: {
notification: { registerDeviceToken },
scanner,
network,
crypto,
documents,
auth: { getPrivateKey: () => Promise.resolve('stubbed-private-key') },
},
listeners: new Map(),
});
await client.registerNotificationsToken('session-id', 'test-token', true);
expect(registerDeviceToken).toHaveBeenCalledOnce();
expect(registerDeviceToken).toHaveBeenCalledWith('session-id', 'test-token', true);
});
describe('when analytics adapter is given', () => {
it('calls that adapter for trackEvent', () => {
const trackEvent = vi.fn();
const client = createSelfClient({
config: {},
adapters: {
notification,
scanner,
network,
crypto,
@@ -183,7 +161,7 @@ describe('createSelfClient', () => {
const getPrivateKey = vi.fn(() => Promise.resolve('stubbed-private-key'));
const client = createSelfClient({
config: {},
adapters: { notification, scanner, network, crypto, documents, auth: { getPrivateKey } },
adapters: { scanner, network, crypto, documents, auth: { getPrivateKey } },
listeners: new Map(),
});
@@ -193,7 +171,7 @@ describe('createSelfClient', () => {
const getPrivateKey = vi.fn(() => Promise.resolve('stubbed-private-key'));
const client = createSelfClient({
config: {},
adapters: { notification, scanner, network, crypto, documents, auth: { getPrivateKey } },
adapters: { scanner, network, crypto, documents, auth: { getPrivateKey } },
listeners: new Map(),
});
await expect(client.hasPrivateKey()).resolves.toBe(true);
@@ -234,6 +212,3 @@ const documents: DocumentsAdapter = {
saveDocument: async () => {},
deleteDocument: async () => {},
};
const notification: NotificationAdapter = {
registerDeviceToken: async () => Promise.resolve(),
};

View File

@@ -14,9 +14,6 @@ const createMockSelfClientWithDocumentsAdapter = (documentsAdapter: DocumentsAda
config: defaultConfig,
listeners: new Map(),
adapters: {
notification: {
registerDeviceToken: async () => Promise.resolve(),
},
auth: {
getPrivateKey: async () => null,
},

View File

@@ -4,7 +4,6 @@
/* eslint-disable sort-exports/sort-exports */
import type { CryptoAdapter, DocumentsAdapter, NetworkAdapter, ScannerAdapter } from '../../src';
import type { NotificationAdapter } from '../../src/types/public';
// Shared test data
export const sampleMRZ = `P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<\nL898902C36UTO7408122F1204159ZE184226B<<<<<10`;
@@ -66,17 +65,12 @@ const mockAuth = {
getPrivateKey: async () => 'stubbed-private-key',
};
const mockNotification: NotificationAdapter = {
registerDeviceToken: async () => Promise.resolve(),
};
export const mockAdapters = {
scanner: mockScanner,
network: mockNetwork,
crypto: mockCrypto,
documents: mockDocuments,
auth: mockAuth,
notification: mockNotification,
};
// Shared test expectations

View File

@@ -81,11 +81,6 @@ export function SelfClientProvider({ children }: PropsWithChildren) {
}
},
},
notification: {
async registerDeviceToken(): Promise<void> {
// No-op notification adapter for the demo application
},
},
}),
[],
);

View File

@@ -328,7 +328,7 @@ export class SelfBackendVerifier {
? verificationConfig.minimumAge <= Number.parseInt(genericDiscloseOutput.minimumAge, 10)
: true,
isOfacValid:
//isOfacValid is true when a person is in OFAC list
//isOfacValid is true when a person is in OFAC list
verificationConfig.ofac !== undefined && verificationConfig.ofac ? cumulativeOfac : false,
},
forbiddenCountriesList,