@umituz/react-native-subscription 2.37.70 → 2.37.72
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/infrastructure/CreditsRepository.ts +21 -1
- package/src/domains/credits/infrastructure/operations/CreditsWriter.ts +50 -0
- package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +56 -34
- package/src/domains/subscription/application/statusChangeHandlers.ts +18 -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.72",
|
|
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",
|
|
@@ -8,8 +8,9 @@ import { refundCreditsOperation } from "../application/RefundCreditsCommand";
|
|
|
8
8
|
import { PURCHASE_TYPE, type PurchaseType } from "../../subscription/core/SubscriptionConstants";
|
|
9
9
|
import { requireFirestore, buildDocRef, type CollectionConfig } from "../../../shared/infrastructure/firestore";
|
|
10
10
|
import { fetchCredits, checkHasCredits } from "./operations/CreditsFetcher";
|
|
11
|
-
import { syncExpiredStatus, syncPremiumMetadata, type PremiumMetadata } from "./operations/CreditsWriter";
|
|
11
|
+
import { syncExpiredStatus, syncPremiumMetadata, createRecoveryCreditsDocument, type PremiumMetadata } from "./operations/CreditsWriter";
|
|
12
12
|
import { initializeCreditsWithRetry } from "./operations/CreditsInitializer";
|
|
13
|
+
import { calculateCreditLimit } from "../application/CreditLimitCalculator";
|
|
13
14
|
|
|
14
15
|
export class CreditsRepository extends BaseRepository {
|
|
15
16
|
constructor(private config: CreditsConfig) {
|
|
@@ -79,6 +80,25 @@ export class CreditsRepository extends BaseRepository {
|
|
|
79
80
|
const db = requireFirestore();
|
|
80
81
|
await syncPremiumMetadata(this.getRef(db, userId), metadata);
|
|
81
82
|
}
|
|
83
|
+
|
|
84
|
+
async ensurePremiumCreditsExist(
|
|
85
|
+
userId: string,
|
|
86
|
+
productId: string,
|
|
87
|
+
willRenew: boolean,
|
|
88
|
+
expirationDate: string | null,
|
|
89
|
+
periodType: string | null,
|
|
90
|
+
): Promise<boolean> {
|
|
91
|
+
const db = requireFirestore();
|
|
92
|
+
const creditLimit = calculateCreditLimit(productId, this.config);
|
|
93
|
+
return createRecoveryCreditsDocument(
|
|
94
|
+
this.getRef(db, userId),
|
|
95
|
+
creditLimit,
|
|
96
|
+
productId,
|
|
97
|
+
willRenew,
|
|
98
|
+
expirationDate,
|
|
99
|
+
periodType,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
82
102
|
}
|
|
83
103
|
|
|
84
104
|
export function createCreditsRepository(config: CreditsConfig): CreditsRepository {
|
|
@@ -5,6 +5,7 @@ import { SUBSCRIPTION_STATUS } from "../../../subscription/core/SubscriptionCons
|
|
|
5
5
|
import { resolveSubscriptionStatus } from "../../../subscription/core/SubscriptionStatus";
|
|
6
6
|
import { toTimestamp } from "../../../../shared/utils/dateConverter";
|
|
7
7
|
import { isPast } from "../../../../utils/dateUtils";
|
|
8
|
+
import { getAppVersion, validatePlatform } from "../../../../utils/appUtils";
|
|
8
9
|
|
|
9
10
|
export async function syncExpiredStatus(ref: DocumentReference): Promise<void> {
|
|
10
11
|
const doc = await getDoc(ref);
|
|
@@ -62,3 +63,52 @@ export async function syncPremiumMetadata(
|
|
|
62
63
|
...(metadata.ownershipType && { ownershipType: metadata.ownershipType }),
|
|
63
64
|
}, { merge: true });
|
|
64
65
|
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Recovery: creates a credits document for premium users who don't have one.
|
|
69
|
+
* This handles edge cases like test store purchases, reinstalls, or failed initializations.
|
|
70
|
+
* Returns true if a new document was created, false if one already existed.
|
|
71
|
+
*/
|
|
72
|
+
export async function createRecoveryCreditsDocument(
|
|
73
|
+
ref: DocumentReference,
|
|
74
|
+
creditLimit: number,
|
|
75
|
+
productId: string,
|
|
76
|
+
willRenew: boolean,
|
|
77
|
+
expirationDate: string | null,
|
|
78
|
+
periodType: string | null,
|
|
79
|
+
): Promise<boolean> {
|
|
80
|
+
const doc = await getDoc(ref);
|
|
81
|
+
if (doc.exists()) return false;
|
|
82
|
+
|
|
83
|
+
const platform = validatePlatform();
|
|
84
|
+
const appVersion = getAppVersion();
|
|
85
|
+
|
|
86
|
+
const isExpired = expirationDate ? isPast(expirationDate) : false;
|
|
87
|
+
const status = resolveSubscriptionStatus({
|
|
88
|
+
isPremium: true,
|
|
89
|
+
willRenew,
|
|
90
|
+
isExpired,
|
|
91
|
+
periodType: periodType ?? undefined,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const expirationTimestamp = expirationDate ? toTimestamp(expirationDate) : null;
|
|
95
|
+
|
|
96
|
+
await setDoc(ref, {
|
|
97
|
+
credits: creditLimit,
|
|
98
|
+
creditLimit,
|
|
99
|
+
isPremium: true,
|
|
100
|
+
status,
|
|
101
|
+
willRenew,
|
|
102
|
+
productId,
|
|
103
|
+
platform,
|
|
104
|
+
appVersion,
|
|
105
|
+
processedPurchases: [],
|
|
106
|
+
purchaseHistory: [],
|
|
107
|
+
createdAt: serverTimestamp(),
|
|
108
|
+
lastUpdatedAt: serverTimestamp(),
|
|
109
|
+
recoveryInitialized: true,
|
|
110
|
+
...(expirationTimestamp && { expirationDate: expirationTimestamp }),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
@@ -10,6 +10,8 @@ import { handleExpiredSubscription, handlePremiumStatusSync } from "./statusChan
|
|
|
10
10
|
import type { PackageType } from "../../revenuecat/core/types";
|
|
11
11
|
|
|
12
12
|
export class SubscriptionSyncProcessor {
|
|
13
|
+
private purchaseInProgress = false;
|
|
14
|
+
|
|
13
15
|
constructor(
|
|
14
16
|
private entitlementId: string,
|
|
15
17
|
private getAnonymousUserId: () => Promise<string>
|
|
@@ -35,43 +37,53 @@ export class SubscriptionSyncProcessor {
|
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
async processPurchase(userId: string, productId: string, customerInfo: CustomerInfo, source?: PurchaseSource, packageType?: PackageType | null) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
40
|
+
this.purchaseInProgress = true;
|
|
41
|
+
try {
|
|
42
|
+
const revenueCatData = extractRevenueCatData(customerInfo, this.entitlementId);
|
|
43
|
+
revenueCatData.packageType = packageType ?? null;
|
|
44
|
+
revenueCatData.revenueCatUserId = await this.getRevenueCatAppUserId();
|
|
45
|
+
const purchaseId = generatePurchaseId(revenueCatData.originalTransactionId, productId);
|
|
46
|
+
|
|
47
|
+
const creditsUserId = await this.getCreditsUserId(userId);
|
|
48
|
+
|
|
49
|
+
await getCreditsRepository().initializeCredits(
|
|
50
|
+
creditsUserId,
|
|
51
|
+
purchaseId,
|
|
52
|
+
productId,
|
|
53
|
+
source ?? PURCHASE_SOURCE.SETTINGS,
|
|
54
|
+
revenueCatData,
|
|
55
|
+
PURCHASE_TYPE.INITIAL
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
emitCreditsUpdated(creditsUserId);
|
|
59
|
+
} finally {
|
|
60
|
+
this.purchaseInProgress = false;
|
|
61
|
+
}
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
async processRenewal(userId: string, productId: string, newExpirationDate: string, customerInfo: CustomerInfo) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
65
|
+
this.purchaseInProgress = true;
|
|
66
|
+
try {
|
|
67
|
+
const revenueCatData = extractRevenueCatData(customerInfo, this.entitlementId);
|
|
68
|
+
revenueCatData.expirationDate = newExpirationDate ?? revenueCatData.expirationDate;
|
|
69
|
+
revenueCatData.revenueCatUserId = await this.getRevenueCatAppUserId();
|
|
70
|
+
const purchaseId = generateRenewalId(revenueCatData.originalTransactionId, productId, newExpirationDate);
|
|
71
|
+
|
|
72
|
+
const creditsUserId = await this.getCreditsUserId(userId);
|
|
73
|
+
|
|
74
|
+
await getCreditsRepository().initializeCredits(
|
|
75
|
+
creditsUserId,
|
|
76
|
+
purchaseId,
|
|
77
|
+
productId,
|
|
78
|
+
PURCHASE_SOURCE.RENEWAL,
|
|
79
|
+
revenueCatData,
|
|
80
|
+
PURCHASE_TYPE.RENEWAL
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
emitCreditsUpdated(creditsUserId);
|
|
84
|
+
} finally {
|
|
85
|
+
this.purchaseInProgress = false;
|
|
86
|
+
}
|
|
75
87
|
}
|
|
76
88
|
|
|
77
89
|
async processStatusChange(
|
|
@@ -82,6 +94,16 @@ export class SubscriptionSyncProcessor {
|
|
|
82
94
|
willRenew?: boolean,
|
|
83
95
|
periodType?: PeriodType
|
|
84
96
|
) {
|
|
97
|
+
// Skip if a purchase is already handling the credits document.
|
|
98
|
+
// Both PurchaseExecutor and CustomerInfoListener fire after a purchase —
|
|
99
|
+
// the purchase handler writes credits + metadata, so the status handler can skip.
|
|
100
|
+
if (this.purchaseInProgress) {
|
|
101
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
102
|
+
console.log("[SubscriptionSyncProcessor] Skipping status change - purchase in progress");
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
85
107
|
const creditsUserId = await this.getCreditsUserId(userId);
|
|
86
108
|
|
|
87
109
|
if (!isPremium && productId) {
|
|
@@ -19,7 +19,24 @@ export const handlePremiumStatusSync = async (
|
|
|
19
19
|
store?: string | null,
|
|
20
20
|
ownershipType?: string | null
|
|
21
21
|
): Promise<void> => {
|
|
22
|
-
|
|
22
|
+
const repo = getCreditsRepository();
|
|
23
|
+
|
|
24
|
+
// Recovery: if premium user has no credits document, create one.
|
|
25
|
+
// Handles edge cases like test store, reinstalls, or failed purchase initialization.
|
|
26
|
+
if (isPremium) {
|
|
27
|
+
const created = await repo.ensurePremiumCreditsExist(
|
|
28
|
+
userId,
|
|
29
|
+
productId,
|
|
30
|
+
willRenew,
|
|
31
|
+
expiresAt,
|
|
32
|
+
periodType,
|
|
33
|
+
);
|
|
34
|
+
if (__DEV__ && created) {
|
|
35
|
+
console.log('[handlePremiumStatusSync] Recovery: created missing credits document for premium user', { userId, productId });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await repo.syncPremiumMetadata(userId, {
|
|
23
40
|
isPremium,
|
|
24
41
|
willRenew,
|
|
25
42
|
expirationDate: expiresAt,
|