rampkit-expo-dev 0.0.65 → 0.0.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/RampKitNative.d.ts +19 -0
- package/build/RampKitNative.js +37 -0
- package/ios/RampKitModule.swift +264 -65
- package/package.json +1 -1
package/build/RampKitNative.d.ts
CHANGED
|
@@ -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
|
};
|
package/build/RampKitNative.js
CHANGED
|
@@ -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
|
};
|
package/ios/RampKitModule.swift
CHANGED
|
@@ -9,14 +9,23 @@ 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
|
+
|
|
13
13
|
// Transaction observer task
|
|
14
14
|
private var transactionObserverTask: Task<Void, Never>?
|
|
15
15
|
private var appId: String?
|
|
16
16
|
private var userId: String?
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
// Queue for transactions received before SDK is configured
|
|
19
|
+
private var pendingTransactions: [Any] = []
|
|
20
|
+
private var isConfigured = false
|
|
21
|
+
|
|
18
22
|
public func definition() -> ModuleDefinition {
|
|
19
23
|
Name("RampKit")
|
|
24
|
+
|
|
25
|
+
// Start transaction observer immediately when module loads
|
|
26
|
+
OnCreate {
|
|
27
|
+
self.startTransactionObserverEarly()
|
|
28
|
+
}
|
|
20
29
|
|
|
21
30
|
// ============================================================================
|
|
22
31
|
// Device Info
|
|
@@ -89,14 +98,30 @@ public class RampKitModule: Module {
|
|
|
89
98
|
// ============================================================================
|
|
90
99
|
// Transaction Observer (StoreKit 2)
|
|
91
100
|
// ============================================================================
|
|
92
|
-
|
|
101
|
+
|
|
93
102
|
AsyncFunction("startTransactionObserver") { (appId: String) in
|
|
94
103
|
self.startTransactionObserver(appId: appId)
|
|
95
104
|
}
|
|
96
|
-
|
|
105
|
+
|
|
97
106
|
AsyncFunction("stopTransactionObserver") { () in
|
|
98
107
|
self.stopTransactionObserver()
|
|
99
108
|
}
|
|
109
|
+
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// Manual Purchase Tracking (Fallback for Superwall/RevenueCat)
|
|
112
|
+
// ============================================================================
|
|
113
|
+
|
|
114
|
+
AsyncFunction("trackPurchaseCompleted") { (productId: String, transactionId: String?, originalTransactionId: String?) in
|
|
115
|
+
await self.trackPurchaseCompletedManually(
|
|
116
|
+
productId: productId,
|
|
117
|
+
transactionId: transactionId,
|
|
118
|
+
originalTransactionId: originalTransactionId
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
AsyncFunction("trackPurchaseFromProduct") { (productId: String) in
|
|
123
|
+
await self.trackPurchaseFromProductId(productId: productId)
|
|
124
|
+
}
|
|
100
125
|
}
|
|
101
126
|
|
|
102
127
|
// MARK: - Device Info Collection
|
|
@@ -386,61 +411,136 @@ public class RampKitModule: Module {
|
|
|
386
411
|
}
|
|
387
412
|
|
|
388
413
|
// MARK: - StoreKit 2 Transaction Observer
|
|
389
|
-
|
|
414
|
+
|
|
415
|
+
/// Called on module init - starts listening BEFORE JavaScript configures us
|
|
416
|
+
private func startTransactionObserverEarly() {
|
|
417
|
+
// Get userId early so we have it ready
|
|
418
|
+
self.userId = getOrCreateUserId()
|
|
419
|
+
|
|
420
|
+
guard #available(iOS 15.0, *) else {
|
|
421
|
+
print("[RampKit] ⚠️ Transaction observer requires iOS 15+")
|
|
422
|
+
return
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Don't restart if already running
|
|
426
|
+
guard transactionObserverTask == nil else {
|
|
427
|
+
print("[RampKit] Transaction observer already running")
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
print("[RampKit] 🚀 Starting transaction observer early (module init)...")
|
|
432
|
+
|
|
433
|
+
transactionObserverTask = Task {
|
|
434
|
+
print("[RampKit] 👂 Transaction.updates loop starting...")
|
|
435
|
+
for await result in Transaction.updates {
|
|
436
|
+
print("[RampKit] 🎉 RECEIVED TRANSACTION UPDATE!")
|
|
437
|
+
await self.handleTransactionUpdate(result)
|
|
438
|
+
}
|
|
439
|
+
print("[RampKit] ⚠️ Transaction.updates loop exited (unexpected)")
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Also check unfinished transactions
|
|
443
|
+
Task {
|
|
444
|
+
await self.handleUnfinishedTransactions()
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/// Called from JavaScript - sets appId and processes any pending transactions
|
|
390
449
|
private func startTransactionObserver(appId: String) {
|
|
391
450
|
self.appId = appId
|
|
392
451
|
self.userId = getOrCreateUserId()
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
//
|
|
452
|
+
self.isConfigured = true
|
|
453
|
+
|
|
454
|
+
print("[RampKit] ✅ Transaction observer configured with appId: \(appId)")
|
|
455
|
+
|
|
456
|
+
// Process any transactions that arrived before we were configured
|
|
398
457
|
if #available(iOS 15.0, *) {
|
|
399
|
-
|
|
400
|
-
await self.
|
|
458
|
+
Task {
|
|
459
|
+
await self.processPendingTransactions()
|
|
401
460
|
}
|
|
402
|
-
|
|
403
|
-
//
|
|
461
|
+
|
|
462
|
+
// Debug: check current entitlements
|
|
404
463
|
Task {
|
|
405
|
-
await self.
|
|
464
|
+
await self.debugCurrentEntitlements()
|
|
406
465
|
}
|
|
407
466
|
}
|
|
408
|
-
|
|
409
|
-
|
|
467
|
+
|
|
468
|
+
// Ensure observer is running (in case OnCreate didn't fire)
|
|
469
|
+
if transactionObserverTask == nil {
|
|
470
|
+
startTransactionObserverEarly()
|
|
471
|
+
}
|
|
410
472
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
473
|
+
|
|
474
|
+
@available(iOS 15.0, *)
|
|
475
|
+
private func handleTransactionUpdate(_ result: VerificationResult<Transaction>) async {
|
|
476
|
+
do {
|
|
477
|
+
let transaction = try self.checkVerified(result)
|
|
478
|
+
print("[RampKit] ✅ Transaction verified: \(transaction.productID)")
|
|
479
|
+
|
|
480
|
+
if self.isConfigured, let _ = self.appId {
|
|
481
|
+
// We're configured, process immediately
|
|
482
|
+
await self.handleTransaction(transaction)
|
|
483
|
+
} else {
|
|
484
|
+
// Not configured yet, queue it
|
|
485
|
+
print("[RampKit] 📦 Queueing transaction (SDK not configured yet): \(transaction.productID)")
|
|
486
|
+
self.pendingTransactions.append(transaction)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Always finish the transaction
|
|
490
|
+
await transaction.finish()
|
|
491
|
+
print("[RampKit] 🏁 Transaction finished: \(transaction.productID)")
|
|
492
|
+
} catch {
|
|
493
|
+
print("[RampKit] ❌ Transaction verification failed: \(error)")
|
|
494
|
+
}
|
|
416
495
|
}
|
|
417
|
-
|
|
496
|
+
|
|
418
497
|
@available(iOS 15.0, *)
|
|
419
|
-
private func
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
498
|
+
private func processPendingTransactions() async {
|
|
499
|
+
guard !pendingTransactions.isEmpty else { return }
|
|
500
|
+
|
|
501
|
+
print("[RampKit] 📤 Processing \(pendingTransactions.count) pending transaction(s)...")
|
|
502
|
+
|
|
503
|
+
for item in pendingTransactions {
|
|
504
|
+
if let transaction = item as? Transaction {
|
|
424
505
|
await self.handleTransaction(transaction)
|
|
425
|
-
await transaction.finish()
|
|
426
|
-
} catch {
|
|
427
|
-
print("[RampKit] Transaction verification failed: \(error)")
|
|
428
506
|
}
|
|
429
507
|
}
|
|
508
|
+
|
|
509
|
+
pendingTransactions.removeAll()
|
|
510
|
+
print("[RampKit] ✅ All pending transactions processed")
|
|
430
511
|
}
|
|
431
|
-
|
|
512
|
+
|
|
513
|
+
@available(iOS 15.0, *)
|
|
514
|
+
private func debugCurrentEntitlements() async {
|
|
515
|
+
print("[RampKit] 🔍 Checking current entitlements...")
|
|
516
|
+
var count = 0
|
|
517
|
+
for await result in Transaction.currentEntitlements {
|
|
518
|
+
if case .verified(let transaction) = result {
|
|
519
|
+
count += 1
|
|
520
|
+
print("[RampKit] 📦 Current entitlement: \(transaction.productID), originalID: \(transaction.originalID), id: \(transaction.id)")
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
print("[RampKit] 🔍 Found \(count) current entitlements")
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private func stopTransactionObserver() {
|
|
527
|
+
transactionObserverTask?.cancel()
|
|
528
|
+
transactionObserverTask = nil
|
|
529
|
+
isConfigured = false
|
|
530
|
+
pendingTransactions.removeAll()
|
|
531
|
+
print("[RampKit] Transaction observer stopped")
|
|
532
|
+
}
|
|
533
|
+
|
|
432
534
|
@available(iOS 15.0, *)
|
|
433
535
|
private func handleUnfinishedTransactions() async {
|
|
434
|
-
|
|
536
|
+
print("[RampKit] 🔍 Checking for unfinished transactions...")
|
|
537
|
+
var count = 0
|
|
435
538
|
for await result in Transaction.unfinished {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
await transaction.finish()
|
|
440
|
-
} catch {
|
|
441
|
-
print("[RampKit] Unfinished transaction verification failed: \(error)")
|
|
442
|
-
}
|
|
539
|
+
count += 1
|
|
540
|
+
print("[RampKit] 📦 Found unfinished transaction #\(count)")
|
|
541
|
+
await self.handleTransactionUpdate(result)
|
|
443
542
|
}
|
|
543
|
+
print("[RampKit] 🔍 Finished checking unfinished transactions. Found: \(count)")
|
|
444
544
|
}
|
|
445
545
|
|
|
446
546
|
@available(iOS 15.0, *)
|
|
@@ -457,36 +557,38 @@ public class RampKitModule: Module {
|
|
|
457
557
|
|
|
458
558
|
@available(iOS 15.0, *)
|
|
459
559
|
private func handleTransaction(_ transaction: Transaction) async {
|
|
460
|
-
|
|
560
|
+
print("[RampKit] 🔄 handleTransaction called for: \(transaction.productID)")
|
|
561
|
+
print("[RampKit] - id: \(transaction.id)")
|
|
562
|
+
print("[RampKit] - originalID: \(transaction.originalID)")
|
|
563
|
+
print("[RampKit] - purchaseDate: \(transaction.purchaseDate)")
|
|
564
|
+
|
|
565
|
+
guard let appId = self.appId, let userId = self.userId else {
|
|
566
|
+
print("[RampKit] ❌ handleTransaction failed: appId=\(self.appId ?? "nil"), userId=\(self.userId ?? "nil")")
|
|
567
|
+
return
|
|
568
|
+
}
|
|
461
569
|
|
|
462
570
|
let formatter = ISO8601DateFormatter()
|
|
463
571
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
464
572
|
|
|
465
|
-
//
|
|
466
|
-
|
|
467
|
-
|
|
573
|
+
// Skip renewals - backend gets these from App Store Server-to-Server notifications
|
|
574
|
+
if transaction.originalID != transaction.id {
|
|
575
|
+
print("[RampKit] ⏭️ Transaction skipped (renewal): \(transaction.productID)")
|
|
576
|
+
return
|
|
577
|
+
}
|
|
468
578
|
|
|
469
|
-
//
|
|
579
|
+
// Skip revocations/cancellations - backend gets these from S2S notifications
|
|
470
580
|
if transaction.revocationDate != nil {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
properties["revocationReason"] = reason == .developerIssue ? "developerIssue" : "other"
|
|
474
|
-
}
|
|
475
|
-
properties["revocationDate"] = transaction.revocationDate.map { formatter.string(from: $0) }
|
|
476
|
-
}
|
|
477
|
-
// Check if this is a renewal (originalID != id)
|
|
478
|
-
else if transaction.originalID != transaction.id {
|
|
479
|
-
eventName = "subscription_renewed"
|
|
480
|
-
}
|
|
481
|
-
// Check if it's a trial or intro offer
|
|
482
|
-
else if let offerType = transaction.offerType, offerType == .introductory {
|
|
483
|
-
eventName = "trial_started"
|
|
484
|
-
}
|
|
485
|
-
// Default to purchase_completed
|
|
486
|
-
else {
|
|
487
|
-
eventName = "purchase_completed"
|
|
581
|
+
print("[RampKit] ⏭️ Transaction skipped (revoked): \(transaction.productID)")
|
|
582
|
+
return
|
|
488
583
|
}
|
|
489
584
|
|
|
585
|
+
print("[RampKit] ✅ Transaction is a new purchase, will send event")
|
|
586
|
+
|
|
587
|
+
// All new purchases (including trials) are tracked as purchase_completed
|
|
588
|
+
// The isTrial and offerType properties indicate trial status
|
|
589
|
+
let eventName = "purchase_completed"
|
|
590
|
+
var properties: [String: Any] = [:]
|
|
591
|
+
|
|
490
592
|
// Build properties (matching iOS SDK PurchaseEventDetails)
|
|
491
593
|
properties["productId"] = transaction.productID
|
|
492
594
|
properties["transactionId"] = String(transaction.id)
|
|
@@ -631,8 +733,8 @@ public class RampKitModule: Module {
|
|
|
631
733
|
var request = URLRequest(url: url)
|
|
632
734
|
request.httpMethod = "POST"
|
|
633
735
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
634
|
-
request.setValue("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
|
|
635
|
-
request.setValue("Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
|
|
736
|
+
request.setValue("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InV1c3RsenV2am1vY2h4a3hhdGZ4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjIxMDM2NTUsImV4cCI6MjA3NzY3OTY1NX0.d5XsIMGnia4n9Pou0IidipyyEfSlwpXFoeDBufMOEwE", forHTTPHeaderField: "apikey")
|
|
737
|
+
request.setValue("Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InV1c3RsenV2am1vY2h4a3hhdGZ4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjIxMDM2NTUsImV4cCI6MjA3NzY3OTY1NX0.d5XsIMGnia4n9Pou0IidipyyEfSlwpXFoeDBufMOEwE", forHTTPHeaderField: "Authorization")
|
|
636
738
|
|
|
637
739
|
do {
|
|
638
740
|
request.httpBody = try JSONSerialization.data(withJSONObject: event)
|
|
@@ -644,7 +746,104 @@ public class RampKitModule: Module {
|
|
|
644
746
|
print("[RampKit] Failed to send purchase event: \(error)")
|
|
645
747
|
}
|
|
646
748
|
}
|
|
647
|
-
|
|
749
|
+
|
|
750
|
+
// MARK: - Manual Purchase Tracking (Fallback for Superwall/RevenueCat)
|
|
751
|
+
|
|
752
|
+
/// Track a purchase manually when you have the transaction IDs
|
|
753
|
+
/// Use this when Superwall or RevenueCat provides the transaction info
|
|
754
|
+
@available(iOS 15.0, *)
|
|
755
|
+
private func trackPurchaseCompletedManually(
|
|
756
|
+
productId: String,
|
|
757
|
+
transactionId: String?,
|
|
758
|
+
originalTransactionId: String?
|
|
759
|
+
) async {
|
|
760
|
+
print("[RampKit] 📲 Manual purchase tracking called for: \(productId)")
|
|
761
|
+
|
|
762
|
+
guard let appId = self.appId, let userId = self.userId else {
|
|
763
|
+
print("[RampKit] ❌ Manual tracking failed: SDK not initialized (appId or userId missing)")
|
|
764
|
+
return
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
let formatter = ISO8601DateFormatter()
|
|
768
|
+
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
769
|
+
|
|
770
|
+
var properties: [String: Any] = [
|
|
771
|
+
"productId": productId,
|
|
772
|
+
"purchaseDate": formatter.string(from: Date()),
|
|
773
|
+
"source": "manual" // Mark as manually tracked
|
|
774
|
+
]
|
|
775
|
+
|
|
776
|
+
// Add transaction IDs if available
|
|
777
|
+
if let transactionId = transactionId {
|
|
778
|
+
properties["transactionId"] = transactionId
|
|
779
|
+
}
|
|
780
|
+
if let originalTransactionId = originalTransactionId {
|
|
781
|
+
properties["originalTransactionId"] = originalTransactionId
|
|
782
|
+
} else if let transactionId = transactionId {
|
|
783
|
+
// Use transactionId as originalTransactionId if not provided (first purchase)
|
|
784
|
+
properties["originalTransactionId"] = transactionId
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Try to get product info from StoreKit
|
|
788
|
+
if let product = await getProduct(for: productId) {
|
|
789
|
+
properties["amount"] = product.price
|
|
790
|
+
properties["currency"] = product.priceFormatStyle.currencyCode
|
|
791
|
+
properties["priceFormatted"] = product.displayPrice
|
|
792
|
+
properties["productType"] = mapProductType(product.type)
|
|
793
|
+
|
|
794
|
+
if let subscription = product.subscription {
|
|
795
|
+
properties["subscriptionPeriod"] = formatSubscriptionPeriod(subscription.subscriptionPeriod)
|
|
796
|
+
properties["subscriptionGroupId"] = subscription.subscriptionGroupID
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Get storefront
|
|
801
|
+
if let storefront = await Storefront.current {
|
|
802
|
+
properties["storefront"] = storefront.countryCode
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
print("[RampKit] 📤 Sending manual purchase_completed event...")
|
|
806
|
+
await sendPurchaseEvent(
|
|
807
|
+
appId: appId,
|
|
808
|
+
userId: userId,
|
|
809
|
+
eventName: "purchase_completed",
|
|
810
|
+
properties: properties
|
|
811
|
+
)
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/// Track a purchase by looking up the latest transaction for a product
|
|
815
|
+
/// Use this when you only know the productId (Superwall doesn't always provide transaction IDs)
|
|
816
|
+
@available(iOS 15.0, *)
|
|
817
|
+
private func trackPurchaseFromProductId(productId: String) async {
|
|
818
|
+
print("[RampKit] 🔍 Looking up transaction for product: \(productId)")
|
|
819
|
+
|
|
820
|
+
// Try to find the latest transaction for this product
|
|
821
|
+
var latestTransaction: Transaction?
|
|
822
|
+
|
|
823
|
+
for await result in Transaction.currentEntitlements {
|
|
824
|
+
if case .verified(let transaction) = result {
|
|
825
|
+
if transaction.productID == productId {
|
|
826
|
+
if latestTransaction == nil || transaction.purchaseDate > latestTransaction!.purchaseDate {
|
|
827
|
+
latestTransaction = transaction
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if let transaction = latestTransaction {
|
|
834
|
+
print("[RampKit] ✅ Found transaction for product: \(productId)")
|
|
835
|
+
await handleTransaction(transaction)
|
|
836
|
+
} else {
|
|
837
|
+
print("[RampKit] ⚠️ No transaction found for product, tracking manually: \(productId)")
|
|
838
|
+
// Fall back to manual tracking without transaction IDs
|
|
839
|
+
await trackPurchaseCompletedManually(
|
|
840
|
+
productId: productId,
|
|
841
|
+
transactionId: nil,
|
|
842
|
+
originalTransactionId: nil
|
|
843
|
+
)
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
648
847
|
// MARK: - Device Helpers
|
|
649
848
|
|
|
650
849
|
private func getDeviceModelIdentifier() -> String {
|
package/package.json
CHANGED