rampkit-expo-dev 0.0.69 → 0.0.71

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<TransactionObserverResult>;
20
20
  stopTransactionObserver(): Promise<void>;
21
+ clearTrackedTransactions(): Promise<number>;
22
+ recheckEntitlements(): Promise<EntitlementCheckResult>;
21
23
  trackPurchaseCompleted(productId: string, transactionId?: string, originalTransactionId?: string): Promise<void>;
22
24
  trackPurchaseFromProduct(productId: string): Promise<void>;
23
25
  }
@@ -86,6 +88,41 @@ export interface NotificationPermissionResult {
86
88
  };
87
89
  error?: string;
88
90
  }
91
+ export interface SentEventResult {
92
+ productId: string;
93
+ transactionId: string;
94
+ originalTransactionId: string;
95
+ purchaseDate: string;
96
+ status: "sent" | "skipped" | "failed" | "error";
97
+ httpStatus?: number;
98
+ error?: string;
99
+ reason?: string;
100
+ amount?: string;
101
+ currency?: string;
102
+ environment?: string;
103
+ }
104
+ export interface TrackedTransactionDetail {
105
+ productId: string;
106
+ transactionId: string;
107
+ originalTransactionId: string;
108
+ purchaseDate: string;
109
+ expirationDate?: string;
110
+ environment?: string;
111
+ status: "already_sent" | "skipped";
112
+ reason?: string;
113
+ }
114
+ export interface EntitlementCheckResult {
115
+ totalFound: number;
116
+ alreadyTracked: number;
117
+ newPurchases: number;
118
+ productIds: string[];
119
+ newProductIds: string[];
120
+ sentEvents?: SentEventResult[];
121
+ skippedReasons?: TrackedTransactionDetail[];
122
+ alreadyTrackedDetails?: TrackedTransactionDetail[];
123
+ trackedIdsCount: number;
124
+ error?: string;
125
+ }
89
126
  export interface TransactionObserverResult {
90
127
  configured: boolean;
91
128
  appId: string;
@@ -93,13 +130,7 @@ export interface TransactionObserverResult {
93
130
  previouslyTrackedCount: number;
94
131
  iOSVersion: string;
95
132
  listenerStarted: boolean;
96
- entitlementCheck?: {
97
- totalFound: number;
98
- alreadyTracked: number;
99
- newPurchases: number;
100
- productIds: string[];
101
- newProductIds: string[];
102
- };
133
+ entitlementCheck?: EntitlementCheckResult;
103
134
  error?: string;
104
135
  }
105
136
  export type ImpactStyle = "light" | "medium" | "heavy" | "rigid" | "soft";
@@ -198,4 +229,19 @@ export declare const TransactionObserver: {
198
229
  * @param productId - The product ID to look up and track
199
230
  */
200
231
  trackPurchaseByProductId(productId: string): Promise<void>;
232
+ /**
233
+ * Clear all tracked transaction IDs from storage
234
+ * Use this for testing to re-trigger tracking of existing purchases
235
+ *
236
+ * @returns The number of tracked transactions that were cleared
237
+ */
238
+ clearTracked(): Promise<number>;
239
+ /**
240
+ * Re-check current entitlements for any new purchases
241
+ * Call this after onboarding finishes or after a paywall is shown
242
+ * to catch any purchases that may have been made
243
+ *
244
+ * @returns The entitlement check result with details of all transactions
245
+ */
246
+ recheck(): Promise<EntitlementCheckResult | null>;
201
247
  };
@@ -80,6 +80,18 @@ function createFallbackModule() {
80
80
  };
81
81
  },
82
82
  async stopTransactionObserver() { },
83
+ async clearTrackedTransactions() { return 0; },
84
+ async recheckEntitlements() {
85
+ return {
86
+ totalFound: 0,
87
+ alreadyTracked: 0,
88
+ newPurchases: 0,
89
+ productIds: [],
90
+ newProductIds: [],
91
+ trackedIdsCount: 0,
92
+ error: "Native module not available - using fallback"
93
+ };
94
+ },
83
95
  async trackPurchaseCompleted(_productId, _transactionId, _originalTransactionId) { },
84
96
  async trackPurchaseFromProduct(_productId) { },
85
97
  };
@@ -278,6 +290,72 @@ exports.Notifications = {
278
290
  // ============================================================================
279
291
  // Transaction Observer API (StoreKit 2 / Google Play Billing)
280
292
  // ============================================================================
293
+ /**
294
+ * Helper function to log entitlement check results with full details
295
+ */
296
+ function logEntitlementCheckResult(result, context) {
297
+ console.log("[RampKit] ");
298
+ console.log("[RampKit] ═══════════════════════════════════════════════════════════");
299
+ console.log(`[RampKit] 📊 ENTITLEMENT CHECK RESULT (${context})`);
300
+ console.log("[RampKit] ═══════════════════════════════════════════════════════════");
301
+ console.log("[RampKit] Total entitlements found:", result.totalFound);
302
+ console.log("[RampKit] Already sent to backend: ", result.alreadyTracked);
303
+ console.log("[RampKit] New events sent: ", result.newPurchases);
304
+ console.log("[RampKit] Tracked IDs in storage: ", result.trackedIdsCount);
305
+ console.log("[RampKit] Product IDs: ", result.productIds);
306
+ // Log already tracked transactions with full details
307
+ if (result.alreadyTrackedDetails && result.alreadyTrackedDetails.length > 0) {
308
+ console.log("[RampKit] ");
309
+ console.log("[RampKit] ✅ ALREADY SENT TRANSACTIONS:");
310
+ for (const tx of result.alreadyTrackedDetails) {
311
+ console.log("[RampKit] ────────────────────────────────────────");
312
+ console.log("[RampKit] 📦 Product:", tx.productId);
313
+ console.log("[RampKit] Transaction ID:", tx.transactionId);
314
+ console.log("[RampKit] Original Transaction ID:", tx.originalTransactionId);
315
+ console.log("[RampKit] Purchase Date:", tx.purchaseDate);
316
+ if (tx.expirationDate) {
317
+ console.log("[RampKit] Expiration Date:", tx.expirationDate);
318
+ }
319
+ if (tx.environment) {
320
+ console.log("[RampKit] Environment:", tx.environment);
321
+ }
322
+ console.log("[RampKit] Status: ✅ ALREADY SENT TO BACKEND");
323
+ }
324
+ }
325
+ // Log newly sent events
326
+ if (result.sentEvents && result.sentEvents.length > 0) {
327
+ console.log("[RampKit] ");
328
+ console.log("[RampKit] 📤 NEWLY SENT EVENTS:");
329
+ for (const event of result.sentEvents) {
330
+ console.log("[RampKit] ────────────────────────────────────────");
331
+ console.log("[RampKit] 📦 Product:", event.productId);
332
+ console.log("[RampKit] Transaction ID:", event.transactionId);
333
+ console.log("[RampKit] Original Transaction ID:", event.originalTransactionId);
334
+ console.log("[RampKit] Purchase Date:", event.purchaseDate);
335
+ console.log("[RampKit] Status:", event.status === "sent" ? "✅ SENT" : `❌ ${event.status.toUpperCase()}`);
336
+ if (event.httpStatus) {
337
+ console.log("[RampKit] HTTP Status:", event.httpStatus);
338
+ }
339
+ if (event.error) {
340
+ console.log("[RampKit] Error:", event.error);
341
+ }
342
+ }
343
+ }
344
+ // Log skipped transactions
345
+ if (result.skippedReasons && result.skippedReasons.length > 0) {
346
+ console.log("[RampKit] ");
347
+ console.log("[RampKit] ⏭️ SKIPPED TRANSACTIONS:");
348
+ for (const skipped of result.skippedReasons) {
349
+ console.log("[RampKit] - Product:", skipped.productId, "| Reason:", skipped.reason);
350
+ }
351
+ }
352
+ if (result.error) {
353
+ console.log("[RampKit] ");
354
+ console.log("[RampKit] ⚠️ Error:", result.error);
355
+ }
356
+ console.log("[RampKit] ═══════════════════════════════════════════════════════════");
357
+ console.log("[RampKit] ");
358
+ }
281
359
  exports.TransactionObserver = {
282
360
  /**
283
361
  * Start listening for purchase transactions
@@ -299,12 +377,7 @@ exports.TransactionObserver = {
299
377
  console.log("[RampKit] - previouslyTrackedCount:", result.previouslyTrackedCount);
300
378
  console.log("[RampKit] - listenerStarted:", result.listenerStarted);
301
379
  if (result.entitlementCheck) {
302
- console.log("[RampKit] 📊 Entitlement check results:");
303
- console.log("[RampKit] - totalFound:", result.entitlementCheck.totalFound);
304
- console.log("[RampKit] - alreadyTracked:", result.entitlementCheck.alreadyTracked);
305
- console.log("[RampKit] - newPurchases:", result.entitlementCheck.newPurchases);
306
- console.log("[RampKit] - productIds:", result.entitlementCheck.productIds);
307
- console.log("[RampKit] - newProductIds:", result.entitlementCheck.newProductIds);
380
+ logEntitlementCheckResult(result.entitlementCheck, "STARTUP");
308
381
  }
309
382
  if (result.error) {
310
383
  console.warn("[RampKit] ⚠️ Error:", result.error);
@@ -363,4 +436,41 @@ exports.TransactionObserver = {
363
436
  console.warn("[RampKit] Failed to track purchase by product:", e);
364
437
  }
365
438
  },
439
+ /**
440
+ * Clear all tracked transaction IDs from storage
441
+ * Use this for testing to re-trigger tracking of existing purchases
442
+ *
443
+ * @returns The number of tracked transactions that were cleared
444
+ */
445
+ async clearTracked() {
446
+ try {
447
+ console.log("[RampKit] 🗑️ Clearing tracked transaction IDs...");
448
+ const count = await RampKitNativeModule.clearTrackedTransactions();
449
+ console.log("[RampKit] ✅ Cleared", count, "tracked transaction IDs");
450
+ return count;
451
+ }
452
+ catch (e) {
453
+ console.warn("[RampKit] ❌ Failed to clear tracked transactions:", e);
454
+ return 0;
455
+ }
456
+ },
457
+ /**
458
+ * Re-check current entitlements for any new purchases
459
+ * Call this after onboarding finishes or after a paywall is shown
460
+ * to catch any purchases that may have been made
461
+ *
462
+ * @returns The entitlement check result with details of all transactions
463
+ */
464
+ async recheck() {
465
+ console.log("[RampKit] 🔄 Re-checking entitlements...");
466
+ try {
467
+ const result = await RampKitNativeModule.recheckEntitlements();
468
+ logEntitlementCheckResult(result, "RECHECK");
469
+ return result;
470
+ }
471
+ catch (e) {
472
+ console.warn("[RampKit] ❌ Failed to recheck entitlements:", e);
473
+ return null;
474
+ }
475
+ },
366
476
  };
package/build/index.d.ts CHANGED
@@ -10,6 +10,6 @@ export { collectDeviceInfo, getSessionDurationSeconds, getSessionStartTime, buil
10
10
  export { default as RampKitNative } from "./RampKitNative";
11
11
  export type { NativeDeviceInfo, NativeLaunchData } from "./RampKitNative";
12
12
  export { Haptics, StoreReview, Notifications, TransactionObserver, isNativeModuleAvailable } from "./RampKitNative";
13
- export type { ImpactStyle, NotificationType, NotificationOptions, NotificationPermissionResult, TransactionObserverResult } from "./RampKitNative";
13
+ export type { ImpactStyle, NotificationType, NotificationOptions, NotificationPermissionResult, TransactionObserverResult, SentEventResult, TrackedTransactionDetail, EntitlementCheckResult } from "./RampKitNative";
14
14
  export type { DeviceInfo, RampKitEvent, EventDevice, EventContext, RampKitConfig, RampKitEventName, RampKitContext, RampKitDeviceContext, RampKitUserContext, NavigationData, ScreenPosition, OnboardingResponse, AppSessionStartedProperties, OnboardingStartedProperties, OnboardingCompletedProperties, OnboardingAbandonedProperties, OptionSelectedProperties, NotificationsResponseProperties, PaywallShownProperties, PurchaseStartedProperties, PurchaseCompletedProperties, PurchaseFailedProperties, PurchaseRestoredProperties, } from "./types";
15
15
  export { SDK_VERSION, CAPABILITIES } from "./constants";
@@ -111,6 +111,23 @@ public class RampKitModule: Module {
111
111
  self.stopTransactionObserver()
112
112
  }
113
113
 
114
+ AsyncFunction("clearTrackedTransactions") { () -> Int in
115
+ let count = self.trackedTransactionIds.count
116
+ self.trackedTransactionIds.removeAll()
117
+ self.saveTrackedTransactions()
118
+ print("[RampKit] 🗑️ Cleared \(count) tracked transaction IDs")
119
+ return count
120
+ }
121
+
122
+ AsyncFunction("recheckEntitlements") { () -> [String: Any] in
123
+ print("[RampKit] 🔄 Re-checking entitlements (called from JS)...")
124
+ if #available(iOS 15.0, *) {
125
+ return await self.checkAndTrackCurrentEntitlements()
126
+ } else {
127
+ return ["error": "iOS 15+ required"]
128
+ }
129
+ }
130
+
114
131
  // ============================================================================
115
132
  // Manual Purchase Tracking (Fallback for Superwall/RevenueCat)
116
133
  // ============================================================================
@@ -472,56 +489,115 @@ public class RampKitModule: Module {
472
489
  @available(iOS 15.0, *)
473
490
  private func checkAndTrackCurrentEntitlements() async -> [String: Any] {
474
491
  print("[RampKit] 🔍 Checking current entitlements for missed purchases...")
492
+ print("[RampKit] 📚 Currently have \(trackedTransactionIds.count) tracked transaction IDs in storage")
493
+
494
+ let formatter = ISO8601DateFormatter()
495
+ formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
475
496
 
476
497
  var foundCount = 0
477
498
  var trackedCount = 0
478
499
  var newCount = 0
479
500
  var productIds: [String] = []
480
501
  var newProductIds: [String] = []
502
+ var sentEvents: [[String: Any]] = []
503
+ var skippedReasons: [[String: Any]] = []
504
+ var alreadyTrackedDetails: [[String: Any]] = [] // NEW: Details of already-tracked transactions
481
505
 
482
506
  for await result in Transaction.currentEntitlements {
483
507
  foundCount += 1
484
508
 
485
509
  guard case .verified(let transaction) = result else {
486
510
  print("[RampKit] ⚠️ Unverified entitlement skipped")
511
+ skippedReasons.append(["productId": "unknown", "reason": "unverified"])
487
512
  continue
488
513
  }
489
514
 
490
515
  let originalId = String(transaction.originalID)
516
+ let transactionId = String(transaction.id)
491
517
  productIds.append(transaction.productID)
492
- print("[RampKit] 📦 Found entitlement: \(transaction.productID), originalID: \(originalId)")
518
+
519
+ // Build transaction details for logging
520
+ var txDetails: [String: Any] = [
521
+ "productId": transaction.productID,
522
+ "transactionId": transactionId,
523
+ "originalTransactionId": originalId,
524
+ "purchaseDate": formatter.string(from: transaction.purchaseDate)
525
+ ]
526
+ if let expirationDate = transaction.expirationDate {
527
+ txDetails["expirationDate"] = formatter.string(from: expirationDate)
528
+ }
529
+ if #available(iOS 16.0, *) {
530
+ txDetails["environment"] = transaction.environment.rawValue
531
+ }
532
+
533
+ print("[RampKit] 📦 Found entitlement:")
534
+ print("[RampKit] - productId: \(transaction.productID)")
535
+ print("[RampKit] - transactionId: \(transactionId)")
536
+ print("[RampKit] - originalTransactionId: \(originalId)")
537
+ print("[RampKit] - purchaseDate: \(formatter.string(from: transaction.purchaseDate))")
493
538
 
494
539
  // Check if we've already tracked this transaction
495
540
  if trackedTransactionIds.contains(originalId) {
496
541
  trackedCount += 1
497
- print("[RampKit] Already tracked: \(transaction.productID)")
542
+ txDetails["status"] = "already_sent"
543
+ alreadyTrackedDetails.append(txDetails)
544
+ print("[RampKit] ✅ STATUS: Already sent to backend (originalTransactionId in tracked set)")
545
+ continue
546
+ }
547
+
548
+ // Skip renewals and revocations
549
+ guard transaction.originalID == transaction.id,
550
+ transaction.revocationDate == nil else {
551
+ let reason = transaction.revocationDate != nil ? "revoked" : "renewal"
552
+ txDetails["status"] = "skipped"
553
+ txDetails["reason"] = reason
554
+ skippedReasons.append(txDetails)
555
+ print("[RampKit] ⏭️ STATUS: Skipped (\(reason))")
498
556
  continue
499
557
  }
500
558
 
501
559
  // NEW transaction we haven't seen!
502
560
  newCount += 1
503
561
  newProductIds.append(transaction.productID)
504
- print("[RampKit] 🆕 NEW purchase detected: \(transaction.productID)")
505
-
506
- // Track it
507
- await self.handleTransaction(transaction)
508
-
509
- // Mark as tracked
510
- trackedTransactionIds.insert(originalId)
511
- saveTrackedTransactions()
562
+ print("[RampKit] 🆕 STATUS: NEW purchase - will send to backend now...")
563
+
564
+ // Track it and get the result
565
+ let sendResult = await self.handleTransactionWithResult(transaction)
566
+ sentEvents.append(sendResult)
567
+
568
+ // Only mark as tracked if send succeeded
569
+ if let status = sendResult["status"] as? String, status == "sent" {
570
+ trackedTransactionIds.insert(originalId)
571
+ saveTrackedTransactions()
572
+ print("[RampKit] ✅ Event sent successfully! HTTP status: \(sendResult["httpStatus"] ?? "unknown")")
573
+ print("[RampKit] ✅ Marked originalTransactionId \(originalId) as tracked")
574
+ } else {
575
+ print("[RampKit] ❌ Send failed: \(sendResult["error"] ?? "unknown error")")
576
+ print("[RampKit] ⚠️ Will retry on next app launch")
577
+ }
512
578
  }
513
579
 
514
- print("[RampKit] 🔍 Entitlement check complete:")
515
- print("[RampKit] - Total found: \(foundCount)")
516
- print("[RampKit] - Already tracked: \(trackedCount)")
517
- print("[RampKit] - NEW (sent events): \(newCount)")
580
+ print("[RampKit] ")
581
+ print("[RampKit] 🔍 ═══════════════════════════════════════════")
582
+ print("[RampKit] 🔍 ENTITLEMENT CHECK SUMMARY:")
583
+ print("[RampKit] 🔍 ═══════════════════════════════════════════")
584
+ print("[RampKit] Total entitlements found: \(foundCount)")
585
+ print("[RampKit] Already sent to backend: \(trackedCount)")
586
+ print("[RampKit] Skipped (renewal/revoked): \(skippedReasons.count)")
587
+ print("[RampKit] NEW events sent: \(newCount)")
588
+ print("[RampKit] Tracked IDs in storage: \(trackedTransactionIds.count)")
589
+ print("[RampKit] 🔍 ═══════════════════════════════════════════")
518
590
 
519
591
  return [
520
592
  "totalFound": foundCount,
521
593
  "alreadyTracked": trackedCount,
522
594
  "newPurchases": newCount,
523
595
  "productIds": productIds,
524
- "newProductIds": newProductIds
596
+ "newProductIds": newProductIds,
597
+ "sentEvents": sentEvents,
598
+ "skippedReasons": skippedReasons,
599
+ "alreadyTrackedDetails": alreadyTrackedDetails, // NEW
600
+ "trackedIdsCount": trackedTransactionIds.count
525
601
  ]
526
602
  }
527
603
 
@@ -553,14 +629,33 @@ public class RampKitModule: Module {
553
629
  continue
554
630
  }
555
631
 
632
+ // Skip renewals - backend gets these from App Store S2S notifications
633
+ if transaction.originalID != transaction.id {
634
+ print("[RampKit] ⏭️ Transaction.updates: skipped (renewal) \(transaction.productID)")
635
+ await transaction.finish()
636
+ continue
637
+ }
638
+
639
+ // Skip revocations - backend gets these from S2S notifications
640
+ if transaction.revocationDate != nil {
641
+ print("[RampKit] ⏭️ Transaction.updates: skipped (revoked) \(transaction.productID)")
642
+ await transaction.finish()
643
+ continue
644
+ }
645
+
556
646
  print("[RampKit] 🆕 Transaction.updates: NEW purchase \(transaction.productID)")
557
647
 
558
- // Track it
559
- await self.handleTransaction(transaction)
648
+ // Track it and check result
649
+ let sendResult = await self.handleTransactionWithResult(transaction)
560
650
 
561
- // Mark as tracked
562
- self.trackedTransactionIds.insert(originalId)
563
- self.saveTrackedTransactions()
651
+ // Only mark as tracked if send succeeded
652
+ if let status = sendResult["status"] as? String, status == "sent" {
653
+ self.trackedTransactionIds.insert(originalId)
654
+ self.saveTrackedTransactions()
655
+ print("[RampKit] ✅ Transaction.updates: Sent and tracked \(transaction.productID)")
656
+ } else {
657
+ print("[RampKit] ⚠️ Transaction.updates: Send failed, will retry \(transaction.productID)")
658
+ }
564
659
 
565
660
  // Finish the transaction
566
661
  await transaction.finish()
@@ -669,6 +764,100 @@ public class RampKitModule: Module {
669
764
  )
670
765
  }
671
766
 
767
+ /// Handle transaction and return result for JS logging
768
+ @available(iOS 15.0, *)
769
+ private func handleTransactionWithResult(_ transaction: Transaction) async -> [String: Any] {
770
+ let formatter = ISO8601DateFormatter()
771
+ formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
772
+
773
+ var result: [String: Any] = [
774
+ "productId": transaction.productID,
775
+ "transactionId": String(transaction.id),
776
+ "originalTransactionId": String(transaction.originalID),
777
+ "purchaseDate": formatter.string(from: transaction.purchaseDate)
778
+ ]
779
+
780
+ guard let appId = self.appId, let userId = self.userId else {
781
+ result["status"] = "error"
782
+ result["error"] = "appId or userId missing"
783
+ return result
784
+ }
785
+
786
+ // Check if this is a renewal (originalID != id)
787
+ if transaction.originalID != transaction.id {
788
+ result["status"] = "skipped"
789
+ result["reason"] = "renewal (originalID != id)"
790
+ return result
791
+ }
792
+
793
+ // Check if revoked
794
+ if transaction.revocationDate != nil {
795
+ result["status"] = "skipped"
796
+ result["reason"] = "revoked"
797
+ return result
798
+ }
799
+
800
+ // Build properties
801
+ var properties: [String: Any] = [:]
802
+ properties["productId"] = transaction.productID
803
+ properties["transactionId"] = String(transaction.id)
804
+ properties["originalTransactionId"] = String(transaction.originalID)
805
+ properties["purchaseDate"] = formatter.string(from: transaction.purchaseDate)
806
+ properties["quantity"] = transaction.purchasedQuantity
807
+ properties["productType"] = mapProductType(transaction.productType)
808
+
809
+ if let expirationDate = transaction.expirationDate {
810
+ properties["expirationDate"] = formatter.string(from: expirationDate)
811
+ }
812
+
813
+ if let offerType = transaction.offerType {
814
+ properties["isTrial"] = offerType == .introductory
815
+ properties["isIntroOffer"] = offerType == .introductory
816
+ properties["offerType"] = formatOfferType(offerType)
817
+ }
818
+
819
+ if let offerId = transaction.offerID {
820
+ properties["offerId"] = offerId
821
+ }
822
+
823
+ properties["storefront"] = transaction.storefrontCountryCode
824
+
825
+ if #available(iOS 16.0, *) {
826
+ properties["environment"] = transaction.environment.rawValue
827
+ result["environment"] = transaction.environment.rawValue
828
+ }
829
+
830
+ // Get price info
831
+ if let product = await getProduct(for: transaction.productID) {
832
+ properties["amount"] = product.price
833
+ properties["currency"] = product.priceFormatStyle.currencyCode
834
+ properties["priceFormatted"] = product.displayPrice
835
+ result["amount"] = "\(product.price)"
836
+ result["currency"] = product.priceFormatStyle.currencyCode
837
+
838
+ if let subscription = product.subscription {
839
+ properties["subscriptionPeriod"] = formatSubscriptionPeriod(subscription.subscriptionPeriod)
840
+ properties["subscriptionGroupId"] = subscription.subscriptionGroupID
841
+ }
842
+ }
843
+
844
+ // Send event
845
+ let sendResult = await sendPurchaseEventWithResult(
846
+ appId: appId,
847
+ userId: userId,
848
+ eventName: "purchase_completed",
849
+ properties: properties
850
+ )
851
+
852
+ result["status"] = sendResult.success ? "sent" : "failed"
853
+ result["httpStatus"] = sendResult.statusCode
854
+ if let error = sendResult.error {
855
+ result["error"] = error
856
+ }
857
+
858
+ return result
859
+ }
860
+
672
861
  @available(iOS 15.0, *)
673
862
  private func formatOfferType(_ offerType: Transaction.OfferType) -> String {
674
863
  if offerType == .introductory {
@@ -726,6 +915,16 @@ public class RampKitModule: Module {
726
915
  }
727
916
 
728
917
  private func sendPurchaseEvent(appId: String, userId: String, eventName: String, properties: [String: Any]) async {
918
+ let _ = await sendPurchaseEventWithResult(appId: appId, userId: userId, eventName: eventName, properties: properties)
919
+ }
920
+
921
+ private struct SendEventResult {
922
+ let success: Bool
923
+ let statusCode: Int
924
+ let error: String?
925
+ }
926
+
927
+ private func sendPurchaseEventWithResult(appId: String, userId: String, eventName: String, properties: [String: Any]) async -> SendEventResult {
729
928
  let event: [String: Any] = [
730
929
  "appId": appId,
731
930
  "appUserId": userId,
@@ -747,23 +946,29 @@ public class RampKitModule: Module {
747
946
  ],
748
947
  "properties": properties
749
948
  ]
750
-
751
- guard let url = URL(string: "https://uustlzuvjmochxkxatfx.supabase.co/functions/v1/app-user-events") else { return }
752
-
949
+
950
+ guard let url = URL(string: "https://uustlzuvjmochxkxatfx.supabase.co/functions/v1/app-user-events") else {
951
+ return SendEventResult(success: false, statusCode: 0, error: "Invalid URL")
952
+ }
953
+
753
954
  var request = URLRequest(url: url)
754
955
  request.httpMethod = "POST"
755
956
  request.setValue("application/json", forHTTPHeaderField: "Content-Type")
756
957
  request.setValue("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InV1c3RsenV2am1vY2h4a3hhdGZ4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjIxMDM2NTUsImV4cCI6MjA3NzY3OTY1NX0.d5XsIMGnia4n9Pou0IidipyyEfSlwpXFoeDBufMOEwE", forHTTPHeaderField: "apikey")
757
958
  request.setValue("Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InV1c3RsenV2am1vY2h4a3hhdGZ4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjIxMDM2NTUsImV4cCI6MjA3NzY3OTY1NX0.d5XsIMGnia4n9Pou0IidipyyEfSlwpXFoeDBufMOEwE", forHTTPHeaderField: "Authorization")
758
-
959
+
759
960
  do {
760
961
  request.httpBody = try JSONSerialization.data(withJSONObject: event)
761
962
  let (_, response) = try await URLSession.shared.data(for: request)
762
963
  if let httpResponse = response as? HTTPURLResponse {
763
964
  print("[RampKit] Purchase event sent: \(eventName) - Status: \(httpResponse.statusCode)")
965
+ let success = httpResponse.statusCode >= 200 && httpResponse.statusCode < 300
966
+ return SendEventResult(success: success, statusCode: httpResponse.statusCode, error: success ? nil : "HTTP \(httpResponse.statusCode)")
764
967
  }
968
+ return SendEventResult(success: false, statusCode: 0, error: "No HTTP response")
765
969
  } catch {
766
970
  print("[RampKit] Failed to send purchase event: \(error)")
971
+ return SendEventResult(success: false, statusCode: 0, error: error.localizedDescription)
767
972
  }
768
973
  }
769
974
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rampkit-expo-dev",
3
- "version": "0.0.69",
3
+ "version": "0.0.71",
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",