@rejourneyco/react-native 1.0.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 (152) hide show
  1. package/android/build.gradle.kts +135 -0
  2. package/android/consumer-rules.pro +10 -0
  3. package/android/proguard-rules.pro +1 -0
  4. package/android/src/main/AndroidManifest.xml +15 -0
  5. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +2981 -0
  6. package/android/src/main/java/com/rejourney/capture/ANRHandler.kt +206 -0
  7. package/android/src/main/java/com/rejourney/capture/ActivityTracker.kt +98 -0
  8. package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +1553 -0
  9. package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +375 -0
  10. package/android/src/main/java/com/rejourney/capture/CrashHandler.kt +153 -0
  11. package/android/src/main/java/com/rejourney/capture/MotionEvent.kt +215 -0
  12. package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +512 -0
  13. package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +773 -0
  14. package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +633 -0
  15. package/android/src/main/java/com/rejourney/capture/ViewSerializer.kt +286 -0
  16. package/android/src/main/java/com/rejourney/core/Constants.kt +117 -0
  17. package/android/src/main/java/com/rejourney/core/Logger.kt +93 -0
  18. package/android/src/main/java/com/rejourney/core/Types.kt +124 -0
  19. package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +162 -0
  20. package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +747 -0
  21. package/android/src/main/java/com/rejourney/network/HttpClientProvider.kt +16 -0
  22. package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +272 -0
  23. package/android/src/main/java/com/rejourney/network/UploadManager.kt +1363 -0
  24. package/android/src/main/java/com/rejourney/network/UploadWorker.kt +492 -0
  25. package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +645 -0
  26. package/android/src/main/java/com/rejourney/touch/GestureClassifier.kt +233 -0
  27. package/android/src/main/java/com/rejourney/touch/KeyboardTracker.kt +158 -0
  28. package/android/src/main/java/com/rejourney/touch/TextInputTracker.kt +181 -0
  29. package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +591 -0
  30. package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +284 -0
  31. package/android/src/main/java/com/rejourney/utils/OEMDetector.kt +154 -0
  32. package/android/src/main/java/com/rejourney/utils/PerfTiming.kt +235 -0
  33. package/android/src/main/java/com/rejourney/utils/Telemetry.kt +297 -0
  34. package/android/src/main/java/com/rejourney/utils/WindowUtils.kt +84 -0
  35. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +187 -0
  36. package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
  37. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +218 -0
  38. package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
  39. package/ios/Capture/RJANRHandler.h +42 -0
  40. package/ios/Capture/RJANRHandler.m +328 -0
  41. package/ios/Capture/RJCaptureEngine.h +275 -0
  42. package/ios/Capture/RJCaptureEngine.m +2062 -0
  43. package/ios/Capture/RJCaptureHeuristics.h +80 -0
  44. package/ios/Capture/RJCaptureHeuristics.m +903 -0
  45. package/ios/Capture/RJCrashHandler.h +46 -0
  46. package/ios/Capture/RJCrashHandler.m +313 -0
  47. package/ios/Capture/RJMotionEvent.h +183 -0
  48. package/ios/Capture/RJMotionEvent.m +183 -0
  49. package/ios/Capture/RJPerformanceManager.h +100 -0
  50. package/ios/Capture/RJPerformanceManager.m +373 -0
  51. package/ios/Capture/RJPixelBufferDownscaler.h +42 -0
  52. package/ios/Capture/RJPixelBufferDownscaler.m +85 -0
  53. package/ios/Capture/RJSegmentUploader.h +146 -0
  54. package/ios/Capture/RJSegmentUploader.m +778 -0
  55. package/ios/Capture/RJVideoEncoder.h +247 -0
  56. package/ios/Capture/RJVideoEncoder.m +1036 -0
  57. package/ios/Capture/RJViewControllerTracker.h +73 -0
  58. package/ios/Capture/RJViewControllerTracker.m +508 -0
  59. package/ios/Capture/RJViewHierarchyScanner.h +215 -0
  60. package/ios/Capture/RJViewHierarchyScanner.m +1464 -0
  61. package/ios/Capture/RJViewSerializer.h +119 -0
  62. package/ios/Capture/RJViewSerializer.m +498 -0
  63. package/ios/Core/RJConstants.h +124 -0
  64. package/ios/Core/RJConstants.m +88 -0
  65. package/ios/Core/RJLifecycleManager.h +85 -0
  66. package/ios/Core/RJLifecycleManager.m +308 -0
  67. package/ios/Core/RJLogger.h +61 -0
  68. package/ios/Core/RJLogger.m +211 -0
  69. package/ios/Core/RJTypes.h +176 -0
  70. package/ios/Core/RJTypes.m +66 -0
  71. package/ios/Core/Rejourney.h +64 -0
  72. package/ios/Core/Rejourney.mm +2495 -0
  73. package/ios/Network/RJDeviceAuthManager.h +94 -0
  74. package/ios/Network/RJDeviceAuthManager.m +967 -0
  75. package/ios/Network/RJNetworkMonitor.h +68 -0
  76. package/ios/Network/RJNetworkMonitor.m +267 -0
  77. package/ios/Network/RJRetryManager.h +73 -0
  78. package/ios/Network/RJRetryManager.m +325 -0
  79. package/ios/Network/RJUploadManager.h +267 -0
  80. package/ios/Network/RJUploadManager.m +2296 -0
  81. package/ios/Privacy/RJPrivacyMask.h +163 -0
  82. package/ios/Privacy/RJPrivacyMask.m +922 -0
  83. package/ios/Rejourney.h +63 -0
  84. package/ios/Touch/RJGestureClassifier.h +130 -0
  85. package/ios/Touch/RJGestureClassifier.m +333 -0
  86. package/ios/Touch/RJTouchInterceptor.h +169 -0
  87. package/ios/Touch/RJTouchInterceptor.m +772 -0
  88. package/ios/Utils/RJEventBuffer.h +112 -0
  89. package/ios/Utils/RJEventBuffer.m +358 -0
  90. package/ios/Utils/RJGzipUtils.h +33 -0
  91. package/ios/Utils/RJGzipUtils.m +89 -0
  92. package/ios/Utils/RJKeychainManager.h +48 -0
  93. package/ios/Utils/RJKeychainManager.m +111 -0
  94. package/ios/Utils/RJPerfTiming.h +209 -0
  95. package/ios/Utils/RJPerfTiming.m +264 -0
  96. package/ios/Utils/RJTelemetry.h +92 -0
  97. package/ios/Utils/RJTelemetry.m +320 -0
  98. package/ios/Utils/RJWindowUtils.h +66 -0
  99. package/ios/Utils/RJWindowUtils.m +133 -0
  100. package/lib/commonjs/NativeRejourney.js +40 -0
  101. package/lib/commonjs/components/Mask.js +79 -0
  102. package/lib/commonjs/index.js +1381 -0
  103. package/lib/commonjs/sdk/autoTracking.js +1259 -0
  104. package/lib/commonjs/sdk/constants.js +151 -0
  105. package/lib/commonjs/sdk/errorTracking.js +199 -0
  106. package/lib/commonjs/sdk/index.js +50 -0
  107. package/lib/commonjs/sdk/metricsTracking.js +204 -0
  108. package/lib/commonjs/sdk/navigation.js +151 -0
  109. package/lib/commonjs/sdk/networkInterceptor.js +412 -0
  110. package/lib/commonjs/sdk/utils.js +363 -0
  111. package/lib/commonjs/types/expo-router.d.js +2 -0
  112. package/lib/commonjs/types/index.js +2 -0
  113. package/lib/module/NativeRejourney.js +38 -0
  114. package/lib/module/components/Mask.js +72 -0
  115. package/lib/module/index.js +1284 -0
  116. package/lib/module/sdk/autoTracking.js +1233 -0
  117. package/lib/module/sdk/constants.js +145 -0
  118. package/lib/module/sdk/errorTracking.js +189 -0
  119. package/lib/module/sdk/index.js +12 -0
  120. package/lib/module/sdk/metricsTracking.js +187 -0
  121. package/lib/module/sdk/navigation.js +143 -0
  122. package/lib/module/sdk/networkInterceptor.js +401 -0
  123. package/lib/module/sdk/utils.js +342 -0
  124. package/lib/module/types/expo-router.d.js +2 -0
  125. package/lib/module/types/index.js +2 -0
  126. package/lib/typescript/NativeRejourney.d.ts +147 -0
  127. package/lib/typescript/components/Mask.d.ts +39 -0
  128. package/lib/typescript/index.d.ts +117 -0
  129. package/lib/typescript/sdk/autoTracking.d.ts +204 -0
  130. package/lib/typescript/sdk/constants.d.ts +120 -0
  131. package/lib/typescript/sdk/errorTracking.d.ts +32 -0
  132. package/lib/typescript/sdk/index.d.ts +9 -0
  133. package/lib/typescript/sdk/metricsTracking.d.ts +58 -0
  134. package/lib/typescript/sdk/navigation.d.ts +33 -0
  135. package/lib/typescript/sdk/networkInterceptor.d.ts +47 -0
  136. package/lib/typescript/sdk/utils.d.ts +148 -0
  137. package/lib/typescript/types/index.d.ts +624 -0
  138. package/package.json +102 -0
  139. package/rejourney.podspec +21 -0
  140. package/src/NativeRejourney.ts +165 -0
  141. package/src/components/Mask.tsx +80 -0
  142. package/src/index.ts +1459 -0
  143. package/src/sdk/autoTracking.ts +1373 -0
  144. package/src/sdk/constants.ts +134 -0
  145. package/src/sdk/errorTracking.ts +231 -0
  146. package/src/sdk/index.ts +11 -0
  147. package/src/sdk/metricsTracking.ts +232 -0
  148. package/src/sdk/navigation.ts +157 -0
  149. package/src/sdk/networkInterceptor.ts +440 -0
  150. package/src/sdk/utils.ts +369 -0
  151. package/src/types/expo-router.d.ts +7 -0
  152. package/src/types/index.ts +739 -0
@@ -0,0 +1,967 @@
1
+ //
2
+ // RJDeviceAuthManager.m
3
+ // Rejourney
4
+ //
5
+ // Device authentication using ECDSA keypairs
6
+ // Copyright (c) 2026 Rejourney
7
+ //
8
+
9
+ #import "RJDeviceAuthManager.h"
10
+ #import "../Core/RJLogger.h"
11
+ #import "../Utils/RJKeychainManager.h"
12
+ #import <Security/Security.h>
13
+
14
+ static NSString *const kKeychainKeyTag = @"com.rejourney.devicekey";
15
+ static NSString *const kKeychainAccessGroup = nil;
16
+ static NSString *const kDeviceCredentialKey = @"RJDeviceCredentialId";
17
+ static NSString *const kUploadTokenKey = @"RJUploadToken";
18
+ static NSString *const kUploadTokenExpiryKey = @"RJUploadTokenExpiry";
19
+
20
+ @interface RJDeviceAuthManager ()
21
+ @property(nonatomic, strong) dispatch_queue_t authQueue;
22
+ @property(nonatomic, copy, nullable) NSString *cachedDeviceCredentialId;
23
+ @property(nonatomic, copy, nullable) NSString *cachedUploadToken;
24
+ @property(nonatomic, strong, nullable) NSDate *uploadTokenExpiry;
25
+ @property(nonatomic, copy, nullable) NSString *apiUrl;
26
+ @property(nonatomic, copy, nullable) NSString *projectPublicKey;
27
+ @property(nonatomic, copy, nullable) NSString *storedBundleId;
28
+ @property(nonatomic, copy, nullable) NSString *storedPlatform;
29
+ @property(nonatomic, copy, nullable) NSString *storedSdkVersion;
30
+ @property(nonatomic, assign) BOOL registrationInProgress;
31
+ @property(nonatomic, strong, nullable) NSMutableArray<RJDeviceTokenCompletionHandler> *pendingTokenCallbacks;
32
+ // Cooldown to prevent flood of failed registrations
33
+ @property(nonatomic, assign) NSTimeInterval lastFailedRegistrationTime;
34
+ @property(nonatomic, assign) NSInteger consecutiveFailures;
35
+ @end
36
+
37
+ // Cooldown constants
38
+ static const NSTimeInterval RJ_AUTH_COOLDOWN_BASE_SECONDS = 5.0; // 5 second base cooldown
39
+ static const NSTimeInterval RJ_AUTH_COOLDOWN_MAX_SECONDS = 300.0; // 5 minute max cooldown
40
+ static const NSInteger RJ_AUTH_MAX_CONSECUTIVE_FAILURES = 10;
41
+
42
+ @implementation RJDeviceAuthManager
43
+
44
+ + (instancetype)sharedManager {
45
+ static RJDeviceAuthManager *sharedInstance = nil;
46
+ static dispatch_once_t onceToken;
47
+ dispatch_once(&onceToken, ^{
48
+ sharedInstance = [[self alloc] init];
49
+ });
50
+ return sharedInstance;
51
+ }
52
+
53
+ - (instancetype)init {
54
+ self = [super init];
55
+ if (self) {
56
+ _authQueue = dispatch_queue_create("com.rejourney.deviceauth",
57
+ DISPATCH_QUEUE_SERIAL);
58
+ _pendingTokenCallbacks = [NSMutableArray array];
59
+ _registrationInProgress = NO;
60
+ [self loadStoredCredentials];
61
+ }
62
+ return self;
63
+ }
64
+
65
+ #pragma mark - Public Methods
66
+
67
+ - (void)registerDeviceWithProjectKey:(NSString *)projectPublicKey
68
+ bundleId:(NSString *)bundleId
69
+ platform:(NSString *)platform
70
+ sdkVersion:(NSString *)sdkVersion
71
+ apiUrl:(NSString *)apiUrl
72
+ completion:(RJDeviceAuthCompletionHandler)completion {
73
+
74
+
75
+ self.apiUrl = apiUrl;
76
+ self.projectPublicKey = projectPublicKey;
77
+ self.storedBundleId = bundleId;
78
+ self.storedPlatform = platform;
79
+ self.storedSdkVersion = sdkVersion;
80
+
81
+ RJLogDebug(@"configured apiUrl: '%@'", apiUrl);
82
+
83
+
84
+ if (self.cachedDeviceCredentialId) {
85
+ RJLogDebug(@"Device already registered with credential: %@",
86
+ self.cachedDeviceCredentialId);
87
+ if (completion) {
88
+ completion(YES, self.cachedDeviceCredentialId, nil);
89
+ }
90
+ return;
91
+ }
92
+
93
+ dispatch_async(self.authQueue, ^{
94
+ @autoreleasepool {
95
+
96
+ SecKeyRef privateKey = [self getOrCreatePrivateKey];
97
+ if (!privateKey) {
98
+ NSError *error =
99
+ [NSError errorWithDomain:@"RJDeviceAuth"
100
+ code:1001
101
+ userInfo:@{
102
+ NSLocalizedDescriptionKey :
103
+ @"Failed to generate ECDSA keypair"
104
+ }];
105
+ RJLogError(@"Failed to generate keypair");
106
+ if (completion) {
107
+ dispatch_async(dispatch_get_main_queue(), ^{
108
+ completion(NO, nil, error);
109
+ });
110
+ }
111
+ return;
112
+ }
113
+
114
+
115
+ SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey);
116
+ if (!publicKey) {
117
+ CFRelease(privateKey);
118
+ NSError *error = [NSError
119
+ errorWithDomain:@"RJDeviceAuth"
120
+ code:1002
121
+ userInfo:@{
122
+ NSLocalizedDescriptionKey : @"Failed to extract public key"
123
+ }];
124
+ RJLogError(@"Failed to extract public key");
125
+ if (completion) {
126
+ dispatch_async(dispatch_get_main_queue(), ^{
127
+ completion(NO, nil, error);
128
+ });
129
+ }
130
+ return;
131
+ }
132
+
133
+ NSString *publicKeyPEM = [self exportPublicKeyToPEM:publicKey];
134
+ CFRelease(publicKey);
135
+ CFRelease(privateKey);
136
+
137
+ if (!publicKeyPEM) {
138
+ NSError *error = [NSError
139
+ errorWithDomain:@"RJDeviceAuth"
140
+ code:1003
141
+ userInfo:@{
142
+ NSLocalizedDescriptionKey : @"Failed to export public key"
143
+ }];
144
+ RJLogError(@"Failed to export public key to PEM");
145
+ if (completion) {
146
+ dispatch_async(dispatch_get_main_queue(), ^{
147
+ completion(NO, nil, error);
148
+ });
149
+ }
150
+ return;
151
+ }
152
+
153
+
154
+ [self registerWithBackend:projectPublicKey
155
+ bundleId:bundleId
156
+ platform:platform
157
+ sdkVersion:sdkVersion
158
+ publicKeyPEM:publicKeyPEM
159
+ completion:completion];
160
+ }
161
+ });
162
+ }
163
+
164
+ - (void)getUploadTokenWithCompletion:
165
+ (RJDeviceTokenCompletionHandler)completion {
166
+
167
+ if ([self hasValidUploadToken]) {
168
+ NSTimeInterval remainingTime =
169
+ [self.uploadTokenExpiry timeIntervalSinceNow];
170
+ RJLogDebug(@"Using cached upload token (expires in %ld seconds)",
171
+ (long)remainingTime);
172
+ if (completion) {
173
+ completion(YES, self.cachedUploadToken, (NSInteger)remainingTime, nil);
174
+ }
175
+ return;
176
+ }
177
+
178
+
179
+ dispatch_async(self.authQueue, ^{
180
+ @autoreleasepool {
181
+ if (!self.cachedDeviceCredentialId) {
182
+ NSError *error = [NSError
183
+ errorWithDomain:@"RJDeviceAuth"
184
+ code:2001
185
+ userInfo:@{
186
+ NSLocalizedDescriptionKey : @"Device not registered"
187
+ }];
188
+ RJLogError(@"Cannot get upload token: device not registered");
189
+ if (completion) {
190
+ dispatch_async(dispatch_get_main_queue(), ^{
191
+ completion(NO, nil, 0, error);
192
+ });
193
+ }
194
+ return;
195
+ }
196
+
197
+
198
+ [self requestChallengeWithCompletion:^(BOOL success, NSString *challenge,
199
+ NSString *nonce, NSError *error) {
200
+ if (!success || !challenge || !nonce) {
201
+ RJLogError(@"Failed to get challenge: %@", error);
202
+ if (completion) {
203
+ completion(NO, nil, 0, error);
204
+ }
205
+ return;
206
+ }
207
+
208
+
209
+ NSString *signature = [self signChallenge:challenge];
210
+ if (!signature) {
211
+ NSError *signError = [NSError
212
+ errorWithDomain:@"RJDeviceAuth"
213
+ code:2002
214
+ userInfo:@{
215
+ NSLocalizedDescriptionKey : @"Failed to sign challenge"
216
+ }];
217
+ RJLogError(@"Failed to sign challenge");
218
+ if (completion) {
219
+ completion(NO, nil, 0, signError);
220
+ }
221
+ return;
222
+ }
223
+
224
+
225
+ [self startSessionWithChallenge:challenge
226
+ nonce:nonce
227
+ signature:signature
228
+ completion:completion];
229
+ }];
230
+ }
231
+ });
232
+ }
233
+
234
+ - (nullable NSString *)deviceCredentialId {
235
+ return self.cachedDeviceCredentialId;
236
+ }
237
+
238
+ - (nullable NSString *)currentUploadToken {
239
+ if ([self hasValidUploadToken]) {
240
+ return self.cachedUploadToken;
241
+ }
242
+ return nil;
243
+ }
244
+
245
+ - (BOOL)hasValidUploadToken {
246
+ if (!self.cachedUploadToken || !self.uploadTokenExpiry) {
247
+ return NO;
248
+ }
249
+
250
+
251
+ NSTimeInterval timeUntilExpiry =
252
+ [self.uploadTokenExpiry timeIntervalSinceNow];
253
+ return timeUntilExpiry > 60.0;
254
+ }
255
+
256
+ - (void)clearAllAuthData {
257
+ [self deletePrivateKey];
258
+
259
+ RJKeychainManager *keychain = [RJKeychainManager sharedManager];
260
+ [keychain deleteValueForKey:kDeviceCredentialKey];
261
+ [keychain deleteValueForKey:kUploadTokenKey];
262
+ [keychain deleteValueForKey:kUploadTokenExpiryKey];
263
+
264
+ self.cachedDeviceCredentialId = nil;
265
+ self.cachedUploadToken = nil;
266
+ self.uploadTokenExpiry = nil;
267
+
268
+ RJLogDebug(@"Cleared all device auth data");
269
+ }
270
+
271
+ #pragma mark - ECDSA Keypair Management
272
+
273
+ - (nullable SecKeyRef)getOrCreatePrivateKey {
274
+ return [self getOrCreatePrivateKeyWithRetry:YES];
275
+ }
276
+
277
+ - (nullable SecKeyRef)getOrCreatePrivateKeyWithRetry:(BOOL)shouldRetryOnFailure {
278
+
279
+ SecKeyRef privateKey = [self loadPrivateKeyFromKeychain];
280
+ if (privateKey) {
281
+
282
+ SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey);
283
+ if (publicKey) {
284
+ CFRelease(publicKey);
285
+ RJLogDebug(@"Loaded existing ECDSA private key");
286
+ return privateKey;
287
+ } else {
288
+
289
+ RJLogWarning(@"Existing private key is corrupted, deleting and regenerating");
290
+ CFRelease(privateKey);
291
+ [self deletePrivateKey];
292
+ [self clearAllAuthData];
293
+ }
294
+ }
295
+
296
+
297
+ RJLogDebug(@"Generating new ECDSA P-256 keypair");
298
+
299
+ NSDictionary *attributes = @{
300
+ (__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeECSECPrimeRandom,
301
+ (__bridge id)kSecAttrKeySizeInBits : @256,
302
+ (__bridge id)kSecPrivateKeyAttrs : @{
303
+ (__bridge id)kSecAttrIsPermanent : @YES,
304
+ (__bridge id)kSecAttrApplicationTag :
305
+ [kKeychainKeyTag dataUsingEncoding:NSUTF8StringEncoding],
306
+ (__bridge id)kSecAttrAccessible :
307
+ (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
308
+ }
309
+ };
310
+
311
+ CFErrorRef error = NULL;
312
+ privateKey =
313
+ SecKeyCreateRandomKey((__bridge CFDictionaryRef)attributes, &error);
314
+
315
+ if (error) {
316
+ CFStringRef errorDesc = CFErrorCopyDescription(error);
317
+ RJLogError(@"Failed to generate ECDSA key: %@",
318
+ (__bridge NSString *)errorDesc);
319
+ if (errorDesc)
320
+ CFRelease(errorDesc);
321
+ CFRelease(error);
322
+
323
+
324
+
325
+ if (shouldRetryOnFailure) {
326
+ RJLogDebug(@"Attempting recovery: deleting existing key and retrying generation");
327
+ [self deletePrivateKey];
328
+ return [self getOrCreatePrivateKeyWithRetry:NO];
329
+ }
330
+
331
+ return NULL;
332
+ }
333
+
334
+ if (!privateKey) {
335
+ RJLogError(@"SecKeyCreateRandomKey returned NULL");
336
+ return NULL;
337
+ }
338
+
339
+ RJLogDebug(@"Successfully generated ECDSA P-256 keypair");
340
+ return privateKey;
341
+ }
342
+
343
+ - (nullable SecKeyRef)loadPrivateKeyFromKeychain {
344
+ NSDictionary *query = @{
345
+ (__bridge id)kSecClass : (__bridge id)kSecClassKey,
346
+ (__bridge id)kSecAttrApplicationTag :
347
+ [kKeychainKeyTag dataUsingEncoding:NSUTF8StringEncoding],
348
+ (__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeECSECPrimeRandom,
349
+ (__bridge id)kSecReturnRef : @YES
350
+ };
351
+
352
+ SecKeyRef privateKey = NULL;
353
+ OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query,
354
+ (CFTypeRef *)&privateKey);
355
+
356
+ if (status == errSecSuccess && privateKey) {
357
+ return privateKey;
358
+ }
359
+
360
+ return NULL;
361
+ }
362
+
363
+ - (void)deletePrivateKey {
364
+ NSDictionary *query = @{
365
+ (__bridge id)kSecClass : (__bridge id)kSecClassKey,
366
+ (__bridge id)kSecAttrApplicationTag :
367
+ [kKeychainKeyTag dataUsingEncoding:NSUTF8StringEncoding],
368
+ (__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeECSECPrimeRandom
369
+ };
370
+
371
+ OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
372
+ if (status == errSecSuccess) {
373
+ RJLogDebug(@"Deleted private key from Keychain");
374
+ }
375
+ }
376
+
377
+ - (nullable NSString *)exportPublicKeyToPEM:(SecKeyRef)publicKey {
378
+ CFErrorRef error = NULL;
379
+ CFDataRef publicKeyDataRef =
380
+ SecKeyCopyExternalRepresentation(publicKey, &error);
381
+
382
+ if (error) {
383
+ CFStringRef errorDesc = CFErrorCopyDescription(error);
384
+ RJLogError(@"Failed to export public key: %@",
385
+ (__bridge NSString *)errorDesc);
386
+ if (errorDesc)
387
+ CFRelease(errorDesc);
388
+ CFRelease(error);
389
+ return nil;
390
+ }
391
+
392
+ if (!publicKeyDataRef) {
393
+ RJLogError(@"Public key data is nil");
394
+ return nil;
395
+ }
396
+
397
+ NSData *publicKeyData = (__bridge NSData *)publicKeyDataRef;
398
+
399
+
400
+
401
+ NSString *base64 = [publicKeyData
402
+ base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
403
+
404
+
405
+ NSString *pem = [NSString
406
+ stringWithFormat:
407
+ @"-----BEGIN PUBLIC KEY-----\n%@\n-----END PUBLIC KEY-----", base64];
408
+
409
+ CFRelease(publicKeyDataRef);
410
+
411
+ return pem;
412
+ }
413
+
414
+ - (nullable NSString *)signChallenge:(NSString *)challenge {
415
+
416
+ SecKeyRef privateKey = [self loadPrivateKeyFromKeychain];
417
+ if (!privateKey) {
418
+ RJLogError(@"Private key not found");
419
+ return nil;
420
+ }
421
+
422
+
423
+ NSData *challengeData = [[NSData alloc] initWithBase64EncodedString:challenge
424
+ options:0];
425
+ if (!challengeData) {
426
+ CFRelease(privateKey);
427
+ RJLogError(@"Invalid challenge base64");
428
+ return nil;
429
+ }
430
+
431
+
432
+ CFErrorRef error = NULL;
433
+ CFDataRef signatureRef = SecKeyCreateSignature(
434
+ privateKey, kSecKeyAlgorithmECDSASignatureMessageX962SHA256,
435
+ (__bridge CFDataRef)challengeData, &error);
436
+
437
+ CFRelease(privateKey);
438
+
439
+ if (error) {
440
+ CFStringRef errorDesc = CFErrorCopyDescription(error);
441
+ RJLogError(@"Failed to sign challenge: %@", (__bridge NSString *)errorDesc);
442
+ if (errorDesc)
443
+ CFRelease(errorDesc);
444
+ CFRelease(error);
445
+ return nil;
446
+ }
447
+
448
+ if (!signatureRef) {
449
+ RJLogError(@"Signature is nil");
450
+ return nil;
451
+ }
452
+
453
+ NSData *signature = (__bridge NSData *)signatureRef;
454
+
455
+
456
+ NSString *base64Sig = [signature base64EncodedStringWithOptions:0];
457
+
458
+ CFRelease(signatureRef);
459
+
460
+ return base64Sig;
461
+ }
462
+
463
+ #pragma mark - Backend Communication
464
+
465
+ - (void)registerWithBackend:(NSString *)projectPublicKey
466
+ bundleId:(NSString *)bundleId
467
+ platform:(NSString *)platform
468
+ sdkVersion:(NSString *)sdkVersion
469
+ publicKeyPEM:(NSString *)publicKeyPEM
470
+ completion:(RJDeviceAuthCompletionHandler)completion {
471
+
472
+ NSString *urlString =
473
+ [NSString stringWithFormat:@"%@/api/devices/register", self.apiUrl];
474
+ RJLogDebug(@"Register URL: %@", urlString);
475
+ NSURL *url = [NSURL URLWithString:urlString];
476
+
477
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
478
+ request.HTTPMethod = @"POST";
479
+ [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
480
+
481
+ NSDictionary *body = @{
482
+ @"projectPublicKey" : projectPublicKey,
483
+ @"bundleId" : bundleId,
484
+ @"platform" : platform,
485
+ @"sdkVersion" : sdkVersion,
486
+ @"devicePublicKey" : publicKeyPEM
487
+ };
488
+
489
+ NSError *jsonError;
490
+ request.HTTPBody = [NSJSONSerialization dataWithJSONObject:body
491
+ options:0
492
+ error:&jsonError];
493
+
494
+ if (jsonError) {
495
+ RJLogError(@"Failed to serialize registration request: %@", jsonError);
496
+ if (completion) {
497
+ dispatch_async(dispatch_get_main_queue(), ^{
498
+ completion(NO, nil, jsonError);
499
+ });
500
+ }
501
+ return;
502
+ }
503
+
504
+ RJLogDebug(@"Registering device with backend...");
505
+
506
+ NSURLSessionDataTask *task = [[NSURLSession sharedSession]
507
+ dataTaskWithRequest:request
508
+ completionHandler:^(NSData *data, NSURLResponse *response,
509
+ NSError *error) {
510
+ if (error) {
511
+ RJLogError(@"Registration request failed: %@", error);
512
+ if (completion) {
513
+ dispatch_async(dispatch_get_main_queue(), ^{
514
+ completion(NO, nil, error);
515
+ });
516
+ }
517
+ return;
518
+ }
519
+
520
+ NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
521
+ if (httpResponse.statusCode != 200) {
522
+ NSString *errorMsg = [NSString
523
+ stringWithFormat:@"Registration failed with status %ld",
524
+ (long)httpResponse.statusCode];
525
+ NSError *httpError = [NSError
526
+ errorWithDomain:@"RJDeviceAuth"
527
+ code:httpResponse.statusCode
528
+ userInfo:@{NSLocalizedDescriptionKey : errorMsg}];
529
+ RJLogError(@"%@", errorMsg);
530
+ if (completion) {
531
+ dispatch_async(dispatch_get_main_queue(), ^{
532
+ completion(NO, nil, httpError);
533
+ });
534
+ }
535
+ return;
536
+ }
537
+
538
+ NSError *parseError;
539
+ NSDictionary *json =
540
+ [NSJSONSerialization JSONObjectWithData:data
541
+ options:0
542
+ error:&parseError];
543
+
544
+ if (parseError || !json[@"deviceCredentialId"]) {
545
+ RJLogError(@"Failed to parse registration response: %@",
546
+ parseError);
547
+ if (completion) {
548
+ dispatch_async(dispatch_get_main_queue(), ^{
549
+ completion(NO, nil, parseError);
550
+ });
551
+ }
552
+ return;
553
+ }
554
+
555
+ NSString *credentialId = json[@"deviceCredentialId"];
556
+ [self saveDeviceCredentialId:credentialId];
557
+
558
+ RJLogDebug(@"Device registered: %@", credentialId);
559
+
560
+ if (completion) {
561
+ dispatch_async(dispatch_get_main_queue(), ^{
562
+ completion(YES, credentialId, nil);
563
+ });
564
+ }
565
+ }];
566
+
567
+ [task resume];
568
+ }
569
+
570
+ - (void)requestChallengeWithCompletion:
571
+ (void (^)(BOOL success, NSString *_Nullable challenge,
572
+ NSString *_Nullable nonce, NSError *_Nullable error))completion {
573
+
574
+ NSString *urlString = [NSString
575
+ stringWithFormat:@"%@/api/devices/challenge", self.apiUrl];
576
+ RJLogDebug(@"Challenge URL: %@", urlString);
577
+ NSURL *url = [NSURL URLWithString:urlString];
578
+
579
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
580
+ request.HTTPMethod = @"POST";
581
+ [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
582
+
583
+ NSDictionary *body = @{@"deviceCredentialId" : self.cachedDeviceCredentialId};
584
+
585
+ NSError *jsonError;
586
+ request.HTTPBody = [NSJSONSerialization dataWithJSONObject:body
587
+ options:0
588
+ error:&jsonError];
589
+
590
+ if (jsonError) {
591
+ RJLogError(@"Failed to serialize challenge request: %@", jsonError);
592
+ if (completion) {
593
+ completion(NO, nil, nil, jsonError);
594
+ }
595
+ return;
596
+ }
597
+
598
+ RJLogDebug(@"Requesting challenge from backend...");
599
+
600
+ NSURLSessionDataTask *task = [[NSURLSession sharedSession]
601
+ dataTaskWithRequest:request
602
+ completionHandler:^(NSData *data, NSURLResponse *response,
603
+ NSError *error) {
604
+ if (error) {
605
+ RJLogError(@"Challenge request failed: %@", error);
606
+ if (completion) {
607
+ completion(NO, nil, nil, error);
608
+ }
609
+ return;
610
+ }
611
+
612
+ NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
613
+ if (httpResponse.statusCode != 200) {
614
+ NSString *errorMsg = [NSString
615
+ stringWithFormat:@"Challenge request failed with status %ld",
616
+ (long)httpResponse.statusCode];
617
+
618
+
619
+ if (data) {
620
+ NSString *body =
621
+ [[NSString alloc] initWithData:data
622
+ encoding:NSUTF8StringEncoding];
623
+ RJLogError(@"Error response body: %@", body);
624
+ }
625
+
626
+
627
+
628
+ if (httpResponse.statusCode == 404 || httpResponse.statusCode == 403) {
629
+ RJLogDebug(@"Device credential rejected by backend (%ld) - "
630
+ @"clearing local credentials",
631
+ (long)httpResponse.statusCode);
632
+ [self clearAllAuthData];
633
+ }
634
+
635
+ NSError *httpError = [NSError
636
+ errorWithDomain:@"RJDeviceAuth"
637
+ code:httpResponse.statusCode
638
+ userInfo:@{NSLocalizedDescriptionKey : errorMsg}];
639
+ RJLogError(@"%@", errorMsg);
640
+ if (completion) {
641
+ completion(NO, nil, nil, httpError);
642
+ }
643
+ return;
644
+ }
645
+
646
+ NSError *parseError;
647
+ NSDictionary *json =
648
+ [NSJSONSerialization JSONObjectWithData:data
649
+ options:0
650
+ error:&parseError];
651
+
652
+ if (parseError || !json[@"challenge"] || !json[@"nonce"]) {
653
+ RJLogError(@"Failed to parse challenge response: %@", parseError);
654
+ if (completion) {
655
+ completion(NO, nil, nil, parseError);
656
+ }
657
+ return;
658
+ }
659
+
660
+ NSString *challenge = json[@"challenge"];
661
+ NSString *nonce = json[@"nonce"];
662
+
663
+ RJLogDebug(@"Received challenge from backend");
664
+
665
+ if (completion) {
666
+ completion(YES, challenge, nonce, nil);
667
+ }
668
+ }];
669
+
670
+ [task resume];
671
+ }
672
+
673
+ - (void)startSessionWithChallenge:(NSString *)challenge
674
+ nonce:(NSString *)nonce
675
+ signature:(NSString *)signature
676
+ completion:(RJDeviceTokenCompletionHandler)completion {
677
+
678
+ NSString *urlString = [NSString
679
+ stringWithFormat:@"%@/api/devices/start-session", self.apiUrl];
680
+ RJLogDebug(@"Start Session URL: %@", urlString);
681
+ NSURL *url = [NSURL URLWithString:urlString];
682
+
683
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
684
+ request.HTTPMethod = @"POST";
685
+ [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
686
+
687
+ NSDictionary *body = @{
688
+ @"deviceCredentialId" : self.cachedDeviceCredentialId,
689
+ @"challenge" : challenge,
690
+ @"signature" : signature,
691
+ @"nonce" : nonce
692
+ };
693
+
694
+ NSError *jsonError;
695
+ request.HTTPBody = [NSJSONSerialization dataWithJSONObject:body
696
+ options:0
697
+ error:&jsonError];
698
+
699
+ if (jsonError) {
700
+ RJLogError(@"Failed to serialize start-session request: %@", jsonError);
701
+ if (completion) {
702
+ dispatch_async(dispatch_get_main_queue(), ^{
703
+ completion(NO, nil, 0, jsonError);
704
+ });
705
+ }
706
+ return;
707
+ }
708
+
709
+ RJLogDebug(@"Starting session with signed challenge...");
710
+
711
+ NSURLSessionDataTask *task = [[NSURLSession sharedSession]
712
+ dataTaskWithRequest:request
713
+ completionHandler:^(NSData *data, NSURLResponse *response,
714
+ NSError *error) {
715
+ if (error) {
716
+ RJLogError(@"Start-session request failed: %@", error);
717
+ if (completion) {
718
+ dispatch_async(dispatch_get_main_queue(), ^{
719
+ completion(NO, nil, 0, error);
720
+ });
721
+ }
722
+ return;
723
+ }
724
+
725
+ NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
726
+ if (httpResponse.statusCode != 200) {
727
+ NSString *errorMsg = [NSString
728
+ stringWithFormat:@"Start-session failed with status %ld",
729
+ (long)httpResponse.statusCode];
730
+ NSError *httpError = [NSError
731
+ errorWithDomain:@"RJDeviceAuth"
732
+ code:httpResponse.statusCode
733
+ userInfo:@{NSLocalizedDescriptionKey : errorMsg}];
734
+ RJLogError(@"%@", errorMsg);
735
+ if (completion) {
736
+ dispatch_async(dispatch_get_main_queue(), ^{
737
+ completion(NO, nil, 0, httpError);
738
+ });
739
+ }
740
+ return;
741
+ }
742
+
743
+ NSError *parseError;
744
+ NSDictionary *json =
745
+ [NSJSONSerialization JSONObjectWithData:data
746
+ options:0
747
+ error:&parseError];
748
+
749
+ if (parseError || !json[@"uploadToken"]) {
750
+ RJLogError(@"Failed to parse start-session response: %@",
751
+ parseError);
752
+ if (completion) {
753
+ dispatch_async(dispatch_get_main_queue(), ^{
754
+ completion(NO, nil, 0, parseError);
755
+ });
756
+ }
757
+ return;
758
+ }
759
+
760
+ NSString *uploadToken = json[@"uploadToken"];
761
+ NSInteger expiresIn = [json[@"expiresIn"] integerValue];
762
+
763
+ [self saveUploadToken:uploadToken expiresIn:expiresIn];
764
+
765
+ RJLogDebug(@"Got upload token (expires in %ld seconds)",
766
+ (long)expiresIn);
767
+
768
+ if (completion) {
769
+ dispatch_async(dispatch_get_main_queue(), ^{
770
+ completion(YES, uploadToken, expiresIn, nil);
771
+ });
772
+ }
773
+ }];
774
+
775
+ [task resume];
776
+ }
777
+
778
+ #pragma mark - Storage
779
+
780
+ - (void)loadStoredCredentials {
781
+ RJKeychainManager *keychain = [RJKeychainManager sharedManager];
782
+
783
+ self.cachedDeviceCredentialId = [keychain stringForKey:kDeviceCredentialKey];
784
+ self.cachedUploadToken = [keychain stringForKey:kUploadTokenKey];
785
+
786
+ NSString *expiryString = [keychain stringForKey:kUploadTokenExpiryKey];
787
+ if (expiryString) {
788
+ self.uploadTokenExpiry =
789
+ [NSDate dateWithTimeIntervalSince1970:[expiryString doubleValue]];
790
+ }
791
+
792
+ if (self.cachedDeviceCredentialId) {
793
+ RJLogDebug(@"Loaded stored device credential: %@",
794
+ self.cachedDeviceCredentialId);
795
+ }
796
+ }
797
+
798
+ - (void)saveDeviceCredentialId:(NSString *)credentialId {
799
+ self.cachedDeviceCredentialId = credentialId;
800
+ [[RJKeychainManager sharedManager] setString:credentialId
801
+ forKey:kDeviceCredentialKey];
802
+ }
803
+
804
+ - (void)saveUploadToken:(NSString *)token expiresIn:(NSInteger)expiresIn {
805
+ self.cachedUploadToken = token;
806
+ self.uploadTokenExpiry = [NSDate dateWithTimeIntervalSinceNow:expiresIn];
807
+
808
+ RJKeychainManager *keychain = [RJKeychainManager sharedManager];
809
+ [keychain setString:token forKey:kUploadTokenKey];
810
+ [keychain setString:[@([self.uploadTokenExpiry timeIntervalSince1970]) stringValue]
811
+ forKey:kUploadTokenExpiryKey];
812
+ }
813
+
814
+ #pragma mark - Auto Registration
815
+
816
+ - (BOOL)canAutoRegister {
817
+ return self.projectPublicKey.length > 0 &&
818
+ self.storedBundleId.length > 0 &&
819
+ self.apiUrl.length > 0;
820
+ }
821
+
822
+ - (BOOL)isDeviceRegistered {
823
+ return self.cachedDeviceCredentialId.length > 0;
824
+ }
825
+
826
+ - (void)getUploadTokenWithAutoRegisterCompletion:(RJDeviceTokenCompletionHandler)completion {
827
+
828
+ if ([self hasValidUploadToken]) {
829
+ NSTimeInterval remainingTime = [self.uploadTokenExpiry timeIntervalSinceNow];
830
+ RJLogDebug(@"Using cached upload token (expires in %ld seconds)", (long)remainingTime);
831
+ if (completion) {
832
+ completion(YES, self.cachedUploadToken, (NSInteger)remainingTime, nil);
833
+ }
834
+ return;
835
+ }
836
+
837
+
838
+ if (self.cachedDeviceCredentialId) {
839
+ [self getUploadTokenWithCompletion:completion];
840
+ return;
841
+ }
842
+
843
+
844
+ if (![self canAutoRegister]) {
845
+ NSError *error = [NSError
846
+ errorWithDomain:@"RJDeviceAuth"
847
+ code:2003
848
+ userInfo:@{
849
+ NSLocalizedDescriptionKey : @"Device not registered and auto-registration not configured"
850
+ }];
851
+ RJLogError(@"Cannot auto-register: missing registration parameters");
852
+ if (completion) {
853
+ dispatch_async(dispatch_get_main_queue(), ^{
854
+ completion(NO, nil, 0, error);
855
+ });
856
+ }
857
+ return;
858
+ }
859
+
860
+ // Check cooldown after consecutive failures to prevent flooding
861
+ NSTimeInterval now = [[NSDate date] timeIntervalSince1970];
862
+ if (self.consecutiveFailures > 0 && self.lastFailedRegistrationTime > 0) {
863
+ // Exponential backoff: 5s, 10s, 20s, 40s... up to 5 minutes
864
+ NSTimeInterval cooldown = MIN(
865
+ RJ_AUTH_COOLDOWN_BASE_SECONDS * pow(2, self.consecutiveFailures - 1),
866
+ RJ_AUTH_COOLDOWN_MAX_SECONDS
867
+ );
868
+ NSTimeInterval timeSinceLastFailure = now - self.lastFailedRegistrationTime;
869
+
870
+ if (timeSinceLastFailure < cooldown) {
871
+ NSTimeInterval remainingCooldown = cooldown - timeSinceLastFailure;
872
+ RJLogDebug(@"Auto-registration in cooldown (%.1fs remaining after %ld failures)",
873
+ remainingCooldown, (long)self.consecutiveFailures);
874
+
875
+ NSError *cooldownError = [NSError
876
+ errorWithDomain:@"RJDeviceAuth"
877
+ code:429
878
+ userInfo:@{NSLocalizedDescriptionKey :
879
+ [NSString stringWithFormat:@"Rate limited - retry in %.0fs", remainingCooldown]}];
880
+ if (completion) {
881
+ dispatch_async(dispatch_get_main_queue(), ^{
882
+ completion(NO, nil, 0, cooldownError);
883
+ });
884
+ }
885
+ return;
886
+ }
887
+ }
888
+
889
+
890
+ @synchronized(self.pendingTokenCallbacks) {
891
+ if (completion) {
892
+ [self.pendingTokenCallbacks addObject:[completion copy]];
893
+ }
894
+
895
+
896
+ if (self.registrationInProgress) {
897
+ RJLogDebug(@"Auto-registration already in progress, callback queued");
898
+ return;
899
+ }
900
+
901
+ self.registrationInProgress = YES;
902
+ }
903
+
904
+ RJLogDebug(@"Device not registered - starting automatic re-registration...");
905
+
906
+ __weak __typeof__(self) weakSelf = self;
907
+ [self registerDeviceWithProjectKey:self.projectPublicKey
908
+ bundleId:self.storedBundleId
909
+ platform:self.storedPlatform ?: @"ios"
910
+ sdkVersion:self.storedSdkVersion ?: @"1.0.0"
911
+ apiUrl:self.apiUrl
912
+ completion:^(BOOL success, NSString *credId, NSError *error) {
913
+ __strong __typeof__(weakSelf) strongSelf = weakSelf;
914
+ if (!strongSelf) return;
915
+
916
+ if (!success) {
917
+ RJLogError(@"Auto-registration failed: %@", error);
918
+ // Track consecutive failures for exponential backoff
919
+ strongSelf.consecutiveFailures++;
920
+ strongSelf.lastFailedRegistrationTime = [[NSDate date] timeIntervalSince1970];
921
+
922
+ if (strongSelf.consecutiveFailures >= RJ_AUTH_MAX_CONSECUTIVE_FAILURES) {
923
+ RJLogError(@"Auto-registration failed %ld times - backing off significantly",
924
+ (long)strongSelf.consecutiveFailures);
925
+ }
926
+
927
+ [strongSelf notifyPendingCallbacksWithSuccess:NO token:nil expiresIn:0 error:error];
928
+ return;
929
+ }
930
+
931
+ // Success! Reset failure tracking
932
+ strongSelf.consecutiveFailures = 0;
933
+ strongSelf.lastFailedRegistrationTime = 0;
934
+
935
+ RJLogDebug(@"Auto-registration successful (credential: %@), fetching upload token...", credId);
936
+
937
+
938
+ [strongSelf getUploadTokenWithCompletion:^(BOOL tokenSuccess, NSString *token,
939
+ NSInteger expiresIn, NSError *tokenError) {
940
+ [strongSelf notifyPendingCallbacksWithSuccess:tokenSuccess
941
+ token:token
942
+ expiresIn:expiresIn
943
+ error:tokenError];
944
+ }];
945
+ }];
946
+ }
947
+
948
+ - (void)notifyPendingCallbacksWithSuccess:(BOOL)success
949
+ token:(NSString *)token
950
+ expiresIn:(NSInteger)expiresIn
951
+ error:(NSError *)error {
952
+ NSArray<RJDeviceTokenCompletionHandler> *callbacks;
953
+
954
+ @synchronized(self.pendingTokenCallbacks) {
955
+ self.registrationInProgress = NO;
956
+ callbacks = [self.pendingTokenCallbacks copy];
957
+ [self.pendingTokenCallbacks removeAllObjects];
958
+ }
959
+
960
+ dispatch_async(dispatch_get_main_queue(), ^{
961
+ for (RJDeviceTokenCompletionHandler callback in callbacks) {
962
+ callback(success, token, expiresIn, error);
963
+ }
964
+ });
965
+ }
966
+
967
+ @end