@umituz/react-native-subscription 2.37.72 → 2.37.74
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/package.json +1 -1
- package/src/domains/credits/application/RefundCreditsCommand.ts +4 -2
- package/src/domains/credits/infrastructure/operations/CreditsWriter.ts +8 -2
- package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +19 -10
- package/src/domains/subscription/application/syncIdGenerators.ts +4 -2
- package/src/domains/subscription/infrastructure/services/CustomerInfoListenerManager.ts +25 -10
- package/src/domains/subscription/infrastructure/services/purchase/PurchaseExecutor.ts +8 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.37.
|
|
3
|
+
"version": "2.37.74",
|
|
4
4
|
"description": "Complete subscription management with RevenueCat, paywall UI, and credits system for React Native apps",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -39,8 +39,10 @@ export async function refundCreditsOperation(
|
|
|
39
39
|
throw new Error(CREDIT_ERROR_CODES.NO_CREDITS);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
const
|
|
43
|
-
const
|
|
42
|
+
const data = docSnap.data();
|
|
43
|
+
const current = data.credits as number;
|
|
44
|
+
const creditLimit = (data.creditLimit as number) ?? Infinity;
|
|
45
|
+
const updated = Math.min(current + amount, creditLimit);
|
|
44
46
|
|
|
45
47
|
tx.update(creditsRef, {
|
|
46
48
|
credits: updated,
|
|
@@ -9,7 +9,10 @@ import { getAppVersion, validatePlatform } from "../../../../utils/appUtils";
|
|
|
9
9
|
|
|
10
10
|
export async function syncExpiredStatus(ref: DocumentReference): Promise<void> {
|
|
11
11
|
const doc = await getDoc(ref);
|
|
12
|
-
if (!doc.exists())
|
|
12
|
+
if (!doc.exists()) {
|
|
13
|
+
console.warn("[CreditsWriter] syncExpiredStatus: credits document does not exist, skipping.", ref.path);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
13
16
|
|
|
14
17
|
await setDoc(ref, {
|
|
15
18
|
isPremium: false,
|
|
@@ -36,7 +39,10 @@ export async function syncPremiumMetadata(
|
|
|
36
39
|
metadata: PremiumMetadata
|
|
37
40
|
): Promise<void> {
|
|
38
41
|
const doc = await getDoc(ref);
|
|
39
|
-
if (!doc.exists())
|
|
42
|
+
if (!doc.exists()) {
|
|
43
|
+
console.warn("[CreditsWriter] syncPremiumMetadata: credits document does not exist, skipping.", ref.path);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
40
46
|
|
|
41
47
|
const isExpired = metadata.expirationDate ? isPast(metadata.expirationDate) : false;
|
|
42
48
|
const status = resolveSubscriptionStatus({
|
|
@@ -25,15 +25,19 @@ export class SubscriptionSyncProcessor {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
private async getCreditsUserId(revenueCatUserId: string): Promise<string> {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
private async getCreditsUserId(revenueCatUserId: string | null | undefined): Promise<string> {
|
|
29
|
+
const trimmed = revenueCatUserId?.trim();
|
|
30
|
+
if (trimmed && trimmed.length > 0) {
|
|
31
|
+
return trimmed;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.warn("[SubscriptionSyncProcessor] revenueCatUserId is empty/null, falling back to anonymousUserId");
|
|
35
|
+
const anonymousId = await this.getAnonymousUserId();
|
|
36
|
+
const trimmedAnonymous = anonymousId?.trim();
|
|
37
|
+
if (!trimmedAnonymous || trimmedAnonymous.length === 0) {
|
|
38
|
+
throw new Error("[SubscriptionSyncProcessor] Cannot resolve credits userId: both revenueCatUserId and anonymousUserId are empty");
|
|
35
39
|
}
|
|
36
|
-
return
|
|
40
|
+
return trimmedAnonymous;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
async processPurchase(userId: string, productId: string, customerInfo: CustomerInfo, source?: PurchaseSource, packageType?: PackageType | null) {
|
|
@@ -41,7 +45,8 @@ export class SubscriptionSyncProcessor {
|
|
|
41
45
|
try {
|
|
42
46
|
const revenueCatData = extractRevenueCatData(customerInfo, this.entitlementId);
|
|
43
47
|
revenueCatData.packageType = packageType ?? null;
|
|
44
|
-
|
|
48
|
+
const revenueCatAppUserId = await this.getRevenueCatAppUserId();
|
|
49
|
+
revenueCatData.revenueCatUserId = revenueCatAppUserId ?? userId;
|
|
45
50
|
const purchaseId = generatePurchaseId(revenueCatData.originalTransactionId, productId);
|
|
46
51
|
|
|
47
52
|
const creditsUserId = await this.getCreditsUserId(userId);
|
|
@@ -66,7 +71,8 @@ export class SubscriptionSyncProcessor {
|
|
|
66
71
|
try {
|
|
67
72
|
const revenueCatData = extractRevenueCatData(customerInfo, this.entitlementId);
|
|
68
73
|
revenueCatData.expirationDate = newExpirationDate ?? revenueCatData.expirationDate;
|
|
69
|
-
|
|
74
|
+
const revenueCatAppUserId = await this.getRevenueCatAppUserId();
|
|
75
|
+
revenueCatData.revenueCatUserId = revenueCatAppUserId ?? userId;
|
|
70
76
|
const purchaseId = generateRenewalId(revenueCatData.originalTransactionId, productId, newExpirationDate);
|
|
71
77
|
|
|
72
78
|
const creditsUserId = await this.getCreditsUserId(userId);
|
|
@@ -112,6 +118,9 @@ export class SubscriptionSyncProcessor {
|
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
if (!isPremium && !productId) {
|
|
121
|
+
// Cancellation: RevenueCat removed entitlement, no productId available.
|
|
122
|
+
// Must still update Firestore to reflect expired/canceled status.
|
|
123
|
+
await handleExpiredSubscription(creditsUserId);
|
|
115
124
|
return;
|
|
116
125
|
}
|
|
117
126
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
const uniqueSuffix = (): string => `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2
|
+
|
|
1
3
|
export const generatePurchaseId = (originalTransactionId: string | null, productId: string): string => {
|
|
2
4
|
return originalTransactionId
|
|
3
5
|
? `purchase_${originalTransactionId}`
|
|
4
|
-
: `purchase_${productId}_${
|
|
6
|
+
: `purchase_${productId}_${uniqueSuffix()}`;
|
|
5
7
|
};
|
|
6
8
|
|
|
7
9
|
export const generateRenewalId = (originalTransactionId: string | null, productId: string, expirationDate: string): string => {
|
|
8
10
|
return originalTransactionId
|
|
9
11
|
? `renewal_${originalTransactionId}_${expirationDate}`
|
|
10
|
-
: `renewal_${productId}_${
|
|
12
|
+
: `renewal_${productId}_${uniqueSuffix()}`;
|
|
11
13
|
};
|
|
@@ -26,9 +26,20 @@ export class CustomerInfoListenerManager {
|
|
|
26
26
|
this.state.resetRenewalState();
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
setupListener(config: RevenueCatConfig):
|
|
29
|
+
setupListener(config: RevenueCatConfig): boolean {
|
|
30
30
|
this.removeListener();
|
|
31
31
|
|
|
32
|
+
try {
|
|
33
|
+
this._createAndAttachListener(config);
|
|
34
|
+
return true;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error("[CustomerInfoListenerManager] Failed to setup listener:", error);
|
|
37
|
+
this.state.currentUserId = null;
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private _createAndAttachListener(config: RevenueCatConfig): void {
|
|
32
43
|
this.state.listener = async (customerInfo: CustomerInfo) => {
|
|
33
44
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
34
45
|
console.log("[CustomerInfoListener] 🔔 LISTENER TRIGGERED!", {
|
|
@@ -43,17 +54,21 @@ export class CustomerInfoListenerManager {
|
|
|
43
54
|
return;
|
|
44
55
|
}
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
try {
|
|
58
|
+
const newRenewalState = await processCustomerInfo(
|
|
59
|
+
customerInfo,
|
|
60
|
+
capturedUserId,
|
|
61
|
+
this.state.renewalState,
|
|
62
|
+
config
|
|
63
|
+
);
|
|
52
64
|
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
if (this.state.currentUserId === capturedUserId) {
|
|
66
|
+
this.state.renewalState = newRenewalState;
|
|
67
|
+
}
|
|
68
|
+
// else: User switched during async operation, discard stale renewal state
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error("[CustomerInfoListener] processCustomerInfo failed:", error);
|
|
55
71
|
}
|
|
56
|
-
// else: User switched during async operation, discard stale renewal state
|
|
57
72
|
};
|
|
58
73
|
|
|
59
74
|
Purchases.addCustomerInfoUpdateListener(this.state.listener);
|
|
@@ -13,15 +13,16 @@ async function executeConsumablePurchase(
|
|
|
13
13
|
): Promise<PurchaseResult> {
|
|
14
14
|
const savedPurchase = getSavedPurchase();
|
|
15
15
|
const source = savedPurchase?.source;
|
|
16
|
-
if (savedPurchase) {
|
|
17
|
-
clearSavedPurchase();
|
|
18
|
-
}
|
|
19
16
|
|
|
20
17
|
try {
|
|
21
18
|
await notifyPurchaseCompleted(config, userId, productId, customerInfo, source, packageType);
|
|
22
19
|
} catch (syncError) {
|
|
23
20
|
// Non-fatal: RevenueCat purchase succeeded, credits sync can be recovered on next session
|
|
24
21
|
console.error('[PurchaseExecutor] Post-purchase sync failed (purchase was successful):', syncError);
|
|
22
|
+
} finally {
|
|
23
|
+
if (savedPurchase) {
|
|
24
|
+
clearSavedPurchase();
|
|
25
|
+
}
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
return {
|
|
@@ -44,9 +45,6 @@ async function executeSubscriptionPurchase(
|
|
|
44
45
|
const isPremium = !!customerInfo.entitlements.active[entitlementIdentifier];
|
|
45
46
|
const savedPurchase = getSavedPurchase();
|
|
46
47
|
const source = savedPurchase?.source;
|
|
47
|
-
if (savedPurchase) {
|
|
48
|
-
clearSavedPurchase();
|
|
49
|
-
}
|
|
50
48
|
|
|
51
49
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
52
50
|
console.log("[PurchaseExecutor] executeSubscriptionPurchase:", {
|
|
@@ -65,6 +63,10 @@ async function executeSubscriptionPurchase(
|
|
|
65
63
|
} catch (syncError) {
|
|
66
64
|
// Non-fatal: RevenueCat purchase succeeded, credits sync can be recovered on next session
|
|
67
65
|
console.error('[PurchaseExecutor] Post-purchase sync failed (purchase was successful):', syncError);
|
|
66
|
+
} finally {
|
|
67
|
+
if (savedPurchase) {
|
|
68
|
+
clearSavedPurchase();
|
|
69
|
+
}
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
return {
|