@umituz/react-native-subscription 2.37.109 → 2.37.110
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/CreditsInitializer.ts +5 -5
- package/src/domains/credits/application/creditDocumentHelpers.ts +1 -1
- package/src/domains/credits/application/creditOperationUtils.ts +1 -1
- package/src/domains/credits/core/Credits.ts +1 -1
- package/src/domains/credits/core/CreditsMapper.ts +1 -1
- package/src/domains/credits/core/UserCreditsDocument.ts +1 -1
- package/src/domains/credits/infrastructure/CreditsRepository.ts +2 -2
- package/src/domains/credits/infrastructure/operations/CreditsInitializer.ts +1 -1
- package/src/domains/credits/infrastructure/operations/CreditsWriter.ts +5 -5
- package/src/domains/revenuecat/core/types/RevenueCatData.ts +1 -1
- package/src/domains/revenuecat/infrastructure/services/userSwitchHandler.ts +1 -1
- package/src/domains/subscription/application/SubscriptionInitializerTypes.ts +1 -1
- package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +3 -3
- package/src/domains/subscription/application/SubscriptionSyncUtils.ts +6 -6
- package/src/domains/subscription/application/syncIdGenerators.ts +6 -6
- package/src/domains/subscription/core/SubscriptionEvents.ts +1 -1
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +1 -1
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.110",
|
|
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",
|
|
@@ -41,14 +41,14 @@ export async function initializeCreditsTransaction(
|
|
|
41
41
|
|
|
42
42
|
// Global cross-user deduplication: prevent the same Apple/Google transaction
|
|
43
43
|
// from allocating credits under multiple Firebase UIDs.
|
|
44
|
-
if (metadata.
|
|
45
|
-
const globalRef = doc(_db, GLOBAL_TRANSACTION_COLLECTION, metadata.
|
|
44
|
+
if (metadata.storeTransactionId) {
|
|
45
|
+
const globalRef = doc(_db, GLOBAL_TRANSACTION_COLLECTION, metadata.storeTransactionId);
|
|
46
46
|
const globalDoc = await transaction.get(globalRef);
|
|
47
47
|
if (globalDoc.exists()) {
|
|
48
48
|
const globalData = globalDoc.data();
|
|
49
49
|
if (globalData?.ownerUserId && globalData.ownerUserId !== userId) {
|
|
50
50
|
console.warn(
|
|
51
|
-
`[CreditsInitializer] Transaction ${metadata.
|
|
51
|
+
`[CreditsInitializer] Transaction ${metadata.storeTransactionId} already processed by user ${globalData.ownerUserId}, skipping for ${userId}`
|
|
52
52
|
);
|
|
53
53
|
return {
|
|
54
54
|
credits: existingData.credits,
|
|
@@ -89,8 +89,8 @@ export async function initializeCreditsTransaction(
|
|
|
89
89
|
transaction.set(creditsRef, creditsData, { merge: true });
|
|
90
90
|
|
|
91
91
|
// Register transaction globally so other UIDs cannot claim the same purchase.
|
|
92
|
-
if (metadata.
|
|
93
|
-
const globalRef = doc(_db, GLOBAL_TRANSACTION_COLLECTION, metadata.
|
|
92
|
+
if (metadata.storeTransactionId) {
|
|
93
|
+
const globalRef = doc(_db, GLOBAL_TRANSACTION_COLLECTION, metadata.storeTransactionId);
|
|
94
94
|
transaction.set(globalRef, {
|
|
95
95
|
ownerUserId: userId,
|
|
96
96
|
purchaseId,
|
|
@@ -68,7 +68,7 @@ export function buildCreditsData({
|
|
|
68
68
|
...(isPurchaseOrRenewal && { lastPurchaseAt: serverTimestamp() }),
|
|
69
69
|
...(expirationTimestamp && { expirationDate: expirationTimestamp }),
|
|
70
70
|
...(metadata.willRenew !== undefined && { willRenew: metadata.willRenew }),
|
|
71
|
-
...(metadata.
|
|
71
|
+
...(metadata.storeTransactionId && { storeTransactionId: metadata.storeTransactionId }),
|
|
72
72
|
...(canceledAtTimestamp && { canceledAt: canceledAtTimestamp }),
|
|
73
73
|
...(billingIssueTimestamp && { billingIssueDetectedAt: billingIssueTimestamp }),
|
|
74
74
|
...(metadata.store && { store: metadata.store }),
|
|
@@ -19,7 +19,7 @@ export interface UserCredits {
|
|
|
19
19
|
willRenew: boolean | null;
|
|
20
20
|
productId: string | null;
|
|
21
21
|
packageType: PackageType | null;
|
|
22
|
-
|
|
22
|
+
storeTransactionId: string | null;
|
|
23
23
|
periodType: string | null;
|
|
24
24
|
credits: number;
|
|
25
25
|
creditLimit: number;
|
|
@@ -41,7 +41,7 @@ export function mapCreditsDocumentToEntity(doc: UserCreditsDocumentRead): UserCr
|
|
|
41
41
|
willRenew: doc.willRenew,
|
|
42
42
|
productId: doc.productId,
|
|
43
43
|
packageType: doc.packageType,
|
|
44
|
-
|
|
44
|
+
storeTransactionId: doc.storeTransactionId,
|
|
45
45
|
periodType,
|
|
46
46
|
credits: doc.credits,
|
|
47
47
|
creditLimit: doc.creditLimit,
|
|
@@ -42,7 +42,7 @@ export interface UserCreditsDocumentRead {
|
|
|
42
42
|
willRenew: boolean | null;
|
|
43
43
|
productId: string | null;
|
|
44
44
|
packageType: PackageType | null;
|
|
45
|
-
|
|
45
|
+
storeTransactionId: string | null;
|
|
46
46
|
store: Store | null;
|
|
47
47
|
ownershipType: OwnershipType | null;
|
|
48
48
|
periodType: string | null;
|
|
@@ -92,7 +92,7 @@ export class CreditsRepository extends BaseRepository {
|
|
|
92
92
|
willRenew: boolean,
|
|
93
93
|
expirationDate: string | null,
|
|
94
94
|
periodType: string | null,
|
|
95
|
-
|
|
95
|
+
storeTransactionId?: string | null,
|
|
96
96
|
): Promise<boolean> {
|
|
97
97
|
const db = requireFirestore();
|
|
98
98
|
const creditLimit = calculateCreditLimit(productId, this.config);
|
|
@@ -105,7 +105,7 @@ export class CreditsRepository extends BaseRepository {
|
|
|
105
105
|
periodType,
|
|
106
106
|
db,
|
|
107
107
|
userId,
|
|
108
|
-
|
|
108
|
+
storeTransactionId,
|
|
109
109
|
);
|
|
110
110
|
}
|
|
111
111
|
}
|
|
@@ -63,7 +63,7 @@ export async function initializeCreditsWithRetry(params: InitializeCreditsParams
|
|
|
63
63
|
source,
|
|
64
64
|
expirationDate: revenueCatData.expirationDate,
|
|
65
65
|
willRenew: revenueCatData.willRenew,
|
|
66
|
-
|
|
66
|
+
storeTransactionId: revenueCatData.storeTransactionId,
|
|
67
67
|
isPremium: revenueCatData.isPremium,
|
|
68
68
|
periodType: revenueCatData.periodType,
|
|
69
69
|
unsubscribeDetectedAt: revenueCatData.unsubscribeDetectedAt,
|
|
@@ -78,7 +78,7 @@ export async function syncPremiumMetadata(
|
|
|
78
78
|
* This handles edge cases like test store purchases, reinstalls, or failed initializations.
|
|
79
79
|
* Returns true if a new document was created, false if one already existed.
|
|
80
80
|
*
|
|
81
|
-
* Cross-user guard: if
|
|
81
|
+
* Cross-user guard: if storeTransactionId is provided and already registered
|
|
82
82
|
* to a different user in the global processedTransactions collection, the recovery
|
|
83
83
|
* document is NOT created (the subscription belongs to another UID).
|
|
84
84
|
*/
|
|
@@ -91,22 +91,22 @@ export async function createRecoveryCreditsDocument(
|
|
|
91
91
|
periodType: string | null,
|
|
92
92
|
db?: Firestore,
|
|
93
93
|
userId?: string,
|
|
94
|
-
|
|
94
|
+
storeTransactionId?: string | null,
|
|
95
95
|
): Promise<boolean> {
|
|
96
96
|
const existingDoc = await getDoc(ref);
|
|
97
97
|
if (existingDoc.exists()) return false;
|
|
98
98
|
|
|
99
99
|
// Cross-user deduplication: if this transaction was already processed by another
|
|
100
100
|
// user, skip recovery to prevent double credit allocation across UIDs.
|
|
101
|
-
if (db && userId &&
|
|
101
|
+
if (db && userId && storeTransactionId) {
|
|
102
102
|
try {
|
|
103
|
-
const globalRef = doc(db, GLOBAL_TRANSACTION_COLLECTION,
|
|
103
|
+
const globalRef = doc(db, GLOBAL_TRANSACTION_COLLECTION, storeTransactionId);
|
|
104
104
|
const globalDoc = await getDoc(globalRef);
|
|
105
105
|
if (globalDoc.exists()) {
|
|
106
106
|
const globalData = globalDoc.data();
|
|
107
107
|
if (globalData?.ownerUserId && globalData.ownerUserId !== userId) {
|
|
108
108
|
console.warn(
|
|
109
|
-
`[CreditsWriter] Recovery skipped: transaction ${
|
|
109
|
+
`[CreditsWriter] Recovery skipped: transaction ${storeTransactionId} belongs to user ${globalData.ownerUserId}, not ${userId}`
|
|
110
110
|
);
|
|
111
111
|
return false;
|
|
112
112
|
}
|
|
@@ -3,7 +3,7 @@ import type { Store, OwnershipType, PackageType } from "./RevenueCatTypes";
|
|
|
3
3
|
export interface RevenueCatData {
|
|
4
4
|
expirationDate: string | null;
|
|
5
5
|
willRenew: boolean | null;
|
|
6
|
-
|
|
6
|
+
storeTransactionId: string | null;
|
|
7
7
|
isPremium: boolean;
|
|
8
8
|
periodType: string | null;
|
|
9
9
|
packageType: PackageType | null;
|
|
@@ -196,7 +196,7 @@ export async function handleInitialConfiguration(
|
|
|
196
196
|
expiresAt: premiumEntitlement.expirationDate ?? undefined,
|
|
197
197
|
willRenew: premiumEntitlement.willRenew,
|
|
198
198
|
periodType: premiumEntitlement.periodType as PeriodType | undefined,
|
|
199
|
-
|
|
199
|
+
storeTransactionId: subscription?.storeTransactionId ?? undefined,
|
|
200
200
|
});
|
|
201
201
|
} else {
|
|
202
202
|
await deps.config.onPremiumStatusChanged({
|
|
@@ -34,7 +34,7 @@ export interface InitializeCreditsMetadata {
|
|
|
34
34
|
type: PurchaseType;
|
|
35
35
|
expirationDate: string | null;
|
|
36
36
|
willRenew: boolean | null;
|
|
37
|
-
|
|
37
|
+
storeTransactionId: string | null;
|
|
38
38
|
isPremium: boolean;
|
|
39
39
|
periodType: string | null;
|
|
40
40
|
unsubscribeDetectedAt: string | null;
|
|
@@ -110,7 +110,7 @@ export class SubscriptionSyncProcessor {
|
|
|
110
110
|
revenueCatData.packageType = event.packageType ?? null;
|
|
111
111
|
const revenueCatAppUserId = await this.getRevenueCatAppUserId();
|
|
112
112
|
revenueCatData.revenueCatUserId = revenueCatAppUserId ?? event.userId;
|
|
113
|
-
const purchaseId = generatePurchaseId(revenueCatData.
|
|
113
|
+
const purchaseId = generatePurchaseId(revenueCatData.storeTransactionId, event.productId);
|
|
114
114
|
|
|
115
115
|
const creditsUserId = await this.getCreditsUserId(event.userId);
|
|
116
116
|
|
|
@@ -140,7 +140,7 @@ export class SubscriptionSyncProcessor {
|
|
|
140
140
|
revenueCatData.expirationDate = event.newExpirationDate ?? revenueCatData.expirationDate;
|
|
141
141
|
const revenueCatAppUserId = await this.getRevenueCatAppUserId();
|
|
142
142
|
revenueCatData.revenueCatUserId = revenueCatAppUserId ?? event.userId;
|
|
143
|
-
const purchaseId = generateRenewalId(revenueCatData.
|
|
143
|
+
const purchaseId = generateRenewalId(revenueCatData.storeTransactionId, event.productId, event.newExpirationDate);
|
|
144
144
|
|
|
145
145
|
const creditsUserId = await this.getCreditsUserId(event.userId);
|
|
146
146
|
|
|
@@ -222,7 +222,7 @@ export class SubscriptionSyncProcessor {
|
|
|
222
222
|
event.willRenew ?? false,
|
|
223
223
|
event.expiresAt ?? null,
|
|
224
224
|
event.periodType ?? null,
|
|
225
|
-
event.
|
|
225
|
+
event.storeTransactionId,
|
|
226
226
|
);
|
|
227
227
|
if (__DEV__ && created) {
|
|
228
228
|
console.log('[SubscriptionSyncProcessor] Recovery: created missing credits document for premium user', {
|
|
@@ -23,7 +23,7 @@ export const extractRevenueCatData = (customerInfo: CustomerInfo, entitlementId:
|
|
|
23
23
|
return {
|
|
24
24
|
expirationDate: null,
|
|
25
25
|
willRenew: null,
|
|
26
|
-
|
|
26
|
+
storeTransactionId: null,
|
|
27
27
|
periodType: null,
|
|
28
28
|
packageType: null,
|
|
29
29
|
isPremium: false,
|
|
@@ -39,11 +39,11 @@ export const extractRevenueCatData = (customerInfo: CustomerInfo, entitlementId:
|
|
|
39
39
|
return {
|
|
40
40
|
expirationDate: entitlement.expirationDate ?? null,
|
|
41
41
|
willRenew: entitlement.willRenew ?? null,
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// Note:
|
|
45
|
-
//
|
|
46
|
-
|
|
42
|
+
// Current transaction ID from Apple/Google — changes with each renewal.
|
|
43
|
+
// Used as dedup key in processedTransactions collection.
|
|
44
|
+
// Note: original_store_transaction_id (stable across renewals) is only
|
|
45
|
+
// available server-side (webhooks/REST API), not in the client SDK.
|
|
46
|
+
storeTransactionId: subscription?.storeTransactionId ?? null,
|
|
47
47
|
periodType: validatePeriodType(entitlement.periodType),
|
|
48
48
|
packageType: null,
|
|
49
49
|
isPremium: true,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
const uniqueSuffix = (): string => `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2
2
|
|
|
3
|
-
export const generatePurchaseId = (
|
|
4
|
-
return
|
|
5
|
-
? `purchase_${
|
|
3
|
+
export const generatePurchaseId = (storeTransactionId: string | null, productId: string): string => {
|
|
4
|
+
return storeTransactionId
|
|
5
|
+
? `purchase_${storeTransactionId}`
|
|
6
6
|
: `purchase_${productId}_${uniqueSuffix()}`;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
export const generateRenewalId = (
|
|
10
|
-
return
|
|
11
|
-
? `renewal_${
|
|
9
|
+
export const generateRenewalId = (storeTransactionId: string | null, productId: string, expirationDate: string): string => {
|
|
10
|
+
return storeTransactionId
|
|
11
|
+
? `renewal_${storeTransactionId}_${expirationDate}`
|
|
12
12
|
: `renewal_${productId}_${uniqueSuffix()}`;
|
|
13
13
|
};
|
|
@@ -39,7 +39,7 @@ export async function syncPremiumStatus(
|
|
|
39
39
|
expiresAt: premiumEntitlement.expirationDate ?? undefined,
|
|
40
40
|
willRenew: premiumEntitlement.willRenew,
|
|
41
41
|
periodType: premiumEntitlement.periodType as PeriodType | undefined,
|
|
42
|
-
|
|
42
|
+
storeTransactionId: subscription?.storeTransactionId ?? undefined,
|
|
43
43
|
});
|
|
44
44
|
} else {
|
|
45
45
|
await config.onPremiumStatusChanged({ userId, isPremium: false });
|