@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/CHANGELOG.md +9 -0
- package/README.md +49 -2
- package/android/src/main/java/dev/authon/reactnative/wallet/AuthonWalletModule.kt +3 -0
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/CredentialManagerPasskeys.kt +61 -19
- package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletError.kt +9 -1
- package/build/appleSignIn.d.ts +26 -0
- package/build/appleSignIn.d.ts.map +1 -0
- package/build/appleSignIn.js +76 -0
- package/build/appleSignIn.js.map +1 -0
- package/build/client.d.ts.map +1 -1
- package/build/client.js +5 -0
- package/build/client.js.map +1 -1
- package/build/errors.d.ts +21 -4
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +12 -0
- package/build/errors.js.map +1 -1
- package/build/index.d.ts +3 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/native.d.ts +9 -0
- package/build/native.d.ts.map +1 -1
- package/build/native.js.map +1 -1
- package/ios/AuthonReactNativeWallet.podspec +3 -1
- package/ios/AuthonWalletModule.swift +26 -0
- package/ios/vendor/AuthonAppleSignIn/AppleSignIn.swift +285 -0
- package/ios/vendor/GENERATED.md +1 -1
- package/package.json +10 -9
- package/src/appleSignIn.ts +94 -0
- package/src/client.ts +5 -0
- package/src/errors.ts +27 -1
- package/src/index.ts +3 -1
- package/src/native.ts +12 -1
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). */
|
package/build/native.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAEA,uEAAuE;AACvE,MAAM,MAAM,YAAY,CAAC,CAAC,IACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GACtB;
|
|
1
|
+
{"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"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"}
|
package/build/native.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native.js","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,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
|
+
}
|
package/ios/vendor/GENERATED.md
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
| {
|
|
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;
|