@lightninglabs/wavelength-react-native 0.1.0

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 (38) hide show
  1. package/LICENSE +19 -0
  2. package/README.md +112 -0
  3. package/WavelengthReactNative.podspec +24 -0
  4. package/android/build.gradle +41 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/engineering/lightning/wavelength/reactnative/WavelengthModule.kt +291 -0
  7. package/android/src/main/java/engineering/lightning/wavelength/reactnative/WavelengthPackage.kt +28 -0
  8. package/dist/NativeWalletdk.d.ts +31 -0
  9. package/dist/NativeWalletdk.d.ts.map +1 -0
  10. package/dist/NativeWalletdk.js +2 -0
  11. package/dist/NativeWavelength.d.ts +31 -0
  12. package/dist/NativeWavelength.d.ts.map +1 -0
  13. package/dist/NativeWavelength.js +2 -0
  14. package/dist/client.d.ts +56 -0
  15. package/dist/client.d.ts.map +1 -0
  16. package/dist/client.js +143 -0
  17. package/dist/config.d.ts +19 -0
  18. package/dist/config.d.ts.map +1 -0
  19. package/dist/config.js +20 -0
  20. package/dist/index.d.ts +46 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +60 -0
  23. package/dist/passkey.d.ts +32 -0
  24. package/dist/passkey.d.ts.map +1 -0
  25. package/dist/passkey.js +244 -0
  26. package/ios/WavelengthModule.h +8 -0
  27. package/ios/WavelengthModule.mm +298 -0
  28. package/ios/WavelengthPasskey.swift +262 -0
  29. package/package.json +70 -0
  30. package/src/NativeWavelength.ts +32 -0
  31. package/src/client.test.ts +307 -0
  32. package/src/client.ts +222 -0
  33. package/src/config.test.ts +28 -0
  34. package/src/config.ts +28 -0
  35. package/src/index.ts +102 -0
  36. package/src/native-dispatch.test.ts +174 -0
  37. package/src/passkey.test.ts +301 -0
  38. package/src/passkey.ts +336 -0
@@ -0,0 +1,298 @@
1
+ #import "WavelengthModule.h"
2
+
3
+ #import <Wavewalletdk/Wavewalletdk.h>
4
+ #import <WavelengthSpec/WavelengthSpec.h>
5
+ #import "WavelengthReactNative-Swift.h"
6
+
7
+ // The single device event name; the body is { kind, payload }.
8
+ static NSString *const kWavelengthEvent = @"wavelengthActivity";
9
+ static NSString *const kWavelengthErrorCode = @"wavelength_error";
10
+
11
+ // The codegen spec conformance lives here (see the note in the header): the
12
+ // generated protocol drags in C++ headers that must stay out of the public
13
+ // Objective-C surface.
14
+ @interface WavelengthModule () <NativeWavelengthSpec>
15
+ @end
16
+
17
+ @implementation WavelengthModule {
18
+ MobileSubscription *_subscription;
19
+ WavelengthPasskey *_passkey;
20
+ }
21
+
22
+ RCT_EXPORT_MODULE(Wavelength)
23
+
24
+ + (BOOL)requiresMainQueueSetup
25
+ {
26
+ return NO;
27
+ }
28
+
29
+ - (NSArray<NSString *> *)supportedEvents
30
+ {
31
+ return @[ kWavelengthEvent ];
32
+ }
33
+
34
+ - (void)getDefaultDataDir:(RCTPromiseResolveBlock)resolve
35
+ reject:(RCTPromiseRejectBlock)reject
36
+ {
37
+ NSURL *appSupport = [[NSFileManager.defaultManager
38
+ URLsForDirectory:NSApplicationSupportDirectory
39
+ inDomains:NSUserDomainMask] firstObject];
40
+ resolve([appSupport URLByAppendingPathComponent:@"wavelength"].path);
41
+ }
42
+
43
+ - (void)passkeySupported:(RCTPromiseResolveBlock)resolve
44
+ reject:(RCTPromiseRejectBlock)reject
45
+ {
46
+ resolve(@([WavelengthPasskey prfSupported]));
47
+ }
48
+
49
+ - (void)passkeyCreate:(NSString *)requestJson
50
+ resolve:(RCTPromiseResolveBlock)resolve
51
+ reject:(RCTPromiseRejectBlock)reject
52
+ {
53
+ dispatch_async(dispatch_get_main_queue(), ^{
54
+ @synchronized (self) {
55
+ if (self->_passkey == nil) {
56
+ self->_passkey = [WavelengthPasskey new];
57
+ }
58
+ }
59
+ [self->_passkey create:requestJson
60
+ completion:^(NSString *json, NSString *errorMessage) {
61
+ if (json != nil) {
62
+ resolve(json);
63
+ } else {
64
+ reject(kWavelengthErrorCode,
65
+ errorMessage ?: @"passkey ceremony failed", nil);
66
+ }
67
+ }];
68
+ });
69
+ }
70
+
71
+ - (void)passkeyGet:(NSString *)requestJson
72
+ resolve:(RCTPromiseResolveBlock)resolve
73
+ reject:(RCTPromiseRejectBlock)reject
74
+ {
75
+ dispatch_async(dispatch_get_main_queue(), ^{
76
+ @synchronized (self) {
77
+ if (self->_passkey == nil) {
78
+ self->_passkey = [WavelengthPasskey new];
79
+ }
80
+ }
81
+ [self->_passkey get:requestJson
82
+ completion:^(NSString *json, NSString *errorMessage) {
83
+ if (json != nil) {
84
+ resolve(json);
85
+ } else {
86
+ reject(kWavelengthErrorCode,
87
+ errorMessage ?: @"passkey ceremony failed", nil);
88
+ }
89
+ }];
90
+ });
91
+ }
92
+
93
+ // call maps a verb name onto the gomobile Mobile* entry points. The facade
94
+ // takes and returns JSON bytes; this switch stays dumb on purpose because all
95
+ // typing lives in the TypeScript layer.
96
+ - (void)call:(NSString *)method
97
+ paramsJson:(NSString *)paramsJson
98
+ resolve:(RCTPromiseResolveBlock)resolve
99
+ reject:(RCTPromiseRejectBlock)reject
100
+ {
101
+ dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
102
+ NSError *error = nil;
103
+ NSData *result = nil;
104
+ NSString *json = @"";
105
+ NSData *params = [paramsJson dataUsingEncoding:NSUTF8StringEncoding];
106
+
107
+ if ([method isEqualToString:@"start"]) {
108
+ MobileStart(paramsJson, &error);
109
+ } else if ([method isEqualToString:@"stop"]) {
110
+ MobileStop(&error);
111
+ } else if ([method isEqualToString:@"getInfo"]) {
112
+ result = MobileGetInfo(&error);
113
+ } else if ([method isEqualToString:@"status"]) {
114
+ result = MobileStatus(&error);
115
+ } else if ([method isEqualToString:@"balance"]) {
116
+ result = MobileBalance(&error);
117
+ } else if ([method isEqualToString:@"createWallet"]) {
118
+ result = MobileCreateWallet(params, &error);
119
+ } else if ([method isEqualToString:@"unlockWallet"]) {
120
+ result = MobileUnlockWallet(params, &error);
121
+ } else if ([method isEqualToString:@"openWalletFromPasskey"]) {
122
+ result = MobileOpenWalletFromPasskey(params, &error);
123
+ } else if ([method isEqualToString:@"deposit"]) {
124
+ result = MobileDeposit(params, &error);
125
+ } else if ([method isEqualToString:@"receive"]) {
126
+ result = MobileReceive(params, &error);
127
+ } else if ([method isEqualToString:@"prepareSend"]) {
128
+ result = MobilePrepareSend(params, &error);
129
+ } else if ([method isEqualToString:@"sendPrepared"]) {
130
+ result = MobileSendPrepared(params, &error);
131
+ } else if ([method isEqualToString:@"list"]) {
132
+ result = MobileList(params, &error);
133
+ } else if ([method isEqualToString:@"exit"]) {
134
+ result = MobileExit(params, &error);
135
+ } else if ([method isEqualToString:@"exitStatus"]) {
136
+ result = MobileExitStatus(params, &error);
137
+ } else if ([method isEqualToString:@"exitSummary"]) {
138
+ result = MobileExitSummary(params, &error);
139
+ } else if ([method isEqualToString:@"getExitPlan"]) {
140
+ result = MobileGetExitPlan(params, &error);
141
+ } else if ([method isEqualToString:@"sweepWallet"]) {
142
+ result = MobileSweepWallet(params, &error);
143
+ } else if ([method isEqualToString:@"confirmedBalanceSat"]) {
144
+ int64_t value = 0;
145
+ MobileConfirmedBalanceSat(&value, &error);
146
+ json = [NSString stringWithFormat:@"%lld",
147
+ (long long)value];
148
+ } else if ([method isEqualToString:@"pendingInboundSat"]) {
149
+ int64_t value = 0;
150
+ MobilePendingInboundSat(&value, &error);
151
+ json = [NSString stringWithFormat:@"%lld",
152
+ (long long)value];
153
+ } else if ([method isEqualToString:@"walletReady"]) {
154
+ BOOL value = NO;
155
+ MobileWalletReady(&value, &error);
156
+ json = value ? @"true" : @"false";
157
+ } else if ([method isEqualToString:@"isRunning"]) {
158
+ json = MobileIsRunning() ? @"true" : @"false";
159
+ } else {
160
+ reject(kWavelengthErrorCode,
161
+ [NSString stringWithFormat:@"unknown wavelength verb: %@", method],
162
+ nil);
163
+ return;
164
+ }
165
+
166
+ if (error != nil) {
167
+ reject(kWavelengthErrorCode, error.localizedDescription, error);
168
+ return;
169
+ }
170
+ if (result != nil) {
171
+ json = [[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding];
172
+ }
173
+ resolve(json ?: @"");
174
+ });
175
+ }
176
+
177
+ // startActivity opens the facade's pull subscription and pumps entries to
178
+ // device events on a background queue, because next() blocks.
179
+ - (void)startActivity:(NSString *)reqJson
180
+ resolve:(RCTPromiseResolveBlock)resolve
181
+ reject:(RCTPromiseRejectBlock)reject
182
+ {
183
+ dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
184
+ // The check, the subscribe, and the store happen in one critical section
185
+ // (mirroring the Kotlin module): separate sections would let two
186
+ // overlapping calls both subscribe, leaking a pump that double-emits
187
+ // every entry.
188
+ MobileSubscription *sub = nil;
189
+ NSError *error = nil;
190
+ @synchronized (self) {
191
+ if (self->_subscription != nil) {
192
+ resolve(nil);
193
+ return;
194
+ }
195
+ NSData *req = [reqJson dataUsingEncoding:NSUTF8StringEncoding];
196
+ sub = MobileSubscribe(req, &error);
197
+ if (error == nil && sub != nil) {
198
+ self->_subscription = sub;
199
+ }
200
+ }
201
+ if (error != nil || sub == nil) {
202
+ reject(kWavelengthErrorCode,
203
+ error.localizedDescription ?: @"wavelength subscribe failed",
204
+ error);
205
+ return;
206
+ }
207
+
208
+ [self pump:sub];
209
+ resolve(nil);
210
+ });
211
+ }
212
+
213
+ - (void)stopActivity:(RCTPromiseResolveBlock)resolve
214
+ reject:(RCTPromiseRejectBlock)reject
215
+ {
216
+ dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
217
+ MobileSubscription *sub = nil;
218
+ @synchronized (self) {
219
+ sub = self->_subscription;
220
+ // Detach before close returns so a queued start can't reuse a pump
221
+ // that's already stopping.
222
+ self->_subscription = nil;
223
+ }
224
+ NSError *error = nil;
225
+ if (sub != nil) {
226
+ [sub close:&error];
227
+ }
228
+ if (error != nil) {
229
+ reject(kWavelengthErrorCode, error.localizedDescription, error);
230
+ return;
231
+ }
232
+ resolve(nil);
233
+ });
234
+ }
235
+
236
+ // pump drains the subscription on a background queue until it ends, emitting
237
+ // one device event per entry and a terminal end or error event.
238
+ - (void)pump:(MobileSubscription *)sub
239
+ {
240
+ dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
241
+ while (YES) {
242
+ NSError *error = nil;
243
+ NSData *entry = [sub next:&error];
244
+ if (error != nil || entry == nil) {
245
+ // A nil entry with no error, or the exact terminal "EOF", is a clean
246
+ // close; a substring match would misclassify failures like
247
+ // "unexpected EOF" as clean ends.
248
+ BOOL eof = error == nil ||
249
+ [error.localizedDescription isEqualToString:@"EOF"];
250
+ BOOL stopped;
251
+ @synchronized (self) {
252
+ stopped = self->_subscription != sub;
253
+ }
254
+ // An intentional stop has no terminal event. A replacement may
255
+ // already be open by the time this old pump unblocks.
256
+ if (stopped) {
257
+ break;
258
+ }
259
+ if (eof) {
260
+ [self sendEventWithName:kWavelengthEvent
261
+ body:@{ @"kind" : @"end", @"payload" : @"" }];
262
+ } else {
263
+ [self sendEventWithName:kWavelengthEvent
264
+ body:@{
265
+ @"kind" : @"error",
266
+ @"payload" : error.localizedDescription
267
+ ?: @"wavelength activity stream failed"
268
+ }];
269
+ }
270
+ break;
271
+ }
272
+ BOOL stopped;
273
+ @synchronized (self) {
274
+ stopped = self->_subscription != sub;
275
+ }
276
+ if (stopped) {
277
+ break;
278
+ }
279
+ NSString *json =
280
+ [[NSString alloc] initWithData:entry encoding:NSUTF8StringEncoding];
281
+ [self sendEventWithName:kWavelengthEvent
282
+ body:@{ @"kind" : @"entry", @"payload" : json ?: @"" }];
283
+ }
284
+ @synchronized (self) {
285
+ if (self->_subscription == sub) {
286
+ self->_subscription = nil;
287
+ }
288
+ }
289
+ });
290
+ }
291
+
292
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
293
+ (const facebook::react::ObjCTurboModule::InitParams &)params
294
+ {
295
+ return std::make_shared<facebook::react::NativeWavelengthSpecJSI>(params);
296
+ }
297
+
298
+ @end
@@ -0,0 +1,262 @@
1
+ import AuthenticationServices
2
+ import CryptoKit
3
+ import Foundation
4
+ import UIKit
5
+
6
+ // The native half of the passkey ceremony: WebAuthn-shaped JSON in, a minimal
7
+ // WebAuthn-shaped JSON response out. The TypeScript parser reads only id and
8
+ // the PRF results; rawId and type ride along for WebAuthn shape fidelity. The JSON-to-
9
+ // ASAuthorization mapping follows react-native-passkey (MIT,
10
+ // https://github.com/f-23/react-native-passkey), which shipped the iOS 18 PRF
11
+ // path this implementation mirrors.
12
+ //
13
+ // Experimental: compiles and is driven by unit-tested TS request shapes, but
14
+ // has not been verified end to end; that needs an Associated Domains
15
+ // entitlement backed by a paid Apple Developer Program team.
16
+ @objc(WavelengthPasskey)
17
+ public final class WavelengthPasskey: NSObject {
18
+
19
+ // Retains in-flight runners; ASAuthorizationController does not retain its
20
+ // delegate, so each ceremony holds itself here until it completes. Typed
21
+ // as AnyObject because stored properties cannot be availability-gated and
22
+ // PasskeyRunner itself requires iOS 18.
23
+ private var inFlight: [AnyObject] = []
24
+
25
+ // PRF requires the iOS 18 AuthenticationServices API.
26
+ @objc public static func prfSupported() -> Bool {
27
+ if #available(iOS 18.0, *) {
28
+ return true
29
+ }
30
+ return false
31
+ }
32
+
33
+ @objc public func create(
34
+ _ requestJson: String,
35
+ completion: @escaping (String?, String?) -> Void
36
+ ) {
37
+ guard #available(iOS 18.0, *) else {
38
+ completion(nil, "passkeys require iOS 18 or newer")
39
+ return
40
+ }
41
+ guard
42
+ let body = parseJson(requestJson),
43
+ let rp = body["rp"] as? [String: Any],
44
+ let rpId = rp["id"] as? String,
45
+ let user = body["user"] as? [String: Any],
46
+ let userName = user["name"] as? String,
47
+ let userIdB64 = user["id"] as? String,
48
+ let userId = Self.dataFromBase64Url(userIdB64),
49
+ let challengeB64 = body["challenge"] as? String,
50
+ let challenge = Self.dataFromBase64Url(challengeB64),
51
+ let salt = prfSalt(body)
52
+ else {
53
+ completion(nil, "malformed passkey registration request")
54
+ return
55
+ }
56
+
57
+ let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
58
+ relyingPartyIdentifier: rpId)
59
+ let request = provider.createCredentialRegistrationRequest(
60
+ challenge: challenge, name: userName, userID: userId)
61
+ request.userVerificationPreference = .required
62
+ request.prf = .inputValues(
63
+ ASAuthorizationPublicKeyCredentialPRFRegistrationInput.InputValues(
64
+ saltInput1: salt, saltInput2: nil))
65
+
66
+ run(
67
+ request: request,
68
+ cancelMessage: "passkey registration was cancelled",
69
+ completion: completion)
70
+ }
71
+
72
+ @objc public func get(
73
+ _ requestJson: String,
74
+ completion: @escaping (String?, String?) -> Void
75
+ ) {
76
+ guard #available(iOS 18.0, *) else {
77
+ completion(nil, "passkeys require iOS 18 or newer")
78
+ return
79
+ }
80
+ guard
81
+ let body = parseJson(requestJson),
82
+ let rpId = body["rpId"] as? String,
83
+ let challengeB64 = body["challenge"] as? String,
84
+ let challenge = Self.dataFromBase64Url(challengeB64),
85
+ let salt = prfSalt(body)
86
+ else {
87
+ completion(nil, "malformed passkey assertion request")
88
+ return
89
+ }
90
+
91
+ let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
92
+ relyingPartyIdentifier: rpId)
93
+ let request = provider.createCredentialAssertionRequest(challenge: challenge)
94
+ request.userVerificationPreference = .required
95
+ if let allowed = body["allowCredentials"] as? [[String: Any]] {
96
+ request.allowedCredentials = allowed.compactMap { entry in
97
+ guard
98
+ let idB64 = entry["id"] as? String,
99
+ let id = Self.dataFromBase64Url(idB64)
100
+ else {
101
+ return nil
102
+ }
103
+ return ASAuthorizationPlatformPublicKeyCredentialDescriptor(
104
+ credentialID: id)
105
+ }
106
+ }
107
+ request.prf = .inputValues(
108
+ ASAuthorizationPublicKeyCredentialPRFRegistrationInput.InputValues(
109
+ saltInput1: salt, saltInput2: nil))
110
+
111
+ run(
112
+ request: request,
113
+ cancelMessage: "passkey authentication was cancelled",
114
+ completion: completion)
115
+ }
116
+
117
+ @available(iOS 18.0, *)
118
+ private func run(
119
+ request: ASAuthorizationRequest,
120
+ cancelMessage: String,
121
+ completion: @escaping (String?, String?) -> Void
122
+ ) {
123
+ let runner = PasskeyRunner(
124
+ cancelMessage: cancelMessage, completion: completion)
125
+ runner.onDone = { [weak self, weak runner] in
126
+ self?.inFlight.removeAll { $0 === runner }
127
+ }
128
+ inFlight.append(runner)
129
+ runner.run(request: request)
130
+ }
131
+
132
+ // prfSalt reads extensions.prf.eval.first (base64url) from a request body.
133
+ private func prfSalt(_ body: [String: Any]) -> Data? {
134
+ guard
135
+ let extensions = body["extensions"] as? [String: Any],
136
+ let prf = extensions["prf"] as? [String: Any],
137
+ let eval = prf["eval"] as? [String: Any],
138
+ let firstB64 = eval["first"] as? String
139
+ else {
140
+ return nil
141
+ }
142
+ return Self.dataFromBase64Url(firstB64)
143
+ }
144
+
145
+ private func parseJson(_ json: String) -> [String: Any]? {
146
+ guard let data = json.data(using: .utf8) else {
147
+ return nil
148
+ }
149
+ return (try? JSONSerialization.jsonObject(with: data)) as? [String: Any]
150
+ }
151
+
152
+ static func base64Url(_ data: Data) -> String {
153
+ data.base64EncodedString()
154
+ .replacingOccurrences(of: "+", with: "-")
155
+ .replacingOccurrences(of: "/", with: "_")
156
+ .replacingOccurrences(of: "=", with: "")
157
+ }
158
+
159
+ static func dataFromBase64Url(_ value: String) -> Data? {
160
+ var s = value
161
+ .replacingOccurrences(of: "-", with: "+")
162
+ .replacingOccurrences(of: "_", with: "/")
163
+ while s.count % 4 != 0 {
164
+ s += "="
165
+ }
166
+ return Data(base64Encoded: s)
167
+ }
168
+ }
169
+
170
+ // PasskeyRunner owns one ceremony: it runs the controller, anchors its UI to
171
+ // the key window, and serializes the credential (or error) back to JSON.
172
+ @available(iOS 18.0, *)
173
+ private final class PasskeyRunner: NSObject,
174
+ ASAuthorizationControllerDelegate,
175
+ ASAuthorizationControllerPresentationContextProviding {
176
+
177
+ private let cancelMessage: String
178
+ private let completion: (String?, String?) -> Void
179
+ var onDone: () -> Void = {}
180
+
181
+ init(cancelMessage: String, completion: @escaping (String?, String?) -> Void) {
182
+ self.cancelMessage = cancelMessage
183
+ self.completion = completion
184
+ }
185
+
186
+ func run(request: ASAuthorizationRequest) {
187
+ let controller = ASAuthorizationController(authorizationRequests: [request])
188
+ controller.delegate = self
189
+ controller.presentationContextProvider = self
190
+ controller.performRequests()
191
+ }
192
+
193
+ func presentationAnchor(
194
+ for controller: ASAuthorizationController
195
+ ) -> ASPresentationAnchor {
196
+ UIApplication.shared.connectedScenes
197
+ .compactMap { $0 as? UIWindowScene }
198
+ .flatMap { $0.windows }
199
+ .first { $0.isKeyWindow } ?? ASPresentationAnchor()
200
+ }
201
+
202
+ func authorizationController(
203
+ controller: ASAuthorizationController,
204
+ didCompleteWithAuthorization authorization: ASAuthorization
205
+ ) {
206
+ switch authorization.credential {
207
+ case let registration
208
+ as ASAuthorizationPlatformPublicKeyCredentialRegistration:
209
+ completion(
210
+ Self.responseJson(
211
+ credentialId: registration.credentialID,
212
+ prfFirst: Self.prfData(registration.prf?.first)),
213
+ nil)
214
+ case let assertion as ASAuthorizationPlatformPublicKeyCredentialAssertion:
215
+ completion(
216
+ Self.responseJson(
217
+ credentialId: assertion.credentialID,
218
+ prfFirst: Self.prfData(assertion.prf?.first)),
219
+ nil)
220
+ default:
221
+ completion(nil, "unexpected credential type from the authenticator")
222
+ }
223
+ onDone()
224
+ }
225
+
226
+ func authorizationController(
227
+ controller: ASAuthorizationController,
228
+ didCompleteWithError error: Error
229
+ ) {
230
+ let asError = error as? ASAuthorizationError
231
+ let message = asError?.code == .canceled
232
+ ? cancelMessage
233
+ : error.localizedDescription
234
+ completion(nil, message)
235
+ onDone()
236
+ }
237
+
238
+ // prfData converts a PRF output (a CryptoKit SymmetricKey) into raw bytes.
239
+ private static func prfData(_ key: SymmetricKey?) -> Data? {
240
+ key?.withUnsafeBytes { Data($0) }
241
+ }
242
+
243
+ // responseJson builds the minimal WebAuthn response the TS parser reads.
244
+ private static func responseJson(credentialId: Data, prfFirst: Data?) -> String? {
245
+ var extensionResults: [String: Any] = [:]
246
+ if let first = prfFirst {
247
+ extensionResults = [
248
+ "prf": ["results": ["first": WavelengthPasskey.base64Url(first)]],
249
+ ]
250
+ }
251
+ let body: [String: Any] = [
252
+ "id": WavelengthPasskey.base64Url(credentialId),
253
+ "rawId": WavelengthPasskey.base64Url(credentialId),
254
+ "type": "public-key",
255
+ "clientExtensionResults": extensionResults,
256
+ ]
257
+ guard let data = try? JSONSerialization.data(withJSONObject: body) else {
258
+ return nil
259
+ }
260
+ return String(data: data, encoding: .utf8)
261
+ }
262
+ }
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@lightninglabs/wavelength-react-native",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "React Native transport for the Wavelength self-custodial Lightning wallet SDK.",
6
+ "license": "MIT",
7
+ "homepage": "https://wavelength.lightning.engineering",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/lightninglabs/wavelength-sdk.git",
11
+ "directory": "packages/react-native"
12
+ },
13
+ "bugs": "https://github.com/lightninglabs/wavelength-sdk/issues",
14
+ "keywords": [
15
+ "lightning",
16
+ "bitcoin",
17
+ "wallet",
18
+ "self-custodial",
19
+ "payments",
20
+ "react-native"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "type": "module",
26
+ "main": "dist/index.js",
27
+ "types": "dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ }
33
+ },
34
+ "sideEffects": false,
35
+ "codegenConfig": {
36
+ "name": "WavelengthSpec",
37
+ "type": "modules",
38
+ "jsSrcsDir": "src",
39
+ "android": {
40
+ "javaPackageName": "engineering.lightning.wavelength.reactnative"
41
+ }
42
+ },
43
+ "dependencies": {
44
+ "@lightninglabs/wavelength-core": "0.1.0"
45
+ },
46
+ "peerDependencies": {
47
+ "react": ">=18.0.0",
48
+ "react-native": ">=0.76.0"
49
+ },
50
+ "devDependencies": {
51
+ "react-native": "0.86.0",
52
+ "@types/react": "^19.2.17",
53
+ "react": "19.2.3"
54
+ },
55
+ "files": [
56
+ "dist",
57
+ "src",
58
+ "android",
59
+ "ios",
60
+ "*.podspec",
61
+ "!android/libs",
62
+ "!ios/Wavewalletdk.xcframework"
63
+ ],
64
+ "scripts": {
65
+ "build": "tsc -p tsconfig.json",
66
+ "typecheck": "tsc -p tsconfig.json --noEmit",
67
+ "test": "node --test",
68
+ "fetch-bindings": "bash scripts/fetch-bindings.sh"
69
+ }
70
+ }
@@ -0,0 +1,32 @@
1
+ import type { TurboModule } from 'react-native';
2
+ import { TurboModuleRegistry } from 'react-native';
3
+
4
+ /**
5
+ * The Turbo Module contract for the wavelength native bridge. The surface is
6
+ * a deliberately thin JSON pipe: `call` dispatches every facade verb by name
7
+ * with a JSON string in and a JSON string out, and the activity stream
8
+ * arrives as 'wavelengthActivity' device events carrying
9
+ * `{ kind: 'entry' | 'end' | 'error', payload: string }`.
10
+ */
11
+ export interface Spec extends TurboModule {
12
+ /** Invokes a facade verb by name with a JSON payload, returning JSON. */
13
+ call(method: string, paramsJson: string): Promise<string>;
14
+ /** Opens the activity subscription; entries arrive as device events. */
15
+ startActivity(reqJson: string): Promise<void>;
16
+ /** Closes the activity subscription; a no-op when none is open. */
17
+ stopActivity(): Promise<void>;
18
+ /** Resolves the platform default wallet data directory. */
19
+ getDefaultDataDir(): Promise<string>;
20
+ /** Reports whether the platform can run a passkey PRF ceremony. */
21
+ passkeySupported(): Promise<boolean>;
22
+ /** Runs a passkey registration ceremony; WebAuthn JSON in and out. */
23
+ passkeyCreate(requestJson: string): Promise<string>;
24
+ /** Runs a passkey assertion ceremony; WebAuthn JSON in and out. */
25
+ passkeyGet(requestJson: string): Promise<string>;
26
+ /** Required by NativeEventEmitter. */
27
+ addListener(eventName: string): void;
28
+ /** Required by NativeEventEmitter. */
29
+ removeListeners(count: number): void;
30
+ }
31
+
32
+ export default TurboModuleRegistry.getEnforcing<Spec>('Wavelength');