@algorandfoundation/react-native-passkey-autofill 1.0.0-canary.2 → 1.0.0-canary.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nvmrc +1 -0
- package/.oxfmtrc.json +4 -0
- package/.oxlintrc.json +14 -0
- package/.releaserc +30 -0
- package/ARCHITECTURE.md +8 -0
- package/CHANGELOG.md +115 -0
- package/CONTRIBUTING.md +15 -5
- package/README.md +150 -11
- package/android/build.gradle +33 -10
- package/android/libs/repo/co/algorand/dP256Android/1.0.0/dP256Android-1.0.0.pom +9 -0
- package/android/src/main/AndroidManifest.xml +9 -3
- package/android/src/main/java/co/algorand/passkeyautofill/CreatePasskeyActivity.kt +396 -44
- package/android/src/main/java/co/algorand/passkeyautofill/GetPasskeyActivity.kt +583 -44
- package/android/src/main/java/co/algorand/passkeyautofill/Prf.kt +184 -0
- package/android/src/main/java/co/algorand/passkeyautofill/ReactNativePasskeyAutofillModule.kt +190 -2
- package/android/src/main/java/co/algorand/passkeyautofill/WebAuthn.kt +135 -0
- package/android/src/main/java/co/algorand/passkeyautofill/auth/BiometricRequirement.kt +86 -0
- package/android/src/main/java/co/algorand/passkeyautofill/credentials/Credential.kt +2 -1
- package/android/src/main/java/co/algorand/passkeyautofill/credentials/CredentialRepository.kt +314 -53
- package/android/src/main/java/co/algorand/passkeyautofill/service/PasskeyAutofillCredentialProviderService.kt +152 -38
- package/android/src/main/java/co/algorand/passkeyautofill/utils/PasskeyUtils.kt +119 -0
- package/android/src/main/res/values/styles.xml +10 -0
- package/app.plugin.js +593 -60
- package/assets/banner.png +0 -0
- package/build/ReactNativePasskeyAutofill.types.d.ts +41 -1
- package/build/ReactNativePasskeyAutofill.types.d.ts.map +1 -1
- package/build/ReactNativePasskeyAutofill.types.js +3 -1
- package/build/ReactNativePasskeyAutofill.types.js.map +1 -1
- package/build/ReactNativePasskeyAutofillModule.d.ts +41 -4
- package/build/ReactNativePasskeyAutofillModule.d.ts.map +1 -1
- package/build/ReactNativePasskeyAutofillModule.js +21 -2
- package/build/ReactNativePasskeyAutofillModule.js.map +1 -1
- package/build/index.d.ts +2 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -2
- package/build/index.js.map +1 -1
- package/e2e/README.md +155 -0
- package/e2e/driver/adb.ts +73 -0
- package/e2e/driver/capabilities.ts +106 -0
- package/e2e/driver/credentialProvider.ts +104 -0
- package/e2e/driver/fingerprint.ts +885 -0
- package/e2e/driver/selectors.ts +103 -0
- package/e2e/driver/session.ts +25 -0
- package/e2e/jest.config.ts +20 -0
- package/e2e/package.json +11 -0
- package/e2e/tests/passkey.test.ts +190 -0
- package/e2e/tsconfig.json +16 -0
- package/ios/AutofillCredentialProvider/BiometricRequirement.swift +34 -0
- package/ios/AutofillCredentialProvider/CredentialProviderViewController.swift +570 -0
- package/ios/AutofillCredentialProvider/PasskeyAutofillCredentialProvider-Bridging-Header.h +1 -0
- package/ios/AutofillCredentialProvider/PasskeyCredentialStore.swift +612 -0
- package/ios/AutofillCredentialProvider/PasskeyKeystoreMMKV.h +25 -0
- package/ios/AutofillCredentialProvider/PasskeyKeystoreMMKV.mm +86 -0
- package/ios/AutofillCredentialProvider/Prf.swift +89 -0
- package/ios/AutofillCredentialProvider/WebAuthn.swift +186 -0
- package/ios/ReactNativePasskeyAutofill.podspec +11 -2
- package/ios/ReactNativePasskeyAutofillModule.swift +170 -15
- package/lefthook.yml +14 -0
- package/package.json +115 -25
- package/pnpm-workspace.yaml +36 -0
- package/src/ReactNativePasskeyAutofill.types.ts +38 -0
- package/src/ReactNativePasskeyAutofillModule.ts +66 -3
- package/src/index.ts +2 -2
- package/tsconfig.json +2 -2
- package/.eslintrc.js +0 -5
- /package/android/libs/{dP256Android-release.aar → repo/co/algorand/dP256Android/1.0.0/dP256Android-1.0.0.aar} +0 -0
|
Binary file
|
|
@@ -1,2 +1,42 @@
|
|
|
1
|
-
export type ReactNativePasskeyAutofillModuleEvents = {
|
|
1
|
+
export type ReactNativePasskeyAutofillModuleEvents = {
|
|
2
|
+
onPasskeyAdded: (event: {
|
|
3
|
+
success: boolean;
|
|
4
|
+
}) => void;
|
|
5
|
+
onPasskeyAuthenticated: (event: {
|
|
6
|
+
success: boolean;
|
|
7
|
+
credentialId?: string;
|
|
8
|
+
}) => void;
|
|
9
|
+
};
|
|
10
|
+
export type PasskeyAutofillCredentialIdentity = {
|
|
11
|
+
credentialId: string;
|
|
12
|
+
relyingPartyIdentifier?: string;
|
|
13
|
+
rpId?: string;
|
|
14
|
+
origin?: string;
|
|
15
|
+
userName?: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
userHandle: string;
|
|
18
|
+
userId?: string;
|
|
19
|
+
privateKey?: string;
|
|
20
|
+
privateKeyBase64?: string;
|
|
21
|
+
publicKey?: string;
|
|
22
|
+
publicKeyBase64?: string;
|
|
23
|
+
createdAt?: number;
|
|
24
|
+
lastUsedAt?: number;
|
|
25
|
+
parentKeyId?: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Capabilities advertised by this credential provider. Exposed so the
|
|
29
|
+
* surrounding wallet UI / RP-side wallet can know which WebAuthn extensions
|
|
30
|
+
* are wired up natively before issuing a passkey request.
|
|
31
|
+
*/
|
|
32
|
+
export type PasskeyAutofillCapabilities = {
|
|
33
|
+
/**
|
|
34
|
+
* Whether the WebAuthn `prf` extension (a.k.a. `hmac-secret`) is supported
|
|
35
|
+
* by this credential provider. PRF outputs are derived deterministically
|
|
36
|
+
* from the wallet HD root secret so that restoring the wallet seed
|
|
37
|
+
* reproduces the same secrets on another device.
|
|
38
|
+
*/
|
|
39
|
+
prf: boolean;
|
|
40
|
+
};
|
|
41
|
+
export declare const PASSKEY_AUTOFILL_CAPABILITIES: PasskeyAutofillCapabilities;
|
|
2
42
|
//# sourceMappingURL=ReactNativePasskeyAutofill.types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactNativePasskeyAutofill.types.d.ts","sourceRoot":"","sources":["../src/ReactNativePasskeyAutofill.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,sCAAsC,GAAG,
|
|
1
|
+
{"version":3,"file":"ReactNativePasskeyAutofill.types.d.ts","sourceRoot":"","sources":["../src/ReactNativePasskeyAutofill.types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,sCAAsC,GAAG;IACnD,cAAc,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;IACtD,sBAAsB,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACtF,CAAC;AAEF,MAAM,MAAM,iCAAiC,GAAG;IAC9C,YAAY,EAAE,MAAM,CAAC;IACrB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,2BAA2B,GAAG;IACxC;;;;;OAKG;IACH,GAAG,EAAE,OAAO,CAAC;CACd,CAAC;AAEF,eAAO,MAAM,6BAA6B,EAAE,2BAE3C,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactNativePasskeyAutofill.types.js","sourceRoot":"","sources":["../src/ReactNativePasskeyAutofill.types.ts"],"names":[],"mappings":"","sourcesContent":["export type ReactNativePasskeyAutofillModuleEvents = {\n\n};\n"]}
|
|
1
|
+
{"version":3,"file":"ReactNativePasskeyAutofill.types.js","sourceRoot":"","sources":["../src/ReactNativePasskeyAutofill.types.ts"],"names":[],"mappings":"AAsCA,MAAM,CAAC,MAAM,6BAA6B,GAAgC;IACxE,GAAG,EAAE,IAAI;CACV,CAAC","sourcesContent":["export type ReactNativePasskeyAutofillModuleEvents = {\n onPasskeyAdded: (event: { success: boolean }) => void;\n onPasskeyAuthenticated: (event: { success: boolean; credentialId?: string }) => void;\n};\n\nexport type PasskeyAutofillCredentialIdentity = {\n credentialId: string;\n relyingPartyIdentifier?: string;\n rpId?: string;\n origin?: string;\n userName?: string;\n name?: string;\n userHandle: string;\n userId?: string;\n privateKey?: string;\n privateKeyBase64?: string;\n publicKey?: string;\n publicKeyBase64?: string;\n createdAt?: number;\n lastUsedAt?: number;\n parentKeyId?: string;\n};\n\n/**\n * Capabilities advertised by this credential provider. Exposed so the\n * surrounding wallet UI / RP-side wallet can know which WebAuthn extensions\n * are wired up natively before issuing a passkey request.\n */\nexport type PasskeyAutofillCapabilities = {\n /**\n * Whether the WebAuthn `prf` extension (a.k.a. `hmac-secret`) is supported\n * by this credential provider. PRF outputs are derived deterministically\n * from the wallet HD root secret so that restoring the wallet seed\n * reproduces the same secrets on another device.\n */\n prf: boolean;\n};\n\nexport const PASSKEY_AUTOFILL_CAPABILITIES: PasskeyAutofillCapabilities = {\n prf: true,\n};\n"]}
|
|
@@ -1,12 +1,49 @@
|
|
|
1
|
-
import { NativeModule } from
|
|
2
|
-
import { ReactNativePasskeyAutofillModuleEvents } from
|
|
1
|
+
import { NativeModule } from "expo";
|
|
2
|
+
import { PasskeyAutofillCredentialIdentity, ReactNativePasskeyAutofillModuleEvents } from "./ReactNativePasskeyAutofill.types";
|
|
3
3
|
declare class ReactNativePasskeyAutofillModule extends NativeModule<ReactNativePasskeyAutofillModuleEvents> {
|
|
4
4
|
setMasterKey(secret: string): Promise<void>;
|
|
5
5
|
setHdRootKeyId(id: string): Promise<void>;
|
|
6
6
|
getHdRootKeyId(): Promise<string | null>;
|
|
7
7
|
configureIntentActions(getPasskeyAction: string, createPasskeyAction: string): Promise<void>;
|
|
8
8
|
clearCredentials(): Promise<void>;
|
|
9
|
+
deleteCredential(credentialId: string): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* iOS: returns the identities currently published to the AutoFill
|
|
12
|
+
* `ASCredentialIdentityStore`. Android: no-op returning `[]` because the
|
|
13
|
+
* native Credential Provider service reads credentials directly from MMKV
|
|
14
|
+
* and there is no separate identity store to query.
|
|
15
|
+
*/
|
|
16
|
+
getStoredCredentials(): Promise<PasskeyAutofillCredentialIdentity[]>;
|
|
17
|
+
/**
|
|
18
|
+
* iOS: returns diagnostic strings from the AutoFill extension.
|
|
19
|
+
* Android: no-op returning `[]`.
|
|
20
|
+
*/
|
|
21
|
+
getDiagnostics(): Promise<string[]>;
|
|
22
|
+
/**
|
|
23
|
+
* Replaces the iOS AutoFill passkey identity store with credentials
|
|
24
|
+
* available to the native Credential Provider extension. Each item must
|
|
25
|
+
* include a credential id, relying party (`relyingPartyIdentifier`,
|
|
26
|
+
* `rpId`, or `origin`), user handle, and P-256 private key material.
|
|
27
|
+
* Android ignores this method (no-op) because the provider reads MMKV
|
|
28
|
+
* directly.
|
|
29
|
+
*/
|
|
30
|
+
replaceCredentialIdentities(credentials: PasskeyAutofillCredentialIdentity[]): Promise<void>;
|
|
31
|
+
refreshCredentialIdentities(): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Resolves to `true` when this app is registered as the active
|
|
34
|
+
* credential/autofill provider on the current device (Android 14+
|
|
35
|
+
* credential provider or iOS AutoFill). Useful for gating passkey UI
|
|
36
|
+
* and for E2E tests that need to confirm the prompt that appears
|
|
37
|
+
* belongs to this provider and not a third party.
|
|
38
|
+
*/
|
|
39
|
+
isProviderActive(): Promise<boolean>;
|
|
40
|
+
/**
|
|
41
|
+
* Opens the OS credential/autofill provider settings screen so the
|
|
42
|
+
* user can enable this app as the active provider. Resolves to `true`
|
|
43
|
+
* if a settings screen could be launched, `false` otherwise.
|
|
44
|
+
*/
|
|
45
|
+
openProviderSettings(): Promise<boolean>;
|
|
9
46
|
}
|
|
10
|
-
declare const
|
|
11
|
-
export default
|
|
47
|
+
declare const nativeModule: ReactNativePasskeyAutofillModule;
|
|
48
|
+
export default nativeModule;
|
|
12
49
|
//# sourceMappingURL=ReactNativePasskeyAutofillModule.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactNativePasskeyAutofillModule.d.ts","sourceRoot":"","sources":["../src/ReactNativePasskeyAutofillModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"ReactNativePasskeyAutofillModule.d.ts","sourceRoot":"","sources":["../src/ReactNativePasskeyAutofillModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAGzD,OAAO,EACL,iCAAiC,EACjC,sCAAsC,EACvC,MAAM,oCAAoC,CAAC;AAE5C,OAAO,OAAO,gCAAiC,SAAQ,YAAY,CAAC,sCAAsC,CAAC;IACzG,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3C,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IACzC,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IACxC,sBAAsB,CAAC,gBAAgB,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAC5F,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IACjC,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IACrD;;;;;OAKG;IACH,oBAAoB,IAAI,OAAO,CAAC,iCAAiC,EAAE,CAAC;IACpE;;;OAGG;IACH,cAAc,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IACnC;;;;;;;OAOG;IACH,2BAA2B,CAAC,WAAW,EAAE,iCAAiC,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAC5F,2BAA2B,IAAI,OAAO,CAAC,IAAI,CAAC;IAC5C;;;;;;OAMG;IACH,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC;IACpC;;;;OAIG;IACH,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC;CACzC;AAGD,QAAA,MAAM,YAAY,kCAEjB,CAAC;AAoBF,eAAe,YAAY,CAAC"}
|
|
@@ -1,4 +1,23 @@
|
|
|
1
|
-
import { requireNativeModule } from
|
|
1
|
+
import { requireNativeModule } from "expo";
|
|
2
|
+
import { Platform } from "react-native";
|
|
2
3
|
// This call loads the native module object from the JSI.
|
|
3
|
-
|
|
4
|
+
const nativeModule = requireNativeModule("ReactNativePasskeyAutofill");
|
|
5
|
+
// A handful of methods are only implemented in the iOS native module because
|
|
6
|
+
// they interact with `ASCredentialIdentityStore`, which has no Android
|
|
7
|
+
// equivalent (the Android Credential Provider service reads credentials
|
|
8
|
+
// directly from MMKV on each request).
|
|
9
|
+
if (Platform.OS === "android") {
|
|
10
|
+
const noops = {
|
|
11
|
+
replaceCredentialIdentities: () => Promise.resolve(),
|
|
12
|
+
refreshCredentialIdentities: () => Promise.resolve(),
|
|
13
|
+
getStoredCredentials: () => Promise.resolve([]),
|
|
14
|
+
getDiagnostics: () => Promise.resolve([]),
|
|
15
|
+
};
|
|
16
|
+
for (const name of Object.keys(noops)) {
|
|
17
|
+
if (typeof nativeModule[name] !== "function") {
|
|
18
|
+
nativeModule[name] = noops[name];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export default nativeModule;
|
|
4
23
|
//# sourceMappingURL=ReactNativePasskeyAutofillModule.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactNativePasskeyAutofillModule.js","sourceRoot":"","sources":["../src/ReactNativePasskeyAutofillModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"ReactNativePasskeyAutofillModule.js","sourceRoot":"","sources":["../src/ReactNativePasskeyAutofillModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAoDxC,yDAAyD;AACzD,MAAM,YAAY,GAAG,mBAAmB,CACtC,4BAA4B,CAC7B,CAAC;AAEF,6EAA6E;AAC7E,uEAAuE;AACvE,wEAAwE;AACxE,uCAAuC;AACvC,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;IAC9B,MAAM,KAAK,GAA6D;QACtE,2BAA2B,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;QACpD,2BAA2B,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;QACpD,oBAAoB,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC/C,cAAc,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;KAC1C,CAAC;IACF,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACtC,IAAI,OAAQ,YAAmD,CAAC,IAAI,CAAC,KAAK,UAAU,EAAE,CAAC;YACpF,YAAmD,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC;AACH,CAAC;AAED,eAAe,YAAY,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo\";\nimport { Platform } from \"react-native\";\n\nimport {\n PasskeyAutofillCredentialIdentity,\n ReactNativePasskeyAutofillModuleEvents,\n} from \"./ReactNativePasskeyAutofill.types\";\n\ndeclare class ReactNativePasskeyAutofillModule extends NativeModule<ReactNativePasskeyAutofillModuleEvents> {\n setMasterKey(secret: string): Promise<void>;\n setHdRootKeyId(id: string): Promise<void>;\n getHdRootKeyId(): Promise<string | null>;\n configureIntentActions(getPasskeyAction: string, createPasskeyAction: string): Promise<void>;\n clearCredentials(): Promise<void>;\n deleteCredential(credentialId: string): Promise<void>;\n /**\n * iOS: returns the identities currently published to the AutoFill\n * `ASCredentialIdentityStore`. Android: no-op returning `[]` because the\n * native Credential Provider service reads credentials directly from MMKV\n * and there is no separate identity store to query.\n */\n getStoredCredentials(): Promise<PasskeyAutofillCredentialIdentity[]>;\n /**\n * iOS: returns diagnostic strings from the AutoFill extension.\n * Android: no-op returning `[]`.\n */\n getDiagnostics(): Promise<string[]>;\n /**\n * Replaces the iOS AutoFill passkey identity store with credentials\n * available to the native Credential Provider extension. Each item must\n * include a credential id, relying party (`relyingPartyIdentifier`,\n * `rpId`, or `origin`), user handle, and P-256 private key material.\n * Android ignores this method (no-op) because the provider reads MMKV\n * directly.\n */\n replaceCredentialIdentities(credentials: PasskeyAutofillCredentialIdentity[]): Promise<void>;\n refreshCredentialIdentities(): Promise<void>;\n /**\n * Resolves to `true` when this app is registered as the active\n * credential/autofill provider on the current device (Android 14+\n * credential provider or iOS AutoFill). Useful for gating passkey UI\n * and for E2E tests that need to confirm the prompt that appears\n * belongs to this provider and not a third party.\n */\n isProviderActive(): Promise<boolean>;\n /**\n * Opens the OS credential/autofill provider settings screen so the\n * user can enable this app as the active provider. Resolves to `true`\n * if a settings screen could be launched, `false` otherwise.\n */\n openProviderSettings(): Promise<boolean>;\n}\n\n// This call loads the native module object from the JSI.\nconst nativeModule = requireNativeModule<ReactNativePasskeyAutofillModule>(\n \"ReactNativePasskeyAutofill\",\n);\n\n// A handful of methods are only implemented in the iOS native module because\n// they interact with `ASCredentialIdentityStore`, which has no Android\n// equivalent (the Android Credential Provider service reads credentials\n// directly from MMKV on each request).\nif (Platform.OS === \"android\") {\n const noops: Record<string, (...args: unknown[]) => Promise<unknown>> = {\n replaceCredentialIdentities: () => Promise.resolve(),\n refreshCredentialIdentities: () => Promise.resolve(),\n getStoredCredentials: () => Promise.resolve([]),\n getDiagnostics: () => Promise.resolve([]),\n };\n for (const name of Object.keys(noops)) {\n if (typeof (nativeModule as unknown as Record<string, unknown>)[name] !== \"function\") {\n (nativeModule as unknown as Record<string, unknown>)[name] = noops[name];\n }\n }\n}\n\nexport default nativeModule;\n"]}
|
package/build/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { default } from
|
|
2
|
-
export * from
|
|
1
|
+
export { default } from "./ReactNativePasskeyAutofillModule";
|
|
2
|
+
export * from "./ReactNativePasskeyAutofill.types";
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAC7D,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAC7D,cAAc,oCAAoC,CAAC"}
|
package/build/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Reexport the native module. On web, it will be resolved to ReactNativePasskeyAutofillModule.web.ts
|
|
2
2
|
// and on native platforms to ReactNativePasskeyAutofillModule.ts
|
|
3
|
-
export { default } from
|
|
4
|
-
export * from
|
|
3
|
+
export { default } from "./ReactNativePasskeyAutofillModule";
|
|
4
|
+
export * from "./ReactNativePasskeyAutofill.types";
|
|
5
5
|
//# sourceMappingURL=index.js.map
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,qGAAqG;AACrG,iEAAiE;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAC7D,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,qGAAqG;AACrG,iEAAiE;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAC7D,cAAc,oCAAoC,CAAC","sourcesContent":["// Reexport the native module. On web, it will be resolved to ReactNativePasskeyAutofillModule.web.ts\n// and on native platforms to ReactNativePasskeyAutofillModule.ts\nexport { default } from \"./ReactNativePasskeyAutofillModule\";\nexport * from \"./ReactNativePasskeyAutofill.types\";\n"]}
|
package/e2e/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# E2E — Appium + WebdriverIO
|
|
2
|
+
|
|
3
|
+
End-to-end tests driving the `example/` app with [Appium 2](https://appium.io)
|
|
4
|
+
through [WebdriverIO](https://webdriver.io). Tests are written in TypeScript
|
|
5
|
+
and executed with Jest (to stay consistent with the rest of the repo).
|
|
6
|
+
|
|
7
|
+
The happy-path test mirrors the example app: it mints a local XHD Ed25519
|
|
8
|
+
key, creates a passkey against `https://debug.liquidauth.com`, and then
|
|
9
|
+
re-uses it for an assertion round-trip.
|
|
10
|
+
|
|
11
|
+
## Layout
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
e2e/
|
|
15
|
+
├── driver/
|
|
16
|
+
│ ├── capabilities.ts # Android (UiAutomator2) + iOS (XCUITest) caps
|
|
17
|
+
│ ├── selectors.ts # testID + system-dialog helpers
|
|
18
|
+
│ └── session.ts # createDriver() — wraps webdriverio.remote()
|
|
19
|
+
├── tests/
|
|
20
|
+
│ └── passkey.test.ts # smoke + passkey create/use against debug.liquidauth.com
|
|
21
|
+
├── jest.config.ts
|
|
22
|
+
├── package.json
|
|
23
|
+
└── tsconfig.json
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
All app-level taps use React Native `testID`s (exposed as
|
|
27
|
+
`accessibility id` on both platforms) via `byTestId("...")`. System
|
|
28
|
+
WebAuthn/biometric dialogs are auto-confirmed by `confirmSystemPrompt()`.
|
|
29
|
+
|
|
30
|
+
## Provider-active precondition
|
|
31
|
+
|
|
32
|
+
The example app exposes a provider-status banner so the suite (and
|
|
33
|
+
human testers) can confirm that _this_ package is the user-selected
|
|
34
|
+
system credential/autofill provider before exercising passkey flows —
|
|
35
|
+
otherwise another installed password manager could silently serve the
|
|
36
|
+
system WebAuthn prompts. The example exposes:
|
|
37
|
+
|
|
38
|
+
- A `testID="provider-status"` label whose text reads `active`,
|
|
39
|
+
`inactive`, or `unknown`.
|
|
40
|
+
- A `testID="open-provider-settings"` CTA that deep-links to the OS
|
|
41
|
+
settings (Android: credential provider picker; iOS: AutoFill
|
|
42
|
+
settings).
|
|
43
|
+
|
|
44
|
+
The first suite step (`surfaces the initial provider status`) is a
|
|
45
|
+
smoke read only. The authoritative assertion runs after the passkey
|
|
46
|
+
creation step: once Android has routed a
|
|
47
|
+
`BeginCreate/BeginGetCredentialRequest` to our
|
|
48
|
+
`PasskeyAutofillCredentialProviderService`, the service stamps an MMKV
|
|
49
|
+
key that `ReactNativePasskeyAutofill.isProviderActive()` picks up and
|
|
50
|
+
the banner flips to `active`. This two-signal design sidesteps the
|
|
51
|
+
fact that `Settings.Secure("credential_service")` is `@hide`-restricted
|
|
52
|
+
on Android 12+ and cannot be read by a regular app.
|
|
53
|
+
|
|
54
|
+
If the passkey flow never reaches the stamp, enable the app manually
|
|
55
|
+
on the device:
|
|
56
|
+
|
|
57
|
+
- **Android 14+**: Settings → Passwords, passkeys & accounts →
|
|
58
|
+
Additional providers → _Passkey AutoFill Example_.
|
|
59
|
+
- **iOS 17+**: Settings → Passwords → Password Options → enable
|
|
60
|
+
_Passkey AutoFill Example_.
|
|
61
|
+
|
|
62
|
+
## Biometric prompts
|
|
63
|
+
|
|
64
|
+
The passkey create/use flow ultimately routes through either the
|
|
65
|
+
system's Single-Tap bottom sheet (when the provider attaches
|
|
66
|
+
`BiometricPromptData` + `CryptoObject`) or the fallback
|
|
67
|
+
`CreatePasskeyActivity` / `GetPasskeyActivity` screens. Both require a
|
|
68
|
+
biometric confirmation from the user. On emulators the suite automates
|
|
69
|
+
this by synthesising finger touches via `adb` (Android) or the
|
|
70
|
+
`enrollBiometric` extension (iOS).
|
|
71
|
+
|
|
72
|
+
## Prerequisites
|
|
73
|
+
|
|
74
|
+
- Node.js 22+, pnpm 10+
|
|
75
|
+
- **Android**: Android SDK, an AVD (API 34 recommended). Biometrics
|
|
76
|
+
are automatically enrolled by the suite on first run.
|
|
77
|
+
- **iOS** (macOS only): Xcode 16.1+, an iOS 17+ Simulator. Face ID is
|
|
78
|
+
automatically enrolled by the suite.
|
|
79
|
+
- Appium drivers. The driver packages are declared as regular `devDependencies`
|
|
80
|
+
and installed by `pnpm install`, so they are version-locked alongside the
|
|
81
|
+
rest of the workspace. The `appium:setup*` scripts then register them with
|
|
82
|
+
the Appium CLI from their local `node_modules` path (no `npm` shell-out,
|
|
83
|
+
no global `~/.appium` mutation required beyond Appium's own manifest):
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pnpm install # installs appium-uiautomator2-driver & appium-xcuitest-driver
|
|
87
|
+
pnpm run appium:setup # register both drivers with Appium
|
|
88
|
+
pnpm run appium:setup:android # Android only
|
|
89
|
+
pnpm run appium:setup:ios # iOS only (macOS)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Running locally
|
|
93
|
+
|
|
94
|
+
1. Install repo deps:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pnpm install
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
2. Build the example app for the target platform:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pnpm run e2e:build:android # produces example/android/app/build/outputs/apk/release/app-release.apk
|
|
104
|
+
pnpm run e2e:build:ios # produces example/ios/build/Build/Products/Release-iphonesimulator/PasskeyAutofillExample.app
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
3. Boot an emulator/simulator, then run:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pnpm run e2e:android
|
|
111
|
+
# or
|
|
112
|
+
pnpm run e2e:ios
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
These scripts start Appium via `start-server-and-test`, wait for
|
|
116
|
+
`http://127.0.0.1:4723/status`, execute the Jest suite, and shut
|
|
117
|
+
Appium down afterwards.
|
|
118
|
+
|
|
119
|
+
### Overrides
|
|
120
|
+
|
|
121
|
+
| Env var | Purpose |
|
|
122
|
+
| ---------------------------- | ------------------------------------------------ |
|
|
123
|
+
| `E2E_PLATFORM` | `android` (default) or `ios` |
|
|
124
|
+
| `E2E_APP_PATH` | Absolute path to a prebuilt `.apk` / `.app` |
|
|
125
|
+
| `APPIUM_HOST`, `APPIUM_PORT` | Appium server (defaults `127.0.0.1:4723`) |
|
|
126
|
+
| `ANDROID_PLATFORM_VERSION` | e.g. `"14"` |
|
|
127
|
+
| `ANDROID_DEVICE_NAME` | e.g. `"Pixel_6_API_34"` |
|
|
128
|
+
| `IOS_PLATFORM_VERSION` | e.g. `"17.5"` |
|
|
129
|
+
| `IOS_DEVICE_NAME` | e.g. `"iPhone 15"` |
|
|
130
|
+
| `WDIO_LOG_LEVEL` | `error` \| `warn` (default) \| `info` \| `debug` |
|
|
131
|
+
|
|
132
|
+
## CI
|
|
133
|
+
|
|
134
|
+
The [`E2E` workflow](../.github/workflows/e2e.yml) runs on
|
|
135
|
+
`workflow_dispatch` or when a PR is labelled `e2e`:
|
|
136
|
+
|
|
137
|
+
- **Android** job builds the release APK, installs the UiAutomator2 driver,
|
|
138
|
+
and runs the suite inside `reactivecircus/android-emulator-runner`.
|
|
139
|
+
- **iOS** job (currently disabled) builds the `.app` for the iPhone Simulator,
|
|
140
|
+
installs the XCUITest driver, boots a simulator, and runs the suite.
|
|
141
|
+
|
|
142
|
+
Screenshots of the test run are uploaded as workflow artifacts for debugging.
|
|
143
|
+
|
|
144
|
+
## Authoring new tests
|
|
145
|
+
|
|
146
|
+
1. Add a stable `testID` to the target React Native component.
|
|
147
|
+
2. Import the helpers:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { createDriver } from "../driver/session";
|
|
151
|
+
import { byTestId, tap, confirmSystemPrompt } from "../driver/selectors";
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
3. Drive the app with `tap(driver, byTestId("..."))`. Use
|
|
155
|
+
`confirmSystemPrompt()` for OS-level WebAuthn/biometric dialogs.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Common ADB helpers to ensure consistent device targeting across the E2E suite.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function getAdbSerial(): string {
|
|
8
|
+
if (process.env.ANDROID_UDID) return process.env.ANDROID_UDID;
|
|
9
|
+
|
|
10
|
+
// Try to find the first connected emulator or device.
|
|
11
|
+
try {
|
|
12
|
+
const devices = execFileSync("adb", ["devices"], {
|
|
13
|
+
encoding: "utf8",
|
|
14
|
+
timeout: 5000,
|
|
15
|
+
})
|
|
16
|
+
.split("\n")
|
|
17
|
+
.slice(1)
|
|
18
|
+
.filter((l) => l.includes("\tdevice"))
|
|
19
|
+
.map((l) => l.split("\t")[0].trim());
|
|
20
|
+
|
|
21
|
+
if (devices.length === 1) return devices[0];
|
|
22
|
+
if (devices.length > 1) {
|
|
23
|
+
const emulators = devices.filter((d) => d.startsWith("emulator-"));
|
|
24
|
+
if (emulators.length > 0) return emulators[0];
|
|
25
|
+
return devices[0];
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
// eslint-disable-next-line no-console
|
|
29
|
+
console.warn("[e2e:adb] failed to list devices via adb:", (err as Error).message);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return "emulator-5554";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Wrap adb so we can swap serials / run headless without inheriting
|
|
37
|
+
* stdin from the caller (Jest runners hate dangling fds).
|
|
38
|
+
*/
|
|
39
|
+
export function adb(args: string[], opts: { timeout?: number } = {}): string {
|
|
40
|
+
const serial = getAdbSerial();
|
|
41
|
+
try {
|
|
42
|
+
return execFileSync("adb", ["-s", serial, ...args], {
|
|
43
|
+
encoding: "utf8",
|
|
44
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
45
|
+
timeout: opts.timeout ?? 15_000,
|
|
46
|
+
}).trim();
|
|
47
|
+
} catch (err) {
|
|
48
|
+
const error = err as Error & { stderr?: string };
|
|
49
|
+
const msg = error.stderr ? `${error.message}\n${error.stderr}` : error.message;
|
|
50
|
+
throw new Error(`adb command failed: adb -s ${serial} ${args.join(" ")}\n${msg}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function adbSilent(args: string[]): string {
|
|
55
|
+
try {
|
|
56
|
+
const serial = getAdbSerial();
|
|
57
|
+
return execFileSync("adb", ["-s", serial, ...args], {
|
|
58
|
+
encoding: "utf8",
|
|
59
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
60
|
+
timeout: 15_000,
|
|
61
|
+
}).trim();
|
|
62
|
+
} catch {
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function adbAsync(args: string[]): void {
|
|
68
|
+
// Fire-and-forget, used for the finger-touch spam where we don't
|
|
69
|
+
// want to block the test thread on adb's per-call latency (~50ms).
|
|
70
|
+
const serial = getAdbSerial();
|
|
71
|
+
const child = execFile("adb", ["-s", serial, ...args], () => undefined);
|
|
72
|
+
child.stdin?.end();
|
|
73
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Platform target for the E2E run. Selected via the `E2E_PLATFORM`
|
|
5
|
+
* environment variable (defaults to `android`).
|
|
6
|
+
*/
|
|
7
|
+
export type Platform = "android" | "ios";
|
|
8
|
+
|
|
9
|
+
export const PLATFORM: Platform = (process.env.E2E_PLATFORM as Platform | undefined) ?? "android";
|
|
10
|
+
|
|
11
|
+
/** Example app identifiers (must match `example/app.json`). */
|
|
12
|
+
export const ANDROID_PACKAGE = "co.algorand.auth.example";
|
|
13
|
+
export const IOS_BUNDLE_ID = "co.algorand.auth.example";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Absolute paths to the built example app binaries. Override with
|
|
17
|
+
* `E2E_APP_PATH` to point at a prebuilt artifact in CI. When omitted
|
|
18
|
+
* the helpers below fall back to the default expo-built location.
|
|
19
|
+
*/
|
|
20
|
+
const repoRoot = path.resolve(__dirname, "..", "..");
|
|
21
|
+
const defaultAndroidApk = path.join(
|
|
22
|
+
repoRoot,
|
|
23
|
+
"example",
|
|
24
|
+
"android",
|
|
25
|
+
"app",
|
|
26
|
+
"build",
|
|
27
|
+
"outputs",
|
|
28
|
+
"apk",
|
|
29
|
+
"release",
|
|
30
|
+
"app-release.apk",
|
|
31
|
+
);
|
|
32
|
+
const defaultIosApp = path.join(
|
|
33
|
+
repoRoot,
|
|
34
|
+
"example",
|
|
35
|
+
"ios",
|
|
36
|
+
"build",
|
|
37
|
+
"Build",
|
|
38
|
+
"Products",
|
|
39
|
+
"Release-iphonesimulator",
|
|
40
|
+
"PasskeyAutofillExample.app",
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export const ANDROID_APP_PATH = process.env.E2E_APP_PATH ?? defaultAndroidApk;
|
|
44
|
+
export const IOS_APP_PATH = process.env.E2E_APP_PATH ?? defaultIosApp;
|
|
45
|
+
|
|
46
|
+
export const APPIUM_HOST = process.env.APPIUM_HOST ?? "127.0.0.1";
|
|
47
|
+
export const APPIUM_PORT = Number(process.env.APPIUM_PORT ?? 4723);
|
|
48
|
+
|
|
49
|
+
export interface Capabilities {
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Android (UiAutomator2) capabilities. `noReset: false` ensures a clean
|
|
55
|
+
* keystore/passkey state between runs so tests are idempotent.
|
|
56
|
+
*/
|
|
57
|
+
export const androidCapabilities: Capabilities = {
|
|
58
|
+
platformName: "Android",
|
|
59
|
+
"appium:automationName": "UiAutomator2",
|
|
60
|
+
// `platformVersion` is intentionally only set when explicitly provided:
|
|
61
|
+
// when omitted, UiAutomator2 attaches to whichever device is currently
|
|
62
|
+
// connected via `adb`, which is the common case on a developer laptop.
|
|
63
|
+
...(process.env.ANDROID_PLATFORM_VERSION
|
|
64
|
+
? { "appium:platformVersion": process.env.ANDROID_PLATFORM_VERSION }
|
|
65
|
+
: {}),
|
|
66
|
+
// Pin the target device when set (matches `ANDROID_UDID` used by the adb
|
|
67
|
+
// helpers) so a run is deterministic when multiple devices are attached.
|
|
68
|
+
...(process.env.ANDROID_UDID ? { "appium:udid": process.env.ANDROID_UDID } : {}),
|
|
69
|
+
"appium:deviceName": process.env.ANDROID_DEVICE_NAME ?? "Android Emulator",
|
|
70
|
+
"appium:app": ANDROID_APP_PATH,
|
|
71
|
+
"appium:appPackage": ANDROID_PACKAGE,
|
|
72
|
+
"appium:autoGrantPermissions": true,
|
|
73
|
+
"appium:noReset": false,
|
|
74
|
+
"appium:fullReset": false,
|
|
75
|
+
"appium:newCommandTimeout": 240,
|
|
76
|
+
// Bumped to absorb cold-AVD session-install flakes: on a fresh
|
|
77
|
+
// `google_apis_playstore` AVD, the UiAutomator2 server APK + the
|
|
78
|
+
// `io.appium.settings` helper app can occasionally take longer than
|
|
79
|
+
// the 30s default to install and start, producing
|
|
80
|
+
// `Appium Settings app is not running after 30000ms` at session
|
|
81
|
+
// creation. These knobs are the documented escape hatches.
|
|
82
|
+
"appium:uiautomator2ServerInstallTimeout": 120_000,
|
|
83
|
+
"appium:uiautomator2ServerLaunchTimeout": 120_000,
|
|
84
|
+
"appium:uiautomator2ServerReadTimeout": 240_000,
|
|
85
|
+
"appium:adbExecTimeout": 60_000,
|
|
86
|
+
"appium:appWaitActivity": "*",
|
|
87
|
+
"appium:appWaitDuration": 60_000,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* iOS (XCUITest) capabilities. Targets the simulator.
|
|
92
|
+
*/
|
|
93
|
+
export const iosCapabilities: Capabilities = {
|
|
94
|
+
platformName: "iOS",
|
|
95
|
+
"appium:automationName": "XCUITest",
|
|
96
|
+
"appium:platformVersion": process.env.IOS_PLATFORM_VERSION ?? "17.5",
|
|
97
|
+
"appium:deviceName": process.env.IOS_DEVICE_NAME ?? "iPhone 15",
|
|
98
|
+
"appium:app": IOS_APP_PATH,
|
|
99
|
+
"appium:bundleId": IOS_BUNDLE_ID,
|
|
100
|
+
"appium:noReset": false,
|
|
101
|
+
"appium:newCommandTimeout": 240,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export function capabilitiesForPlatform(platform: Platform = PLATFORM): Capabilities {
|
|
105
|
+
return platform === "ios" ? iosCapabilities : androidCapabilities;
|
|
106
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { PLATFORM, ANDROID_PACKAGE } from "./capabilities";
|
|
2
|
+
import { adbSilent } from "./adb";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Credential Manager provider registration helper (Android only).
|
|
6
|
+
*
|
|
7
|
+
* Why this file exists
|
|
8
|
+
* --------------------
|
|
9
|
+
* On Android 14 the Credential Manager only routes `BeginCreate*` /
|
|
10
|
+
* `BeginGet*` requests to credential-provider services that the user has
|
|
11
|
+
* explicitly enabled in Settings → Passwords, passkeys & accounts →
|
|
12
|
+
* Additional providers. A freshly booted AVD has **no** providers
|
|
13
|
+
* enabled, so:
|
|
14
|
+
*
|
|
15
|
+
* - `handleCreatePasskey` opens a Credential Manager sheet that lists
|
|
16
|
+
* zero providers, the consent screen is dismissed by the system, and
|
|
17
|
+
* our service never receives a `BeginCreateCredentialRequest` — so
|
|
18
|
+
* `provider-status` stays `inactive` forever.
|
|
19
|
+
* - Every test after "creates an XHD Ed25519 key" then fails because
|
|
20
|
+
* the passkey flow deadlocks on an empty system UI.
|
|
21
|
+
*
|
|
22
|
+
* Settings → Additional providers writes the enabled component names to
|
|
23
|
+
* the secure setting `credential_service` (comma-separated) and the
|
|
24
|
+
* primary provider to `credential_service_primary`. We can do the same
|
|
25
|
+
* thing via `adb shell settings put secure ...` — the `adb shell` user
|
|
26
|
+
* already holds `WRITE_SECURE_SETTINGS`, so no root / magisk required.
|
|
27
|
+
*
|
|
28
|
+
* This helper is idempotent and a no-op on iOS / when the
|
|
29
|
+
* target APK isn't installed.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
function isAndroidTarget(): boolean {
|
|
33
|
+
return PLATFORM === "android";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let registered = false;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Enable our passkey provider service as the system's credential
|
|
40
|
+
* provider so Credential Manager actually routes requests to it.
|
|
41
|
+
*
|
|
42
|
+
* Returns `true` if the `credential_service` secure setting ends up
|
|
43
|
+
* containing our component. Safe to call repeatedly; no-ops on iOS.
|
|
44
|
+
*/
|
|
45
|
+
// Must stay in sync with `android/src/main/AndroidManifest.xml` and
|
|
46
|
+
// `android/build.gradle` (`namespace`). The component name is:
|
|
47
|
+
// <applicationId>/<library-namespace>.service.PasskeyAutofillCredentialProviderService
|
|
48
|
+
// i.e. the host app id + the FQCN of the service declared by the
|
|
49
|
+
// library whose manifest is merged into the example app.
|
|
50
|
+
const SERVICE_FQCN = "co.algorand.passkeyautofill.service.PasskeyAutofillCredentialProviderService";
|
|
51
|
+
|
|
52
|
+
export async function ensureAndroidCredentialProviderEnabled(): Promise<boolean> {
|
|
53
|
+
if (!isAndroidTarget()) return false;
|
|
54
|
+
if (registered) return true;
|
|
55
|
+
|
|
56
|
+
const component = `${ANDROID_PACKAGE}/${SERVICE_FQCN}`;
|
|
57
|
+
|
|
58
|
+
// Guard: the service must actually be installed — otherwise the
|
|
59
|
+
// Credential Manager silently drops the component on first use and
|
|
60
|
+
// our `settings put` is effectively a no-op.
|
|
61
|
+
const pmCheck = adbSilent(["shell", "pm", "list", "packages", ANDROID_PACKAGE]);
|
|
62
|
+
if (!pmCheck.includes(ANDROID_PACKAGE)) {
|
|
63
|
+
// eslint-disable-next-line no-console
|
|
64
|
+
console.warn(
|
|
65
|
+
`[e2e] credential-provider: ${ANDROID_PACKAGE} not installed yet; ` +
|
|
66
|
+
"skipping provider registration.",
|
|
67
|
+
);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Write both the primary slot and the list slot. Different AOSP
|
|
72
|
+
// builds read one or the other (Android 14 stock uses
|
|
73
|
+
// `credential_service_primary` for the default, `credential_service`
|
|
74
|
+
// for additional providers); setting both makes us the only
|
|
75
|
+
// configured provider either way.
|
|
76
|
+
adbSilent(["shell", "settings", "put", "secure", "credential_service", component]);
|
|
77
|
+
adbSilent(["shell", "settings", "put", "secure", "credential_service_primary", component]);
|
|
78
|
+
|
|
79
|
+
// Some OEM Settings UIs also persist the enabled providers into
|
|
80
|
+
// `autofill_service` when Autofill & Credential Manager share the
|
|
81
|
+
// provider. Best-effort mirror; failure is fine.
|
|
82
|
+
adbSilent(["shell", "settings", "put", "secure", "autofill_service", component]);
|
|
83
|
+
|
|
84
|
+
// Verify what the system actually stored. `settings get` returns the
|
|
85
|
+
// literal value or "null" if unset.
|
|
86
|
+
const got = adbSilent(["shell", "settings", "get", "secure", "credential_service"]);
|
|
87
|
+
const gotPrimary = adbSilent([
|
|
88
|
+
"shell",
|
|
89
|
+
"settings",
|
|
90
|
+
"get",
|
|
91
|
+
"secure",
|
|
92
|
+
"credential_service_primary",
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
const ok = got.includes(component) || gotPrimary.includes(component);
|
|
96
|
+
// eslint-disable-next-line no-console
|
|
97
|
+
console.log(
|
|
98
|
+
`[e2e] credential-provider: component=${component} registered=${ok} ` +
|
|
99
|
+
`(credential_service=${got || "null"}, primary=${gotPrimary || "null"})`,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
registered = ok;
|
|
103
|
+
return ok;
|
|
104
|
+
}
|