craft-native 0.0.173 → 0.0.174
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/dist/android/src/index.d.ts +2 -0
- package/dist/android/src/index.js +4 -1
- package/dist/android/templates/AndroidManifest.xml.template +2 -0
- package/dist/android/templates/CraftNativeShell.kt.template +50 -0
- package/dist/android/templates/MainActivityNative.kt.template +441 -27
- package/dist/android/templates/build.gradle.kts.app.template +3 -0
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +2 -0
- package/dist/ios/src/index.js +1 -0
- package/dist/ios/templates/CraftApp.swift +5 -0
- package/dist/ios/templates/CraftNativeActions.swift +174 -0
- package/dist/ios/templates/CraftNativeFlatList.swift +1 -1
- package/dist/ios/templates/CraftNativeScreen.swift +437 -38
- package/package.json +1 -1
|
@@ -94,6 +94,9 @@ dependencies {
|
|
|
94
94
|
// Google Play Location Services (for geolocation)
|
|
95
95
|
implementation("com.google.android.gms:play-services-location:21.1.0")
|
|
96
96
|
|
|
97
|
+
// Native STX Map overlays and trail routes
|
|
98
|
+
implementation("com.google.android.gms:play-services-maps:18.2.0")
|
|
99
|
+
|
|
97
100
|
// Google Play In-App Review
|
|
98
101
|
implementation("com.google.android.play:review:2.0.1")
|
|
99
102
|
implementation("com.google.android.play:review-ktx:2.0.1")
|
package/dist/cli.js
CHANGED
|
@@ -3303,7 +3303,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3303
3303
|
`);
|
|
3304
3304
|
}
|
|
3305
3305
|
// package.json
|
|
3306
|
-
var version = "0.0.
|
|
3306
|
+
var version = "0.0.174";
|
|
3307
3307
|
|
|
3308
3308
|
// bin/cli.ts
|
|
3309
3309
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/dist/ios/src/index.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface CraftConfig {
|
|
|
47
47
|
enableFilePicker?: boolean;
|
|
48
48
|
enableFileDownload?: boolean;
|
|
49
49
|
enableSocialAuth?: boolean;
|
|
50
|
+
/** Native passkeys; requires a matching `webcredentials:` associated domain. */
|
|
51
|
+
enablePasskeys?: boolean;
|
|
50
52
|
enableAudioRecording?: boolean;
|
|
51
53
|
enableVideoRecording?: boolean;
|
|
52
54
|
enableMotionSensors?: boolean;
|
package/dist/ios/src/index.js
CHANGED
|
@@ -1239,6 +1239,9 @@ struct CraftConfig: Codable {
|
|
|
1239
1239
|
var enableFilePicker: Bool = false
|
|
1240
1240
|
var enableFileDownload: Bool = false
|
|
1241
1241
|
var enableSocialAuth: Bool = false
|
|
1242
|
+
/// Native passkeys through AuthenticationServices. Requires a matching
|
|
1243
|
+
/// `webcredentials:` associated-domain entry.
|
|
1244
|
+
var enablePasskeys: Bool = false
|
|
1242
1245
|
var enableAudioRecording: Bool = false
|
|
1243
1246
|
var enableVideoRecording: Bool = false
|
|
1244
1247
|
var enableMotionSensors: Bool = false
|
|
@@ -1264,6 +1267,8 @@ struct CraftConfig: Codable {
|
|
|
1264
1267
|
var enableScheduledWorkouts: Bool = false
|
|
1265
1268
|
var devServerURL: String? = nil
|
|
1266
1269
|
var trustedOrigins: [String] = []
|
|
1270
|
+
/// Domains allowed to back native passkey relying-party identifiers.
|
|
1271
|
+
var associatedDomains: [String] = []
|
|
1267
1272
|
/// The longest the launch splash stays up when the page neither says it is
|
|
1268
1273
|
/// ready nor paints.
|
|
1269
1274
|
var splashMaxSeconds: Double = 3
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
+
import AuthenticationServices
|
|
2
3
|
import LocalAuthentication
|
|
3
4
|
import SQLite3
|
|
4
5
|
import Security
|
|
@@ -50,6 +51,7 @@ enum CraftNativeActions {
|
|
|
50
51
|
// The in-app browser and the system's web sign-in sheet (CraftBrowser).
|
|
51
52
|
values.append("browser")
|
|
52
53
|
if config.enableSocialAuth { values.append("appleSignIn") }
|
|
54
|
+
if config.enablePasskeys && !CraftNativePasskeys.domains(in: config).isEmpty { values.append("passkeys") }
|
|
53
55
|
if config.enablePushNotifications { values.append("push") }
|
|
54
56
|
if config.enableShare { values.append("share") }
|
|
55
57
|
// The screen's brightness and the status bar's style need nothing
|
|
@@ -247,6 +249,23 @@ enum CraftNativeActions {
|
|
|
247
249
|
}
|
|
248
250
|
}
|
|
249
251
|
return
|
|
252
|
+
case ("Passkeys", "isAvailable"):
|
|
253
|
+
sync = .success(CraftNativePasskeys.isAvailable(config: config))
|
|
254
|
+
case ("Passkeys", "create"), ("Passkeys", "get"):
|
|
255
|
+
guard config.enablePasskeys else {
|
|
256
|
+
complete(requestToken, failure("CAPABILITY_DISABLED", "Passkeys are disabled"), completion)
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
DispatchQueue.main.async {
|
|
260
|
+
CraftNativePasskeys.shared.perform(
|
|
261
|
+
requestToken: requestToken,
|
|
262
|
+
method: method,
|
|
263
|
+
options: args.first as? [String: Any] ?? [:],
|
|
264
|
+
config: config,
|
|
265
|
+
completion: completion
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
return
|
|
250
269
|
case ("Dialog", _):
|
|
251
270
|
// The system's own alert, confirmation and action sheet, the same
|
|
252
271
|
// ones a page gets (CraftNativeUI.swift), over the native screen.
|
|
@@ -490,6 +509,7 @@ enum CraftNativeActions {
|
|
|
490
509
|
cancellationLock.unlock()
|
|
491
510
|
context?.invalidate()
|
|
492
511
|
task?.cancel()
|
|
512
|
+
CraftNativePasskeys.shared.cancel(requestToken: requestToken)
|
|
493
513
|
}
|
|
494
514
|
|
|
495
515
|
/// `fetch` for native screens: one URLSession data task per request, keyed
|
|
@@ -1123,6 +1143,160 @@ enum CraftNativeActions {
|
|
|
1123
1143
|
}
|
|
1124
1144
|
}
|
|
1125
1145
|
|
|
1146
|
+
/// The native WebAuthn half for screens without a browser context. The
|
|
1147
|
+
/// server still owns challenges and verifies the returned client data; this
|
|
1148
|
+
/// class only translates WebAuthn options into Apple's passkey sheet and
|
|
1149
|
+
/// returns the authenticator response as base64url strings.
|
|
1150
|
+
final class CraftNativePasskeys: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
|
|
1151
|
+
static let shared = CraftNativePasskeys()
|
|
1152
|
+
|
|
1153
|
+
private struct Pending {
|
|
1154
|
+
let requestToken: String
|
|
1155
|
+
let completion: (Result<Any, CraftNativeActionError>) -> Void
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
private var pending: [ObjectIdentifier: Pending] = [:]
|
|
1159
|
+
private var controllers: [String: ASAuthorizationController] = [:]
|
|
1160
|
+
|
|
1161
|
+
static func domains(in config: CraftConfig) -> Set<String> {
|
|
1162
|
+
Set(config.associatedDomains.compactMap { entry in
|
|
1163
|
+
let value = entry.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1164
|
+
guard value.lowercased().hasPrefix("webcredentials:") else { return nil }
|
|
1165
|
+
let domain = String(value.dropFirst("webcredentials:".count)).split(separator: "?", maxSplits: 1).first.map(String.init) ?? ""
|
|
1166
|
+
let normalized = domain.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
1167
|
+
return normalized.isEmpty ? nil : normalized
|
|
1168
|
+
})
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
static func isAvailable(config: CraftConfig) -> Bool {
|
|
1172
|
+
if #available(iOS 15.0, *) {
|
|
1173
|
+
return config.enablePasskeys && !domains(in: config).isEmpty
|
|
1174
|
+
}
|
|
1175
|
+
return false
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
func perform(
|
|
1179
|
+
requestToken: String,
|
|
1180
|
+
method: String,
|
|
1181
|
+
options: [String: Any],
|
|
1182
|
+
config: CraftConfig,
|
|
1183
|
+
completion: @escaping (Result<Any, CraftNativeActionError>) -> Void
|
|
1184
|
+
) {
|
|
1185
|
+
guard #available(iOS 15.0, *) else {
|
|
1186
|
+
completion(.failure(CraftNativeActionError(code: "UNAVAILABLE", message: "Passkeys require iOS 15 or later")))
|
|
1187
|
+
return
|
|
1188
|
+
}
|
|
1189
|
+
guard Self.isAvailable(config: config) else {
|
|
1190
|
+
completion(.failure(CraftNativeActionError(code: "UNAVAILABLE", message: "Passkeys need a matching webcredentials associated domain")))
|
|
1191
|
+
return
|
|
1192
|
+
}
|
|
1193
|
+
guard let relyingParty = relyingPartyID(options: options, config: config),
|
|
1194
|
+
let challenge = data(options["challenge"]) else {
|
|
1195
|
+
completion(.failure(CraftNativeActionError(code: "INVALID_ARGUMENT", message: "Passkeys need a base64url challenge and an associated relying-party id")))
|
|
1196
|
+
return
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: relyingParty)
|
|
1200
|
+
let request: ASAuthorizationRequest
|
|
1201
|
+
if method == "create" {
|
|
1202
|
+
guard let user = options["user"] as? [String: Any],
|
|
1203
|
+
let name = user["name"] as? String, !name.isEmpty,
|
|
1204
|
+
let userID = data(user["id"]) else {
|
|
1205
|
+
completion(.failure(CraftNativeActionError(code: "INVALID_ARGUMENT", message: "Passkeys.create needs user.name and a base64url user.id")))
|
|
1206
|
+
return
|
|
1207
|
+
}
|
|
1208
|
+
request = provider.createCredentialRegistrationRequest(challenge: challenge, name: name, userID: userID)
|
|
1209
|
+
} else {
|
|
1210
|
+
let assertion = provider.createCredentialAssertionRequest(challenge: challenge)
|
|
1211
|
+
if let allowCredentials = options["allowCredentials"] as? [[String: Any]] {
|
|
1212
|
+
assertion.allowedCredentials = allowCredentials.compactMap { descriptor in
|
|
1213
|
+
guard let credentialID = data(descriptor["id"]) else { return nil }
|
|
1214
|
+
return ASAuthorizationPlatformPublicKeyCredentialDescriptor(credentialID: credentialID)
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
request = assertion
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
let controller = ASAuthorizationController(authorizationRequests: [request])
|
|
1221
|
+
controller.delegate = self
|
|
1222
|
+
controller.presentationContextProvider = self
|
|
1223
|
+
let key = ObjectIdentifier(controller)
|
|
1224
|
+
pending[key] = Pending(requestToken: requestToken, completion: completion)
|
|
1225
|
+
controllers[requestToken] = controller
|
|
1226
|
+
controller.performRequests()
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
func cancel(requestToken: String) {
|
|
1230
|
+
controllers.removeValue(forKey: requestToken)
|
|
1231
|
+
pending = pending.filter { $0.value.requestToken != requestToken }
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
|
|
1235
|
+
let key = ObjectIdentifier(controller)
|
|
1236
|
+
guard let request = pending.removeValue(forKey: key) else { return }
|
|
1237
|
+
controllers.removeValue(forKey: request.requestToken)
|
|
1238
|
+
if let registration = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialRegistration {
|
|
1239
|
+
request.completion(.success([
|
|
1240
|
+
"id": Self.base64url(registration.credentialID),
|
|
1241
|
+
"rawId": Self.base64url(registration.credentialID),
|
|
1242
|
+
"type": "public-key",
|
|
1243
|
+
"response": [
|
|
1244
|
+
"clientDataJSON": Self.base64url(registration.rawClientDataJSON),
|
|
1245
|
+
"attestationObject": Self.base64url(registration.rawAttestationObject),
|
|
1246
|
+
],
|
|
1247
|
+
]))
|
|
1248
|
+
return
|
|
1249
|
+
}
|
|
1250
|
+
if let assertion = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion {
|
|
1251
|
+
var response: [String: Any] = [
|
|
1252
|
+
"clientDataJSON": Self.base64url(assertion.rawClientDataJSON),
|
|
1253
|
+
"authenticatorData": Self.base64url(assertion.rawAuthenticatorData),
|
|
1254
|
+
"signature": Self.base64url(assertion.signature),
|
|
1255
|
+
]
|
|
1256
|
+
if let userID = assertion.userID { response["userHandle"] = Self.base64url(userID) }
|
|
1257
|
+
request.completion(.success([
|
|
1258
|
+
"id": Self.base64url(assertion.credentialID),
|
|
1259
|
+
"rawId": Self.base64url(assertion.credentialID),
|
|
1260
|
+
"type": "public-key",
|
|
1261
|
+
"response": response,
|
|
1262
|
+
]))
|
|
1263
|
+
return
|
|
1264
|
+
}
|
|
1265
|
+
request.completion(.failure(CraftNativeActionError(code: "PASSKEY_FAILED", message: "Apple returned an unsupported passkey credential")))
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
|
|
1269
|
+
let key = ObjectIdentifier(controller)
|
|
1270
|
+
guard let request = pending.removeValue(forKey: key) else { return }
|
|
1271
|
+
controllers.removeValue(forKey: request.requestToken)
|
|
1272
|
+
let cancelled = (error as? ASAuthorizationError)?.code == .canceled
|
|
1273
|
+
request.completion(.failure(CraftNativeActionError(code: cancelled ? "CANCELLED" : "PASSKEY_FAILED", message: error.localizedDescription)))
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
|
|
1277
|
+
CraftPresenter.keyWindow() ?? ASPresentationAnchor()
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
private func relyingPartyID(options: [String: Any], config: CraftConfig) -> String? {
|
|
1281
|
+
let value = (options["rpId"] as? String) ?? (options["rp"] as? [String: Any])?["id"] as? String
|
|
1282
|
+
let domains = Self.domains(in: config)
|
|
1283
|
+
let candidate = value?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() ?? (domains.count == 1 ? domains.first : nil)
|
|
1284
|
+
guard let candidate, domains.contains(candidate), candidate.range(of: #"^[a-z0-9.-]+$"#, options: .regularExpression) != nil else { return nil }
|
|
1285
|
+
return candidate
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
private func data(_ value: Any?) -> Data? {
|
|
1289
|
+
guard let string = value as? String, !string.isEmpty else { return nil }
|
|
1290
|
+
var encoded = string.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
|
|
1291
|
+
encoded += String(repeating: "=", count: (4 - encoded.count % 4) % 4)
|
|
1292
|
+
return Data(base64Encoded: encoded)
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
private static func base64url(_ data: Data) -> String {
|
|
1296
|
+
data.base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "")
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1126
1300
|
// MARK: - Synchronous reads for a screen's first frame
|
|
1127
1301
|
|
|
1128
1302
|
extension CraftNativeActions {
|
|
@@ -277,7 +277,7 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
280
|
-
|
|
280
|
+
craftScrollGesture(gestureRecognizer, runsWith: otherGestureRecognizer)
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
var visibleItemIdentities: [String] {
|