rampkit-expo-dev 0.0.66 → 0.0.68

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.
@@ -18,6 +18,8 @@ interface RampKitNativeModule {
18
18
  getNotificationPermissions(): Promise<NotificationPermissionResult>;
19
19
  startTransactionObserver(appId: string): Promise<void>;
20
20
  stopTransactionObserver(): Promise<void>;
21
+ trackPurchaseCompleted(productId: string, transactionId?: string, originalTransactionId?: string): Promise<void>;
22
+ trackPurchaseFromProduct(productId: string): Promise<void>;
21
23
  }
22
24
  export interface NativeDeviceInfo {
23
25
  appUserId: string;
@@ -162,4 +164,21 @@ export declare const TransactionObserver: {
162
164
  * Stop listening for purchase transactions
163
165
  */
164
166
  stop(): Promise<void>;
167
+ /**
168
+ * Manually track a purchase completion
169
+ * Use this when Superwall/RevenueCat reports a purchase but the automatic
170
+ * observer doesn't catch it (they finish transactions before we see them)
171
+ *
172
+ * @param productId - The product ID (e.g., "com.app.yearly")
173
+ * @param transactionId - Optional transaction ID if available
174
+ * @param originalTransactionId - Optional original transaction ID (for renewals)
175
+ */
176
+ trackPurchase(productId: string, transactionId?: string, originalTransactionId?: string): Promise<void>;
177
+ /**
178
+ * Track a purchase by looking up the product's latest transaction
179
+ * Use this when you only have the productId (common with Superwall)
180
+ *
181
+ * @param productId - The product ID to look up and track
182
+ */
183
+ trackPurchaseByProductId(productId: string): Promise<void>;
165
184
  };
@@ -62,6 +62,8 @@ function createFallbackModule() {
62
62
  },
63
63
  async startTransactionObserver(_appId) { },
64
64
  async stopTransactionObserver() { },
65
+ async trackPurchaseCompleted(_productId, _transactionId, _originalTransactionId) { },
66
+ async trackPurchaseFromProduct(_productId) { },
65
67
  };
66
68
  }
67
69
  function generateFallbackUserId() {
@@ -285,4 +287,39 @@ exports.TransactionObserver = {
285
287
  console.warn("[RampKit] Failed to stop transaction observer:", e);
286
288
  }
287
289
  },
290
+ /**
291
+ * Manually track a purchase completion
292
+ * Use this when Superwall/RevenueCat reports a purchase but the automatic
293
+ * observer doesn't catch it (they finish transactions before we see them)
294
+ *
295
+ * @param productId - The product ID (e.g., "com.app.yearly")
296
+ * @param transactionId - Optional transaction ID if available
297
+ * @param originalTransactionId - Optional original transaction ID (for renewals)
298
+ */
299
+ async trackPurchase(productId, transactionId, originalTransactionId) {
300
+ try {
301
+ console.log("[RampKit] Manually tracking purchase:", productId);
302
+ await RampKitNativeModule.trackPurchaseCompleted(productId, transactionId, originalTransactionId);
303
+ console.log("[RampKit] Purchase tracked successfully:", productId);
304
+ }
305
+ catch (e) {
306
+ console.warn("[RampKit] Failed to track purchase:", e);
307
+ }
308
+ },
309
+ /**
310
+ * Track a purchase by looking up the product's latest transaction
311
+ * Use this when you only have the productId (common with Superwall)
312
+ *
313
+ * @param productId - The product ID to look up and track
314
+ */
315
+ async trackPurchaseByProductId(productId) {
316
+ try {
317
+ console.log("[RampKit] Looking up and tracking purchase for:", productId);
318
+ await RampKitNativeModule.trackPurchaseFromProduct(productId);
319
+ console.log("[RampKit] Purchase lookup and tracking complete:", productId);
320
+ }
321
+ catch (e) {
322
+ console.warn("[RampKit] Failed to track purchase by product:", e);
323
+ }
324
+ },
288
325
  };
@@ -9,14 +9,27 @@ public class RampKitModule: Module {
9
9
  private let installDateKey = "rk_install_date"
10
10
  private let launchCountKey = "rk_launch_count"
11
11
  private let lastLaunchKey = "rk_last_launch"
12
-
12
+ private let trackedTransactionsKey = "rk_tracked_transactions"
13
+
13
14
  // Transaction observer task
14
15
  private var transactionObserverTask: Task<Void, Never>?
15
16
  private var appId: String?
16
17
  private var userId: String?
17
-
18
+ private var isConfigured = false
19
+
20
+ // Set of already-tracked originalTransactionIds to prevent duplicates
21
+ private var trackedTransactionIds: Set<String> = []
22
+
18
23
  public func definition() -> ModuleDefinition {
19
24
  Name("RampKit")
25
+
26
+ // OnCreate runs when JS first requires the module - may be too late for some transactions
27
+ OnCreate {
28
+ print("[RampKit] ⚡ OnCreate called - module being initialized")
29
+ self.loadTrackedTransactions()
30
+ self.userId = self.getOrCreateUserId()
31
+ print("[RampKit] ⚡ OnCreate complete, userId: \(self.userId ?? "nil")")
32
+ }
20
33
 
21
34
  // ============================================================================
22
35
  // Device Info
@@ -89,14 +102,30 @@ public class RampKitModule: Module {
89
102
  // ============================================================================
90
103
  // Transaction Observer (StoreKit 2)
91
104
  // ============================================================================
92
-
105
+
93
106
  AsyncFunction("startTransactionObserver") { (appId: String) in
94
107
  self.startTransactionObserver(appId: appId)
95
108
  }
96
-
109
+
97
110
  AsyncFunction("stopTransactionObserver") { () in
98
111
  self.stopTransactionObserver()
99
112
  }
113
+
114
+ // ============================================================================
115
+ // Manual Purchase Tracking (Fallback for Superwall/RevenueCat)
116
+ // ============================================================================
117
+
118
+ AsyncFunction("trackPurchaseCompleted") { (productId: String, transactionId: String?, originalTransactionId: String?) in
119
+ await self.trackPurchaseCompletedManually(
120
+ productId: productId,
121
+ transactionId: transactionId,
122
+ originalTransactionId: originalTransactionId
123
+ )
124
+ }
125
+
126
+ AsyncFunction("trackPurchaseFromProduct") { (productId: String) in
127
+ await self.trackPurchaseFromProductId(productId: productId)
128
+ }
100
129
  }
101
130
 
102
131
  // MARK: - Device Info Collection
@@ -224,8 +253,23 @@ public class RampKitModule: Module {
224
253
  SecItemAdd(addQuery as CFDictionary, nil)
225
254
  }
226
255
 
256
+ // MARK: - Tracked Transactions (Deduplication)
257
+
258
+ private func loadTrackedTransactions() {
259
+ if let stored = UserDefaults.standard.array(forKey: trackedTransactionsKey) as? [String] {
260
+ trackedTransactionIds = Set(stored)
261
+ print("[RampKit] 📚 Loaded \(trackedTransactionIds.count) tracked transaction IDs")
262
+ }
263
+ }
264
+
265
+ private func saveTrackedTransactions() {
266
+ let array = Array(trackedTransactionIds)
267
+ UserDefaults.standard.set(array, forKey: trackedTransactionsKey)
268
+ print("[RampKit] 💾 Saved \(trackedTransactionIds.count) tracked transaction IDs")
269
+ }
270
+
227
271
  // MARK: - Launch Tracking
228
-
272
+
229
273
  private func getLaunchTrackingData() -> [String: Any?] {
230
274
  let defaults = UserDefaults.standard
231
275
  let now = ISO8601DateFormatter().string(from: Date())
@@ -386,78 +430,137 @@ public class RampKitModule: Module {
386
430
  }
387
431
 
388
432
  // MARK: - StoreKit 2 Transaction Observer
389
-
433
+
434
+ /// Called from JavaScript - sets appId and IMMEDIATELY checks for purchases we may have missed
390
435
  private func startTransactionObserver(appId: String) {
391
436
  self.appId = appId
392
437
  self.userId = getOrCreateUserId()
393
-
394
- // Cancel existing observer if any
395
- transactionObserverTask?.cancel()
396
-
397
- // Start listening for transactions (iOS 15+)
438
+ self.isConfigured = true
439
+
440
+ print("[RampKit] ✅ Transaction observer configured with appId: \(appId)")
441
+ print("[RampKit] 📊 Already tracked \(trackedTransactionIds.count) transactions")
442
+
398
443
  if #available(iOS 15.0, *) {
399
- transactionObserverTask = Task {
400
- await self.listenForTransactions()
444
+ // CRITICAL: Check current entitlements for any purchases we missed
445
+ // This is the KEY mechanism for catching Superwall/RevenueCat purchases
446
+ Task {
447
+ await self.checkAndTrackCurrentEntitlements()
401
448
  }
402
-
403
- // Also check for any unfinished transactions
449
+
450
+ // Also start listening for future transactions
404
451
  Task {
405
- await self.handleUnfinishedTransactions()
452
+ await self.startTransactionUpdatesListener()
406
453
  }
407
454
  }
408
-
409
- print("[RampKit] Transaction observer started")
410
455
  }
411
-
412
- private func stopTransactionObserver() {
413
- transactionObserverTask?.cancel()
414
- transactionObserverTask = nil
415
- print("[RampKit] Transaction observer stopped")
416
- }
417
-
456
+
457
+ /// Check all current entitlements and track any we haven't seen before
458
+ /// This catches purchases made by Superwall/RevenueCat before our observer started
418
459
  @available(iOS 15.0, *)
419
- private func listenForTransactions() async {
420
- // Listen for transaction updates
421
- for await result in Transaction.updates {
422
- do {
423
- let transaction = try self.checkVerified(result)
424
- await self.handleTransaction(transaction)
425
- await transaction.finish()
426
- } catch {
427
- print("[RampKit] Transaction verification failed: \(error)")
460
+ private func checkAndTrackCurrentEntitlements() async {
461
+ print("[RampKit] 🔍 Checking current entitlements for missed purchases...")
462
+
463
+ var foundCount = 0
464
+ var trackedCount = 0
465
+ var newCount = 0
466
+
467
+ for await result in Transaction.currentEntitlements {
468
+ foundCount += 1
469
+
470
+ guard case .verified(let transaction) = result else {
471
+ print("[RampKit] ⚠️ Unverified entitlement skipped")
472
+ continue
428
473
  }
474
+
475
+ let originalId = String(transaction.originalID)
476
+ print("[RampKit] 📦 Found entitlement: \(transaction.productID), originalID: \(originalId)")
477
+
478
+ // Check if we've already tracked this transaction
479
+ if trackedTransactionIds.contains(originalId) {
480
+ trackedCount += 1
481
+ print("[RampKit] ✓ Already tracked: \(transaction.productID)")
482
+ continue
483
+ }
484
+
485
+ // NEW transaction we haven't seen!
486
+ newCount += 1
487
+ print("[RampKit] 🆕 NEW purchase detected: \(transaction.productID)")
488
+
489
+ // Track it
490
+ await self.handleTransaction(transaction)
491
+
492
+ // Mark as tracked
493
+ trackedTransactionIds.insert(originalId)
494
+ saveTrackedTransactions()
429
495
  }
496
+
497
+ print("[RampKit] 🔍 Entitlement check complete:")
498
+ print("[RampKit] - Total found: \(foundCount)")
499
+ print("[RampKit] - Already tracked: \(trackedCount)")
500
+ print("[RampKit] - NEW (sent events): \(newCount)")
430
501
  }
431
-
502
+
503
+ /// Start listening for Transaction.updates (for future purchases)
432
504
  @available(iOS 15.0, *)
433
- private func handleUnfinishedTransactions() async {
434
- // Handle any transactions that weren't finished
435
- for await result in Transaction.unfinished {
436
- do {
437
- let transaction = try self.checkVerified(result)
505
+ private func startTransactionUpdatesListener() async {
506
+ guard transactionObserverTask == nil else {
507
+ print("[RampKit] Transaction updates listener already running")
508
+ return
509
+ }
510
+
511
+ print("[RampKit] 👂 Starting Transaction.updates listener...")
512
+
513
+ transactionObserverTask = Task {
514
+ for await result in Transaction.updates {
515
+ print("[RampKit] 🎉 Transaction.updates received!")
516
+
517
+ guard case .verified(let transaction) = result else {
518
+ print("[RampKit] ⚠️ Unverified transaction update")
519
+ continue
520
+ }
521
+
522
+ let originalId = String(transaction.originalID)
523
+
524
+ // Skip if already tracked
525
+ if self.trackedTransactionIds.contains(originalId) {
526
+ print("[RampKit] ✓ Transaction.updates: Already tracked \(transaction.productID)")
527
+ await transaction.finish()
528
+ continue
529
+ }
530
+
531
+ print("[RampKit] 🆕 Transaction.updates: NEW purchase \(transaction.productID)")
532
+
533
+ // Track it
438
534
  await self.handleTransaction(transaction)
535
+
536
+ // Mark as tracked
537
+ self.trackedTransactionIds.insert(originalId)
538
+ self.saveTrackedTransactions()
539
+
540
+ // Finish the transaction
439
541
  await transaction.finish()
440
- } catch {
441
- print("[RampKit] Unfinished transaction verification failed: \(error)")
442
542
  }
443
543
  }
444
544
  }
445
-
446
- @available(iOS 15.0, *)
447
- private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
448
- switch result {
449
- case .unverified(_, let error):
450
- throw error
451
- case .verified(let safe):
452
- return safe
453
- @unknown default:
454
- fatalError("Unknown VerificationResult case")
455
- }
545
+
546
+ private func stopTransactionObserver() {
547
+ transactionObserverTask?.cancel()
548
+ transactionObserverTask = nil
549
+ isConfigured = false
550
+ print("[RampKit] Transaction observer stopped")
456
551
  }
457
-
552
+
458
553
  @available(iOS 15.0, *)
459
554
  private func handleTransaction(_ transaction: Transaction) async {
460
- guard let appId = self.appId, let userId = self.userId else { return }
555
+ print("[RampKit] 🔄 handleTransaction called for: \(transaction.productID)")
556
+ print("[RampKit] - id: \(transaction.id)")
557
+ print("[RampKit] - originalID: \(transaction.originalID)")
558
+ print("[RampKit] - purchaseDate: \(transaction.purchaseDate)")
559
+
560
+ guard let appId = self.appId, let userId = self.userId else {
561
+ print("[RampKit] ❌ handleTransaction failed: appId=\(self.appId ?? "nil"), userId=\(self.userId ?? "nil")")
562
+ return
563
+ }
461
564
 
462
565
  let formatter = ISO8601DateFormatter()
463
566
  formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
@@ -474,6 +577,8 @@ public class RampKitModule: Module {
474
577
  return
475
578
  }
476
579
 
580
+ print("[RampKit] ✅ Transaction is a new purchase, will send event")
581
+
477
582
  // All new purchases (including trials) are tracked as purchase_completed
478
583
  // The isTrial and offerType properties indicate trial status
479
584
  let eventName = "purchase_completed"
@@ -636,7 +741,104 @@ public class RampKitModule: Module {
636
741
  print("[RampKit] Failed to send purchase event: \(error)")
637
742
  }
638
743
  }
639
-
744
+
745
+ // MARK: - Manual Purchase Tracking (Fallback for Superwall/RevenueCat)
746
+
747
+ /// Track a purchase manually when you have the transaction IDs
748
+ /// Use this when Superwall or RevenueCat provides the transaction info
749
+ @available(iOS 15.0, *)
750
+ private func trackPurchaseCompletedManually(
751
+ productId: String,
752
+ transactionId: String?,
753
+ originalTransactionId: String?
754
+ ) async {
755
+ print("[RampKit] 📲 Manual purchase tracking called for: \(productId)")
756
+
757
+ guard let appId = self.appId, let userId = self.userId else {
758
+ print("[RampKit] ❌ Manual tracking failed: SDK not initialized (appId or userId missing)")
759
+ return
760
+ }
761
+
762
+ let formatter = ISO8601DateFormatter()
763
+ formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
764
+
765
+ var properties: [String: Any] = [
766
+ "productId": productId,
767
+ "purchaseDate": formatter.string(from: Date()),
768
+ "source": "manual" // Mark as manually tracked
769
+ ]
770
+
771
+ // Add transaction IDs if available
772
+ if let transactionId = transactionId {
773
+ properties["transactionId"] = transactionId
774
+ }
775
+ if let originalTransactionId = originalTransactionId {
776
+ properties["originalTransactionId"] = originalTransactionId
777
+ } else if let transactionId = transactionId {
778
+ // Use transactionId as originalTransactionId if not provided (first purchase)
779
+ properties["originalTransactionId"] = transactionId
780
+ }
781
+
782
+ // Try to get product info from StoreKit
783
+ if let product = await getProduct(for: productId) {
784
+ properties["amount"] = product.price
785
+ properties["currency"] = product.priceFormatStyle.currencyCode
786
+ properties["priceFormatted"] = product.displayPrice
787
+ properties["productType"] = mapProductType(product.type)
788
+
789
+ if let subscription = product.subscription {
790
+ properties["subscriptionPeriod"] = formatSubscriptionPeriod(subscription.subscriptionPeriod)
791
+ properties["subscriptionGroupId"] = subscription.subscriptionGroupID
792
+ }
793
+ }
794
+
795
+ // Get storefront
796
+ if let storefront = await Storefront.current {
797
+ properties["storefront"] = storefront.countryCode
798
+ }
799
+
800
+ print("[RampKit] 📤 Sending manual purchase_completed event...")
801
+ await sendPurchaseEvent(
802
+ appId: appId,
803
+ userId: userId,
804
+ eventName: "purchase_completed",
805
+ properties: properties
806
+ )
807
+ }
808
+
809
+ /// Track a purchase by looking up the latest transaction for a product
810
+ /// Use this when you only know the productId (Superwall doesn't always provide transaction IDs)
811
+ @available(iOS 15.0, *)
812
+ private func trackPurchaseFromProductId(productId: String) async {
813
+ print("[RampKit] 🔍 Looking up transaction for product: \(productId)")
814
+
815
+ // Try to find the latest transaction for this product
816
+ var latestTransaction: Transaction?
817
+
818
+ for await result in Transaction.currentEntitlements {
819
+ if case .verified(let transaction) = result {
820
+ if transaction.productID == productId {
821
+ if latestTransaction == nil || transaction.purchaseDate > latestTransaction!.purchaseDate {
822
+ latestTransaction = transaction
823
+ }
824
+ }
825
+ }
826
+ }
827
+
828
+ if let transaction = latestTransaction {
829
+ print("[RampKit] ✅ Found transaction for product: \(productId)")
830
+ await handleTransaction(transaction)
831
+ } else {
832
+ print("[RampKit] ⚠️ No transaction found for product, tracking manually: \(productId)")
833
+ // Fall back to manual tracking without transaction IDs
834
+ await trackPurchaseCompletedManually(
835
+ productId: productId,
836
+ transactionId: nil,
837
+ originalTransactionId: nil
838
+ )
839
+ }
840
+ }
841
+
640
842
  // MARK: - Device Helpers
641
843
 
642
844
  private func getDeviceModelIdentifier() -> String {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rampkit-expo-dev",
3
- "version": "0.0.66",
3
+ "version": "0.0.68",
4
4
  "description": "The Expo SDK for RampKit. Build, test, and personalize app onboardings with instant updates.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",