@umituz/react-native-subscription 2.37.43 → 2.37.45
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/creditOperationUtils.ts +1 -1
- package/src/domains/credits/infrastructure/operations/CreditsInitializer.ts +21 -16
- package/src/domains/revenuecat/infrastructure/services/UserSwitchMutex.ts +2 -1
- package/src/domains/revenuecat/infrastructure/services/userSwitchHandler.ts +5 -5
- package/src/domains/subscription/application/SubscriptionSyncUtils.ts +1 -2
- package/src/domains/subscription/infrastructure/services/purchase/PurchaseErrorHandler.ts +11 -9
- package/src/domains/subscription/infrastructure/services/purchase/PurchaseExecutor.ts +12 -2
- package/src/domains/subscription/infrastructure/utils/trialEligibilityUtils.ts +1 -1
- package/src/domains/trial/infrastructure/DeviceTrialRepository.ts +23 -18
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.45",
|
|
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",
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import { PURCHASE_ID_PREFIXES, PROCESSED_PURCHASES_WINDOW } from "../core/CreditsConstants";
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
export function calculateNewCredits({ metadata, existingData, creditLimit
|
|
14
|
+
export function calculateNewCredits({ metadata, existingData, creditLimit }: CalculateCreditsParams): number {
|
|
15
15
|
const isExpired = metadata.expirationDate ? isPast(metadata.expirationDate) : false;
|
|
16
16
|
const isPremium = metadata.isPremium;
|
|
17
17
|
const status = resolveSubscriptionStatus({
|
|
@@ -19,23 +19,26 @@ interface InitializeCreditsParams {
|
|
|
19
19
|
type?: PurchaseType;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function isTransientError(error:
|
|
22
|
+
function isTransientError(error: unknown): boolean {
|
|
23
|
+
const code = error instanceof Error ? (error as Error & { code?: string }).code : undefined;
|
|
24
|
+
const message = error instanceof Error ? error.message : '';
|
|
25
|
+
|
|
23
26
|
return (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
code === 'already-exists' ||
|
|
28
|
+
code === 'DEADLINE_EXCEEDED' ||
|
|
29
|
+
code === 'UNAVAILABLE' ||
|
|
30
|
+
code === 'RESOURCE_EXHAUSTED' ||
|
|
28
31
|
// Firestore transaction contention: document was modified between our read and write.
|
|
29
32
|
// runTransaction does not auto-retry on failed-precondition, so we do it here.
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
code === 'failed-precondition' ||
|
|
34
|
+
code === 'FAILED_PRECONDITION' ||
|
|
32
35
|
// Firestore transaction aborted due to concurrent modification.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
code === 'aborted' ||
|
|
37
|
+
code === 'ABORTED' ||
|
|
38
|
+
message.includes('already-exists') ||
|
|
39
|
+
message.includes('timeout') ||
|
|
40
|
+
message.includes('unavailable') ||
|
|
41
|
+
message.includes('failed-precondition')
|
|
39
42
|
);
|
|
40
43
|
}
|
|
41
44
|
|
|
@@ -46,7 +49,7 @@ export async function initializeCreditsWithRetry(params: InitializeCreditsParams
|
|
|
46
49
|
const cfg = { ...config, creditLimit };
|
|
47
50
|
|
|
48
51
|
const maxRetries = 3;
|
|
49
|
-
let lastError:
|
|
52
|
+
let lastError: unknown;
|
|
50
53
|
|
|
51
54
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
52
55
|
try {
|
|
@@ -76,7 +79,7 @@ export async function initializeCreditsWithRetry(params: InitializeCreditsParams
|
|
|
76
79
|
data: result.finalData ? mapCreditsDocumentToEntity(result.finalData) : null,
|
|
77
80
|
error: null,
|
|
78
81
|
};
|
|
79
|
-
} catch (error:
|
|
82
|
+
} catch (error: unknown) {
|
|
80
83
|
lastError = error;
|
|
81
84
|
|
|
82
85
|
if (isTransientError(error) && attempt < maxRetries - 1) {
|
|
@@ -98,7 +101,9 @@ export async function initializeCreditsWithRetry(params: InitializeCreditsParams
|
|
|
98
101
|
? lastError
|
|
99
102
|
: 'Unknown error during credit initialization';
|
|
100
103
|
|
|
101
|
-
const errorCode = lastError
|
|
104
|
+
const errorCode = lastError instanceof Error
|
|
105
|
+
? (lastError as Error & { code?: string }).code ?? 'UNKNOWN_ERROR'
|
|
106
|
+
: 'UNKNOWN_ERROR';
|
|
102
107
|
|
|
103
108
|
return {
|
|
104
109
|
success: false,
|
|
@@ -18,7 +18,8 @@ class UserSwitchMutexImpl {
|
|
|
18
18
|
}
|
|
19
19
|
try {
|
|
20
20
|
await this.activeSwitchPromise;
|
|
21
|
-
} catch {
|
|
21
|
+
} catch (_ignored) {
|
|
22
|
+
// Intentional: waiting for active switch to complete without failing
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const timeSinceLastSwitch = Date.now() - this.lastSwitchTime;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import Purchases, { type CustomerInfo } from "react-native-purchases";
|
|
1
|
+
import Purchases, { type CustomerInfo, type PurchasesOfferings } from "react-native-purchases";
|
|
2
2
|
import type { InitializeResult } from "../../../../shared/application/ports/IRevenueCatService";
|
|
3
3
|
import type { InitializerDeps } from "./RevenueCatInitializer.types";
|
|
4
4
|
import { FAILED_INITIALIZATION_RESULT } from "./initializerConstants";
|
|
@@ -7,7 +7,7 @@ import { getPremiumEntitlement } from "../../core/types";
|
|
|
7
7
|
|
|
8
8
|
declare const __DEV__: boolean;
|
|
9
9
|
|
|
10
|
-
function buildSuccessResult(deps: InitializerDeps, customerInfo: CustomerInfo, offerings:
|
|
10
|
+
function buildSuccessResult(deps: InitializerDeps, customerInfo: CustomerInfo, offerings: PurchasesOfferings | null): InitializeResult {
|
|
11
11
|
const isPremium = !!customerInfo.entitlements.active[deps.config.entitlementIdentifier];
|
|
12
12
|
return { success: true, offering: offerings?.current ?? null, isPremium };
|
|
13
13
|
}
|
|
@@ -17,14 +17,14 @@ function buildSuccessResult(deps: InitializerDeps, customerInfo: CustomerInfo, o
|
|
|
17
17
|
* Empty offerings (no products configured in RevenueCat dashboard) should NOT
|
|
18
18
|
* block SDK initialization. The SDK is still usable for premium checks, purchases, etc.
|
|
19
19
|
*/
|
|
20
|
-
async function fetchOfferingsSafe(): Promise<
|
|
20
|
+
async function fetchOfferingsSafe(): Promise<PurchasesOfferings | null> {
|
|
21
21
|
try {
|
|
22
22
|
return await Purchases.getOfferings();
|
|
23
23
|
} catch (error) {
|
|
24
24
|
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
25
25
|
console.warn('[UserSwitchHandler] Offerings fetch failed (non-fatal):', error);
|
|
26
26
|
}
|
|
27
|
-
return
|
|
27
|
+
return null;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -165,7 +165,7 @@ export async function handleInitialConfiguration(
|
|
|
165
165
|
console.log('[UserSwitchHandler] ✅ Initial configuration completed:', {
|
|
166
166
|
revenueCatUserId: currentUserId,
|
|
167
167
|
activeEntitlements: Object.keys(customerInfo.entitlements.active),
|
|
168
|
-
offeringsCount: offerings
|
|
168
|
+
offeringsCount: offerings?.all ? Object.keys(offerings.all).length : 0,
|
|
169
169
|
});
|
|
170
170
|
}
|
|
171
171
|
|
|
@@ -17,8 +17,7 @@ export const extractRevenueCatData = (customerInfo: CustomerInfo, entitlementId:
|
|
|
17
17
|
throw new Error('[extractRevenueCatData] entitlementId is required');
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const entitlement = customerInfo.entitlements.active[entitlementId]
|
|
21
|
-
?? customerInfo.entitlements.all[entitlementId];
|
|
20
|
+
const entitlement = customerInfo.entitlements.active[entitlementId];
|
|
22
21
|
|
|
23
22
|
const isPremium = !!customerInfo.entitlements.active[entitlementId];
|
|
24
23
|
|
|
@@ -24,18 +24,20 @@ export async function handleAlreadyPurchasedError(
|
|
|
24
24
|
): Promise<PurchaseResult> {
|
|
25
25
|
try {
|
|
26
26
|
const restoreResult = await handleRestore(deps, userId);
|
|
27
|
-
if (restoreResult.success && restoreResult.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
if (restoreResult.success && restoreResult.customerInfo) {
|
|
28
|
+
if (restoreResult.isPremium) {
|
|
29
|
+
await notifyPurchaseCompleted(
|
|
30
|
+
deps.config,
|
|
31
|
+
userId,
|
|
32
|
+
pkg.product.identifier,
|
|
33
|
+
restoreResult.customerInfo,
|
|
34
|
+
getSavedPurchase()?.source
|
|
35
|
+
);
|
|
36
|
+
}
|
|
35
37
|
clearSavedPurchase();
|
|
36
38
|
return {
|
|
37
39
|
success: true,
|
|
38
|
-
isPremium:
|
|
40
|
+
isPremium: restoreResult.isPremium ?? false,
|
|
39
41
|
customerInfo: restoreResult.customerInfo,
|
|
40
42
|
productId: restoreResult.productId || pkg.product.identifier,
|
|
41
43
|
};
|
|
@@ -17,7 +17,12 @@ async function executeConsumablePurchase(
|
|
|
17
17
|
clearSavedPurchase();
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
try {
|
|
21
|
+
await notifyPurchaseCompleted(config, userId, productId, customerInfo, source, packageType);
|
|
22
|
+
} catch (syncError) {
|
|
23
|
+
// Non-fatal: RevenueCat purchase succeeded, credits sync can be recovered on next session
|
|
24
|
+
console.error('[PurchaseExecutor] Post-purchase sync failed (purchase was successful):', syncError);
|
|
25
|
+
}
|
|
21
26
|
|
|
22
27
|
return {
|
|
23
28
|
success: true,
|
|
@@ -55,7 +60,12 @@ async function executeSubscriptionPurchase(
|
|
|
55
60
|
});
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
|
|
63
|
+
try {
|
|
64
|
+
await notifyPurchaseCompleted(config, userId, productId, customerInfo, source, packageType);
|
|
65
|
+
} catch (syncError) {
|
|
66
|
+
// Non-fatal: RevenueCat purchase succeeded, credits sync can be recovered on next session
|
|
67
|
+
console.error('[PurchaseExecutor] Post-purchase sync failed (purchase was successful):', syncError);
|
|
68
|
+
}
|
|
59
69
|
|
|
60
70
|
return {
|
|
61
71
|
success: true,
|
|
@@ -22,23 +22,28 @@ export class DeviceTrialRepository {
|
|
|
22
22
|
if (!this.db) return false;
|
|
23
23
|
const ref = doc(this.db, DEVICE_TRIALS_COLLECTION, deviceId);
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
25
|
+
try {
|
|
26
|
+
// Atomic check-then-act: ensure createdAt is set only once
|
|
27
|
+
await runTransaction(async (tx: Transaction) => {
|
|
28
|
+
const snap = await tx.get(ref);
|
|
29
|
+
const existingData = snap.data();
|
|
30
|
+
|
|
31
|
+
const updateData: Record<string, unknown> = {
|
|
32
|
+
...data,
|
|
33
|
+
updatedAt: serverTimestamp(),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (!existingData?.createdAt) {
|
|
37
|
+
updateData.createdAt = serverTimestamp();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
tx.set(ref, updateData, { merge: true });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return true;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('[DeviceTrialRepository] Failed to save record:', error instanceof Error ? error.message : String(error));
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
43
48
|
}
|
|
44
49
|
}
|