@authon/react-native-wallet 0.4.0 → 0.5.1

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/build/native.d.ts CHANGED
@@ -8,6 +8,9 @@ export type NativeResult<T> = {
8
8
  message?: string;
9
9
  diagnosticId?: string | null;
10
10
  retryable?: boolean;
11
+ reason?: string | null;
12
+ phase?: string | null;
13
+ clientAttemptId?: string | null;
11
14
  };
12
15
  export interface NativeConfig {
13
16
  publishableKey: string;
@@ -36,6 +39,12 @@ export interface AuthonWalletNativeModule {
36
39
  upgradeBackup(id: string, token: string): Promise<NativeResult<unknown>>;
37
40
  restore(id: string, token: string, reset: boolean): Promise<NativeResult<unknown>>;
38
41
  revokeDevice(id: string, token: string, deviceId: string): Promise<NativeResult<unknown>>;
42
+ /** iOS only: native Sign in with Apple; resolves the server's sign-in result (JSON), never Apple's tokens. */
43
+ signInWithApple?(config: {
44
+ publishableKey: string;
45
+ apiUrl: string;
46
+ appId: string | null;
47
+ }): Promise<NativeResult<unknown>>;
39
48
  }
40
49
  export declare function getNativeModule(): AuthonWalletNativeModule | null;
41
50
  /** Tests only: replaces the native module (pass undefined to restore). */
@@ -1 +1 @@
1
- {"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"native.js","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AAyChE,IAAI,QAAqD,CAAC;AAE1D,MAAM,UAAU,eAAe;IAC7B,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IAC5C,OAAO,2BAA2B,CAA2B,cAAc,CAAC,CAAC;AAC/E,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,yBAAyB,CAAC,MAAmD;IAC3F,QAAQ,GAAG,MAAM,CAAC;AACpB,CAAC"}
1
+ {"version":3,"file":"native.js","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AAoDhE,IAAI,QAAqD,CAAC;AAE1D,MAAM,UAAU,eAAe;IAC7B,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IAC5C,OAAO,2BAA2B,CAA2B,cAAc,CAAC,CAAC;AAC/E,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,yBAAyB,CAAC,MAAmD;IAC3F,QAAQ,GAAG,MAAM,CAAC;AACpB,CAAC"}
@@ -17,12 +17,14 @@ Pod::Spec.new do |s|
17
17
  s.dependency 'ExpoModulesCore'
18
18
  s.frameworks = 'Security', 'CryptoKit', 'LocalAuthentication', 'AuthenticationServices', 'SwiftUI'
19
19
 
20
- # vendor/ is generated from swift/Sources by scripts/sync-native-wallet.mjs.
20
+ # vendor/ is generated from swift/Sources by scripts/sync-native-wallet.mjs (wallet core and the
21
+ # standalone native Sign in with Apple).
21
22
  # CSecp256k1: compile only build/*.c (they include src/*.c); CKeccak: sha3.c, ripemd160.c and memzero.c;
22
23
  # CMonocypher (Ed25519, deterministic like the web wallet): both .c files, no flags.
23
24
  s.source_files = [
24
25
  '*.swift',
25
26
  'vendor/AuthonWallet/**/*.swift',
27
+ 'vendor/AuthonAppleSignIn/*.swift',
26
28
  'vendor/CSecp256k1/build/*.c',
27
29
  'vendor/CSecp256k1/include/*.h',
28
30
  'vendor/CKeccak/sha3.c',
@@ -18,6 +18,12 @@ public final class AuthonWalletModule: Module {
18
18
  self.envelope { try self.configure(id, config); return NSNull() }
19
19
  }
20
20
 
21
+ // Native Sign in with Apple. Resolves the API's sign-in result as JSON (a session or an MFA
22
+ // challenge); Apple's identity token, authorization code, nonce and verifier stay in Swift.
23
+ AsyncFunction("signInWithApple") { (config: [String: Any]) async -> [String: Any] in
24
+ await Self.signInWithApple(config)
25
+ }
26
+
21
27
  AsyncFunction("release") { (id: String) -> [String: Any] in
22
28
  self.lock.withLock { self.wallets[id] = nil }
23
29
  return ["ok": true, "value": NSNull()]
@@ -153,6 +159,26 @@ public final class AuthonWalletModule: Module {
153
159
  }
154
160
  }
155
161
 
162
+ private static func signInWithApple(_ config: [String: Any]) async -> [String: Any] {
163
+ guard let publishableKey = config["publishableKey"] as? String, !publishableKey.isEmpty else {
164
+ return ["ok": false, "code": "OAUTH_START_FAILED", "message": "publishableKey required", "diagnosticId": NSNull(), "retryable": false]
165
+ }
166
+ let apiUrl = (config["apiUrl"] as? String).flatMap { $0.isEmpty ? nil : $0 } ?? "https://api.authon.dev"
167
+ let appId = (config["appId"] as? String).flatMap { $0.isEmpty ? nil : $0 }
168
+ do {
169
+ let result = try await AuthonAppleSignIn(publishableKey: publishableKey, apiURL: apiUrl, appId: appId).signIn()
170
+ let data = try JSONSerialization.data(withJSONObject: result)
171
+ return ["ok": true, "value": String(decoding: data, as: UTF8.self)]
172
+ } catch let e as AppleSignInError {
173
+ return [
174
+ "ok": false, "code": e.code, "message": e.message, "diagnosticId": orNull(e.diagnosticId),
175
+ "serverCode": orNull(e.serverCode), "retryable": e.retryable,
176
+ ]
177
+ } catch {
178
+ return ["ok": false, "code": "API_ERROR", "message": "unexpected error", "diagnosticId": NSNull(), "retryable": true]
179
+ }
180
+ }
181
+
156
182
  private static func failure(_ error: Error) -> [String: Any] {
157
183
  let e: WalletError
158
184
  if let w = error as? WalletError {
@@ -0,0 +1,285 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from swift/Sources/AuthonAppleSignIn/AppleSignIn.swift. Do not edit: change the source and run the script.
2
+ import AuthenticationServices
3
+ import CryptoKit
4
+ import Foundation
5
+ import Security
6
+ #if canImport(UIKit)
7
+ import UIKit
8
+ #elseif canImport(AppKit)
9
+ import AppKit
10
+ #endif
11
+
12
+ /// Error of the native Apple sign-in. `code` is a sign-in code shared by every Authon SDK
13
+ /// (docs/wallet/NATIVE.md section 10): OAUTH_CANCELLED (show nothing), OAUTH_START_FAILED,
14
+ /// OAUTH_REDIRECT_NOT_ALLOWED (app not registered or approved), OAUTH_PROVIDER_ERROR,
15
+ /// OAUTH_STATE_MISMATCH, OAUTH_EXCHANGE_FAILED, ACCOUNT_LINK_REQUIRED, NETWORK_ERROR,
16
+ /// RATE_LIMITED, API_ERROR.
17
+ public struct AppleSignInError: Error, Equatable, LocalizedError, Sendable {
18
+ public let code: String
19
+ public let message: String
20
+ /// The server's request ID when the error came from an API response.
21
+ public let diagnosticId: String?
22
+ /// The server's own `code`, when it sent one (e.g. APP_NOT_APPROVED).
23
+ public let serverCode: String?
24
+ public let statusCode: Int
25
+ /// Whether the same call can succeed again. False for Apple ID sheet failures: showing the
26
+ /// same sheet again does not help (e.g. no Apple account on the device).
27
+ public let retryable: Bool
28
+
29
+ public var errorDescription: String? { message }
30
+
31
+ init(_ code: String, _ message: String, diagnosticId: String? = nil, serverCode: String? = nil, statusCode: Int = 0, retryable: Bool? = nil) {
32
+ self.code = code
33
+ self.message = message
34
+ self.diagnosticId = diagnosticId
35
+ self.serverCode = serverCode
36
+ self.statusCode = statusCode
37
+ self.retryable = retryable ?? ["OAUTH_START_FAILED", "OAUTH_PROVIDER_ERROR", "OAUTH_STATE_MISMATCH", "OAUTH_EXCHANGE_FAILED", "NETWORK_ERROR", "RATE_LIMITED", "API_ERROR"].contains(code)
38
+ }
39
+ }
40
+
41
+ /// What the Apple ID sheet returned. Credentials only travel to the Authon API.
42
+ public struct AppleIDResult: Sendable {
43
+ public var identityToken: Data?
44
+ public var authorizationCode: Data?
45
+ public var fullName: PersonNameComponents?
46
+
47
+ public init(identityToken: Data?, authorizationCode: Data?, fullName: PersonNameComponents?) {
48
+ self.identityToken = identityToken
49
+ self.authorizationCode = authorizationCode
50
+ self.fullName = fullName
51
+ }
52
+ }
53
+
54
+ /// Shows the Apple ID sheet for a nonce (already hashed: lowercase hex SHA-256 of the server nonce).
55
+ public protocol AppleIDAuthorizing: Sendable {
56
+ func authorize(hashedNonce: String) async throws -> AppleIDResult
57
+ }
58
+
59
+ /// Native Sign in with Apple against Authon's nonce-bound flow:
60
+ /// `POST /v1/auth/oauth/native/apple/start` → Apple ID sheet → `POST /v1/auth/oauth/native/apple/complete`.
61
+ /// There is no browser and no web fallback. An attempt is used once: after any failure, start again.
62
+ /// The identity token, authorization code, nonce and verifier are never logged or returned.
63
+ public final class AuthonAppleSignIn: @unchecked Sendable {
64
+ private let base: String
65
+ private let publishableKey: String
66
+ private let appId: String?
67
+ private let authorizer: AppleIDAuthorizing
68
+ private let session: URLSession
69
+
70
+ /// - Parameters:
71
+ /// - appId: `TEAMID.bundleId` as registered in the Authon console; detected from the default
72
+ /// keychain access group when nil.
73
+ /// - anchor: The window for the Apple ID sheet (the key window when nil).
74
+ public convenience init(publishableKey: String, apiURL: String = "https://api.authon.dev", appId: String? = nil,
75
+ anchor: (@MainActor () -> ASPresentationAnchor?)? = nil) {
76
+ self.init(publishableKey: publishableKey, apiURL: apiURL, appId: appId, authorizer: SystemAppleIDAuthorizer(anchor: anchor), session: .shared)
77
+ }
78
+
79
+ public init(publishableKey: String, apiURL: String, appId: String?, authorizer: AppleIDAuthorizing, session: URLSession = .shared) {
80
+ var b = apiURL
81
+ while b.hasSuffix("/") { b.removeLast() }
82
+ base = b
83
+ self.publishableKey = publishableKey
84
+ self.appId = appId
85
+ self.authorizer = authorizer
86
+ self.session = session
87
+ }
88
+
89
+ /// Runs the sign-in and returns the API's JSON result: a session
90
+ /// (`accessToken`, `refreshToken`, `expiresIn`, `user`) or an MFA challenge (`mfaRequired`, `mfaToken`).
91
+ public func signIn() async throws -> [String: Any] {
92
+ guard let appId = appId ?? Self.detectAppIdentifier() else {
93
+ throw AppleSignInError("OAUTH_REDIRECT_NOT_ALLOWED", "appId (TEAMID.bundleId) could not be determined")
94
+ }
95
+ var verifierBytes = [UInt8](repeating: 0, count: 32)
96
+ guard SecRandomCopyBytes(kSecRandomDefault, verifierBytes.count, &verifierBytes) == errSecSuccess else {
97
+ throw AppleSignInError("OAUTH_START_FAILED", "random source unavailable")
98
+ }
99
+ let verifier = Self.base64url(Data(verifierBytes))
100
+ let challenge = Self.base64url(Data(SHA256.hash(data: Data(verifier.utf8))))
101
+
102
+ let start: [String: Any]
103
+ do {
104
+ start = try await post("/v1/auth/oauth/native/apple/start", ["appId": appId, "codeChallenge": challenge])
105
+ } catch let e as AppleSignInError where e.code == "API_ERROR" {
106
+ throw AppleSignInError("OAUTH_START_FAILED", e.message, diagnosticId: e.diagnosticId, serverCode: e.serverCode, statusCode: e.statusCode)
107
+ }
108
+ guard let attemptId = start["attemptId"] as? String, let nonce = start["nonce"] as? String, !nonce.isEmpty else {
109
+ throw AppleSignInError("OAUTH_START_FAILED", "Unexpected response from the server")
110
+ }
111
+
112
+ let apple = try await authorizer.authorize(hashedNonce: Self.hashNonce(nonce))
113
+ guard let tokenData = apple.identityToken, let identityToken = String(data: tokenData, encoding: .utf8),
114
+ let codeData = apple.authorizationCode, let authorizationCode = String(data: codeData, encoding: .utf8) else {
115
+ throw AppleSignInError("OAUTH_PROVIDER_ERROR", "Apple returned no identity token or authorization code", retryable: false)
116
+ }
117
+ var body: [String: Any] = ["attemptId": attemptId, "codeVerifier": verifier, "identityToken": identityToken, "authorizationCode": authorizationCode]
118
+ if let name = apple.fullName.map({ PersonNameComponentsFormatter.localizedString(from: $0, style: .default) })?
119
+ .trimmingCharacters(in: .whitespacesAndNewlines), !name.isEmpty {
120
+ body["displayName"] = name
121
+ }
122
+ // Single use: no retry, whatever the outcome.
123
+ return try await post("/v1/auth/oauth/native/apple/complete", body)
124
+ }
125
+
126
+ /// Lowercase hex SHA-256 of the server nonce (what ASAuthorizationAppleIDRequest.nonce carries).
127
+ public static func hashNonce(_ nonce: String) -> String {
128
+ SHA256.hash(data: Data(nonce.utf8)).map { String(format: "%02x", $0) }.joined()
129
+ }
130
+
131
+ static func base64url(_ data: Data) -> String {
132
+ data.base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "")
133
+ }
134
+
135
+ // MARK: - HTTP
136
+
137
+ private func post(_ path: String, _ body: [String: Any]) async throws -> [String: Any] {
138
+ guard let url = URL(string: base + path) else { throw AppleSignInError("API_ERROR", "Invalid URL") }
139
+ var req = URLRequest(url: url)
140
+ req.httpMethod = "POST"
141
+ req.setValue("application/json", forHTTPHeaderField: "content-type")
142
+ req.setValue("application/json", forHTTPHeaderField: "accept")
143
+ req.setValue(publishableKey, forHTTPHeaderField: "x-api-key")
144
+ req.httpShouldHandleCookies = false
145
+ req.httpBody = try JSONSerialization.data(withJSONObject: body)
146
+ let data: Data
147
+ let response: URLResponse
148
+ do {
149
+ (data, response) = try await session.data(for: req)
150
+ } catch {
151
+ throw AppleSignInError("NETWORK_ERROR", "The server could not be reached")
152
+ }
153
+ let http = response as? HTTPURLResponse
154
+ let status = http?.statusCode ?? 0
155
+ let json = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any]
156
+ guard (200..<300).contains(status) else {
157
+ let serverCode = json?["code"] as? String
158
+ let id = (json?["requestId"] as? String) ?? http?.value(forHTTPHeaderField: "X-Request-Id")
159
+ let message = (json?["message"] as? String) ?? HTTPURLResponse.localizedString(forStatusCode: status)
160
+ throw AppleSignInError(Self.code(status: status, serverCode: serverCode), message, diagnosticId: id, serverCode: serverCode, statusCode: status)
161
+ }
162
+ guard let json else { throw AppleSignInError("API_ERROR", "Unexpected response from the server", statusCode: status) }
163
+ return json
164
+ }
165
+
166
+ static func code(status: Int, serverCode: String?) -> String {
167
+ switch serverCode {
168
+ case "OAUTH_STATE_INVALID": return "OAUTH_STATE_MISMATCH"
169
+ case "OAUTH_EXCHANGE_FAILED": return "OAUTH_EXCHANGE_FAILED"
170
+ case "OAUTH_REDIRECT_NOT_ALLOWED", "APP_NOT_REGISTERED", "APP_NOT_APPROVED": return "OAUTH_REDIRECT_NOT_ALLOWED"
171
+ case "EXPLICIT_IDENTITY_LINK_REQUIRED": return "ACCOUNT_LINK_REQUIRED"
172
+ case "RATE_LIMITED": return "RATE_LIMITED"
173
+ default: return status == 429 ? "RATE_LIMITED" : "API_ERROR"
174
+ }
175
+ }
176
+
177
+ /// `TEAMID.bundleId` from the app's default keychain access group.
178
+ static func detectAppIdentifier() -> String? {
179
+ var q: [String: Any] = [
180
+ kSecClass as String: kSecClassGenericPassword,
181
+ kSecAttrService as String: "dev.authon.apple-sign-in.app-id",
182
+ kSecAttrAccount as String: "probe",
183
+ kSecReturnAttributes as String: true,
184
+ ]
185
+ #if os(macOS)
186
+ q[kSecUseDataProtectionKeychain as String] = true
187
+ #endif
188
+ var out: AnyObject?
189
+ var status = SecItemCopyMatching(q as CFDictionary, &out)
190
+ if status == errSecItemNotFound {
191
+ var add = q
192
+ add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
193
+ status = SecItemAdd(add as CFDictionary, &out)
194
+ }
195
+ guard status == errSecSuccess, let attrs = out as? [String: Any], let group = attrs[kSecAttrAccessGroup as String] as? String,
196
+ group.contains(".") else { return nil }
197
+ return group
198
+ }
199
+ }
200
+
201
+ /// The system Apple ID sheet (ASAuthorizationController), shown immediately.
202
+ public final class SystemAppleIDAuthorizer: AppleIDAuthorizing, @unchecked Sendable {
203
+ private let anchor: (@MainActor () -> ASPresentationAnchor?)?
204
+
205
+ public init(anchor: (@MainActor () -> ASPresentationAnchor?)? = nil) {
206
+ self.anchor = anchor
207
+ }
208
+
209
+ @MainActor
210
+ public func authorize(hashedNonce: String) async throws -> AppleIDResult {
211
+ let request = ASAuthorizationAppleIDProvider().createRequest()
212
+ request.requestedScopes = [.fullName, .email]
213
+ request.nonce = hashedNonce
214
+ let delegate = AppleIDDelegate(anchor: anchor?() ?? Self.keyWindow())
215
+ let credential = try await delegate.run(request)
216
+ return AppleIDResult(identityToken: credential.identityToken, authorizationCode: credential.authorizationCode, fullName: credential.fullName)
217
+ }
218
+
219
+ @MainActor
220
+ static func keyWindow() -> ASPresentationAnchor {
221
+ #if canImport(UIKit)
222
+ let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
223
+ return scenes.flatMap(\.windows).first(where: \.isKeyWindow) ?? scenes.first?.windows.first ?? ASPresentationAnchor()
224
+ #else
225
+ return NSApplication.shared.keyWindow ?? NSApplication.shared.windows.first ?? ASPresentationAnchor()
226
+ #endif
227
+ }
228
+
229
+ /// ASAuthorizationError → sign-in code. Only `.canceled` is a cancel; everything else (including
230
+ /// 1000 `.unknown` after dismissing "sign in to your Apple account") is a provider error that
231
+ /// retrying the same sheet will not fix.
232
+ static func map(_ error: Error) -> AppleSignInError {
233
+ if let e = error as? ASAuthorizationError, e.code == .canceled {
234
+ return AppleSignInError("OAUTH_CANCELLED", "Sign in with Apple was cancelled")
235
+ }
236
+ return AppleSignInError("OAUTH_PROVIDER_ERROR", "Sign in with Apple failed (\((error as NSError).code))", retryable: false)
237
+ }
238
+ }
239
+
240
+ @MainActor
241
+ private final class AppleIDDelegate: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
242
+ private let anchor: ASPresentationAnchor
243
+ private var continuation: CheckedContinuation<ASAuthorizationAppleIDCredential, Error>?
244
+ private var retain: AppleIDDelegate?
245
+
246
+ init(anchor: ASPresentationAnchor) {
247
+ self.anchor = anchor
248
+ }
249
+
250
+ func run(_ request: ASAuthorizationRequest) async throws -> ASAuthorizationAppleIDCredential {
251
+ try await withCheckedThrowingContinuation { cont in
252
+ continuation = cont
253
+ retain = self
254
+ let controller = ASAuthorizationController(authorizationRequests: [request])
255
+ controller.delegate = self
256
+ controller.presentationContextProvider = self
257
+ controller.performRequests()
258
+ }
259
+ }
260
+
261
+ nonisolated func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
262
+ MainActor.assumeIsolated { anchor }
263
+ }
264
+
265
+ nonisolated func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
266
+ MainActor.assumeIsolated {
267
+ if let credential = authorization.credential as? ASAuthorizationAppleIDCredential {
268
+ finish(.success(credential))
269
+ } else {
270
+ finish(.failure(AppleSignInError("OAUTH_PROVIDER_ERROR", "Unexpected Apple credential type", retryable: false)))
271
+ }
272
+ }
273
+ }
274
+
275
+ nonisolated func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
276
+ let mapped = SystemAppleIDAuthorizer.map(error)
277
+ MainActor.assumeIsolated { finish(.failure(mapped)) }
278
+ }
279
+
280
+ private func finish(_ result: Result<ASAuthorizationAppleIDCredential, Error>) {
281
+ continuation?.resume(with: result)
282
+ continuation = nil
283
+ retain = nil
284
+ }
285
+ }
@@ -1,4 +1,4 @@
1
1
  # Generated sources
2
2
 
3
- Everything in this folder is copied from `swift/Sources/{AuthonWallet,CSecp256k1,CKeccak,CMonocypher}` by
3
+ Everything in this folder is copied from `swift/Sources/{AuthonWallet,AuthonAppleSignIn,CSecp256k1,CKeccak,CMonocypher}` by
4
4
  `scripts/sync-native-wallet.mjs` (run by the package build). Do not edit these files; change the native SDK sources and run the script. `--check` fails on any difference.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@authon/react-native-wallet",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Authon embedded wallet for React Native (Expo native module wrapping the Swift and Kotlin wallet SDKs)",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -24,6 +24,14 @@
24
24
  "README.md",
25
25
  "CHANGELOG.md"
26
26
  ],
27
+ "scripts": {
28
+ "sync:native": "node ../../scripts/sync-native-wallet.mjs",
29
+ "check:native": "node ../../scripts/sync-native-wallet.mjs --check",
30
+ "build": "node ../../scripts/sync-native-wallet.mjs && tsc -p tsconfig.build.json",
31
+ "prepublishOnly": "node ../../scripts/sync-native-wallet.mjs --check && tsc -p tsconfig.build.json",
32
+ "test": "vitest run --root ../.. --passWithNoTests packages/react-native-wallet",
33
+ "typecheck": "tsc --noEmit"
34
+ },
27
35
  "license": "MIT",
28
36
  "repository": {
29
37
  "type": "git",
@@ -58,12 +66,5 @@
58
66
  "expo-modules-core": "55.0.26",
59
67
  "react": "^19.0.0",
60
68
  "typescript": "^5.9.3"
61
- },
62
- "scripts": {
63
- "sync:native": "node ../../scripts/sync-native-wallet.mjs",
64
- "check:native": "node ../../scripts/sync-native-wallet.mjs --check",
65
- "build": "node ../../scripts/sync-native-wallet.mjs && tsc -p tsconfig.build.json",
66
- "test": "vitest run --root ../.. --passWithNoTests packages/react-native-wallet",
67
- "typecheck": "tsc --noEmit"
68
69
  }
69
- }
70
+ }
@@ -0,0 +1,94 @@
1
+ import { Platform } from 'expo-modules-core';
2
+ import { getNativeModule } from './native';
3
+
4
+ declare const require: (id: string) => unknown;
5
+
6
+ /** The part of @authon/react-native's AuthonMobileClient this helper uses. */
7
+ export interface AppleSignInClient {
8
+ getApiUrl(): string;
9
+ getPublishableKey(): string;
10
+ adoptSignInResult(result: never): Promise<unknown>;
11
+ }
12
+
13
+ type AuthErrorCtor = new (
14
+ code: string,
15
+ message?: string,
16
+ options?: { diagnosticId?: string | null; retryable?: boolean },
17
+ ) => Error & { code: string; diagnosticId: string | null; retryable: boolean };
18
+
19
+ /** Fallback with the same shape as @authon/react-native's AuthonAuthError. */
20
+ class AppleSignInError extends Error {
21
+ readonly code: string;
22
+ readonly diagnosticId: string | null;
23
+ readonly retryable: boolean;
24
+ constructor(code: string, message?: string, options: { diagnosticId?: string | null; retryable?: boolean } = {}) {
25
+ super(message ?? code);
26
+ this.name = 'AuthonAuthError';
27
+ this.code = code;
28
+ this.diagnosticId = options.diagnosticId ?? null;
29
+ this.retryable = options.retryable ?? false;
30
+ }
31
+ }
32
+
33
+ function authError(code: string, message: string, options: { diagnosticId?: string | null; retryable?: boolean } = {}) {
34
+ let Ctor: AuthErrorCtor = AppleSignInError;
35
+ try {
36
+ const mod = require('@authon/react-native') as { AuthonAuthError?: AuthErrorCtor } | undefined;
37
+ if (mod?.AuthonAuthError) Ctor = mod.AuthonAuthError;
38
+ } catch {
39
+ // @authon/react-native not installed: same fields, local class.
40
+ }
41
+ return new Ctor(code, message, options);
42
+ }
43
+
44
+ export interface SignInWithAppleNativeOptions {
45
+ /** `TEAMID.bundleId` as registered in the Authon console; detected from the app when omitted. */
46
+ appId?: string;
47
+ }
48
+
49
+ /**
50
+ * Native Sign in with Apple on iOS: shows the system Apple sheet right away (no browser, no web page)
51
+ * and makes the resulting session the session of `client` (an AuthonMobileClient from
52
+ * @authon/react-native, e.g. `useAuthon().client`), like a completed OAuth sign-in.
53
+ *
54
+ * JS never sees Apple's identity token, authorization code, nonce or the PKCE verifier; only the
55
+ * final result reaches JS. Errors are AuthonAuthError with the shared sign-in codes: OAUTH_CANCELLED
56
+ * (the user closed the sheet, show nothing), OAUTH_REDIRECT_NOT_ALLOWED (app not registered or not
57
+ * approved; `serverCode` tells APP_NOT_REGISTERED from APP_NOT_APPROVED), OAUTH_STATE_MISMATCH
58
+ * (server OAUTH_STATE_INVALID), OAUTH_EXCHANGE_FAILED, ACCOUNT_LINK_REQUIRED, NETWORK_ERROR,
59
+ * RATE_LIMITED, API_ERROR, with `diagnosticId` when the server answered.
60
+ * MFA: throws AuthonMfaRequiredError, finish with verifyMfa(). Cancel and errors never fall back to
61
+ * a web flow. Android: UNSUPPORTED_PLATFORM, use the normal OAuth flow (`startOAuth('apple')`).
62
+ */
63
+ export async function signInWithAppleNative(client: AppleSignInClient, options: SignInWithAppleNativeOptions = {}): Promise<unknown> {
64
+ if (Platform.OS !== 'ios') {
65
+ throw authError('UNSUPPORTED_PLATFORM', 'Native Sign in with Apple is iOS only; on Android use startOAuth("apple")');
66
+ }
67
+ const native = getNativeModule();
68
+ if (!native?.signInWithApple) {
69
+ throw authError('NOT_CONFIGURED', '@authon/react-native-wallet native module not found. Use a development build and rebuild the app.');
70
+ }
71
+ const result = await native.signInWithApple({
72
+ publishableKey: client.getPublishableKey(),
73
+ apiUrl: client.getApiUrl(),
74
+ appId: options.appId ?? null,
75
+ });
76
+ if (!result || typeof result !== 'object') throw authError('API_ERROR', 'Apple sign-in returned no result');
77
+ if (result.ok !== true) {
78
+ const code = typeof result.code === 'string' && result.code ? result.code : 'API_ERROR';
79
+ const error = authError(code, typeof result.message === 'string' && result.message ? result.message : code, {
80
+ diagnosticId: typeof result.diagnosticId === 'string' && result.diagnosticId ? result.diagnosticId : null,
81
+ retryable: result.retryable === true,
82
+ });
83
+ // The server's own code (APP_NOT_REGISTERED, APP_NOT_APPROVED, OAUTH_STATE_INVALID, ...), when sent.
84
+ const serverCode = (result as { serverCode?: unknown }).serverCode;
85
+ throw Object.assign(error, { serverCode: typeof serverCode === 'string' && serverCode ? serverCode : null });
86
+ }
87
+ let session: unknown;
88
+ try {
89
+ session = typeof result.value === 'string' ? JSON.parse(result.value) : result.value;
90
+ } catch {
91
+ throw authError('API_ERROR', 'Apple sign-in returned an unreadable result');
92
+ }
93
+ return client.adoptSignInResult(session as never);
94
+ }
package/src/client.ts CHANGED
@@ -26,6 +26,8 @@ let counter = 0;
26
26
 
27
27
  const isObject = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
28
28
  const str = (v: unknown): string | null => (typeof v === 'string' ? v : null);
29
+ /** Error detail fields are short identifiers; anything else (free text) is dropped. */
30
+ const wireToken = (v: unknown): string | null => (typeof v === 'string' && /^[A-Za-z0-9._-]{1,64}$/.test(v) ? v : null);
29
31
 
30
32
  /** Copies only the public fields, so nothing else the native side might add reaches the app. */
31
33
  function toWallet(value: unknown): EmbeddedWallet {
@@ -413,6 +415,9 @@ export class AuthonWalletClient {
413
415
  throw new AuthonWalletError(code, typeof result.message === 'string' && result.message ? result.message : code, {
414
416
  diagnosticId: typeof result.diagnosticId === 'string' && result.diagnosticId ? result.diagnosticId : null,
415
417
  retryable: result.retryable === true,
418
+ reason: wireToken(result.reason),
419
+ phase: wireToken(result.phase),
420
+ clientAttemptId: wireToken(result.clientAttemptId),
416
421
  });
417
422
  }
418
423
 
package/src/errors.ts CHANGED
@@ -42,17 +42,43 @@ export type WalletErrorCode =
42
42
  | 'UNKNOWN'
43
43
  | (string & {});
44
44
 
45
+ /** Narrower causes under a code, set only when the native wallet is certain of them. */
46
+ export type WalletErrorReason =
47
+ /** Android: the passkey provider could not validate the RP for this app (Digital Asset Links). Code `ORIGIN_NOT_ALLOWED`. */
48
+ | 'PASSKEY_RP_VALIDATION_FAILED'
49
+ | (string & {});
50
+
51
+ export interface AuthonWalletErrorOptions {
52
+ diagnosticId?: string | null;
53
+ retryable?: boolean;
54
+ reason?: WalletErrorReason | null;
55
+ phase?: string | null;
56
+ clientAttemptId?: string | null;
57
+ }
58
+
45
59
  export class AuthonWalletError extends Error {
46
60
  readonly code: WalletErrorCode;
47
61
  /** The server's request ID when the failure came from an API response; log this, never secrets. */
48
62
  readonly diagnosticId: string | null;
49
63
  /** Whether trying the same call again can succeed (from the native wallet's code table). */
50
64
  readonly retryable: boolean;
51
- constructor(code: WalletErrorCode, message?: string, options: { diagnosticId?: string | null; retryable?: boolean } = {}) {
65
+ /** A narrower cause under `code` (e.g. `PASSKEY_RP_VALIDATION_FAILED`), or null when not certain. */
66
+ readonly reason: WalletErrorReason | null;
67
+ /** The step that failed, e.g. `passkey.create` or `passkey.create.rpValidation`; null when not reported (iOS, older natives). */
68
+ readonly phase: string | null;
69
+ /**
70
+ * A random ID the device made for an Android passkey failure (not for a cancel; null on iOS). It is
71
+ * not a server request ID: no API request was involved, so `diagnosticId` stays null for these.
72
+ */
73
+ readonly clientAttemptId: string | null;
74
+ constructor(code: WalletErrorCode, message?: string, options: AuthonWalletErrorOptions = {}) {
52
75
  super(message ?? code);
53
76
  this.name = 'AuthonWalletError';
54
77
  this.code = code;
55
78
  this.diagnosticId = options.diagnosticId ?? null;
56
79
  this.retryable = options.retryable ?? false;
80
+ this.reason = options.reason ?? null;
81
+ this.phase = options.phase ?? null;
82
+ this.clientAttemptId = options.clientAttemptId ?? null;
57
83
  }
58
84
  }
package/src/index.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  export { AuthonWalletClient, createAuthonWallet } from './client';
2
2
  export { AuthonWalletError } from './errors';
3
- export type { WalletErrorCode } from './errors';
3
+ export type { WalletErrorCode, WalletErrorReason, AuthonWalletErrorOptions } from './errors';
4
4
  export { useAuthonWallet } from './useAuthonWallet';
5
+ export { signInWithAppleNative } from './appleSignIn';
6
+ export type { AppleSignInClient, SignInWithAppleNativeOptions } from './appleSignIn';
5
7
  export type { UseAuthonWalletOptions, UseAuthonWalletResult } from './useAuthonWallet';
6
8
  export type {
7
9
  AuthonWalletOptions,
package/src/native.ts CHANGED
@@ -3,7 +3,16 @@ import { requireOptionalNativeModule } from 'expo-modules-core';
3
3
  /** Every native function resolves an envelope instead of rejecting. */
4
4
  export type NativeResult<T> =
5
5
  | { ok: true; value: T }
6
- | { ok: false; code: string; message?: string; diagnosticId?: string | null; retryable?: boolean };
6
+ | {
7
+ ok: false;
8
+ code: string;
9
+ message?: string;
10
+ diagnosticId?: string | null;
11
+ retryable?: boolean;
12
+ reason?: string | null;
13
+ phase?: string | null;
14
+ clientAttemptId?: string | null;
15
+ };
7
16
 
8
17
  export interface NativeConfig {
9
18
  publishableKey: string;
@@ -37,6 +46,8 @@ export interface AuthonWalletNativeModule {
37
46
  upgradeBackup(id: string, token: string): Promise<NativeResult<unknown>>;
38
47
  restore(id: string, token: string, reset: boolean): Promise<NativeResult<unknown>>;
39
48
  revokeDevice(id: string, token: string, deviceId: string): Promise<NativeResult<unknown>>;
49
+ /** iOS only: native Sign in with Apple; resolves the server's sign-in result (JSON), never Apple's tokens. */
50
+ signInWithApple?(config: { publishableKey: string; apiUrl: string; appId: string | null }): Promise<NativeResult<unknown>>;
40
51
  }
41
52
 
42
53
  let override: AuthonWalletNativeModule | null | undefined;