From ae1b3faebceb76e986f18079ffd6d4986777a412 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 19 Aug 2025 11:57:57 -0700 Subject: [PATCH 01/18] feat: expose MRZ parsing on client --- packages/mobile-sdk-alpha/src/browser.ts | 2 +- packages/mobile-sdk-alpha/src/client.ts | 2 ++ packages/mobile-sdk-alpha/src/index.ts | 2 +- packages/mobile-sdk-alpha/src/types/public.ts | 1 + packages/mobile-sdk-alpha/tests/client.test.ts | 8 ++++++++ packages/mobile-sdk-alpha/tests/processing/mrz.test.ts | 3 ++- 6 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/mobile-sdk-alpha/src/browser.ts b/packages/mobile-sdk-alpha/src/browser.ts index 31b31266d..2e824e752 100644 --- a/packages/mobile-sdk-alpha/src/browser.ts +++ b/packages/mobile-sdk-alpha/src/browser.ts @@ -41,7 +41,7 @@ export type { SdkErrorCategory } from './errors'; export { SCANNER_ERROR_CODES, notImplemented, sdkError } from './errors'; export { createSelfClient } from './client'; export { defaultConfig } from './config/defaults'; -export { extractMRZInfo, formatDateToYYMMDD, scanMRZ } from './mrz'; +export { formatDateToYYMMDD, scanMRZ } from './mrz'; // Core functions export { isPassportDataValid } from './validation/document'; diff --git a/packages/mobile-sdk-alpha/src/client.ts b/packages/mobile-sdk-alpha/src/client.ts index 61f4f43ea..428f13f63 100644 --- a/packages/mobile-sdk-alpha/src/client.ts +++ b/packages/mobile-sdk-alpha/src/client.ts @@ -1,6 +1,7 @@ import { defaultConfig } from './config/defaults'; import { mergeConfig } from './config/merge'; import { notImplemented } from './errors'; +import { extractMRZInfo as parseMRZInfo } from './processing/mrz'; import type { Adapters, Config, @@ -102,6 +103,7 @@ export function createSelfClient({ config, adapters }: { config: Config; adapter validateDocument, checkRegistration, generateProof, + extractMRZInfo: parseMRZInfo, on, emit, }; diff --git a/packages/mobile-sdk-alpha/src/index.ts b/packages/mobile-sdk-alpha/src/index.ts index d1b51fb1c..275d450e4 100644 --- a/packages/mobile-sdk-alpha/src/index.ts +++ b/packages/mobile-sdk-alpha/src/index.ts @@ -57,7 +57,7 @@ export { createSelfClient } from './client'; export { defaultConfig } from './config/defaults'; -export { extractMRZInfo, formatDateToYYMMDD, scanMRZ } from './mrz'; +export { formatDateToYYMMDD, scanMRZ } from './mrz'; // Core functions export { isPassportDataValid } from './validation/document'; diff --git a/packages/mobile-sdk-alpha/src/types/public.ts b/packages/mobile-sdk-alpha/src/types/public.ts index cbf3fefa3..f807ace3c 100644 --- a/packages/mobile-sdk-alpha/src/types/public.ts +++ b/packages/mobile-sdk-alpha/src/types/public.ts @@ -128,6 +128,7 @@ export interface SelfClient { timeoutMs?: number; }, ): Promise; + extractMRZInfo(mrz: string): MRZInfo; on(event: E, cb: (payload: SDKEventMap[E]) => void): Unsubscribe; emit(event: E, payload: SDKEventMap[E]): void; } diff --git a/packages/mobile-sdk-alpha/tests/client.test.ts b/packages/mobile-sdk-alpha/tests/client.test.ts index 822fc7795..776792ab0 100644 --- a/packages/mobile-sdk-alpha/tests/client.test.ts +++ b/packages/mobile-sdk-alpha/tests/client.test.ts @@ -77,6 +77,14 @@ describe('createSelfClient', () => { eventSet?.forEach(fn => fn({ step: 'two' })); expect(cb).toHaveBeenCalledTimes(1); }); + + it('parses MRZ via client', () => { + const client = createSelfClient({ config: {}, adapters: { scanner, network, crypto } }); + const sample = `P Date: Tue, 19 Aug 2025 12:23:14 -0700 Subject: [PATCH 02/18] feat: add SelfClient provider --- packages/mobile-sdk-alpha/package.json | 9 + packages/mobile-sdk-alpha/src/browser.ts | 3 +- packages/mobile-sdk-alpha/src/context.tsx | 24 + packages/mobile-sdk-alpha/src/index.ts | 3 +- .../mobile-sdk-alpha/tests/provider.test.tsx | 45 ++ packages/mobile-sdk-alpha/tsconfig.json | 2 +- yarn.lock | 490 +++++++++++++++++- 7 files changed, 551 insertions(+), 25 deletions(-) create mode 100644 packages/mobile-sdk-alpha/src/context.tsx create mode 100644 packages/mobile-sdk-alpha/tests/provider.test.tsx diff --git a/packages/mobile-sdk-alpha/package.json b/packages/mobile-sdk-alpha/package.json index ade92d853..82951a917 100644 --- a/packages/mobile-sdk-alpha/package.json +++ b/packages/mobile-sdk-alpha/package.json @@ -54,7 +54,13 @@ "@selfxyz/common": "workspace:*", "tslib": "^2.6.2" }, + "peerDependencies": { + "react": "^18.3.1" + }, "devDependencies": { + "@testing-library/react": "^14.1.2", + "@types/react": "^18.3.4", + "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "eslint": "^8.57.0", @@ -63,7 +69,10 @@ "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-sort-exports": "^0.8.0", + "jsdom": "^24.0.0", "prettier": "^3.5.3", + "react": "^18.3.1", + "react-dom": "^18.3.1", "tsup": "^8.0.1", "typescript": "^5.9.2", "vitest": "^1.6.0" diff --git a/packages/mobile-sdk-alpha/src/browser.ts b/packages/mobile-sdk-alpha/src/browser.ts index 2e824e752..adf7ae09b 100644 --- a/packages/mobile-sdk-alpha/src/browser.ts +++ b/packages/mobile-sdk-alpha/src/browser.ts @@ -39,9 +39,10 @@ export type { QRProofOptions } from './qr'; export type { SdkErrorCategory } from './errors'; export { SCANNER_ERROR_CODES, notImplemented, sdkError } from './errors'; +export { SelfClientContext, SelfClientProvider, useSelfClient } from './context'; export { createSelfClient } from './client'; export { defaultConfig } from './config/defaults'; -export { formatDateToYYMMDD, scanMRZ } from './mrz'; +export { extractMRZInfo, formatDateToYYMMDD, scanMRZ } from './mrz'; // Core functions export { isPassportDataValid } from './validation/document'; diff --git a/packages/mobile-sdk-alpha/src/context.tsx b/packages/mobile-sdk-alpha/src/context.tsx new file mode 100644 index 000000000..5f793672b --- /dev/null +++ b/packages/mobile-sdk-alpha/src/context.tsx @@ -0,0 +1,24 @@ +import { createContext, type PropsWithChildren, useContext, useMemo } from 'react'; + +import { createSelfClient } from './client'; +import type { Adapters, Config, SelfClient } from './types/public'; + +const SelfClientContext = createContext(null); + +export interface SelfClientProviderProps { + config: Config; + adapters: Partial; +} + +export { SelfClientContext }; + +export function SelfClientProvider({ config, adapters, children }: PropsWithChildren) { + const client = useMemo(() => createSelfClient({ config, adapters }), [config, adapters]); + return {children}; +} + +export function useSelfClient(): SelfClient { + const ctx = useContext(SelfClientContext); + if (!ctx) throw new Error('useSelfClient must be used within a SelfClientProvider'); + return ctx; +} diff --git a/packages/mobile-sdk-alpha/src/index.ts b/packages/mobile-sdk-alpha/src/index.ts index 275d450e4..02bf6c19a 100644 --- a/packages/mobile-sdk-alpha/src/index.ts +++ b/packages/mobile-sdk-alpha/src/index.ts @@ -53,11 +53,12 @@ export { sdkError, } from './errors'; +export { SelfClientContext, SelfClientProvider, useSelfClient } from './context'; export { createSelfClient } from './client'; export { defaultConfig } from './config/defaults'; -export { formatDateToYYMMDD, scanMRZ } from './mrz'; +export { extractMRZInfo, formatDateToYYMMDD, scanMRZ } from './mrz'; // Core functions export { isPassportDataValid } from './validation/document'; diff --git a/packages/mobile-sdk-alpha/tests/provider.test.tsx b/packages/mobile-sdk-alpha/tests/provider.test.tsx new file mode 100644 index 000000000..780e50954 --- /dev/null +++ b/packages/mobile-sdk-alpha/tests/provider.test.tsx @@ -0,0 +1,45 @@ +/* @vitest-environment jsdom */ +import type { ReactNode } from 'react'; +import { describe, expect, it } from 'vitest'; + +import type { CryptoAdapter, NetworkAdapter, ScannerAdapter } from '../src/adapters/index'; +import { SelfClientProvider, useSelfClient } from '../src/index'; + +import { renderHook } from '@testing-library/react'; + +const scanner: ScannerAdapter = { + scan: async () => ({ mode: 'mrz', passportNumber: '', dateOfBirth: '', dateOfExpiry: '' }), +}; + +const network: NetworkAdapter = { + http: { fetch: async () => new Response(null) }, + ws: { + connect: () => ({ + send: () => {}, + close: () => {}, + onMessage: () => {}, + onError: () => {}, + onClose: () => {}, + }), + }, +}; + +const crypto: CryptoAdapter = { + hash: async () => new Uint8Array(), + sign: async () => new Uint8Array(), +}; + +describe('SelfClientProvider', () => { + it('provides client through context', () => { + const wrapper = ({ children }: { children: ReactNode }) => ( + + {children} + + ); + const { result } = renderHook(() => useSelfClient(), { wrapper }); + const sample = `P=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10c0/eac918213de265ef7cb3d4ca348b891a51a520d839aa51cdb8ca93d4fa7ff9f6ccb339ccee89e4075324097f0a55157c89fa3f7147bde9d8d7e90335dc087b53 + languageName: node + linkType: hard + +"xml-name-validator@npm:^5.0.0": + version: 5.0.0 + resolution: "xml-name-validator@npm:5.0.0" + checksum: 10c0/3fcf44e7b73fb18be917fdd4ccffff3639373c7cb83f8fc35df6001fecba7942f1dbead29d91ebb8315e2f2ff786b508f0c9dc0215b6353f9983c6b7d62cb1f5 + languageName: node + linkType: hard + +"xmlchars@npm:^2.2.0": + version: 2.2.0 + resolution: "xmlchars@npm:2.2.0" + checksum: 10c0/b64b535861a6f310c5d9bfa10834cf49127c71922c297da9d4d1b45eeaae40bf9b4363275876088fbe2667e5db028d2cd4f8ee72eed9bede840a67d57dab7593 + languageName: node + linkType: hard + "xmlhttprequest-ssl@npm:~2.1.1": version: 2.1.2 resolution: "xmlhttprequest-ssl@npm:2.1.2" From aa1c1042116acb21a34ff0ea80f30eea53b81d48 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 19 Aug 2025 12:35:59 -0700 Subject: [PATCH 03/18] feat: add react native entry --- packages/mobile-sdk-alpha/package.json | 9 ++- packages/mobile-sdk-alpha/src/react-native.ts | 56 +++++++++++++++++++ .../tests/react-native.test.ts | 16 ++++++ packages/mobile-sdk-alpha/tsup.config.ts | 2 + 4 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 packages/mobile-sdk-alpha/src/react-native.ts create mode 100644 packages/mobile-sdk-alpha/tests/react-native.test.ts diff --git a/packages/mobile-sdk-alpha/package.json b/packages/mobile-sdk-alpha/package.json index 82951a917..b7b088e44 100644 --- a/packages/mobile-sdk-alpha/package.json +++ b/packages/mobile-sdk-alpha/package.json @@ -1,6 +1,6 @@ { "name": "@selfxyz/mobile-sdk-alpha", - "version": "0.1.0", + "version": "2.6.4", "description": "Self SDK (alpha) for registering and proving. Adapters-first, RN-first with web shims.", "keywords": [ "self", @@ -14,7 +14,7 @@ "exports": { ".": { "types": "./dist/esm/index.d.ts", - "react-native": "./dist/esm/index.js", + "react-native": "./dist/esm/react-native.js", "browser": "./dist/esm/browser.js", "import": "./dist/esm/index.js", "require": "./dist/cjs/index.cjs", @@ -24,6 +24,11 @@ "types": "./dist/esm/browser.d.ts", "import": "./dist/esm/browser.js", "require": "./dist/cjs/browser.cjs" + }, + "./react-native": { + "types": "./dist/esm/react-native.d.ts", + "import": "./dist/esm/react-native.js", + "require": "./dist/cjs/react-native.cjs" } }, "main": "./dist/cjs/index.cjs", diff --git a/packages/mobile-sdk-alpha/src/react-native.ts b/packages/mobile-sdk-alpha/src/react-native.ts new file mode 100644 index 000000000..122aeb20e --- /dev/null +++ b/packages/mobile-sdk-alpha/src/react-native.ts @@ -0,0 +1,56 @@ +// React Native-friendly exports with explicit tree-shaking friendly imports + +// Types +export type { + Adapters, + ClockAdapter, + Config, + CryptoAdapter, + HttpAdapter, + LogLevel, + LoggerAdapter, + MRZInfo, + MRZValidation, + NetworkAdapter, + Progress, + ProofHandle, + ProofRequest, + RegistrationInput, + RegistrationStatus, + SDKEvent, + SDKEventMap, + ScanMode, + ScanOpts, + ScanResult, + ScannerAdapter, + SelfClient, + StorageAdapter, + Unsubscribe, + ValidationInput, + ValidationResult, + WsAdapter, + WsConn, +} from './types/public'; + +export type { DG1, DG2, NFCScanOptions, ParsedNFCResponse } from './nfc'; +export type { MRZScanOptions } from './mrz'; +export type { PassportValidationCallbacks } from './validation/document'; +export type { QRProofOptions } from './qr'; +export type { SdkErrorCategory } from './errors'; + +export { SCANNER_ERROR_CODES, notImplemented, sdkError } from './errors'; +export { SelfClientContext, SelfClientProvider, useSelfClient } from './context'; +export { createSelfClient } from './client'; +export { defaultConfig } from './config/defaults'; +export { extractMRZInfo, formatDateToYYMMDD, scanMRZ } from './mrz'; + +// Core functions +export { isPassportDataValid } from './validation/document'; + +export { mergeConfig } from './config/merge'; + +export { parseNFCResponse, scanNFC } from './nfc'; + +export { scanQRProof } from './qr'; + +export { webScannerShim } from './adapters/web/shims'; diff --git a/packages/mobile-sdk-alpha/tests/react-native.test.ts b/packages/mobile-sdk-alpha/tests/react-native.test.ts new file mode 100644 index 000000000..87ea991e0 --- /dev/null +++ b/packages/mobile-sdk-alpha/tests/react-native.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; + +import { createSelfClient, extractMRZInfo } from '../src/react-native'; + +describe('react-native entry', () => { + it('exposes createSelfClient', () => { + expect(typeof createSelfClient).toBe('function'); + }); + + it('parses MRZ via react-native entry', () => { + const sample = `P Date: Tue, 19 Aug 2025 13:07:20 -0700 Subject: [PATCH 04/18] feat: add mobile sdk entry wrapper --- packages/mobile-sdk-alpha/src/client.ts | 5 ++ packages/mobile-sdk-alpha/src/context.tsx | 4 +- packages/mobile-sdk-alpha/src/entry/index.tsx | 16 ++++++ packages/mobile-sdk-alpha/src/index.ts | 1 + packages/mobile-sdk-alpha/src/react-native.ts | 1 + packages/mobile-sdk-alpha/src/types/public.ts | 1 + .../mobile-sdk-alpha/tests/client.test.ts | 8 +++ .../mobile-sdk-alpha/tests/entry.test.tsx | 50 +++++++++++++++++++ .../mobile-sdk-alpha/tests/provider.test.tsx | 2 + 9 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 packages/mobile-sdk-alpha/src/entry/index.tsx create mode 100644 packages/mobile-sdk-alpha/tests/entry.test.tsx diff --git a/packages/mobile-sdk-alpha/src/client.ts b/packages/mobile-sdk-alpha/src/client.ts index 428f13f63..dab782e43 100644 --- a/packages/mobile-sdk-alpha/src/client.ts +++ b/packages/mobile-sdk-alpha/src/client.ts @@ -78,6 +78,10 @@ export function createSelfClient({ config, adapters }: { config: Config; adapter return { registered: false, reason: 'SELF_REG_STATUS_STUB' }; } + async function registerDocument(_input: RegistrationInput): Promise { + return { registered: false, reason: 'SELF_REG_STATUS_STUB' }; + } + async function generateProof( _req: ProofRequest, opts: { @@ -102,6 +106,7 @@ export function createSelfClient({ config, adapters }: { config: Config; adapter scanDocument, validateDocument, checkRegistration, + registerDocument, generateProof, extractMRZInfo: parseMRZInfo, on, diff --git a/packages/mobile-sdk-alpha/src/context.tsx b/packages/mobile-sdk-alpha/src/context.tsx index 5f793672b..9f2b4de43 100644 --- a/packages/mobile-sdk-alpha/src/context.tsx +++ b/packages/mobile-sdk-alpha/src/context.tsx @@ -7,12 +7,12 @@ const SelfClientContext = createContext(null); export interface SelfClientProviderProps { config: Config; - adapters: Partial; + adapters?: Partial; } export { SelfClientContext }; -export function SelfClientProvider({ config, adapters, children }: PropsWithChildren) { +export function SelfClientProvider({ config, adapters = {}, children }: PropsWithChildren) { const client = useMemo(() => createSelfClient({ config, adapters }), [config, adapters]); return {children}; } diff --git a/packages/mobile-sdk-alpha/src/entry/index.tsx b/packages/mobile-sdk-alpha/src/entry/index.tsx new file mode 100644 index 000000000..3836cd04b --- /dev/null +++ b/packages/mobile-sdk-alpha/src/entry/index.tsx @@ -0,0 +1,16 @@ +import type { ReactNode } from 'react'; + +import { SelfClientProvider } from '../context'; +import type { Adapters, Config } from '../types/public'; + +export interface SelfMobileSdkProps { + config: Config; + adapters?: Partial; + children?: ReactNode; +} + +export const SelfMobileSdk = ({ config, adapters = {}, children }: SelfMobileSdkProps) => ( + + {children} + +); diff --git a/packages/mobile-sdk-alpha/src/index.ts b/packages/mobile-sdk-alpha/src/index.ts index 02bf6c19a..f19919b41 100644 --- a/packages/mobile-sdk-alpha/src/index.ts +++ b/packages/mobile-sdk-alpha/src/index.ts @@ -54,6 +54,7 @@ export { } from './errors'; export { SelfClientContext, SelfClientProvider, useSelfClient } from './context'; +export { SelfMobileSdk } from './entry'; export { createSelfClient } from './client'; export { defaultConfig } from './config/defaults'; diff --git a/packages/mobile-sdk-alpha/src/react-native.ts b/packages/mobile-sdk-alpha/src/react-native.ts index 122aeb20e..44ce05371 100644 --- a/packages/mobile-sdk-alpha/src/react-native.ts +++ b/packages/mobile-sdk-alpha/src/react-native.ts @@ -40,6 +40,7 @@ export type { SdkErrorCategory } from './errors'; export { SCANNER_ERROR_CODES, notImplemented, sdkError } from './errors'; export { SelfClientContext, SelfClientProvider, useSelfClient } from './context'; +export { SelfMobileSdk } from './entry'; export { createSelfClient } from './client'; export { defaultConfig } from './config/defaults'; export { extractMRZInfo, formatDateToYYMMDD, scanMRZ } from './mrz'; diff --git a/packages/mobile-sdk-alpha/src/types/public.ts b/packages/mobile-sdk-alpha/src/types/public.ts index f807ace3c..dc13ffbe8 100644 --- a/packages/mobile-sdk-alpha/src/types/public.ts +++ b/packages/mobile-sdk-alpha/src/types/public.ts @@ -120,6 +120,7 @@ export interface SelfClient { scanDocument(opts: ScanOpts & { signal?: AbortSignal }): Promise; validateDocument(input: ValidationInput): Promise; checkRegistration(input: RegistrationInput): Promise; + registerDocument(input: RegistrationInput): Promise; generateProof( req: ProofRequest, opts?: { diff --git a/packages/mobile-sdk-alpha/tests/client.test.ts b/packages/mobile-sdk-alpha/tests/client.test.ts index 776792ab0..a4c6f105c 100644 --- a/packages/mobile-sdk-alpha/tests/client.test.ts +++ b/packages/mobile-sdk-alpha/tests/client.test.ts @@ -85,6 +85,14 @@ describe('createSelfClient', () => { expect(info.passportNumber).toBe('L898902C3'); expect(info.validation.overall).toBe(true); }); + + it('returns stub registration status', async () => { + const client = createSelfClient({ config: {}, adapters: { scanner, network, crypto } }); + await expect(client.registerDocument({} as any)).resolves.toEqual({ + registered: false, + reason: 'SELF_REG_STATUS_STUB', + }); + }); }); const scanner: ScannerAdapter = { diff --git a/packages/mobile-sdk-alpha/tests/entry.test.tsx b/packages/mobile-sdk-alpha/tests/entry.test.tsx new file mode 100644 index 000000000..94b085f3f --- /dev/null +++ b/packages/mobile-sdk-alpha/tests/entry.test.tsx @@ -0,0 +1,50 @@ +/* @vitest-environment jsdom */ +import React from 'react'; + +import type { CryptoAdapter, NetworkAdapter, ScannerAdapter } from '../src/adapters'; +import { SelfMobileSdk, useSelfClient } from '../src/index'; + +// eslint-disable-next-line import/no-unresolved +// @ts-ignore +import { render, screen } from '@testing-library/react'; + +const sample = `P{info.passportNumber}; +} + +const scanner: ScannerAdapter = { + scan: async () => ({ mode: 'mrz', passportNumber: '', dateOfBirth: '', dateOfExpiry: '' }), +}; + +const network: NetworkAdapter = { + http: { fetch: async () => new Response(null) }, + ws: { + connect: () => ({ + send: () => {}, + close: () => {}, + onMessage: () => {}, + onError: () => {}, + onClose: () => {}, + }), + }, +}; + +const crypto: CryptoAdapter = { + hash: async () => new Uint8Array(), + sign: async () => new Uint8Array(), +}; + +describe('SelfMobileSdk', () => { + it('provides client to children', () => { + render( + + + , + ); + expect(screen.getByText('L898902C3')).toBeTruthy(); + }); +}); diff --git a/packages/mobile-sdk-alpha/tests/provider.test.tsx b/packages/mobile-sdk-alpha/tests/provider.test.tsx index 780e50954..73b0b2c4d 100644 --- a/packages/mobile-sdk-alpha/tests/provider.test.tsx +++ b/packages/mobile-sdk-alpha/tests/provider.test.tsx @@ -5,6 +5,8 @@ import { describe, expect, it } from 'vitest'; import type { CryptoAdapter, NetworkAdapter, ScannerAdapter } from '../src/adapters/index'; import { SelfClientProvider, useSelfClient } from '../src/index'; +// eslint-disable-next-line import/no-unresolved +// @ts-ignore import { renderHook } from '@testing-library/react'; const scanner: ScannerAdapter = { From 855be20806e5d46d4e565b85fd147d247c9a2882 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 19 Aug 2025 14:14:54 -0700 Subject: [PATCH 05/18] upgrade packages --- packages/mobile-sdk-alpha/package.json | 4 +++- yarn.lock | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/mobile-sdk-alpha/package.json b/packages/mobile-sdk-alpha/package.json index b7b088e44..f455438f5 100644 --- a/packages/mobile-sdk-alpha/package.json +++ b/packages/mobile-sdk-alpha/package.json @@ -60,7 +60,8 @@ "tslib": "^2.6.2" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^18.3.1", + "react-native": "^0.75.4" }, "devDependencies": { "@testing-library/react": "^14.1.2", @@ -78,6 +79,7 @@ "prettier": "^3.5.3", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-native": "0.75.4", "tsup": "^8.0.1", "typescript": "^5.9.2", "vitest": "^1.6.0" diff --git a/yarn.lock b/yarn.lock index a1b5aac3e..bdfb9bff3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5266,12 +5266,14 @@ __metadata: prettier: "npm:^3.5.3" react: "npm:^18.3.1" react-dom: "npm:^18.3.1" + react-native: "npm:0.75.4" tslib: "npm:^2.6.2" tsup: "npm:^8.0.1" typescript: "npm:^5.9.2" vitest: "npm:^1.6.0" peerDependencies: react: ^18.3.1 + react-native: ^0.75.4 languageName: unknown linkType: soft From f558df72ac0a932063f27699f9617ff224f900b9 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 19 Aug 2025 14:43:01 -0700 Subject: [PATCH 06/18] save wip --- packages/mobile-sdk-alpha/package.json | 6 - .../src/components/SelfMobileSdk.tsx | 188 ++++++++++++++++++ packages/mobile-sdk-alpha/src/context.tsx | 16 +- packages/mobile-sdk-alpha/src/index.ts | 3 +- packages/mobile-sdk-alpha/src/react-native.ts | 57 ------ .../tests/SelfMobileSdk.test.tsx | 16 ++ .../mobile-sdk-alpha/tests/entry.test.tsx | 7 +- .../mobile-sdk-alpha/tests/provider.test.tsx | 7 +- .../tests/react-native.test.ts | 33 ++- packages/mobile-sdk-alpha/tsup.config.ts | 2 - packages/mobile-sdk-alpha/vitest.config.ts | 2 +- 11 files changed, 256 insertions(+), 81 deletions(-) create mode 100644 packages/mobile-sdk-alpha/src/components/SelfMobileSdk.tsx delete mode 100644 packages/mobile-sdk-alpha/src/react-native.ts create mode 100644 packages/mobile-sdk-alpha/tests/SelfMobileSdk.test.tsx diff --git a/packages/mobile-sdk-alpha/package.json b/packages/mobile-sdk-alpha/package.json index f455438f5..4e9abf110 100644 --- a/packages/mobile-sdk-alpha/package.json +++ b/packages/mobile-sdk-alpha/package.json @@ -14,7 +14,6 @@ "exports": { ".": { "types": "./dist/esm/index.d.ts", - "react-native": "./dist/esm/react-native.js", "browser": "./dist/esm/browser.js", "import": "./dist/esm/index.js", "require": "./dist/cjs/index.cjs", @@ -24,11 +23,6 @@ "types": "./dist/esm/browser.d.ts", "import": "./dist/esm/browser.js", "require": "./dist/cjs/browser.cjs" - }, - "./react-native": { - "types": "./dist/esm/react-native.d.ts", - "import": "./dist/esm/react-native.js", - "require": "./dist/cjs/react-native.cjs" } }, "main": "./dist/cjs/index.cjs", diff --git a/packages/mobile-sdk-alpha/src/components/SelfMobileSdk.tsx b/packages/mobile-sdk-alpha/src/components/SelfMobileSdk.tsx new file mode 100644 index 000000000..669513b65 --- /dev/null +++ b/packages/mobile-sdk-alpha/src/components/SelfMobileSdk.tsx @@ -0,0 +1,188 @@ +import type { ReactNode } from 'react'; +import { useCallback, useLayoutEffect, useState } from 'react'; + +import type { DocumentCategory, PassportData } from '@selfxyz/common'; + +import { SelfClientProvider, useSelfClient } from '../context'; +import type { Adapters, Config } from '../types/public'; + +// Local interface for document metadata +interface DocumentMetadata { + id: string; + documentType: string; + documentCategory: DocumentCategory; + data: string; + mock: boolean; + isRegistered?: boolean; +} + +interface DocumentData { + data: PassportData; + metadata: DocumentMetadata; +} + +interface External { + getSecret: () => Promise; + getAllDocuments: () => Promise<{ + [documentId: string]: DocumentData; + }>; + setDocument: (doc: DocumentData, documentId: string) => Promise; + onOnboardingSuccess: () => void; + onOnboardingFailure: (error: Error) => void; + onDisclosureSuccess: () => void; + onDisclosureFailure: (error: Error) => void; +} + +interface SelfMobileSdkProps { + config: Config; + adapters?: Partial; + external: External; + children?: ReactNode; +} + +// Simple placeholder components - these would be replaced with actual UI components +const PassportCameraScreen = ({ onMRZDetected }: { onMRZDetected: (mrzData: any) => void }) => ( +
+

Passport Camera

+ +
+); + +const QrCodeScreen = ({ onSuccess, onFailure }: { onSuccess: () => void; onFailure: (error: Error) => void }) => ( +
+

QR Code Scanner

+ + +
+); + +const NFCScannerScreen = ({ onSuccess, onFailure }: { onSuccess: () => void; onFailure: (error: Error) => void }) => { + const client = useSelfClient(); + + const onNFCScan = useCallback( + async (_nfcData: any) => { + try { + // scan the document + // register the document + onSuccess(); + } catch (error) { + onFailure(error as Error); + } + }, + [client, onSuccess, onFailure], + ); + + return ( +
+

NFC Scanner

+ +
+ ); +}; + +const OnboardingScreen = ({ + onSuccess, + onFailure, + _setDocument, +}: { + onSuccess: () => void; + onFailure: (error: Error) => void; + _setDocument: (doc: DocumentData, documentId: string) => Promise; +}) => { + const [mrzData, setMrzData] = useState(null); + const client = useSelfClient(); + + const onMRZDetected = useCallback( + async (mrzData: any) => { + try { + const status = await client.registerDocument({ + scan: { + mode: 'mrz', + passportNumber: mrzData.documentNumber, + dateOfBirth: mrzData.birthDate, + dateOfExpiry: mrzData.expiryDate, + issuingCountry: mrzData.countryCode, + }, + }); + + if (status.registered) { + setMrzData(mrzData); + onSuccess(); + } else { + onFailure(new Error('Registration failed')); + } + } catch (error) { + onFailure(error as Error); + } + }, + [client, onSuccess, onFailure], + ); + + return ( +
+

Onboarding

+ {!mrzData && } + {mrzData && } +
+ ); +}; + +const SelfMobileSdkContent = ({ external }: { external: External }) => { + const { + getAllDocuments, + onOnboardingSuccess, + onOnboardingFailure, + onDisclosureSuccess, + onDisclosureFailure, + setDocument, + } = external; + + const [documents, setDocuments] = useState<{ + [documentId: string]: DocumentData; + }>({}); + + const [isLoading, setIsLoading] = useState(true); + + useLayoutEffect(() => { + getAllDocuments() + .then(documents => { + setDocuments(documents); + setIsLoading(false); + }) + .catch(error => { + console.error('Failed to load documents:', error); + setIsLoading(false); + }); + }, [getAllDocuments]); + + if (isLoading) { + return
Loading documents...
; + } + + // Check if user has any registered documents + const hasRegisteredDocuments = Object.values(documents).some(doc => doc.metadata.isRegistered); + + if (Object.keys(documents).length === 0 || !hasRegisteredDocuments) { + // Start onboarding flow + return ( + + ); + } + + // Show disclosure flow + return ; +}; + +export const SelfMobileSdk = ({ config, adapters = {}, external, children }: SelfMobileSdkProps) => { + return ( + + {children || } + + ); +}; diff --git a/packages/mobile-sdk-alpha/src/context.tsx b/packages/mobile-sdk-alpha/src/context.tsx index 9f2b4de43..574f2cb72 100644 --- a/packages/mobile-sdk-alpha/src/context.tsx +++ b/packages/mobile-sdk-alpha/src/context.tsx @@ -1,4 +1,4 @@ -import { createContext, type PropsWithChildren, useContext, useMemo } from 'react'; +import { createContext, type PropsWithChildren, useContext, useEffect, useState } from 'react'; import { createSelfClient } from './client'; import type { Adapters, Config, SelfClient } from './types/public'; @@ -13,7 +13,19 @@ export interface SelfClientProviderProps { export { SelfClientContext }; export function SelfClientProvider({ config, adapters = {}, children }: PropsWithChildren) { - const client = useMemo(() => createSelfClient({ config, adapters }), [config, adapters]); + const [client, setClient] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const clientInstance = createSelfClient({ config, adapters }); + setClient(clientInstance); + setIsLoading(false); + }, [config, adapters]); + + if (isLoading) { + return
Loading...
; // Simple loading state, can be customized + } + return {children}; } diff --git a/packages/mobile-sdk-alpha/src/index.ts b/packages/mobile-sdk-alpha/src/index.ts index f19919b41..d3fc65407 100644 --- a/packages/mobile-sdk-alpha/src/index.ts +++ b/packages/mobile-sdk-alpha/src/index.ts @@ -55,11 +55,12 @@ export { export { SelfClientContext, SelfClientProvider, useSelfClient } from './context'; export { SelfMobileSdk } from './entry'; +export { SelfMobileSdk as SelfMobileSdkHighLevel } from './components/SelfMobileSdk'; export { createSelfClient } from './client'; export { defaultConfig } from './config/defaults'; -export { extractMRZInfo, formatDateToYYMMDD, scanMRZ } from './mrz'; +export { formatDateToYYMMDD, scanMRZ } from './mrz'; // Core functions export { isPassportDataValid } from './validation/document'; diff --git a/packages/mobile-sdk-alpha/src/react-native.ts b/packages/mobile-sdk-alpha/src/react-native.ts deleted file mode 100644 index 44ce05371..000000000 --- a/packages/mobile-sdk-alpha/src/react-native.ts +++ /dev/null @@ -1,57 +0,0 @@ -// React Native-friendly exports with explicit tree-shaking friendly imports - -// Types -export type { - Adapters, - ClockAdapter, - Config, - CryptoAdapter, - HttpAdapter, - LogLevel, - LoggerAdapter, - MRZInfo, - MRZValidation, - NetworkAdapter, - Progress, - ProofHandle, - ProofRequest, - RegistrationInput, - RegistrationStatus, - SDKEvent, - SDKEventMap, - ScanMode, - ScanOpts, - ScanResult, - ScannerAdapter, - SelfClient, - StorageAdapter, - Unsubscribe, - ValidationInput, - ValidationResult, - WsAdapter, - WsConn, -} from './types/public'; - -export type { DG1, DG2, NFCScanOptions, ParsedNFCResponse } from './nfc'; -export type { MRZScanOptions } from './mrz'; -export type { PassportValidationCallbacks } from './validation/document'; -export type { QRProofOptions } from './qr'; -export type { SdkErrorCategory } from './errors'; - -export { SCANNER_ERROR_CODES, notImplemented, sdkError } from './errors'; -export { SelfClientContext, SelfClientProvider, useSelfClient } from './context'; -export { SelfMobileSdk } from './entry'; -export { createSelfClient } from './client'; -export { defaultConfig } from './config/defaults'; -export { extractMRZInfo, formatDateToYYMMDD, scanMRZ } from './mrz'; - -// Core functions -export { isPassportDataValid } from './validation/document'; - -export { mergeConfig } from './config/merge'; - -export { parseNFCResponse, scanNFC } from './nfc'; - -export { scanQRProof } from './qr'; - -export { webScannerShim } from './adapters/web/shims'; diff --git a/packages/mobile-sdk-alpha/tests/SelfMobileSdk.test.tsx b/packages/mobile-sdk-alpha/tests/SelfMobileSdk.test.tsx new file mode 100644 index 000000000..d290d90fb --- /dev/null +++ b/packages/mobile-sdk-alpha/tests/SelfMobileSdk.test.tsx @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; + +// Test that the component can be imported +describe('SelfMobileSdk', () => { + it('can be imported successfully', async () => { + const { SelfMobileSdk } = await import('../src/components/SelfMobileSdk'); + expect(SelfMobileSdk).toBeDefined(); + expect(typeof SelfMobileSdk).toBe('function'); + }); + + it('has the expected props interface', async () => { + const { SelfMobileSdk } = await import('../src/components/SelfMobileSdk'); + + expect(SelfMobileSdk).toBeDefined(); + }); +}); diff --git a/packages/mobile-sdk-alpha/tests/entry.test.tsx b/packages/mobile-sdk-alpha/tests/entry.test.tsx index 94b085f3f..261d251a1 100644 --- a/packages/mobile-sdk-alpha/tests/entry.test.tsx +++ b/packages/mobile-sdk-alpha/tests/entry.test.tsx @@ -1,11 +1,9 @@ /* @vitest-environment jsdom */ import React from 'react'; -import type { CryptoAdapter, NetworkAdapter, ScannerAdapter } from '../src/adapters'; +import type { CryptoAdapter, NetworkAdapter, ScannerAdapter } from '../src'; import { SelfMobileSdk, useSelfClient } from '../src/index'; -// eslint-disable-next-line import/no-unresolved -// @ts-ignore import { render, screen } from '@testing-library/react'; const sample = `P new Response(null) }, + // Return a minimal stub to avoid relying on global Response in JSDOM/Node + http: { fetch: async () => ({ ok: true }) as any }, ws: { connect: () => ({ send: () => {}, diff --git a/packages/mobile-sdk-alpha/tests/provider.test.tsx b/packages/mobile-sdk-alpha/tests/provider.test.tsx index 73b0b2c4d..22b212e57 100644 --- a/packages/mobile-sdk-alpha/tests/provider.test.tsx +++ b/packages/mobile-sdk-alpha/tests/provider.test.tsx @@ -2,11 +2,9 @@ import type { ReactNode } from 'react'; import { describe, expect, it } from 'vitest'; -import type { CryptoAdapter, NetworkAdapter, ScannerAdapter } from '../src/adapters/index'; +import type { CryptoAdapter, NetworkAdapter, ScannerAdapter } from '../src'; import { SelfClientProvider, useSelfClient } from '../src/index'; -// eslint-disable-next-line import/no-unresolved -// @ts-ignore import { renderHook } from '@testing-library/react'; const scanner: ScannerAdapter = { @@ -14,7 +12,8 @@ const scanner: ScannerAdapter = { }; const network: NetworkAdapter = { - http: { fetch: async () => new Response(null) }, + // Return a minimal stub to avoid relying on global Response in JSDOM/Node + http: { fetch: async () => ({ ok: true }) as any }, ws: { connect: () => ({ send: () => {}, diff --git a/packages/mobile-sdk-alpha/tests/react-native.test.ts b/packages/mobile-sdk-alpha/tests/react-native.test.ts index 87ea991e0..ee3fafee5 100644 --- a/packages/mobile-sdk-alpha/tests/react-native.test.ts +++ b/packages/mobile-sdk-alpha/tests/react-native.test.ts @@ -1,15 +1,40 @@ import { describe, expect, it } from 'vitest'; -import { createSelfClient, extractMRZInfo } from '../src/react-native'; +import { createSelfClient } from '../src/index'; -describe('react-native entry', () => { +describe('main entry', () => { it('exposes createSelfClient', () => { expect(typeof createSelfClient).toBe('function'); }); - it('parses MRZ via react-native entry', () => { + it('parses MRZ via client API', () => { const sample = `P ({ mode: 'mrz' as const, passportNumber: '', dateOfBirth: '', dateOfExpiry: '' }), + }; + + const network = { + // Return a minimal stub to avoid relying on global Response in JSDOM/Node + http: { fetch: async () => ({ ok: true }) as any }, + ws: { + connect: () => ({ + send: () => {}, + close: () => {}, + onMessage: () => {}, + onError: () => {}, + onClose: () => {}, + }), + }, + }; + + const crypto = { + hash: async () => new Uint8Array(), + sign: async () => new Uint8Array(), + }; + + const client = createSelfClient({ config: {}, adapters: { scanner, network, crypto } }); + const info = client.extractMRZInfo(sample); expect(info.passportNumber).toBe('L898902C3'); expect(info.validation.overall).toBe(true); }); diff --git a/packages/mobile-sdk-alpha/tsup.config.ts b/packages/mobile-sdk-alpha/tsup.config.ts index 6b25bb615..bbb388742 100644 --- a/packages/mobile-sdk-alpha/tsup.config.ts +++ b/packages/mobile-sdk-alpha/tsup.config.ts @@ -5,7 +5,6 @@ export default defineConfig([ entry: { index: 'src/index.ts', browser: 'src/browser.ts', - 'react-native': 'src/react-native.ts', }, format: ['esm'], dts: true, @@ -20,7 +19,6 @@ export default defineConfig([ entry: { index: 'src/index.ts', browser: 'src/browser.ts', - 'react-native': 'src/react-native.ts', }, format: ['cjs'], dts: false, diff --git a/packages/mobile-sdk-alpha/vitest.config.ts b/packages/mobile-sdk-alpha/vitest.config.ts index f5d24052e..67f041196 100644 --- a/packages/mobile-sdk-alpha/vitest.config.ts +++ b/packages/mobile-sdk-alpha/vitest.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, - environment: 'node', + environment: 'jsdom', setupFiles: ['./tests/setup.ts'], }, }); From cc660a22771067fc9716cff2c089fc247488dd99 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 19 Aug 2025 14:47:45 -0700 Subject: [PATCH 07/18] clean up tests to support new arch --- .../tests/SelfMobileSdk.test.tsx | 21 ++++++-- .../mobile-sdk-alpha/tests/client.test.tsx | 32 +++++++++++++ .../mobile-sdk-alpha/tests/entry.test.tsx | 48 +++++++------------ .../mobile-sdk-alpha/tests/provider.test.tsx | 46 ++++++------------ .../tests/react-native.test.ts | 41 ---------------- .../tests/utils/testHelpers.ts | 40 ++++++++++++++++ 6 files changed, 123 insertions(+), 105 deletions(-) create mode 100644 packages/mobile-sdk-alpha/tests/client.test.tsx delete mode 100644 packages/mobile-sdk-alpha/tests/react-native.test.ts create mode 100644 packages/mobile-sdk-alpha/tests/utils/testHelpers.ts diff --git a/packages/mobile-sdk-alpha/tests/SelfMobileSdk.test.tsx b/packages/mobile-sdk-alpha/tests/SelfMobileSdk.test.tsx index d290d90fb..eb7aa8eef 100644 --- a/packages/mobile-sdk-alpha/tests/SelfMobileSdk.test.tsx +++ b/packages/mobile-sdk-alpha/tests/SelfMobileSdk.test.tsx @@ -1,16 +1,31 @@ import { describe, expect, it } from 'vitest'; -// Test that the component can be imported -describe('SelfMobileSdk', () => { +describe('High-Level SelfMobileSdk Component', () => { it('can be imported successfully', async () => { const { SelfMobileSdk } = await import('../src/components/SelfMobileSdk'); expect(SelfMobileSdk).toBeDefined(); expect(typeof SelfMobileSdk).toBe('function'); }); - it('has the expected props interface', async () => { + it('accepts the expected props interface', async () => { const { SelfMobileSdk } = await import('../src/components/SelfMobileSdk'); + // Test that the component accepts the expected props structure + const mockExternal = { + getSecret: async () => 'test-secret', + getAllDocuments: async () => ({}), + setDocument: async () => true, + onOnboardingSuccess: () => {}, + onOnboardingFailure: () => {}, + onDisclosureSuccess: () => {}, + onDisclosureFailure: () => {}, + }; + expect(SelfMobileSdk).toBeDefined(); + // The component should accept these props without throwing + expect(() => { + // This is just a type check - we're not actually rendering + const _props = { config: {}, external: mockExternal }; + }).not.toThrow(); }); }); diff --git a/packages/mobile-sdk-alpha/tests/client.test.tsx b/packages/mobile-sdk-alpha/tests/client.test.tsx new file mode 100644 index 000000000..30eb61f5a --- /dev/null +++ b/packages/mobile-sdk-alpha/tests/client.test.tsx @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; + +import { createSelfClient } from '../src/index'; +import { expectedMRZResult, mockAdapters, sampleMRZ } from './utils/testHelpers'; + +describe('createSelfClient API', () => { + it('creates a client instance with expected methods', () => { + const client = createSelfClient({ config: {}, adapters: mockAdapters }); + + expect(typeof client.extractMRZInfo).toBe('function'); + expect(typeof client.registerDocument).toBe('function'); + expect(typeof client.validateDocument).toBe('function'); + }); + + it('parses MRZ data correctly', () => { + const client = createSelfClient({ config: {}, adapters: mockAdapters }); + const info = client.extractMRZInfo(sampleMRZ); + + expect(info.passportNumber).toBe(expectedMRZResult.passportNumber); + expect(info.validation.overall).toBe(expectedMRZResult.validation.overall); + }); + + it('accepts different adapter configurations', () => { + const clientWithAllAdapters = createSelfClient({ + config: {}, + adapters: mockAdapters, + }); + + expect(clientWithAllAdapters).toBeDefined(); + expect(typeof clientWithAllAdapters.extractMRZInfo).toBe('function'); + }); +}); diff --git a/packages/mobile-sdk-alpha/tests/entry.test.tsx b/packages/mobile-sdk-alpha/tests/entry.test.tsx index 261d251a1..0bcd0aaac 100644 --- a/packages/mobile-sdk-alpha/tests/entry.test.tsx +++ b/packages/mobile-sdk-alpha/tests/entry.test.tsx @@ -1,49 +1,37 @@ /* @vitest-environment jsdom */ import React from 'react'; +import { describe, expect, it } from 'vitest'; -import type { CryptoAdapter, NetworkAdapter, ScannerAdapter } from '../src'; import { SelfMobileSdk, useSelfClient } from '../src/index'; +import { expectedMRZResult, mockAdapters, sampleMRZ } from './utils/testHelpers'; import { render, screen } from '@testing-library/react'; -const sample = `P{info.passportNumber}; } -const scanner: ScannerAdapter = { - scan: async () => ({ mode: 'mrz', passportNumber: '', dateOfBirth: '', dateOfExpiry: '' }), -}; - -const network: NetworkAdapter = { - // Return a minimal stub to avoid relying on global Response in JSDOM/Node - http: { fetch: async () => ({ ok: true }) as any }, - ws: { - connect: () => ({ - send: () => {}, - close: () => {}, - onMessage: () => {}, - onError: () => {}, - onClose: () => {}, - }), - }, -}; +describe('SelfMobileSdk Entry Component', () => { + it('provides client to children and enables MRZ parsing', () => { + render( + + + , + ); -const crypto: CryptoAdapter = { - hash: async () => new Uint8Array(), - sign: async () => new Uint8Array(), -}; + expect(screen.getByText(expectedMRZResult.passportNumber)).toBeTruthy(); + }); -describe('SelfMobileSdk', () => { - it('provides client to children', () => { + it('renders children correctly', () => { + const testMessage = 'Test Child Component'; render( - - + +
{testMessage}
, ); - expect(screen.getByText('L898902C3')).toBeTruthy(); + + expect(screen.getByText(testMessage)).toBeTruthy(); }); }); diff --git a/packages/mobile-sdk-alpha/tests/provider.test.tsx b/packages/mobile-sdk-alpha/tests/provider.test.tsx index 22b212e57..6ef20ac3c 100644 --- a/packages/mobile-sdk-alpha/tests/provider.test.tsx +++ b/packages/mobile-sdk-alpha/tests/provider.test.tsx @@ -2,45 +2,29 @@ import type { ReactNode } from 'react'; import { describe, expect, it } from 'vitest'; -import type { CryptoAdapter, NetworkAdapter, ScannerAdapter } from '../src'; import { SelfClientProvider, useSelfClient } from '../src/index'; +import { expectedMRZResult, mockAdapters, sampleMRZ } from './utils/testHelpers'; import { renderHook } from '@testing-library/react'; -const scanner: ScannerAdapter = { - scan: async () => ({ mode: 'mrz', passportNumber: '', dateOfBirth: '', dateOfExpiry: '' }), -}; - -const network: NetworkAdapter = { - // Return a minimal stub to avoid relying on global Response in JSDOM/Node - http: { fetch: async () => ({ ok: true }) as any }, - ws: { - connect: () => ({ - send: () => {}, - close: () => {}, - onMessage: () => {}, - onError: () => {}, - onClose: () => {}, - }), - }, -}; - -const crypto: CryptoAdapter = { - hash: async () => new Uint8Array(), - sign: async () => new Uint8Array(), -}; - -describe('SelfClientProvider', () => { - it('provides client through context', () => { +describe('SelfClientProvider Context', () => { + it('provides client through context with MRZ parsing capability', () => { const wrapper = ({ children }: { children: ReactNode }) => ( - + {children} ); + const { result } = renderHook(() => useSelfClient(), { wrapper }); - const sample = `P { + expect(() => { + renderHook(() => useSelfClient()); + }).toThrow('useSelfClient must be used within a SelfClientProvider'); }); }); diff --git a/packages/mobile-sdk-alpha/tests/react-native.test.ts b/packages/mobile-sdk-alpha/tests/react-native.test.ts deleted file mode 100644 index ee3fafee5..000000000 --- a/packages/mobile-sdk-alpha/tests/react-native.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { createSelfClient } from '../src/index'; - -describe('main entry', () => { - it('exposes createSelfClient', () => { - expect(typeof createSelfClient).toBe('function'); - }); - - it('parses MRZ via client API', () => { - const sample = `P ({ mode: 'mrz' as const, passportNumber: '', dateOfBirth: '', dateOfExpiry: '' }), - }; - - const network = { - // Return a minimal stub to avoid relying on global Response in JSDOM/Node - http: { fetch: async () => ({ ok: true }) as any }, - ws: { - connect: () => ({ - send: () => {}, - close: () => {}, - onMessage: () => {}, - onError: () => {}, - onClose: () => {}, - }), - }, - }; - - const crypto = { - hash: async () => new Uint8Array(), - sign: async () => new Uint8Array(), - }; - - const client = createSelfClient({ config: {}, adapters: { scanner, network, crypto } }); - const info = client.extractMRZInfo(sample); - expect(info.passportNumber).toBe('L898902C3'); - expect(info.validation.overall).toBe(true); - }); -}); diff --git a/packages/mobile-sdk-alpha/tests/utils/testHelpers.ts b/packages/mobile-sdk-alpha/tests/utils/testHelpers.ts new file mode 100644 index 000000000..6e06bad26 --- /dev/null +++ b/packages/mobile-sdk-alpha/tests/utils/testHelpers.ts @@ -0,0 +1,40 @@ +/* eslint-disable sort-exports/sort-exports */ +import type { CryptoAdapter, NetworkAdapter, ScannerAdapter } from '../../src'; + +// Shared test data +export const sampleMRZ = `P ({ mode: 'mrz', passportNumber: '', dateOfBirth: '', dateOfExpiry: '' }), +}; + +export const mockNetwork: NetworkAdapter = { + http: { fetch: async () => ({ ok: true }) as any }, + ws: { + connect: () => ({ + send: () => {}, + close: () => {}, + onMessage: () => {}, + onError: () => {}, + onClose: () => {}, + }), + }, +}; + +export const mockCrypto: CryptoAdapter = { + hash: async () => new Uint8Array(), + sign: async () => new Uint8Array(), +}; + +export const mockAdapters = { + scanner: mockScanner, + network: mockNetwork, + crypto: mockCrypto, +}; + +// Shared test expectations +export const expectedMRZResult = { + passportNumber: 'L898902C3', + validation: { overall: true }, +}; From 3a5efc4efbfaccf28f93b9aaf2f1861db445280b Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 19 Aug 2025 15:04:58 -0700 Subject: [PATCH 08/18] abstract SelfMobileSdk --- .../src/components/SelfMobileSdk.tsx | 190 +++--------------- .../src/components/flows/OnboardingFlow.tsx | 52 +++++ .../components/screens/NFCScannerScreen.tsx | 28 +++ .../screens/PassportCameraScreen.tsx | 15 ++ .../src/components/screens/QRCodeScreen.tsx | 9 + .../src/hooks/useDocumentManager.ts | 34 ++++ packages/mobile-sdk-alpha/src/index.ts | 23 ++- packages/mobile-sdk-alpha/src/types/ui.ts | 39 ++++ 8 files changed, 232 insertions(+), 158 deletions(-) create mode 100644 packages/mobile-sdk-alpha/src/components/flows/OnboardingFlow.tsx create mode 100644 packages/mobile-sdk-alpha/src/components/screens/NFCScannerScreen.tsx create mode 100644 packages/mobile-sdk-alpha/src/components/screens/PassportCameraScreen.tsx create mode 100644 packages/mobile-sdk-alpha/src/components/screens/QRCodeScreen.tsx create mode 100644 packages/mobile-sdk-alpha/src/hooks/useDocumentManager.ts create mode 100644 packages/mobile-sdk-alpha/src/types/ui.ts diff --git a/packages/mobile-sdk-alpha/src/components/SelfMobileSdk.tsx b/packages/mobile-sdk-alpha/src/components/SelfMobileSdk.tsx index 669513b65..fdbfc47b1 100644 --- a/packages/mobile-sdk-alpha/src/components/SelfMobileSdk.tsx +++ b/packages/mobile-sdk-alpha/src/components/SelfMobileSdk.tsx @@ -1,188 +1,64 @@ import type { ReactNode } from 'react'; -import { useCallback, useLayoutEffect, useState } from 'react'; -import type { DocumentCategory, PassportData } from '@selfxyz/common'; - -import { SelfClientProvider, useSelfClient } from '../context'; +import { SelfClientProvider } from '../context'; +import { useDocumentManager } from '../hooks/useDocumentManager'; import type { Adapters, Config } from '../types/public'; - -// Local interface for document metadata -interface DocumentMetadata { - id: string; - documentType: string; - documentCategory: DocumentCategory; - data: string; - mock: boolean; - isRegistered?: boolean; -} - -interface DocumentData { - data: PassportData; - metadata: DocumentMetadata; -} - -interface External { - getSecret: () => Promise; - getAllDocuments: () => Promise<{ - [documentId: string]: DocumentData; - }>; - setDocument: (doc: DocumentData, documentId: string) => Promise; - onOnboardingSuccess: () => void; - onOnboardingFailure: (error: Error) => void; - onDisclosureSuccess: () => void; - onDisclosureFailure: (error: Error) => void; -} +import type { ExternalAdapter } from '../types/ui'; +import { OnboardingFlow } from './flows/OnboardingFlow'; +import { QRCodeScreen } from './screens/QRCodeScreen'; interface SelfMobileSdkProps { config: Config; adapters?: Partial; - external: External; + external: ExternalAdapter; children?: ReactNode; + // Optional custom components + customScreens?: { + PassportCamera?: ReactNode; + NFCScanner?: ReactNode; + QRScanner?: ReactNode; + }; } -// Simple placeholder components - these would be replaced with actual UI components -const PassportCameraScreen = ({ onMRZDetected }: { onMRZDetected: (mrzData: any) => void }) => ( -
-

Passport Camera

- -
-); - -const QrCodeScreen = ({ onSuccess, onFailure }: { onSuccess: () => void; onFailure: (error: Error) => void }) => ( -
-

QR Code Scanner

- - -
-); - -const NFCScannerScreen = ({ onSuccess, onFailure }: { onSuccess: () => void; onFailure: (error: Error) => void }) => { - const client = useSelfClient(); - - const onNFCScan = useCallback( - async (_nfcData: any) => { - try { - // scan the document - // register the document - onSuccess(); - } catch (error) { - onFailure(error as Error); - } - }, - [client, onSuccess, onFailure], - ); - - return ( -
-

NFC Scanner

- -
- ); -}; - -const OnboardingScreen = ({ - onSuccess, - onFailure, - _setDocument, +const SelfMobileSdkContent = ({ + external, + customScreens = {}, }: { - onSuccess: () => void; - onFailure: (error: Error) => void; - _setDocument: (doc: DocumentData, documentId: string) => Promise; + external: ExternalAdapter; + customScreens?: SelfMobileSdkProps['customScreens']; }) => { - const [mrzData, setMrzData] = useState(null); - const client = useSelfClient(); - - const onMRZDetected = useCallback( - async (mrzData: any) => { - try { - const status = await client.registerDocument({ - scan: { - mode: 'mrz', - passportNumber: mrzData.documentNumber, - dateOfBirth: mrzData.birthDate, - dateOfExpiry: mrzData.expiryDate, - issuingCountry: mrzData.countryCode, - }, - }); - - if (status.registered) { - setMrzData(mrzData); - onSuccess(); - } else { - onFailure(new Error('Registration failed')); - } - } catch (error) { - onFailure(error as Error); - } - }, - [client, onSuccess, onFailure], - ); - - return ( -
-

Onboarding

- {!mrzData && } - {mrzData && } -
- ); -}; - -const SelfMobileSdkContent = ({ external }: { external: External }) => { - const { - getAllDocuments, - onOnboardingSuccess, - onOnboardingFailure, - onDisclosureSuccess, - onDisclosureFailure, - setDocument, - } = external; - - const [documents, setDocuments] = useState<{ - [documentId: string]: DocumentData; - }>({}); - - const [isLoading, setIsLoading] = useState(true); - - useLayoutEffect(() => { - getAllDocuments() - .then(documents => { - setDocuments(documents); - setIsLoading(false); - }) - .catch(error => { - console.error('Failed to load documents:', error); - setIsLoading(false); - }); - }, [getAllDocuments]); + const { documents, isLoading, hasRegisteredDocuments } = useDocumentManager(external); if (isLoading) { return
Loading documents...
; } // Check if user has any registered documents - const hasRegisteredDocuments = Object.values(documents).some(doc => doc.metadata.isRegistered); + const hasDocuments = Object.keys(documents).length > 0 && hasRegisteredDocuments(); - if (Object.keys(documents).length === 0 || !hasRegisteredDocuments) { - // Start onboarding flow + if (!hasDocuments) { return ( - + ); } // Show disclosure flow - return ; + return ( + customScreens.QRScanner || ( + + ) + ); }; -export const SelfMobileSdk = ({ config, adapters = {}, external, children }: SelfMobileSdkProps) => { +export const SelfMobileSdk = ({ config, adapters = {}, external, children, customScreens }: SelfMobileSdkProps) => { return ( - {children || } + {children || } ); }; diff --git a/packages/mobile-sdk-alpha/src/components/flows/OnboardingFlow.tsx b/packages/mobile-sdk-alpha/src/components/flows/OnboardingFlow.tsx new file mode 100644 index 000000000..0b0858877 --- /dev/null +++ b/packages/mobile-sdk-alpha/src/components/flows/OnboardingFlow.tsx @@ -0,0 +1,52 @@ +import type { ReactNode } from 'react'; +import { useCallback, useState } from 'react'; + +import { useSelfClient } from '../../context'; +import type { DocumentData, ExternalAdapter } from '../../types/ui'; +import { NFCScannerScreen } from '../screens/NFCScannerScreen'; +import { PassportCameraScreen } from '../screens/PassportCameraScreen'; + +interface OnboardingFlowProps { + external: ExternalAdapter; + setDocument: (doc: DocumentData, documentId: string) => Promise; + PassportCamera?: ReactNode; + NFCScanner?: ReactNode; +} + +export const OnboardingFlow = ({ external, setDocument, PassportCamera, NFCScanner }: OnboardingFlowProps) => { + const [mrzData, setMrzData] = useState(null); + const client = useSelfClient(); + + const handleMRZDetected = useCallback( + async (mrzData: any) => { + try { + const status = await client.registerDocument({ + scan: { + mode: 'mrz', + passportNumber: mrzData.documentNumber, + dateOfBirth: mrzData.birthDate, + dateOfExpiry: mrzData.expiryDate, + issuingCountry: mrzData.countryCode, + }, + }); + + if (status.registered) { + setMrzData(mrzData); + } else { + external.onOnboardingFailure(new Error('Registration failed')); + } + } catch (error) { + external.onOnboardingFailure(error as Error); + } + }, + [client, external, setDocument], + ); + + if (!mrzData) { + return PassportCamera || ; + } + + return ( + NFCScanner || + ); +}; diff --git a/packages/mobile-sdk-alpha/src/components/screens/NFCScannerScreen.tsx b/packages/mobile-sdk-alpha/src/components/screens/NFCScannerScreen.tsx new file mode 100644 index 000000000..8f15a69f6 --- /dev/null +++ b/packages/mobile-sdk-alpha/src/components/screens/NFCScannerScreen.tsx @@ -0,0 +1,28 @@ +import { useCallback } from 'react'; + +import { useSelfClient } from '../../context'; +import type { ScreenProps } from '../../types/ui'; + +export const NFCScannerScreen = ({ onSuccess, onFailure }: ScreenProps) => { + const client = useSelfClient(); + + const onNFCScan = useCallback( + async (_nfcData: any) => { + try { + // scan the document + // register the document + onSuccess(); + } catch (error) { + onFailure(error as Error); + } + }, + [client, onSuccess, onFailure], + ); + + return ( +
+

NFC Scanner

+ +
+ ); +}; diff --git a/packages/mobile-sdk-alpha/src/components/screens/PassportCameraScreen.tsx b/packages/mobile-sdk-alpha/src/components/screens/PassportCameraScreen.tsx new file mode 100644 index 000000000..a9e2756b1 --- /dev/null +++ b/packages/mobile-sdk-alpha/src/components/screens/PassportCameraScreen.tsx @@ -0,0 +1,15 @@ +import type { PassportCameraProps } from '../../types/ui'; + +// Simple placeholder component - this would be replaced with actual camera UI +export const PassportCameraScreen = ({ onMRZDetected }: PassportCameraProps) => ( +
+

Passport Camera

+ +
+); diff --git a/packages/mobile-sdk-alpha/src/components/screens/QRCodeScreen.tsx b/packages/mobile-sdk-alpha/src/components/screens/QRCodeScreen.tsx new file mode 100644 index 000000000..1a23d605c --- /dev/null +++ b/packages/mobile-sdk-alpha/src/components/screens/QRCodeScreen.tsx @@ -0,0 +1,9 @@ +import type { ScreenProps } from '../../types/ui'; + +export const QRCodeScreen = ({ onSuccess, onFailure }: ScreenProps) => ( +
+

QR Code Scanner

+ + +
+); diff --git a/packages/mobile-sdk-alpha/src/hooks/useDocumentManager.ts b/packages/mobile-sdk-alpha/src/hooks/useDocumentManager.ts new file mode 100644 index 000000000..3e0942fe6 --- /dev/null +++ b/packages/mobile-sdk-alpha/src/hooks/useDocumentManager.ts @@ -0,0 +1,34 @@ +import { useCallback, useLayoutEffect, useState } from 'react'; + +import type { DocumentData, ExternalAdapter } from '../types/ui'; + +export const useDocumentManager = (external: ExternalAdapter) => { + const [documents, setDocuments] = useState<{ + [documentId: string]: DocumentData; + }>({}); + const [isLoading, setIsLoading] = useState(true); + + useLayoutEffect(() => { + external + .getAllDocuments() + .then(documents => { + setDocuments(documents); + setIsLoading(false); + }) + .catch(error => { + console.error('Failed to load documents:', error); + setIsLoading(false); + }); + }, [external]); + + const hasRegisteredDocuments = useCallback(() => { + return Object.values(documents).some(doc => doc.metadata.isRegistered); + }, [documents]); + + return { + documents, + isLoading, + hasRegisteredDocuments, + setDocuments, + }; +}; diff --git a/packages/mobile-sdk-alpha/src/index.ts b/packages/mobile-sdk-alpha/src/index.ts index d3fc65407..bba66f064 100644 --- a/packages/mobile-sdk-alpha/src/index.ts +++ b/packages/mobile-sdk-alpha/src/index.ts @@ -33,15 +33,18 @@ export type { // MRZ module export type { DG1, DG2, NFCScanOptions, ParsedNFCResponse } from './nfc'; +export type { DocumentData, DocumentMetadata, ExternalAdapter, PassportCameraProps, ScreenProps } from './types/ui'; + export type { MRZScanOptions } from './mrz'; // QR module export type { PassportValidationCallbacks } from './validation/document'; export type { QRProofOptions } from './qr'; - // Error handling export type { SdkErrorCategory } from './errors'; + +// UI Types export { InitError, LivenessError, @@ -53,9 +56,24 @@ export { sdkError, } from './errors'; +export { NFCScannerScreen } from './components/screens/NFCScannerScreen'; + +// Flow Components +export { OnboardingFlow } from './components/flows/OnboardingFlow'; + +// Screen Components +export { PassportCameraScreen } from './components/screens/PassportCameraScreen'; + +export { QRCodeScreen } from './components/screens/QRCodeScreen'; + +// Context and Client export { SelfClientContext, SelfClientProvider, useSelfClient } from './context'; + +// Components export { SelfMobileSdk } from './entry'; + export { SelfMobileSdk as SelfMobileSdkHighLevel } from './components/SelfMobileSdk'; + export { createSelfClient } from './client'; export { defaultConfig } from './config/defaults'; @@ -71,5 +89,8 @@ export { mergeConfig } from './config/merge'; export { parseNFCResponse, scanNFC } from './nfc'; export { scanQRProof } from './qr'; + +// Hooks +export { useDocumentManager } from './hooks/useDocumentManager'; // Error handling export { webScannerShim } from './adapters/web/shims'; diff --git a/packages/mobile-sdk-alpha/src/types/ui.ts b/packages/mobile-sdk-alpha/src/types/ui.ts new file mode 100644 index 000000000..1f2032535 --- /dev/null +++ b/packages/mobile-sdk-alpha/src/types/ui.ts @@ -0,0 +1,39 @@ +import type { DocumentCategory, PassportData } from '@selfxyz/common'; + +// Document-related types +export interface DocumentMetadata { + id: string; + documentType: string; + documentCategory: DocumentCategory; + data: string; + mock: boolean; + isRegistered?: boolean; +} + +export interface DocumentData { + data: PassportData; + metadata: DocumentMetadata; +} + +// External adapter interface +export interface ExternalAdapter { + getSecret: () => Promise; + getAllDocuments: () => Promise<{ + [documentId: string]: DocumentData; + }>; + setDocument: (doc: DocumentData, documentId: string) => Promise; + onOnboardingSuccess: () => void; + onOnboardingFailure: (error: Error) => void; + onDisclosureSuccess: () => void; + onDisclosureFailure: (error: Error) => void; +} + +// Screen component props +export interface ScreenProps { + onSuccess: () => void; + onFailure: (error: Error) => void; +} + +export interface PassportCameraProps { + onMRZDetected: (mrzData: any) => void; +} From f213ad803fb8f68d6d5a16c0eea6f0c91a4a0993 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 19 Aug 2025 15:15:38 -0700 Subject: [PATCH 09/18] pr feedback --- packages/mobile-sdk-alpha/src/browser.ts | 5 +++++ packages/mobile-sdk-alpha/src/context.tsx | 15 ++------------- .../src/hooks/useDocumentManager.ts | 4 ++-- packages/mobile-sdk-alpha/src/index.ts | 5 +++-- .../mobile-sdk-alpha/tests/SelfMobileSdk.test.tsx | 4 ++-- packages/mobile-sdk-alpha/tests/client.test.ts | 2 +- packages/mobile-sdk-alpha/tests/client.test.tsx | 14 +++++++++++++- .../mobile-sdk-alpha/tests/utils/testHelpers.ts | 15 ++++++++++++++- 8 files changed, 42 insertions(+), 22 deletions(-) diff --git a/packages/mobile-sdk-alpha/src/browser.ts b/packages/mobile-sdk-alpha/src/browser.ts index adf7ae09b..fc0c1bbcf 100644 --- a/packages/mobile-sdk-alpha/src/browser.ts +++ b/packages/mobile-sdk-alpha/src/browser.ts @@ -40,8 +40,13 @@ export type { SdkErrorCategory } from './errors'; export { SCANNER_ERROR_CODES, notImplemented, sdkError } from './errors'; export { SelfClientContext, SelfClientProvider, useSelfClient } from './context'; +// Browser-only high-level component (DOM-based) +export { SelfMobileSdk as SelfMobileSdkHighLevel } from './components/SelfMobileSdk'; + export { createSelfClient } from './client'; + export { defaultConfig } from './config/defaults'; + export { extractMRZInfo, formatDateToYYMMDD, scanMRZ } from './mrz'; // Core functions diff --git a/packages/mobile-sdk-alpha/src/context.tsx b/packages/mobile-sdk-alpha/src/context.tsx index 574f2cb72..f001a34cf 100644 --- a/packages/mobile-sdk-alpha/src/context.tsx +++ b/packages/mobile-sdk-alpha/src/context.tsx @@ -1,4 +1,4 @@ -import { createContext, type PropsWithChildren, useContext, useEffect, useState } from 'react'; +import { createContext, type PropsWithChildren, useContext, useMemo } from 'react'; import { createSelfClient } from './client'; import type { Adapters, Config, SelfClient } from './types/public'; @@ -13,18 +13,7 @@ export interface SelfClientProviderProps { export { SelfClientContext }; export function SelfClientProvider({ config, adapters = {}, children }: PropsWithChildren) { - const [client, setClient] = useState(null); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - const clientInstance = createSelfClient({ config, adapters }); - setClient(clientInstance); - setIsLoading(false); - }, [config, adapters]); - - if (isLoading) { - return
Loading...
; // Simple loading state, can be customized - } + const client = useMemo(() => createSelfClient({ config, adapters }), [config, adapters]); return {children}; } diff --git a/packages/mobile-sdk-alpha/src/hooks/useDocumentManager.ts b/packages/mobile-sdk-alpha/src/hooks/useDocumentManager.ts index 3e0942fe6..d58e74be6 100644 --- a/packages/mobile-sdk-alpha/src/hooks/useDocumentManager.ts +++ b/packages/mobile-sdk-alpha/src/hooks/useDocumentManager.ts @@ -1,4 +1,4 @@ -import { useCallback, useLayoutEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import type { DocumentData, ExternalAdapter } from '../types/ui'; @@ -8,7 +8,7 @@ export const useDocumentManager = (external: ExternalAdapter) => { }>({}); const [isLoading, setIsLoading] = useState(true); - useLayoutEffect(() => { + useEffect(() => { external .getAllDocuments() .then(documents => { diff --git a/packages/mobile-sdk-alpha/src/index.ts b/packages/mobile-sdk-alpha/src/index.ts index bba66f064..a662109dd 100644 --- a/packages/mobile-sdk-alpha/src/index.ts +++ b/packages/mobile-sdk-alpha/src/index.ts @@ -72,12 +72,13 @@ export { SelfClientContext, SelfClientProvider, useSelfClient } from './context' // Components export { SelfMobileSdk } from './entry'; -export { SelfMobileSdk as SelfMobileSdkHighLevel } from './components/SelfMobileSdk'; - export { createSelfClient } from './client'; export { defaultConfig } from './config/defaults'; +/** @deprecated Use createSelfClient().extractMRZInfo or import from './mrz' */ +export { extractMRZInfo } from './mrz'; + export { formatDateToYYMMDD, scanMRZ } from './mrz'; // Core functions diff --git a/packages/mobile-sdk-alpha/tests/SelfMobileSdk.test.tsx b/packages/mobile-sdk-alpha/tests/SelfMobileSdk.test.tsx index eb7aa8eef..57feaacbb 100644 --- a/packages/mobile-sdk-alpha/tests/SelfMobileSdk.test.tsx +++ b/packages/mobile-sdk-alpha/tests/SelfMobileSdk.test.tsx @@ -2,13 +2,13 @@ import { describe, expect, it } from 'vitest'; describe('High-Level SelfMobileSdk Component', () => { it('can be imported successfully', async () => { - const { SelfMobileSdk } = await import('../src/components/SelfMobileSdk'); + const { SelfMobileSdk } = await import('../src'); expect(SelfMobileSdk).toBeDefined(); expect(typeof SelfMobileSdk).toBe('function'); }); it('accepts the expected props interface', async () => { - const { SelfMobileSdk } = await import('../src/components/SelfMobileSdk'); + const { SelfMobileSdk } = await import('../src'); // Test that the component accepts the expected props structure const mockExternal = { diff --git a/packages/mobile-sdk-alpha/tests/client.test.ts b/packages/mobile-sdk-alpha/tests/client.test.ts index a4c6f105c..fd538c71c 100644 --- a/packages/mobile-sdk-alpha/tests/client.test.ts +++ b/packages/mobile-sdk-alpha/tests/client.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import type { CryptoAdapter, NetworkAdapter, ScannerAdapter } from '../src/adapters/index'; +import type { CryptoAdapter, NetworkAdapter, ScannerAdapter } from '../src'; import { createSelfClient } from '../src/index'; describe('createSelfClient', () => { diff --git a/packages/mobile-sdk-alpha/tests/client.test.tsx b/packages/mobile-sdk-alpha/tests/client.test.tsx index 30eb61f5a..f0ec62a1b 100644 --- a/packages/mobile-sdk-alpha/tests/client.test.tsx +++ b/packages/mobile-sdk-alpha/tests/client.test.tsx @@ -1,7 +1,8 @@ import { describe, expect, it } from 'vitest'; import { createSelfClient } from '../src/index'; -import { expectedMRZResult, mockAdapters, sampleMRZ } from './utils/testHelpers'; +import { MrzParseError } from '../src/processing/mrz'; +import { badCheckDigitsMRZ, expectedMRZResult, invalidMRZ, mockAdapters, sampleMRZ } from './utils/testHelpers'; describe('createSelfClient API', () => { it('creates a client instance with expected methods', () => { @@ -29,4 +30,15 @@ describe('createSelfClient API', () => { expect(clientWithAllAdapters).toBeDefined(); expect(typeof clientWithAllAdapters.extractMRZInfo).toBe('function'); }); + + it('throws MrzParseError for malformed MRZ input', () => { + const client = createSelfClient({ config: {}, adapters: mockAdapters }); + expect(() => client.extractMRZInfo(invalidMRZ)).toThrowError(MrzParseError); + }); + + it('flags invalid check digits', () => { + const client = createSelfClient({ config: {}, adapters: mockAdapters }); + const info = client.extractMRZInfo(badCheckDigitsMRZ); + expect(info.validation.overall).toBe(false); + }); }); diff --git a/packages/mobile-sdk-alpha/tests/utils/testHelpers.ts b/packages/mobile-sdk-alpha/tests/utils/testHelpers.ts index 6e06bad26..7aab88013 100644 --- a/packages/mobile-sdk-alpha/tests/utils/testHelpers.ts +++ b/packages/mobile-sdk-alpha/tests/utils/testHelpers.ts @@ -3,6 +3,9 @@ import type { CryptoAdapter, NetworkAdapter, ScannerAdapter } from '../../src'; // Shared test data export const sampleMRZ = `P ({ ok: true }) as any }, + // Return a minimal stub to avoid relying on global Response in JSDOM/Node + http: { + fetch: async () => + ({ + ok: true, + status: 200, + text: async () => '', + json: async () => ({}), + arrayBuffer: async () => new ArrayBuffer(0), + }) as any, + }, ws: { connect: () => ({ send: () => {}, From e47bb56965ba4c3a250f33806c9a70506256c8a2 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 19 Aug 2025 17:28:34 -0700 Subject: [PATCH 10/18] updates --- docs/gigamind/pr-analysis-workflow.md | 68 +++++++++++++++++----- docs/templates/pr-action-items-template.md | 37 ++++++++++-- 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/docs/gigamind/pr-analysis-workflow.md b/docs/gigamind/pr-analysis-workflow.md index d86bfebf8..e3e88ec0e 100644 --- a/docs/gigamind/pr-analysis-workflow.md +++ b/docs/gigamind/pr-analysis-workflow.md @@ -15,11 +15,17 @@ Replace `{{NUMBER}}` with the actual PR number you want to analyze. ## Core Workflow Neuron -### PR Analysis Workflow - v6 #w1x2y3 -When analyzing GitHub PRs for action items OR when asked to 'giga fetch PR {{NUMBER}} and create an issue file: 1) Use giga_read_pr to fetch PR data and CodeRabbit comments, 2) Focus on unresolved comments that DON'T have '✅ Addressed' status, 3) Group related issues by root cause (not just severity) - e.g., multiple DOM element issues → single React Native compatibility problem, 4) Prioritize by 'blocking merge' vs 'architectural' vs 'polish', 5) ALWAYS create PR-{{NUMBER}}-ACTION_ITEMS.md in repo root, 6) Use template from docs/templates/pr-action-items-template.md, 7) Focus on specific file paths, line numbers, and clear fixes, 8) Include code examples for complex fixes, 9) Add testing requirements and breaking changes, 10) NEVER use mcp_giga_plan - create real markdown files. An item is 'actionable' if it has: specific file path and line number, clear problem description, specific fix or action required, and priority level (blocking vs non-blocking). Always group related comments by root cause rather than treating each comment separately. +### PR Analysis Workflow - v7 #w1x2y3 +When analyzing GitHub PRs for action items OR when asked to 'giga fetch PR {{NUMBER}} and create an issue file: 1) Use giga_read_pr to fetch PR data and CodeRabbit comments, 2) Focus on unresolved comments that DON'T have '✅ Addressed' status, 3) Group related issues by root cause (not just severity) - e.g., multiple DOM element issues → single React Native compatibility problem, 4) Prioritize by 'blocking merge' vs 'architectural' vs 'polish', 5) ALWAYS create PR-{{NUMBER}}-ACTION-ITEMS.md in repo root, 6) Use template from docs/templates/pr-action-items-template.md, 7) Focus on specific file paths, line numbers, and clear fixes, 8) Include code examples for complex fixes, 9) Add testing requirements and breaking changes, 10) NEVER use mcp_giga_plan - create real markdown files. An item is 'actionable' if it has: specific file path and line number, clear problem description, specific fix or action required, and priority level (blocking vs non-blocking). Always group related comments by root cause rather than treating each comment separately. ## Supporting Neurons +### PR File Naming Convention - v1 #n1o2p3 +When creating PR analysis files, ALWAYS use the naming pattern PR-{NUMBER}-ACTION-ITEMS.md in the project root directory. This follows the gitignore pattern that prevents accidental commits of PR action files. NEVER use underscores (PR_925_Issue.md) or different naming patterns. The gitignore specifically excludes PR-*-ACTION*.md files, so this naming convention ensures the files are tracked properly and won't be accidentally ignored. + +### CodeRabbit Comment Analysis - v1 #q4r5s6 +When analyzing CodeRabbit comments: 1) Look for review comments with type "review", 2) Focus on comments without "✅ Addressed" status, 3) Extract file paths and line numbers from comment metadata (file_path and line_number fields), 4) Group related suggestions by root cause (e.g., all DOM element issues → React Native compatibility), 5) Prioritize critical issues (React Native compatibility, security, memory leaks) over code quality suggestions, 6) Include CodeRabbit comment IDs in the analysis for traceability, 7) Categorize comments by severity: Critical (blocking merge), Architecture (design issues), Code Quality (polish). + ### PR Testing Status Tracking - v1 #s1t2u3 When creating PR action items, always include a comprehensive testing status section. Track which test suites are passing vs failing, and include specific error messages when available. Common patterns: "yarn workspace @selfxyz/package test" failures, build errors, lint issues. Include both the current status and required actions to fix failing tests. This helps prioritize which issues are blocking vs non-blocking. @@ -44,8 +50,8 @@ Template files use kebab-case naming: 'pr-action-items-template.md' (with hyphen ### Fastlane Workspace Path Robust Resolution - v1 #j1k2l3 The fastlane workspace path resolution should be robust to handle cases where workspace filename differs from scheme name. The implementation should: 1) First try scheme-named workspace (e.g., OpenPassport.xcworkspace for OpenPassport scheme), 2) Fall back to project-named workspace (e.g., Self.xcworkspace for Self project) if scheme-named doesn't exist, 3) Raise a clear error with checked paths if neither exists to fail CI fast. This prevents build failures when workspace and scheme names don't match, and provides clear debugging information when no workspace is found. The logic should be: scheme_workspace_path = File.expand_path("../ios/#{PROJECT_SCHEME}.xcworkspace", Dir.pwd); project_workspace_path = File.expand_path("../ios/#{PROJECT_NAME}.xcworkspace", Dir.pwd); then check File.exist? on each path in order of preference. -### PR Action Items Template - v2 #zfda91 -The PR action items template should be value-first and focused on actionable content. Structure: 1) Critical Issues (Blocking Merge) - specific file:line with clear actions, 2) Required Actions - grouped by root cause with specific fixes, 3) Testing Checklist - specific tests to run, 4) Breaking Changes - specific changes and migration needs. Remove generic sections that don't apply to most PRs. Focus on problem → solution → validation pattern. Template location: docs/templates/pr-action-items-template.md, generated files: PR-{{NUMBER}}-ACTION_ITEMS.md in project root. Prioritize actionable items over pretty formatting. Always include specific file paths, line numbers, and code examples for complex fixes. +### PR Action Items Template - v3 #zfda91 +The PR action items template should be value-first and focused on actionable content. Structure: 1) Critical Issues (Blocking Merge) - specific file:line with clear actions and CodeRabbit comment IDs, 2) Required Actions - grouped by root cause with specific fixes and code examples, 3) CodeRabbit Analysis Summary - resolved vs unresolved comments with categorization, 4) Testing Checklist - specific tests to run, 5) Breaking Changes - specific changes and migration needs. Remove generic sections that don't apply to most PRs. Focus on problem → solution → validation pattern. Template location: docs/templates/pr-action-items-template.md, generated files: PR-{{NUMBER}}-ACTION_ITEMS.md in project root. Prioritize actionable items over pretty formatting. Always include specific file paths, line numbers, CodeRabbit comment IDs, and code examples for complex fixes. ### Neuron Naming Convention - v1 When creating new neurons, ALWAYS add a unique short hash (6-8 characters) to the end of the title to make neurons easier to find and distinguish. This hash should be unique for EACH neuron and automatically updated when creating new neurons. Use format: "Title - v1 #abc123" or "Title - v2 #def456". This helps with neuron discovery, prevents naming conflicts, and makes it easier to track neuron versions and updates. @@ -53,6 +59,9 @@ When creating new neurons, ALWAYS add a unique short hash (6-8 characters) to th ### Prefer markdown file lists over plans Never create plans using the plan tool. Instead, prefer creating markdown file lists for task tracking, documentation, or organized information when needed. +### Gitignore Pattern Compliance - v1 #h2i3j4 +When creating files in the project, ALWAYS check the .gitignore file first to understand what patterns are excluded. Common patterns to avoid: files with underscores in names, files in certain directories, or files matching specific patterns. For PR analysis files, the gitignore specifically excludes "PR-*-ACTION*.md" files, so use the exact pattern PR-{NUMBER}-ACTION-ITEMS.md to ensure files are tracked properly. This prevents accidentally creating files that will be ignored by git. + ## Template File The workflow uses this template file located at `docs/templates/pr-action-items-template.md`: @@ -63,12 +72,12 @@ INSTRUCTIONS FOR AGENTS: - Use giga_read_pr to fetch PR data and CodeRabbit comments - Focus on unresolved comments without '✅ Addressed' status - Group related issues by root cause (not just severity) -- Include specific file paths and line numbers -- Provide clear, actionable fixes -- Add code examples for complex issues -- Do not paste or include secrets (API keys, mnemonics, private keys, tokens, credentials) in code blocks or logs; redact sensitive values. +- Include specific file paths and line numbers from CodeRabbit metadata +- Provide clear, actionable fixes with code examples - Prioritize by "blocking merge" vs "architectural" vs "polish" -- Create file as PR-{{NUMBER}}-ACTION_ITEMS.md in project root +- Create file as PR-{{NUMBER}}-ACTION-ITEMS.md in project root +- Follow gitignore pattern: PR-*-ACTION*.md +- Include CodeRabbit comment IDs for traceability --> # PR {{PR_NUMBER}} Action Items @@ -86,19 +95,28 @@ INSTRUCTIONS FOR AGENTS: ### 1. {{ISSUE_TITLE}} **Files:** `{{FILE_PATH}}:{{LINE_NUMBER}}` +**CodeRabbit Comment:** {{COMMENT_ID}} **Problem:** {{PROBLEM_DESCRIPTION}} **Fix:** {{SPECIFIC_FIX_OR_ACTION}} +**Code Example:** +```{{LANGUAGE}} +{{CODE_EXAMPLE}} +``` + ### 2. {{ISSUE_TITLE}} **Files:** `{{FILE_PATH}}:{{LINE_NUMBER}}` +**CodeRabbit Comment:** {{COMMENT_ID}} **Problem:** {{PROBLEM_DESCRIPTION}} **Fix:** {{SPECIFIC_FIX_OR_ACTION}} ## Required Actions ### Issue 1: {{GROUPED_ISSUE_TITLE}} -**Files:** `{{FILE_PATH}}:{{LINE_NUMBER}}`, `{{FILE_PATH}}:{{LINE_NUMBER}}` **Root Cause:** {{ROOT_CAUSE_DESCRIPTION}} +**Files Affected:** +- `{{FILE_PATH}}:{{LINE_NUMBER}}` - {{ISSUE_DESCRIPTION}} +- `{{FILE_PATH}}:{{LINE_NUMBER}}` - {{ISSUE_DESCRIPTION}} **Actions:** - [ ] {{SPECIFIC_ACTION_1}} @@ -111,13 +129,29 @@ INSTRUCTIONS FOR AGENTS: ``` ### Issue 2: {{GROUPED_ISSUE_TITLE}} -**Files:** `{{FILE_PATH}}:{{LINE_NUMBER}}` **Root Cause:** {{ROOT_CAUSE_DESCRIPTION}} +**Files Affected:** +- `{{FILE_PATH}}:{{LINE_NUMBER}}` - {{ISSUE_DESCRIPTION}} **Actions:** - [ ] {{SPECIFIC_ACTION_1}} - [ ] {{SPECIFIC_ACTION_2}} +## CodeRabbit Analysis Summary + +### Resolved Comments ✅ +- {{RESOLVED_COMMENT_1}} +- {{RESOLVED_COMMENT_2}} + +### Unresolved Comments 🔴 +- {{UNRESOLVED_COMMENT_1}} - {{STATUS}} +- {{UNRESOLVED_COMMENT_2}} - {{STATUS}} + +### Comment Categories +- **Critical:** {{COUNT}} comments (React Native compatibility, security, memory leaks) +- **Architecture:** {{COUNT}} comments (API design, type safety) +- **Code Quality:** {{COUNT}} comments (testing, imports, documentation) + ## Testing Checklist ### Before Merge @@ -163,6 +197,8 @@ INSTRUCTIONS FOR AGENTS: --- **Last Updated:** {{DATE}} +**CodeRabbit Comments Analyzed:** {{TOTAL_COMMENTS}} +**Unresolved Issues:** {{UNRESOLVED_COUNT}} ``` ## Workflow Steps Summary @@ -171,7 +207,7 @@ INSTRUCTIONS FOR AGENTS: 2. **Filter Comments**: Focus on unresolved comments without '✅ Addressed' status 3. **Group Issues**: Group related issues by root cause, not just severity 4. **Prioritize**: Categorize as 'blocking merge' vs 'architectural' vs 'polish' -5. **Create File**: Generate PR-{{NUMBER}}-ACTION_ITEMS.md in repo root +5. **Create File**: Generate PR-{{NUMBER}}-ACTION-ITEMS.md in repo root 6. **Use Template**: Apply the template from docs/templates/pr-action-items-template.md 7. **Include Details**: Add specific file paths, line numbers, and clear fixes 8. **Add Examples**: Include code examples for complex fixes @@ -183,11 +219,13 @@ INSTRUCTIONS FOR AGENTS: - **Actionable Items**: Must have specific file path, line number, clear problem description, specific fix, and priority level - **Root Cause Grouping**: Group related comments by root cause rather than treating each separately - **Value-First**: Focus on actionable content over pretty formatting -- **Specific Details**: Always include file paths, line numbers, and code examples for complex fixes +- **Specific Details**: Always include file paths, line numbers, CodeRabbit comment IDs, and code examples for complex fixes - **Testing Focus**: Include comprehensive testing status and requirements +- **File Naming**: Always use PR-{NUMBER}-ACTION-ITEMS.md pattern to comply with gitignore +- **CodeRabbit Integration**: Include comment IDs and categorize by severity for better traceability --- **Export Date:** {{CURRENT_DATE}} -**Neuron Count:** 12 neurons exported -**Template Version:** Current as of export +**Neuron Count:** 15 neurons exported +**Template Version:** v3 as of export diff --git a/docs/templates/pr-action-items-template.md b/docs/templates/pr-action-items-template.md index 3d0577acf..8a60c1bcb 100644 --- a/docs/templates/pr-action-items-template.md +++ b/docs/templates/pr-action-items-template.md @@ -3,11 +3,11 @@ INSTRUCTIONS FOR AGENTS: - Use giga_read_pr to fetch PR data and CodeRabbit comments - Focus on unresolved comments without '✅ Addressed' status - Group related issues by root cause (not just severity) -- Include specific file paths and line numbers -- Provide clear, actionable fixes -- Add code examples for complex issues +- Include specific file paths and line numbers from CodeRabbit metadata +- Provide clear, actionable fixes with code examples - Prioritize by "blocking merge" vs "architectural" vs "polish" - Create file as PR-{{NUMBER}}-ACTION-ITEMS.md in project root +- Follow gitignore pattern: PR-*-ACTION*.md --> # PR {{PR_NUMBER}} Action Items @@ -25,19 +25,28 @@ INSTRUCTIONS FOR AGENTS: ### 1. {{ISSUE_TITLE}} **Files:** `{{FILE_PATH}}:{{LINE_NUMBER}}` +**CodeRabbit Comment:** {{COMMENT_ID}} **Problem:** {{PROBLEM_DESCRIPTION}} **Fix:** {{SPECIFIC_FIX_OR_ACTION}} +**Code Example:** +```{{LANGUAGE}} +{{CODE_EXAMPLE}} +``` + ### 2. {{ISSUE_TITLE}} **Files:** `{{FILE_PATH}}:{{LINE_NUMBER}}` +**CodeRabbit Comment:** {{COMMENT_ID}} **Problem:** {{PROBLEM_DESCRIPTION}} **Fix:** {{SPECIFIC_FIX_OR_ACTION}} ## Required Actions ### Issue 1: {{GROUPED_ISSUE_TITLE}} -**Files:** `{{FILE_PATH}}:{{LINE_NUMBER}}`, `{{FILE_PATH}}:{{LINE_NUMBER}}` **Root Cause:** {{ROOT_CAUSE_DESCRIPTION}} +**Files Affected:** +- `{{FILE_PATH}}:{{LINE_NUMBER}}` - {{ISSUE_DESCRIPTION}} +- `{{FILE_PATH}}:{{LINE_NUMBER}}` - {{ISSUE_DESCRIPTION}} **Actions:** - [ ] {{SPECIFIC_ACTION_1}} @@ -50,13 +59,29 @@ INSTRUCTIONS FOR AGENTS: ``` ### Issue 2: {{GROUPED_ISSUE_TITLE}} -**Files:** `{{FILE_PATH}}:{{LINE_NUMBER}}` **Root Cause:** {{ROOT_CAUSE_DESCRIPTION}} +**Files Affected:** +- `{{FILE_PATH}}:{{LINE_NUMBER}}` - {{ISSUE_DESCRIPTION}} **Actions:** - [ ] {{SPECIFIC_ACTION_1}} - [ ] {{SPECIFIC_ACTION_2}} +## CodeRabbit Analysis Summary + +### Resolved Comments ✅ +- {{RESOLVED_COMMENT_1}} +- {{RESOLVED_COMMENT_2}} + +### Unresolved Comments 🔴 +- {{UNRESOLVED_COMMENT_1}} - {{STATUS}} +- {{UNRESOLVED_COMMENT_2}} - {{STATUS}} + +### Comment Categories +- **Critical:** {{COUNT}} comments (React Native compatibility, security, memory leaks) +- **Architecture:** {{COUNT}} comments (API design, type safety) +- **Code Quality:** {{COUNT}} comments (testing, imports, documentation) + ## Testing Checklist ### Before Merge @@ -102,3 +127,5 @@ INSTRUCTIONS FOR AGENTS: --- **Last Updated:** {{DATE}} +**CodeRabbit Comments Analyzed:** {{TOTAL_COMMENTS}} +**Unresolved Issues:** {{UNRESOLVED_COUNT}} From fc850a3595e5dc9bb227fba07bace0cd53475d95 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 19 Aug 2025 17:32:06 -0700 Subject: [PATCH 11/18] pr updates --- packages/mobile-sdk-alpha/package.json | 3 ++- packages/mobile-sdk-alpha/src/browser.ts | 1 + .../src/components/flows/OnboardingFlow.tsx | 22 ++++++++++++------- .../components/screens/NFCScannerScreen.tsx | 11 ++++++---- .../screens/PassportCameraScreen.tsx | 16 +++++++++----- .../src/components/screens/QRCodeScreen.tsx | 16 +++++++++----- packages/mobile-sdk-alpha/src/types/ui.ts | 7 +++++- 7 files changed, 51 insertions(+), 25 deletions(-) diff --git a/packages/mobile-sdk-alpha/package.json b/packages/mobile-sdk-alpha/package.json index 4e9abf110..db28cecf4 100644 --- a/packages/mobile-sdk-alpha/package.json +++ b/packages/mobile-sdk-alpha/package.json @@ -55,7 +55,8 @@ }, "peerDependencies": { "react": "^18.3.1", - "react-native": "^0.75.4" + "react-native": "^0.75.4", + "tamagui": "^1.126.0" }, "devDependencies": { "@testing-library/react": "^14.1.2", diff --git a/packages/mobile-sdk-alpha/src/browser.ts b/packages/mobile-sdk-alpha/src/browser.ts index fc0c1bbcf..617efb01c 100644 --- a/packages/mobile-sdk-alpha/src/browser.ts +++ b/packages/mobile-sdk-alpha/src/browser.ts @@ -47,6 +47,7 @@ export { createSelfClient } from './client'; export { defaultConfig } from './config/defaults'; +/** @deprecated Use createSelfClient().extractMRZInfo or import from './mrz' */ export { extractMRZInfo, formatDateToYYMMDD, scanMRZ } from './mrz'; // Core functions diff --git a/packages/mobile-sdk-alpha/src/components/flows/OnboardingFlow.tsx b/packages/mobile-sdk-alpha/src/components/flows/OnboardingFlow.tsx index 0b0858877..d3b36d126 100644 --- a/packages/mobile-sdk-alpha/src/components/flows/OnboardingFlow.tsx +++ b/packages/mobile-sdk-alpha/src/components/flows/OnboardingFlow.tsx @@ -1,16 +1,16 @@ -import type { ReactNode } from 'react'; +import type { ComponentType } from 'react'; import { useCallback, useState } from 'react'; import { useSelfClient } from '../../context'; -import type { DocumentData, ExternalAdapter } from '../../types/ui'; +import type { DocumentData, ExternalAdapter, PassportCameraProps, ScreenProps } from '../../types/ui'; import { NFCScannerScreen } from '../screens/NFCScannerScreen'; import { PassportCameraScreen } from '../screens/PassportCameraScreen'; interface OnboardingFlowProps { external: ExternalAdapter; setDocument: (doc: DocumentData, documentId: string) => Promise; - PassportCamera?: ReactNode; - NFCScanner?: ReactNode; + PassportCamera?: ComponentType; + NFCScanner?: ComponentType; } export const OnboardingFlow = ({ external, setDocument, PassportCamera, NFCScanner }: OnboardingFlowProps) => { @@ -43,10 +43,16 @@ export const OnboardingFlow = ({ external, setDocument, PassportCamera, NFCScann ); if (!mrzData) { - return PassportCamera || ; + if (PassportCamera) { + const PCam = PassportCamera as ComponentType; + return ; + } + return ; } - return ( - NFCScanner || - ); + if (NFCScanner) { + const NFC = NFCScanner as ComponentType; + return ; + } + return ; }; diff --git a/packages/mobile-sdk-alpha/src/components/screens/NFCScannerScreen.tsx b/packages/mobile-sdk-alpha/src/components/screens/NFCScannerScreen.tsx index 8f15a69f6..cce129dbc 100644 --- a/packages/mobile-sdk-alpha/src/components/screens/NFCScannerScreen.tsx +++ b/packages/mobile-sdk-alpha/src/components/screens/NFCScannerScreen.tsx @@ -1,4 +1,5 @@ import { useCallback } from 'react'; +import { Button, Text, YStack } from 'tamagui'; import { useSelfClient } from '../../context'; import type { ScreenProps } from '../../types/ui'; @@ -20,9 +21,11 @@ export const NFCScannerScreen = ({ onSuccess, onFailure }: ScreenProps) => { ); return ( -
-

NFC Scanner

- -
+ + + NFC Scanner + + + ); }; diff --git a/packages/mobile-sdk-alpha/src/components/screens/PassportCameraScreen.tsx b/packages/mobile-sdk-alpha/src/components/screens/PassportCameraScreen.tsx index a9e2756b1..c318d8818 100644 --- a/packages/mobile-sdk-alpha/src/components/screens/PassportCameraScreen.tsx +++ b/packages/mobile-sdk-alpha/src/components/screens/PassportCameraScreen.tsx @@ -1,15 +1,19 @@ +import { Button, Text, YStack } from 'tamagui'; + import type { PassportCameraProps } from '../../types/ui'; // Simple placeholder component - this would be replaced with actual camera UI export const PassportCameraScreen = ({ onMRZDetected }: PassportCameraProps) => ( -
-

Passport Camera

- -
+ + ); diff --git a/packages/mobile-sdk-alpha/src/components/screens/QRCodeScreen.tsx b/packages/mobile-sdk-alpha/src/components/screens/QRCodeScreen.tsx index 1a23d605c..0979964c1 100644 --- a/packages/mobile-sdk-alpha/src/components/screens/QRCodeScreen.tsx +++ b/packages/mobile-sdk-alpha/src/components/screens/QRCodeScreen.tsx @@ -1,9 +1,15 @@ +import { Button, Text, YStack } from 'tamagui'; + import type { ScreenProps } from '../../types/ui'; export const QRCodeScreen = ({ onSuccess, onFailure }: ScreenProps) => ( -
-

QR Code Scanner

- - -
+ + + QR Code Scanner + + + + ); diff --git a/packages/mobile-sdk-alpha/src/types/ui.ts b/packages/mobile-sdk-alpha/src/types/ui.ts index 1f2032535..7ac2e71ee 100644 --- a/packages/mobile-sdk-alpha/src/types/ui.ts +++ b/packages/mobile-sdk-alpha/src/types/ui.ts @@ -1,11 +1,16 @@ import type { DocumentCategory, PassportData } from '@selfxyz/common'; // Document-related types +/** + * Document metadata - must NOT contain plaintext MRZ/PII + * All sensitive payloads belong only in DocumentData.data (typed as PassportData) + * or in encrypted storage referenced by the opaque token + */ export interface DocumentMetadata { id: string; documentType: string; documentCategory: DocumentCategory; - data: string; + encryptedBlobRef?: string; // opaque pointer; no plaintext PII mock: boolean; isRegistered?: boolean; } From ef728f8e05146473c124e7a4341e74513edbbea1 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 19 Aug 2025 17:35:18 -0700 Subject: [PATCH 12/18] update lock --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index bdfb9bff3..a7cbc21f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5274,6 +5274,7 @@ __metadata: peerDependencies: react: ^18.3.1 react-native: ^0.75.4 + tamagui: ^1.126.0 languageName: unknown linkType: soft From bfdf1e1c15085d4283ef9e8bd7ee205728435173 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 19 Aug 2025 17:36:43 -0700 Subject: [PATCH 13/18] updates --- docs/templates/pr-action-items-template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/templates/pr-action-items-template.md b/docs/templates/pr-action-items-template.md index 8a60c1bcb..94deffdb1 100644 --- a/docs/templates/pr-action-items-template.md +++ b/docs/templates/pr-action-items-template.md @@ -1,7 +1,7 @@ - -# PR {{PR_NUMBER}} Action Items - -## Overview -**Title:** {{PR_TITLE}} -**Author:** {{AUTHOR}} -**Status:** {{STATUS}} -**Created:** {{DATE}} -**Branch:** {{BRANCH}} - -{{PR_SUMMARY}} - -## Critical Issues (Blocking Merge) +## Analysis Summary +**After thorough review of all 15 CodeRabbit comments, ALL issues have been resolved in subsequent commits. The PR is ready for merge.** -### 1. {{ISSUE_TITLE}} -**Files:** `{{FILE_PATH}}:{{LINE_NUMBER}}` -**CodeRabbit Comment:** {{COMMENT_ID}} -**Problem:** {{PROBLEM_DESCRIPTION}} -**Fix:** {{SPECIFIC_FIX_OR_ACTION}} - -**Code Example:** -```{{LANGUAGE}} -{{CODE_EXAMPLE}} +### Unresolved Comments 🔴 (0/15) +**None** - All comments have been addressed. ``` -### 2. {{ISSUE_TITLE}} -**Files:** `{{FILE_PATH}}:{{LINE_NUMBER}}` -**CodeRabbit Comment:** {{COMMENT_ID}} -**Problem:** {{PROBLEM_DESCRIPTION}} -**Fix:** {{SPECIFIC_FIX_OR_ACTION}} - -## Required Actions - -### Issue 1: {{GROUPED_ISSUE_TITLE}} -**Root Cause:** {{ROOT_CAUSE_DESCRIPTION}} -**Files Affected:** -- `{{FILE_PATH}}:{{LINE_NUMBER}}` - {{ISSUE_DESCRIPTION}} -- `{{FILE_PATH}}:{{LINE_NUMBER}}` - {{ISSUE_DESCRIPTION}} - -**Actions:** -- [ ] {{SPECIFIC_ACTION_1}} -- [ ] {{SPECIFIC_ACTION_2}} -- [ ] {{SPECIFIC_ACTION_3}} +### When Unresolved Issues Exist (Rare) +```markdown +## Critical Issues (Blocking Merge) -**Code Example:** -```{{LANGUAGE}} -{{CODE_EXAMPLE}} +### 1. Security Vulnerability - PII in Logs +**Files:** `src/logger.ts:45` +**CodeRabbit Comment:** 2286479767 +**Problem:** Logging sensitive user data without redaction +**Status:** ⚠️ **CRITICAL** - Security risk, blocks merge ``` -### Issue 2: {{GROUPED_ISSUE_TITLE}} -**Root Cause:** {{ROOT_CAUSE_DESCRIPTION}} -**Files Affected:** -- `{{FILE_PATH}}:{{LINE_NUMBER}}` - {{ISSUE_DESCRIPTION}} - -**Actions:** -- [ ] {{SPECIFIC_ACTION_1}} -- [ ] {{SPECIFIC_ACTION_2}} - -## CodeRabbit Analysis Summary - -### Resolved Comments ✅ -- {{RESOLVED_COMMENT_1}} -- {{RESOLVED_COMMENT_2}} - -### Unresolved Comments 🔴 -- {{UNRESOLVED_COMMENT_1}} - {{STATUS}} -- {{UNRESOLVED_COMMENT_2}} - {{STATUS}} - -### Comment Categories -- **Critical:** {{COUNT}} comments (React Native compatibility, security, memory leaks) -- **Architecture:** {{COUNT}} comments (API design, type safety) -- **Code Quality:** {{COUNT}} comments (testing, imports, documentation) - -## Testing Checklist +## Metrics Tracking -### Before Merge -- [ ] {{SPECIFIC_TEST_TO_RUN}} -- [ ] {{SPECIFIC_VALIDATION}} -- [ ] {{SPECIFIC_BUILD_TEST}} +Track these metrics for each PR analysis: +- Total CodeRabbit comments analyzed +- Comments with "✅ Addressed" status (excluded) +- Low/nitpick severity comments (excluded) +- Medium+ severity unresolved comments (included) +- Final unresolved count in action items -### Post-Merge -- [ ] {{INTEGRATION_TEST}} -- [ ] {{PERFORMANCE_TEST}} +## Continuous Improvement -## Breaking Changes - -### For Consumers -- [ ] {{SPECIFIC_BREAKING_CHANGE}} -- [ ] {{MIGRATION_NEEDED}} - -### Migration Guide -- [ ] Update import statements -- [ ] Replace deprecated API calls -- [ ] Handle new dependencies - -## Implementation Priority - -### Phase 1: Critical Fixes (Blocking Merge) -1. {{CRITICAL_FIX_1}} -2. {{CRITICAL_FIX_2}} - -### Phase 2: Architecture Improvements -1. {{ARCHITECTURE_FIX_1}} -2. {{ARCHITECTURE_FIX_2}} - -### Phase 3: Polish & Documentation -1. {{POLISH_ITEM_1}} -2. {{POLISH_ITEM_2}} - -## Notes - -- {{SPECIFIC_NOTE_1}} -- {{SPECIFIC_NOTE_2}} -- {{SPECIFIC_NOTE_3}} - ---- - -**Last Updated:** {{DATE}} -**CodeRabbit Comments Analyzed:** {{TOTAL_COMMENTS}} -**Unresolved Issues:** {{UNRESOLVED_COUNT}} -``` +### Weekly Review +- Audit recent PR action items for false positives/negatives +- Check if any "resolved" issues were actually unresolved +- Verify severity classifications were accurate +- Update filtering rules based on patterns -## Workflow Steps Summary - -1. **Fetch PR Data**: Use `giga_read_pr` to get PR details and CodeRabbit comments -2. **Filter Comments**: Focus on unresolved comments without '✅ Addressed' status -3. **Group Issues**: Group related issues by root cause, not just severity -4. **Prioritize**: Categorize as 'blocking merge' vs 'architectural' vs 'polish' -5. **Create File**: Generate PR-{{NUMBER}}-ACTION-ITEMS.md in repo root -6. **Use Template**: Apply the template from docs/templates/pr-action-items-template.md -7. **Include Details**: Add specific file paths, line numbers, and clear fixes -8. **Add Examples**: Include code examples for complex fixes -9. **Add Testing**: Include testing requirements and breaking changes -10. **Avoid Plans**: Never use mcp_giga_plan - create real markdown files - -## Key Principles - -- **Actionable Items**: Must have specific file path, line number, clear problem description, specific fix, and priority level -- **Root Cause Grouping**: Group related comments by root cause rather than treating each separately -- **Value-First**: Focus on actionable content over pretty formatting -- **Specific Details**: Always include file paths, line numbers, CodeRabbit comment IDs, and code examples for complex fixes -- **Testing Focus**: Include comprehensive testing status and requirements -- **File Naming**: Always use PR-{NUMBER}-ACTION-ITEMS.md pattern to comply with gitignore -- **CodeRabbit Integration**: Include comment IDs and categorize by severity for better traceability +### Template Updates +- Add new comment type mappings as CodeRabbit evolves +- Refine severity criteria based on project needs +- Update verification checklist based on common mistakes +- Enhance automation where possible --- -**Export Date:** {{CURRENT_DATE}} -**Neuron Count:** 15 neurons exported -**Template Version:** v3 as of export +**Remember**: It's better to exclude a borderline issue than include a nitpick. The goal is actionable, high-impact feedback only. diff --git a/docs/templates/pr-action-items-template.md b/docs/templates/pr-action-items-template.md index 94deffdb1..d51117b55 100644 --- a/docs/templates/pr-action-items-template.md +++ b/docs/templates/pr-action-items-template.md @@ -1,13 +1,40 @@ # PR {{PR_NUMBER}} Action Items @@ -21,8 +48,28 @@ INSTRUCTIONS FOR AGENTS: {{PR_SUMMARY}} +## Analysis Summary + + +**After thorough review of all {{TOTAL_COMMENTS}} CodeRabbit comments, ALL issues have been resolved in subsequent commits. The PR is ready for merge.** + +### Resolved Comments ✅ ({{TOTAL_COMMENTS}}/{{TOTAL_COMMENTS}}) +All comments show ✅ "Addressed" status, indicating they were fixed in commits: +- {{RESOLVED_CATEGORY_1}} - Fixed in commits {{COMMIT_RANGE}} +- {{RESOLVED_CATEGORY_2}} - Fixed in commits {{COMMIT_RANGE}} + +### Unresolved Comments 🔴 (0/{{TOTAL_COMMENTS}}) +**None** - All comments have been addressed. + +## Conclusion +**This PR is ready for merge.** All CodeRabbit issues have been resolved. + + + ## Critical Issues (Blocking Merge) + + ### 1. {{ISSUE_TITLE}} **Files:** `{{FILE_PATH}}:{{LINE_NUMBER}}` **CodeRabbit Comment:** {{COMMENT_ID}} @@ -126,6 +173,26 @@ INSTRUCTIONS FOR AGENTS: --- +## Agent Verification Checklist + +**BEFORE FINALIZING - VERIFY EACH ITEM:** +- [ ] ✅ Read ALL {{TOTAL_COMMENTS}} CodeRabbit comments thoroughly +- [ ] ✅ Checked each comment for "✅ Addressed" status +- [ ] ✅ Excluded all nitpick/style/documentation-only suggestions +- [ ] ✅ Only included MEDIUM+ severity issues (security, architecture, functionality) +- [ ] ✅ Verified unresolved count is accurate +- [ ] ✅ If 0 unresolved issues, used "All issues resolved" template +- [ ] ✅ Double-checked that each "unresolved" issue is actually unresolved + +**SEVERITY VERIFICATION:** +- [ ] ✅ CRITICAL: Security, memory leaks, platform compatibility ({{CRITICAL_COUNT}}) +- [ ] ✅ HIGH: API inconsistencies, type safety, architecture ({{HIGH_COUNT}}) +- [ ] ✅ MEDIUM: Test coverage, performance, minor architecture ({{MEDIUM_COUNT}}) +- [ ] ❌ LOW/NITPICK: Style, naming, docs (EXCLUDED - {{EXCLUDED_COUNT}}) + +--- + **Last Updated:** {{DATE}} **CodeRabbit Comments Analyzed:** {{TOTAL_COMMENTS}} -**Unresolved Issues:** {{UNRESOLVED_COUNT}} +**Unresolved Medium+ Issues:** {{UNRESOLVED_COUNT}} +**Excluded Low/Nitpick Issues:** {{EXCLUDED_COUNT}} From 6f33e54bf303def851e9675ab261a3816619e054 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 19 Aug 2025 18:29:05 -0700 Subject: [PATCH 16/18] fix tests --- packages/mobile-sdk-alpha/tests/setup.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/mobile-sdk-alpha/tests/setup.ts b/packages/mobile-sdk-alpha/tests/setup.ts index a1d0e5425..af81ba6ce 100644 --- a/packages/mobile-sdk-alpha/tests/setup.ts +++ b/packages/mobile-sdk-alpha/tests/setup.ts @@ -26,3 +26,20 @@ if (typeof global !== 'undefined') { console.log = originalConsole.log; }; } + +// Mock window.matchMedia for Tamagui components +if (typeof window !== 'undefined') { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), // deprecated + removeListener: vi.fn(), // deprecated + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), + }); +} From 7c3fba8aaf30e5b1bc73223b4a077e6c3251e8ae Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 19 Aug 2025 18:44:45 -0700 Subject: [PATCH 17/18] test: verify provider memoization and add jsdoc (#929) --- packages/mobile-sdk-alpha/src/client.ts | 12 ++++++++ packages/mobile-sdk-alpha/src/context.tsx | 29 +++++++++++++++++++ .../mobile-sdk-alpha/tests/provider.test.tsx | 21 +++++++++++++- 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/packages/mobile-sdk-alpha/src/client.ts b/packages/mobile-sdk-alpha/src/client.ts index dab782e43..9c3f9642b 100644 --- a/packages/mobile-sdk-alpha/src/client.ts +++ b/packages/mobile-sdk-alpha/src/client.ts @@ -20,6 +20,11 @@ import type { ValidationResult, } from './types/public'; +/** + * Optional adapter implementations used when a consumer does not provide their + * own. These defaults are intentionally minimal no-ops suitable for tests and + * non-production environments. + */ const optionalDefaults: Partial = { storage: { get: async () => null, @@ -37,6 +42,13 @@ const optionalDefaults: Partial = { }, }; +/** + * Creates a fully configured {@link SelfClient} instance. + * + * The function validates that all required adapters are supplied and merges the + * provided configuration with sensible defaults. Missing optional adapters are + * filled with benign no-op implementations. + */ export function createSelfClient({ config, adapters }: { config: Config; adapters: Partial }): SelfClient { const cfg = mergeConfig(defaultConfig, config); const required: (keyof Adapters)[] = ['scanner', 'network', 'crypto']; diff --git a/packages/mobile-sdk-alpha/src/context.tsx b/packages/mobile-sdk-alpha/src/context.tsx index f001a34cf..e0a2cabe3 100644 --- a/packages/mobile-sdk-alpha/src/context.tsx +++ b/packages/mobile-sdk-alpha/src/context.tsx @@ -3,21 +3,50 @@ import { createContext, type PropsWithChildren, useContext, useMemo } from 'reac import { createSelfClient } from './client'; import type { Adapters, Config, SelfClient } from './types/public'; +/** + * React context holding a {@link SelfClient} instance. + * + * The context is intentionally initialised with `null` so that consumers + * outside of a {@link SelfClientProvider} can be detected and an informative + * error can be thrown. + */ const SelfClientContext = createContext(null); +/** + * Props for {@link SelfClientProvider}. + * + * @public + */ export interface SelfClientProviderProps { + /** SDK configuration options. */ config: Config; + /** + * Partial set of adapter implementations. Any missing optional adapters will + * be replaced with default no-op implementations. + */ adapters?: Partial; } export { SelfClientContext }; +/** + * Provides a memoised {@link SelfClient} instance to all descendant components + * via {@link SelfClientContext}. + * + * Consumers should ensure that `config` and `adapters` are referentially stable + * (e.g. wrapped in `useMemo`) to avoid recreating the client on every render. + */ export function SelfClientProvider({ config, adapters = {}, children }: PropsWithChildren) { const client = useMemo(() => createSelfClient({ config, adapters }), [config, adapters]); return {children}; } +/** + * Retrieves the current {@link SelfClient} from context. + * + * @throws If used outside of a {@link SelfClientProvider}. + */ export function useSelfClient(): SelfClient { const ctx = useContext(SelfClientContext); if (!ctx) throw new Error('useSelfClient must be used within a SelfClientProvider'); diff --git a/packages/mobile-sdk-alpha/tests/provider.test.tsx b/packages/mobile-sdk-alpha/tests/provider.test.tsx index 6ef20ac3c..77e36c629 100644 --- a/packages/mobile-sdk-alpha/tests/provider.test.tsx +++ b/packages/mobile-sdk-alpha/tests/provider.test.tsx @@ -1,7 +1,8 @@ /* @vitest-environment jsdom */ import type { ReactNode } from 'react'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; +import * as clientModule from '../src/client'; import { SelfClientProvider, useSelfClient } from '../src/index'; import { expectedMRZResult, mockAdapters, sampleMRZ } from './utils/testHelpers'; @@ -27,4 +28,22 @@ describe('SelfClientProvider Context', () => { renderHook(() => useSelfClient()); }).toThrow('useSelfClient must be used within a SelfClientProvider'); }); + + it('memoises the client instance across re-renders', () => { + const spy = vi.spyOn(clientModule, 'createSelfClient'); + const config = {}; + const adapters = mockAdapters; + const wrapper = ({ children }: { children: ReactNode }) => ( + + {children} + + ); + + const { result, rerender } = renderHook(() => useSelfClient(), { wrapper }); + const first = result.current; + rerender(); + expect(result.current).toBe(first); + expect(spy).toHaveBeenCalledTimes(1); + spy.mockRestore(); + }); }); From 74a3c073d9d16d32827e735ac17610478ecef560 Mon Sep 17 00:00:00 2001 From: Justin Hernandez Date: Tue, 19 Aug 2025 18:54:14 -0700 Subject: [PATCH 18/18] pr feedback --- docs/templates/pr-action-items-template.md | 2 ++ .../src/components/flows/OnboardingFlow.tsx | 13 ++++++------ .../screens/PassportCameraScreen.tsx | 20 ++++++++++++++++++- packages/mobile-sdk-alpha/src/types/ui.ts | 4 +++- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/docs/templates/pr-action-items-template.md b/docs/templates/pr-action-items-template.md index d51117b55..0581cd0e5 100644 --- a/docs/templates/pr-action-items-template.md +++ b/docs/templates/pr-action-items-template.md @@ -7,6 +7,7 @@ CRITICAL FILTERING RULES - MUST FOLLOW EXACTLY: ✅ NO "nitpick" or "suggestion" labels (medium+ severity only) ✅ MEDIUM to CRITICAL impact (affects functionality, security, or architecture) ✅ NOT cosmetic/style issues (unless security/performance related) + ✅ DO NOT include secrets, access tokens, API keys, private keys, MRZ data, or any PII. Redact sensitive values and replace with placeholders. 2. VERIFICATION PROCESS - MANDATORY: - Read EACH comment's status field carefully @@ -26,6 +27,7 @@ CRITICAL FILTERING RULES - MUST FOLLOW EXACTLY: - Verify each "unresolved" issue is actually unresolved - Remove any that have been addressed in subsequent commits - If NO unresolved medium+ issues exist, state "All issues resolved ✅" + - Run a final pass to ensure no credentials, secrets, or PII are present in examples, logs, or screenshots. 5. EXECUTION: - Use giga_read_pr to fetch PR data and CodeRabbit comments diff --git a/packages/mobile-sdk-alpha/src/components/flows/OnboardingFlow.tsx b/packages/mobile-sdk-alpha/src/components/flows/OnboardingFlow.tsx index d3b36d126..438f9089a 100644 --- a/packages/mobile-sdk-alpha/src/components/flows/OnboardingFlow.tsx +++ b/packages/mobile-sdk-alpha/src/components/flows/OnboardingFlow.tsx @@ -2,6 +2,7 @@ import type { ComponentType } from 'react'; import { useCallback, useState } from 'react'; import { useSelfClient } from '../../context'; +import type { MRZInfo } from '../../types/public'; import type { DocumentData, ExternalAdapter, PassportCameraProps, ScreenProps } from '../../types/ui'; import { NFCScannerScreen } from '../screens/NFCScannerScreen'; import { PassportCameraScreen } from '../screens/PassportCameraScreen'; @@ -14,19 +15,19 @@ interface OnboardingFlowProps { } export const OnboardingFlow = ({ external, setDocument, PassportCamera, NFCScanner }: OnboardingFlowProps) => { - const [mrzData, setMrzData] = useState(null); + const [mrzData, setMrzData] = useState(null); const client = useSelfClient(); const handleMRZDetected = useCallback( - async (mrzData: any) => { + async (mrzData: MRZInfo) => { try { const status = await client.registerDocument({ scan: { mode: 'mrz', - passportNumber: mrzData.documentNumber, - dateOfBirth: mrzData.birthDate, - dateOfExpiry: mrzData.expiryDate, - issuingCountry: mrzData.countryCode, + passportNumber: mrzData.passportNumber, + dateOfBirth: mrzData.dateOfBirth, + dateOfExpiry: mrzData.dateOfExpiry, + issuingCountry: mrzData.issuingCountry, }, }); diff --git a/packages/mobile-sdk-alpha/src/components/screens/PassportCameraScreen.tsx b/packages/mobile-sdk-alpha/src/components/screens/PassportCameraScreen.tsx index c318d8818..073323299 100644 --- a/packages/mobile-sdk-alpha/src/components/screens/PassportCameraScreen.tsx +++ b/packages/mobile-sdk-alpha/src/components/screens/PassportCameraScreen.tsx @@ -10,7 +10,25 @@ export const PassportCameraScreen = ({ onMRZDetected }: PassportCameraProps) =>