@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.
Files changed (66) hide show
  1. package/.nvmrc +1 -0
  2. package/.oxfmtrc.json +4 -0
  3. package/.oxlintrc.json +14 -0
  4. package/.releaserc +30 -0
  5. package/ARCHITECTURE.md +8 -0
  6. package/CHANGELOG.md +115 -0
  7. package/CONTRIBUTING.md +15 -5
  8. package/README.md +150 -11
  9. package/android/build.gradle +33 -10
  10. package/android/libs/repo/co/algorand/dP256Android/1.0.0/dP256Android-1.0.0.pom +9 -0
  11. package/android/src/main/AndroidManifest.xml +9 -3
  12. package/android/src/main/java/co/algorand/passkeyautofill/CreatePasskeyActivity.kt +396 -44
  13. package/android/src/main/java/co/algorand/passkeyautofill/GetPasskeyActivity.kt +583 -44
  14. package/android/src/main/java/co/algorand/passkeyautofill/Prf.kt +184 -0
  15. package/android/src/main/java/co/algorand/passkeyautofill/ReactNativePasskeyAutofillModule.kt +190 -2
  16. package/android/src/main/java/co/algorand/passkeyautofill/WebAuthn.kt +135 -0
  17. package/android/src/main/java/co/algorand/passkeyautofill/auth/BiometricRequirement.kt +86 -0
  18. package/android/src/main/java/co/algorand/passkeyautofill/credentials/Credential.kt +2 -1
  19. package/android/src/main/java/co/algorand/passkeyautofill/credentials/CredentialRepository.kt +314 -53
  20. package/android/src/main/java/co/algorand/passkeyautofill/service/PasskeyAutofillCredentialProviderService.kt +152 -38
  21. package/android/src/main/java/co/algorand/passkeyautofill/utils/PasskeyUtils.kt +119 -0
  22. package/android/src/main/res/values/styles.xml +10 -0
  23. package/app.plugin.js +593 -60
  24. package/assets/banner.png +0 -0
  25. package/build/ReactNativePasskeyAutofill.types.d.ts +41 -1
  26. package/build/ReactNativePasskeyAutofill.types.d.ts.map +1 -1
  27. package/build/ReactNativePasskeyAutofill.types.js +3 -1
  28. package/build/ReactNativePasskeyAutofill.types.js.map +1 -1
  29. package/build/ReactNativePasskeyAutofillModule.d.ts +41 -4
  30. package/build/ReactNativePasskeyAutofillModule.d.ts.map +1 -1
  31. package/build/ReactNativePasskeyAutofillModule.js +21 -2
  32. package/build/ReactNativePasskeyAutofillModule.js.map +1 -1
  33. package/build/index.d.ts +2 -2
  34. package/build/index.d.ts.map +1 -1
  35. package/build/index.js +2 -2
  36. package/build/index.js.map +1 -1
  37. package/e2e/README.md +155 -0
  38. package/e2e/driver/adb.ts +73 -0
  39. package/e2e/driver/capabilities.ts +106 -0
  40. package/e2e/driver/credentialProvider.ts +104 -0
  41. package/e2e/driver/fingerprint.ts +885 -0
  42. package/e2e/driver/selectors.ts +103 -0
  43. package/e2e/driver/session.ts +25 -0
  44. package/e2e/jest.config.ts +20 -0
  45. package/e2e/package.json +11 -0
  46. package/e2e/tests/passkey.test.ts +190 -0
  47. package/e2e/tsconfig.json +16 -0
  48. package/ios/AutofillCredentialProvider/BiometricRequirement.swift +34 -0
  49. package/ios/AutofillCredentialProvider/CredentialProviderViewController.swift +570 -0
  50. package/ios/AutofillCredentialProvider/PasskeyAutofillCredentialProvider-Bridging-Header.h +1 -0
  51. package/ios/AutofillCredentialProvider/PasskeyCredentialStore.swift +612 -0
  52. package/ios/AutofillCredentialProvider/PasskeyKeystoreMMKV.h +25 -0
  53. package/ios/AutofillCredentialProvider/PasskeyKeystoreMMKV.mm +86 -0
  54. package/ios/AutofillCredentialProvider/Prf.swift +89 -0
  55. package/ios/AutofillCredentialProvider/WebAuthn.swift +186 -0
  56. package/ios/ReactNativePasskeyAutofill.podspec +11 -2
  57. package/ios/ReactNativePasskeyAutofillModule.swift +170 -15
  58. package/lefthook.yml +14 -0
  59. package/package.json +115 -25
  60. package/pnpm-workspace.yaml +36 -0
  61. package/src/ReactNativePasskeyAutofill.types.ts +38 -0
  62. package/src/ReactNativePasskeyAutofillModule.ts +66 -3
  63. package/src/index.ts +2 -2
  64. package/tsconfig.json +2 -2
  65. package/.eslintrc.js +0 -5
  66. /package/android/libs/{dP256Android-release.aar → repo/co/algorand/dP256Android/1.0.0/dP256Android-1.0.0.aar} +0 -0
package/app.plugin.js CHANGED
@@ -1,14 +1,507 @@
1
1
  const {
2
2
  withAndroidManifest,
3
+ withEntitlementsPlist,
4
+ withInfoPlist,
3
5
  withStringsXml,
6
+ withAppBuildGradle,
4
7
  withProjectBuildGradle,
5
8
  withMainApplication,
6
9
  withDangerousMod,
10
+ withXcodeProject,
7
11
  AndroidConfig,
8
12
  } = require("@expo/config-plugins");
9
13
  const path = require("path");
10
14
  const fs = require("fs");
11
15
 
16
+ const IOS_EXTENSION_NAME = "PasskeyAutofillCredentialProvider";
17
+ const IOS_EXTENSION_FILES = [
18
+ "CredentialProviderViewController.swift",
19
+ "PasskeyKeystoreMMKV.h",
20
+ "PasskeyKeystoreMMKV.mm",
21
+ "PasskeyAutofillCredentialProvider-Bridging-Header.h",
22
+ "PasskeyCredentialStore.swift",
23
+ "WebAuthn.swift",
24
+ "BiometricRequirement.swift",
25
+ "Prf.swift",
26
+ ];
27
+ const IOS_EXTENSION_SOURCE_FILES = IOS_EXTENSION_FILES.filter((file) => !file.endsWith(".h"));
28
+ const DETERMINISTIC_P256_PACKAGE_URL =
29
+ "https://github.com/algorandfoundation/deterministic-P256-swift/";
30
+ const DETERMINISTIC_P256_PRODUCT_NAME = "deterministicP256-swift";
31
+
32
+ const getAssociatedDomain = (site) => {
33
+ try {
34
+ return new URL(site).host;
35
+ } catch {
36
+ return site.replace(/^https?:\/\//, "").split("/")[0];
37
+ }
38
+ };
39
+
40
+ const getIosBundleIdentifier = (config) =>
41
+ config.ios?.bundleIdentifier ||
42
+ config.ios?.bundleIdentifierOverride ||
43
+ "co.algorand.auth.example";
44
+
45
+ const getAppGroup = (config, props) =>
46
+ props.appGroup || `group.${getIosBundleIdentifier(config)}.passkey-autofill`;
47
+
48
+ const AAGUID_REGEX =
49
+ /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
50
+
51
+ // Optional authenticator AAGUID. When set, both platforms embed it in attestation
52
+ // responses so the credential provider presents a consistent identity to relying parties.
53
+ const getAaguid = (props) => {
54
+ if (!props.aaguid) {
55
+ return null;
56
+ }
57
+ const value = String(props.aaguid).trim();
58
+ if (!AAGUID_REGEX.test(value)) {
59
+ throw new Error(
60
+ `react-native-passkey-autofill: "aaguid" must be a UUID string, received "${props.aaguid}".`,
61
+ );
62
+ }
63
+ return value;
64
+ };
65
+
66
+ const BIOMETRIC_REQUIREMENT_VALUES = ["strong", "strongOrCredential", "weakOrCredential"];
67
+ const DEFAULT_BIOMETRIC_REQUIREMENT = "strongOrCredential";
68
+ // Must match `META_DATA_KEY` in android/.../auth/BiometricRequirement.kt.
69
+ const ANDROID_BIOMETRIC_META_DATA_NAME = "co.algorand.passkeyautofill.BIOMETRIC_REQUIREMENT";
70
+ // Must match `infoDictionaryKey` in ios/AutofillCredentialProvider/BiometricRequirement.swift.
71
+ const IOS_BIOMETRIC_INFO_PLIST_KEY = "ReactNativePasskeyAutofillBiometricRequirement";
72
+
73
+ // Authenticators a passkey operation will accept. Default is intentionally more permissive than
74
+ // strong-only: it leaves iOS unchanged (passcode still allowed) and lets Android accept the device
75
+ // credential. See docs/superpowers/specs/2026-05-28-configurable-biometric-requirement-design.md.
76
+ const getBiometricRequirement = (props) => {
77
+ if (props.biometricRequirement == null) {
78
+ return DEFAULT_BIOMETRIC_REQUIREMENT;
79
+ }
80
+ const value = String(props.biometricRequirement).trim();
81
+ if (!BIOMETRIC_REQUIREMENT_VALUES.includes(value)) {
82
+ throw new Error(
83
+ `react-native-passkey-autofill: "biometricRequirement" must be one of ${BIOMETRIC_REQUIREMENT_VALUES.join(
84
+ ", ",
85
+ )}, received "${props.biometricRequirement}".`,
86
+ );
87
+ }
88
+ return value;
89
+ };
90
+
91
+ const normalizeXcodeName = (name) => String(name || "").replace(/^"|"$/g, "");
92
+
93
+ const getExtensionTarget = (project) => {
94
+ const targets = project.pbxNativeTargetSection();
95
+ const targetUuid = Object.keys(targets).find((uuid) => {
96
+ const target = targets[uuid];
97
+ return (
98
+ target?.isa === "PBXNativeTarget" &&
99
+ target.productType === '"com.apple.product-type.app-extension"' &&
100
+ normalizeXcodeName(target.name) === IOS_EXTENSION_NAME
101
+ );
102
+ });
103
+
104
+ return targetUuid ? { uuid: targetUuid, pbxNativeTarget: targets[targetUuid] } : null;
105
+ };
106
+
107
+ const getDevelopmentTeam = (project) => {
108
+ const buildConfigs = project.pbxXCBuildConfigurationSection();
109
+ const config = Object.keys(buildConfigs)
110
+ .filter((key) => !key.endsWith("_comment"))
111
+ .map((key) => buildConfigs[key])
112
+ .find((entry) => {
113
+ const team = entry?.buildSettings?.DEVELOPMENT_TEAM;
114
+ return team && team !== '"$(DEVELOPMENT_TEAM)"' && team !== "$(DEVELOPMENT_TEAM)";
115
+ });
116
+
117
+ return config?.buildSettings?.DEVELOPMENT_TEAM;
118
+ };
119
+
120
+ const writePlist = (filePath, body) => {
121
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
122
+ fs.writeFileSync(filePath, body);
123
+ };
124
+
125
+ const extensionInfoPlist = ({
126
+ label,
127
+ supportedDomains,
128
+ aaguid,
129
+ biometricRequirement,
130
+ }) => `<?xml version="1.0" encoding="UTF-8"?>
131
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
132
+ <plist version="1.0">
133
+ <dict>
134
+ <key>CFBundleDisplayName</key>
135
+ <string>${label}</string>
136
+ <key>CFBundleExecutable</key>
137
+ <string>$(EXECUTABLE_NAME)</string>
138
+ <key>CFBundleIdentifier</key>
139
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
140
+ <key>CFBundleName</key>
141
+ <string>$(PRODUCT_NAME)</string>
142
+ <key>CFBundlePackageType</key>
143
+ <string>XPC!</string>
144
+ <key>CFBundleShortVersionString</key>
145
+ <string>$(MARKETING_VERSION)</string>
146
+ <key>CFBundleVersion</key>
147
+ <string>$(CURRENT_PROJECT_VERSION)</string>
148
+ <key>ReactNativePasskeyAutofillAppGroup</key>
149
+ <string>$(PASSKEY_AUTOFILL_APP_GROUP)</string>
150
+ <key>AppGroupIdentifier</key>
151
+ <string>$(PASSKEY_AUTOFILL_APP_GROUP)</string>
152
+ ${aaguid ? ` <key>ReactNativePasskeyAutofillAAGUID</key>\n <string>${aaguid}</string>\n` : ""} <key>${IOS_BIOMETRIC_INFO_PLIST_KEY}</key>
153
+ <string>${biometricRequirement}</string>
154
+ <key>NSFaceIDUsageDescription</key>
155
+ <string>Rocca uses Face ID to create and use passkeys.</string>
156
+ <key>NSExtension</key>
157
+ <dict>
158
+ <key>ASCredentialProviderExtensionSupportedDomains</key>
159
+ <array>
160
+ ${supportedDomains.map((domain) => ` <string>${domain}</string>`).join("\n")}
161
+ </array>
162
+ <key>CFBundleDisplayName</key>
163
+ <string>${label}</string>
164
+ <key>NSExtensionAttributes</key>
165
+ <dict>
166
+ <key>ASCredentialProviderExtensionCapabilities</key>
167
+ <dict>
168
+ <key>ProvidesOneTimeCodes</key>
169
+ <false/>
170
+ <key>ProvidesPasskeys</key>
171
+ <true/>
172
+ <key>ProvidesPasswords</key>
173
+ <false/>
174
+ <key>ProvidesTextToInsert</key>
175
+ <false/>
176
+ <key>ShowsConfigurationUI</key>
177
+ <false/>
178
+ </dict>
179
+ </dict>
180
+ <key>NSExtensionPointIdentifier</key>
181
+ <string>com.apple.authentication-services-credential-provider-ui</string>
182
+ <key>NSExtensionPrincipalClass</key>
183
+ <string>$(PRODUCT_MODULE_NAME).CredentialProviderViewController</string>
184
+ </dict>
185
+ </dict>
186
+ </plist>
187
+ `;
188
+
189
+ const extensionEntitlementsPlist = ({ appGroup }) => `<?xml version="1.0" encoding="UTF-8"?>
190
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
191
+ <plist version="1.0">
192
+ <dict>
193
+ <key>com.apple.developer.authentication-services.autofill-credential-provider</key>
194
+ <true/>
195
+ <key>com.apple.security.application-groups</key>
196
+ <array>
197
+ <string>${appGroup}</string>
198
+ </array>
199
+ </dict>
200
+ </plist>
201
+ `;
202
+
203
+ const withIosPasskeyAutofill = (config, props = {}) => {
204
+ const site = props.site || "https://debug.liquidauth.com";
205
+ const label = props.label || "My Credential Provider";
206
+ const associatedDomain = getAssociatedDomain(site);
207
+ const supportedDomains = props.supportedDomains || [associatedDomain];
208
+ const appGroup = getAppGroup(config, props);
209
+ const aaguid = getAaguid(props);
210
+ const biometricRequirement = getBiometricRequirement(props);
211
+
212
+ config = withInfoPlist(config, (config) => {
213
+ config.modResults.ReactNativePasskeyAutofillAppGroup = appGroup;
214
+ config.modResults.AppGroupIdentifier = appGroup;
215
+ return config;
216
+ });
217
+
218
+ config = withEntitlementsPlist(config, (config) => {
219
+ const domains = new Set(config.modResults["com.apple.developer.associated-domains"] || []);
220
+ domains.add(`webcredentials:${associatedDomain}`);
221
+ config.modResults["com.apple.developer.associated-domains"] = [...domains];
222
+ config.modResults["com.apple.developer.authentication-services.autofill-credential-provider"] =
223
+ true;
224
+
225
+ const appGroups = new Set(config.modResults["com.apple.security.application-groups"] || []);
226
+ appGroups.add(appGroup);
227
+ config.modResults["com.apple.security.application-groups"] = [...appGroups];
228
+ return config;
229
+ });
230
+
231
+ config = withDangerousMod(config, [
232
+ "ios",
233
+ async (config) => {
234
+ const iosRoot = config.modRequest.platformProjectRoot;
235
+ const extensionRoot = path.join(iosRoot, IOS_EXTENSION_NAME);
236
+ const sourceRoot = path.join(__dirname, "ios/AutofillCredentialProvider");
237
+
238
+ fs.mkdirSync(extensionRoot, { recursive: true });
239
+ for (const file of IOS_EXTENSION_FILES) {
240
+ fs.copyFileSync(path.join(sourceRoot, file), path.join(extensionRoot, file));
241
+ }
242
+
243
+ writePlist(
244
+ path.join(extensionRoot, `${IOS_EXTENSION_NAME}-Info.plist`),
245
+ extensionInfoPlist({ label, supportedDomains, aaguid, biometricRequirement }),
246
+ );
247
+ writePlist(
248
+ path.join(extensionRoot, `${IOS_EXTENSION_NAME}.entitlements`),
249
+ extensionEntitlementsPlist({ appGroup }),
250
+ );
251
+ return config;
252
+ },
253
+ ]);
254
+
255
+ config = withXcodeProject(config, (config) => {
256
+ const project = config.modResults;
257
+ const bundleIdentifier = `${getIosBundleIdentifier(config)}.PasskeyAutofillCredentialProvider`;
258
+ const buildNumber = config.ios?.buildNumber || config.versionCode || "1";
259
+ const rawDevelopmentTeam =
260
+ props.developmentTeam || props.appleTeamId || getDevelopmentTeam(project);
261
+ const developmentTeam = rawDevelopmentTeam
262
+ ? rawDevelopmentTeam.startsWith('"')
263
+ ? rawDevelopmentTeam
264
+ : `"${rawDevelopmentTeam}"`
265
+ : '"$(DEVELOPMENT_TEAM)"';
266
+ let target =
267
+ getExtensionTarget(project) ||
268
+ project.addTarget(IOS_EXTENSION_NAME, "app_extension", IOS_EXTENSION_NAME, bundleIdentifier);
269
+
270
+ if (target?.uuid) {
271
+ ensureSourceFilesInTarget(
272
+ project,
273
+ target.uuid,
274
+ IOS_EXTENSION_SOURCE_FILES.map((file) => `${IOS_EXTENSION_NAME}/${file}`),
275
+ );
276
+ if (!hasBuildPhase(project, target.uuid, "PBXFrameworksBuildPhase")) {
277
+ project.addBuildPhase(
278
+ ["AuthenticationServices.framework", "CryptoKit.framework"],
279
+ "PBXFrameworksBuildPhase",
280
+ "Frameworks",
281
+ target.uuid,
282
+ );
283
+ }
284
+ addSwiftPackageProduct(project, {
285
+ targetUuid: target.uuid,
286
+ packageName: "deterministic-P256-swift",
287
+ productName: DETERMINISTIC_P256_PRODUCT_NAME,
288
+ repositoryURL: props.deterministicP256PackageURL || DETERMINISTIC_P256_PACKAGE_URL,
289
+ branch: props.deterministicP256PackageBranch || "main",
290
+ });
291
+ setExtensionBuildSettings(project, target.uuid, {
292
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: '"AccentColor"',
293
+ APPLICATION_EXTENSION_API_ONLY: "YES",
294
+ CODE_SIGN_ENTITLEMENTS: `"${IOS_EXTENSION_NAME}/${IOS_EXTENSION_NAME}.entitlements"`,
295
+ CURRENT_PROJECT_VERSION: `"${buildNumber}"`,
296
+ DEVELOPMENT_TEAM: developmentTeam,
297
+ IPHONEOS_DEPLOYMENT_TARGET: "17.0",
298
+ MARKETING_VERSION: `"${config.version || "1.0.0"}"`,
299
+ PASSKEY_AUTOFILL_APP_GROUP: `"${appGroup}"`,
300
+ PRODUCT_BUNDLE_IDENTIFIER: `"${bundleIdentifier}"`,
301
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS: "PASSKEY_AUTOFILL_EXTENSION",
302
+ HEADER_SEARCH_PATHS: [
303
+ '"$(inherited)"',
304
+ '"$(SRCROOT)/Pods/Headers/Public"',
305
+ '"$(SRCROOT)/Pods/Headers/Public/MMKVCore"',
306
+ ],
307
+ LIBRARY_SEARCH_PATHS: [
308
+ '"$(inherited)"',
309
+ '"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/MMKVCore"',
310
+ ],
311
+ OTHER_LDFLAGS: ['"$(inherited)"', '"-lMMKVCore"', '"-lc++"', '"-lz"'],
312
+ SWIFT_OBJC_BRIDGING_HEADER: `"${IOS_EXTENSION_NAME}/PasskeyAutofillCredentialProvider-Bridging-Header.h"`,
313
+ SWIFT_VERSION: "5.9",
314
+ });
315
+ }
316
+
317
+ return config;
318
+ });
319
+
320
+ return config;
321
+ };
322
+
323
+ const addSwiftPackageProduct = (
324
+ project,
325
+ { targetUuid, packageName, productName, repositoryURL, branch },
326
+ ) => {
327
+ const objects = project.hash.project.objects;
328
+ objects.XCRemoteSwiftPackageReference = objects.XCRemoteSwiftPackageReference || {};
329
+ objects.XCSwiftPackageProductDependency = objects.XCSwiftPackageProductDependency || {};
330
+ objects.PBXBuildFile = objects.PBXBuildFile || {};
331
+
332
+ const packageComment = `XCRemoteSwiftPackageReference "${packageName}"`;
333
+ let packageUuid = Object.keys(objects.XCRemoteSwiftPackageReference).find(
334
+ (key) =>
335
+ !key.endsWith("_comment") &&
336
+ objects.XCRemoteSwiftPackageReference[key].repositoryURL === `"${repositoryURL}"`,
337
+ );
338
+ if (!packageUuid) {
339
+ packageUuid = project.generateUuid();
340
+ objects.XCRemoteSwiftPackageReference[packageUuid] = {
341
+ isa: "XCRemoteSwiftPackageReference",
342
+ repositoryURL: `"${repositoryURL}"`,
343
+ requirement: {
344
+ branch,
345
+ kind: "branch",
346
+ },
347
+ };
348
+ objects.XCRemoteSwiftPackageReference[`${packageUuid}_comment`] = packageComment;
349
+ }
350
+
351
+ const firstProject = project.getFirstProject().firstProject;
352
+ firstProject.packageReferences = firstProject.packageReferences || [];
353
+ if (!firstProject.packageReferences.some((reference) => reference.value === packageUuid)) {
354
+ firstProject.packageReferences.push({ value: packageUuid, comment: packageComment });
355
+ }
356
+
357
+ let productUuid = Object.keys(objects.XCSwiftPackageProductDependency).find(
358
+ (key) =>
359
+ !key.endsWith("_comment") &&
360
+ objects.XCSwiftPackageProductDependency[key].package === packageUuid &&
361
+ objects.XCSwiftPackageProductDependency[key].productName === `"${productName}"`,
362
+ );
363
+ if (!productUuid) {
364
+ productUuid = project.generateUuid();
365
+ objects.XCSwiftPackageProductDependency[productUuid] = {
366
+ isa: "XCSwiftPackageProductDependency",
367
+ package: packageUuid,
368
+ productName: `"${productName}"`,
369
+ };
370
+ objects.XCSwiftPackageProductDependency[`${productUuid}_comment`] = productName;
371
+ }
372
+
373
+ const target = project.pbxNativeTargetSection()[targetUuid];
374
+ target.packageProductDependencies = target.packageProductDependencies || [];
375
+ if (!target.packageProductDependencies.some((dependency) => dependency.value === productUuid)) {
376
+ target.packageProductDependencies.push({ value: productUuid, comment: productName });
377
+ }
378
+
379
+ const frameworksPhase = target.buildPhases
380
+ .map((phase) => ({ phase, value: objects.PBXFrameworksBuildPhase?.[phase.value] }))
381
+ .find(({ value }) => value)?.value;
382
+ if (!frameworksPhase) {
383
+ return;
384
+ }
385
+ const alreadyLinked = frameworksPhase.files.some(
386
+ (file) => file.comment === `${productName} in Frameworks`,
387
+ );
388
+ if (!alreadyLinked) {
389
+ const buildFileUuid = project.generateUuid();
390
+ objects.PBXBuildFile[buildFileUuid] = {
391
+ isa: "PBXBuildFile",
392
+ productRef: productUuid,
393
+ productRef_comment: productName,
394
+ };
395
+ objects.PBXBuildFile[`${buildFileUuid}_comment`] = `${productName} in Frameworks`;
396
+ frameworksPhase.files.push({
397
+ value: buildFileUuid,
398
+ comment: `${productName} in Frameworks`,
399
+ });
400
+ }
401
+ };
402
+
403
+ const hasBuildPhase = (project, targetUuid, phaseType) => {
404
+ const target = project.pbxNativeTargetSection()[targetUuid];
405
+ return target.buildPhases.some((phase) => project.hash.project.objects[phaseType]?.[phase.value]);
406
+ };
407
+
408
+ const getBuildPhase = (project, targetUuid, phaseType) => {
409
+ const target = project.pbxNativeTargetSection()[targetUuid];
410
+ const phases = project.hash.project.objects[phaseType] || {};
411
+ const phaseRef = target.buildPhases.find((phase) => phases[phase.value]);
412
+ return phaseRef ? phases[phaseRef.value] : null;
413
+ };
414
+
415
+ const getFileType = (filePath) => {
416
+ if (filePath.endsWith(".swift")) return "sourcecode.swift";
417
+ if (filePath.endsWith(".mm")) return "sourcecode.cpp.objcpp";
418
+ if (filePath.endsWith(".m")) return "sourcecode.c.objc";
419
+ if (filePath.endsWith(".h")) return "sourcecode.c.h";
420
+ return "text";
421
+ };
422
+
423
+ const getOrCreateFileReference = (project, filePath) => {
424
+ const fileReferences = project.pbxFileReferenceSection();
425
+ const existingUuid = Object.keys(fileReferences).find((uuid) => {
426
+ if (uuid.endsWith("_comment")) return false;
427
+ const refPath = fileReferences[uuid]?.path;
428
+ return refPath === filePath || refPath === `"${filePath}"`;
429
+ });
430
+ if (existingUuid) {
431
+ return existingUuid;
432
+ }
433
+
434
+ const uuid = project.generateUuid();
435
+ const basename = path.basename(filePath);
436
+ fileReferences[uuid] = {
437
+ isa: "PBXFileReference",
438
+ fileEncoding: 4,
439
+ lastKnownFileType: getFileType(filePath),
440
+ name: basename,
441
+ path: filePath,
442
+ sourceTree: '"<group>"',
443
+ };
444
+ fileReferences[`${uuid}_comment`] = basename;
445
+ return uuid;
446
+ };
447
+
448
+ const getOrCreateBuildFile = (project, fileRefUuid, basename) => {
449
+ const buildFiles = project.pbxBuildFileSection();
450
+ const existingUuid = Object.keys(buildFiles).find((uuid) => {
451
+ if (uuid.endsWith("_comment")) return false;
452
+ return buildFiles[uuid]?.fileRef === fileRefUuid;
453
+ });
454
+ if (existingUuid) {
455
+ return existingUuid;
456
+ }
457
+
458
+ const uuid = project.generateUuid();
459
+ buildFiles[uuid] = {
460
+ isa: "PBXBuildFile",
461
+ fileRef: fileRefUuid,
462
+ fileRef_comment: basename,
463
+ };
464
+ buildFiles[`${uuid}_comment`] = `${basename} in Sources`;
465
+ return uuid;
466
+ };
467
+
468
+ const ensureSourceFilesInTarget = (project, targetUuid, files) => {
469
+ const sourcesPhase = getBuildPhase(project, targetUuid, "PBXSourcesBuildPhase");
470
+ if (!sourcesPhase) {
471
+ project.addBuildPhase(files, "PBXSourcesBuildPhase", "Sources", targetUuid);
472
+ return;
473
+ }
474
+
475
+ for (const filePath of files) {
476
+ const basename = path.basename(filePath);
477
+ const alreadyInSources = sourcesPhase.files.some(
478
+ (file) => file.comment === `${basename} in Sources`,
479
+ );
480
+ if (alreadyInSources) {
481
+ continue;
482
+ }
483
+
484
+ const fileRefUuid = getOrCreateFileReference(project, filePath);
485
+ const buildFileUuid = getOrCreateBuildFile(project, fileRefUuid, basename);
486
+ sourcesPhase.files.push({
487
+ value: buildFileUuid,
488
+ comment: `${basename} in Sources`,
489
+ });
490
+ }
491
+ };
492
+
493
+ const setExtensionBuildSettings = (project, targetUuid, settings) => {
494
+ const target = project.pbxNativeTargetSection()[targetUuid];
495
+ const configurationList = project.pbxXCConfigurationList()[target.buildConfigurationList];
496
+ for (const config of configurationList.buildConfigurations) {
497
+ const buildConfig = project.pbxXCBuildConfigurationSection()[config.value];
498
+ buildConfig.buildSettings = {
499
+ ...buildConfig.buildSettings,
500
+ ...settings,
501
+ };
502
+ }
503
+ };
504
+
12
505
  const withAndroidCookieModule = (config) => {
13
506
  return withDangerousMod(config, [
14
507
  "android",
@@ -16,11 +509,7 @@ const withAndroidCookieModule = (config) => {
16
509
  const projectRoot = config.modRequest.projectRoot;
17
510
  const packageId = config.android?.package || "co.algorand.auth.example";
18
511
  const packagePath = packageId.replace(/\./g, "/");
19
- const targetDir = path.join(
20
- projectRoot,
21
- "android/app/src/main/java",
22
- packagePath
23
- );
512
+ const targetDir = path.join(projectRoot, "android/app/src/main/java", packagePath);
24
513
 
25
514
  // Ensure the directory exists
26
515
  fs.mkdirSync(targetDir, { recursive: true });
@@ -80,14 +569,8 @@ class CookiePackage : ReactPackage {
80
569
  }
81
570
  `;
82
571
 
83
- fs.writeFileSync(
84
- path.join(targetDir, "CookieModule.kt"),
85
- cookieModuleContent
86
- );
87
- fs.writeFileSync(
88
- path.join(targetDir, "CookiePackage.kt"),
89
- cookiePackageContent
90
- );
572
+ fs.writeFileSync(path.join(targetDir, "CookieModule.kt"), cookieModuleContent);
573
+ fs.writeFileSync(path.join(targetDir, "CookiePackage.kt"), cookiePackageContent);
91
574
 
92
575
  return config;
93
576
  },
@@ -112,10 +595,7 @@ const withUserAgent = (config) => {
112
595
 
113
596
  imports.forEach((imp) => {
114
597
  if (!content.includes(imp)) {
115
- content = content.replace(
116
- /package .*\n/,
117
- (match) => `${match}${imp}\n`
118
- );
598
+ content = content.replace(/package .*\n/, (match) => `${match}${imp}\n`);
119
599
  }
120
600
  });
121
601
 
@@ -145,17 +625,14 @@ const withUserAgent = (config) => {
145
625
  `;
146
626
 
147
627
  if (!content.includes("OkHttpClientProvider.setOkHttpClientFactory")) {
148
- content = content.replace(
149
- /super\.onCreate\(\)/,
150
- `super.onCreate()${okHttpClientCode}`
151
- );
628
+ content = content.replace(/super\.onCreate\(\)/, `super.onCreate()${okHttpClientCode}`);
152
629
  }
153
630
 
154
631
  // Register CookiePackage if it exists
155
632
  if (!content.includes("add(CookiePackage())")) {
156
633
  content = content.replace(
157
634
  /PackageList\(this\)\.packages\.apply \{/,
158
- `PackageList(this).packages.apply {\n add(CookiePackage())`
635
+ `PackageList(this).packages.apply {\n add(CookiePackage())`,
159
636
  );
160
637
  }
161
638
 
@@ -165,9 +642,11 @@ const withUserAgent = (config) => {
165
642
  };
166
643
 
167
644
  const withPasskeyAutofill = (config, props = {}) => {
168
- const site = props.site || "https://fido.shore-tech.net";
645
+ const site = props.site || "https://debug.liquidauth.com";
169
646
  const label = props.label || "My Credential Provider";
647
+ const aaguid = getAaguid(props);
170
648
 
649
+ config = withIosPasskeyAutofill(config, props);
171
650
  config = withAndroidCookieModule(config);
172
651
  config = withUserAgent(config);
173
652
 
@@ -175,17 +654,13 @@ const withPasskeyAutofill = (config, props = {}) => {
175
654
  // 1. Add asset_statements meta-data to MainActivity
176
655
  config = withAndroidManifest(config, async (config) => {
177
656
  const mainActivity = config.modResults.manifest.application[0].activity.find(
178
- (a) => a["$"]["android:name"] === ".MainActivity"
657
+ (a) => a["$"]["android:name"] === ".MainActivity",
179
658
  );
180
659
  if (mainActivity) {
181
660
  if (!mainActivity["meta-data"]) {
182
661
  mainActivity["meta-data"] = [];
183
662
  }
184
- if (
185
- !mainActivity["meta-data"].find(
186
- (m) => m["$"]["android:name"] === "asset_statements"
187
- )
188
- ) {
663
+ if (!mainActivity["meta-data"].find((m) => m["$"]["android:name"] === "asset_statements")) {
189
664
  mainActivity["meta-data"].push({
190
665
  $: {
191
666
  "android:name": "asset_statements",
@@ -197,48 +672,104 @@ const withPasskeyAutofill = (config, props = {}) => {
197
672
  return config;
198
673
  });
199
674
 
675
+ // Expose the configured biometric requirement to the native provider/activities.
676
+ config = withAndroidManifest(config, (config) => {
677
+ const application = config.modResults.manifest.application[0];
678
+ if (!application["meta-data"]) {
679
+ application["meta-data"] = [];
680
+ }
681
+ const name = ANDROID_BIOMETRIC_META_DATA_NAME;
682
+ const existing = application["meta-data"].find((m) => m["$"]["android:name"] === name);
683
+ const value = getBiometricRequirement(props);
684
+ if (existing) {
685
+ existing["$"]["android:value"] = value;
686
+ } else {
687
+ application["meta-data"].push({ $: { "android:name": name, "android:value": value } });
688
+ }
689
+ return config;
690
+ });
691
+
200
692
  // 2. Add asset_statements string to strings.xml
201
693
  config = withStringsXml(config, (config) => {
202
- config.modResults = AndroidConfig.Strings.setStringItem(
203
- [
204
- {
205
- $: { name: "asset_statements", translatable: "false" },
206
- _: JSON.stringify([
207
- {
208
- relation: [
209
- "delegate_permission/common.handle_all_urls",
210
- "delegate_permission/common.get_login_creds",
211
- ],
212
- target: {
213
- namespace: "web",
214
- site: site,
215
- },
694
+ const stringItems = [
695
+ {
696
+ $: { name: "asset_statements", translatable: "false" },
697
+ _: JSON.stringify([
698
+ {
699
+ relation: [
700
+ "delegate_permission/common.handle_all_urls",
701
+ "delegate_permission/common.get_login_creds",
702
+ ],
703
+ target: {
704
+ namespace: "web",
705
+ site: site,
216
706
  },
217
- ]),
218
- },
219
- {
220
- $: { name: "passkey_autofill_label", translatable: "true" },
221
- _: label,
222
- },
223
- ],
224
- config.modResults
225
- );
707
+ },
708
+ ]),
709
+ },
710
+ {
711
+ $: { name: "passkey_autofill_label", translatable: "true" },
712
+ _: label,
713
+ },
714
+ ];
715
+ if (aaguid) {
716
+ stringItems.push({
717
+ $: { name: "passkey_autofill_aaguid", translatable: "false" },
718
+ _: aaguid,
719
+ });
720
+ }
721
+ config.modResults = AndroidConfig.Strings.setStringItem(stringItems, config.modResults);
722
+ return config;
723
+ });
724
+
725
+ // 3. Inject `pickFirst '**/libmmkv.so'` into the consumer app's
726
+ // `android/app/build.gradle`. The native `libmmkv.so` is bundled by both
727
+ // this module (via `io.github.zhongwuzw:mmkv`) and `react-native-mmkv`
728
+ // (which also depends on the same artifact, sometimes pinned to a different
729
+ // version). When two copies end up on the merged-libs path, AGP fails
730
+ // `mergeDebugNativeLibs` with: "2 files found with path
731
+ // 'lib/<abi>/libmmkv.so'". Picking the first occurrence is safe because
732
+ // the `com.tencent.mmkv.MMKV` Java surface we touch is ABI-stable across
733
+ // the 2.x range.
734
+ config = withAppBuildGradle(config, (config) => {
735
+ const marker = "pickFirst '**/libmmkv.so'";
736
+ if (config.modResults.contents.includes(marker)) {
737
+ return config;
738
+ }
739
+ let contents = config.modResults.contents;
740
+ if (/packagingOptions\s*\{/.test(contents)) {
741
+ contents = contents.replace(/packagingOptions\s*\{/, `packagingOptions {\n ${marker}`);
742
+ } else if (/android\s*\{/.test(contents)) {
743
+ contents = contents.replace(
744
+ /android\s*\{/,
745
+ `android {\n packagingOptions {\n ${marker}\n }\n`,
746
+ );
747
+ }
748
+ config.modResults.contents = contents;
226
749
  return config;
227
750
  });
228
751
 
229
- // 3. Add flatDir repository for local AAR
752
+ // 4. Add local Maven repository for local AAR
230
753
  config = withProjectBuildGradle(config, (config) => {
231
- if (config.modResults.contents.includes("dP256Android-release")) {
754
+ if (config.modResults.contents.includes("android/libs/repo")) {
232
755
  return config;
233
756
  }
234
- const libsDir = path.join(__dirname, "android/libs");
757
+ // Dynamically find the path to the library's android/libs/repo directory
758
+ const projectRoot = config.modRequest.projectRoot;
759
+ const libraryRepoPath = path.join(__dirname, "android/libs/repo");
760
+ const relativeRepoPath = path.relative(projectRoot, libraryRepoPath).replace(/\\/g, "/");
761
+
762
+ // In Gradle, rootDir is the android directory of the app.
763
+ // So the path to the repo relative to rootDir is ../<relativeRepoPath>
764
+ const repoInjectedCode = `allprojects {
765
+ repositories {
766
+ maven {
767
+ url = uri("\${rootDir}/../${relativeRepoPath}")
768
+ }`;
769
+
235
770
  config.modResults.contents = config.modResults.contents.replace(
236
771
  /allprojects\s*{[\s\n]*repositories\s*{/,
237
- `allprojects {
238
- repositories {
239
- flatDir {
240
- dirs "${libsDir}"
241
- }`
772
+ repoInjectedCode,
242
773
  );
243
774
  return config;
244
775
  });
@@ -247,3 +778,5 @@ const withPasskeyAutofill = (config, props = {}) => {
247
778
  };
248
779
 
249
780
  module.exports = withPasskeyAutofill;
781
+ module.exports.getBiometricRequirement = getBiometricRequirement;
782
+ module.exports.getAaguid = getAaguid;