@umituz/react-native-subscription 2.27.27 → 2.27.29
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/domain/entities/Credits.ts +2 -0
- package/src/index.ts +2 -0
- package/src/infrastructure/services/CreditsInitializer.ts +18 -4
- package/src/init/index.ts +3 -1
- package/src/presentation/hooks/useAuthAwarePurchase.ts +25 -9
- package/src/presentation/hooks/useCredits.ts +3 -3
- package/src/presentation/hooks/useFreeCreditsInit.ts +17 -1
- package/src/presentation/hooks/useSubscriptionStatus.ts +3 -3
- package/src/revenuecat/infrastructure/handlers/PackageHandler.ts +2 -0
- package/src/revenuecat/infrastructure/services/CustomerInfoListenerManager.ts +7 -1
- package/src/revenuecat/infrastructure/utils/PremiumStatusSyncer.ts +10 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.27.
|
|
3
|
+
"version": "2.27.29",
|
|
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",
|
package/src/index.ts
CHANGED
|
@@ -87,6 +87,8 @@ export type {
|
|
|
87
87
|
DeductCreditsResult,
|
|
88
88
|
PurchaseSource,
|
|
89
89
|
PurchaseType,
|
|
90
|
+
CreditAllocation,
|
|
91
|
+
PackageAllocationMap,
|
|
90
92
|
} from "./domain/entities/Credits";
|
|
91
93
|
export { DEFAULT_CREDITS_CONFIG } from "./domain/entities/Credits";
|
|
92
94
|
export { InsufficientCreditsError } from "./domain/errors/InsufficientCreditsError";
|
|
@@ -29,6 +29,7 @@ import { getCreditAllocation } from "../../utils/creditMapper";
|
|
|
29
29
|
|
|
30
30
|
interface InitializationResult {
|
|
31
31
|
credits: number;
|
|
32
|
+
alreadyProcessed?: boolean;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export interface InitializeCreditsMetadata {
|
|
@@ -58,7 +59,7 @@ export async function initializeCreditsTransaction(
|
|
|
58
59
|
let processedPurchases: string[] = existingData?.processedPurchases || [];
|
|
59
60
|
|
|
60
61
|
if (existingData && purchaseId && processedPurchases.includes(purchaseId)) {
|
|
61
|
-
return { credits: existingData.credits, alreadyProcessed: true }
|
|
62
|
+
return { credits: existingData.credits, alreadyProcessed: true };
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
if (existingData?.purchasedAt) {
|
|
@@ -109,18 +110,31 @@ export async function initializeCreditsTransaction(
|
|
|
109
110
|
|
|
110
111
|
const status = resolveSubscriptionStatus({ isPremium, willRenew, isExpired: !isPremium, periodType });
|
|
111
112
|
|
|
113
|
+
// Determine if this is a status sync (not a new purchase or renewal)
|
|
114
|
+
// Status sync should preserve existing credits, only update metadata
|
|
115
|
+
const isStatusSync = purchaseId?.startsWith("status_sync_") ?? false;
|
|
116
|
+
const isNewPurchaseOrRenewal = purchaseId?.startsWith("purchase_") || purchaseId?.startsWith("renewal_");
|
|
117
|
+
|
|
112
118
|
let newCredits = creditLimit;
|
|
113
|
-
if (status === SUBSCRIPTION_STATUS.TRIAL)
|
|
114
|
-
|
|
119
|
+
if (status === SUBSCRIPTION_STATUS.TRIAL) {
|
|
120
|
+
newCredits = TRIAL_CONFIG.CREDITS;
|
|
121
|
+
} else if (status === SUBSCRIPTION_STATUS.TRIAL_CANCELED) {
|
|
122
|
+
newCredits = 0;
|
|
123
|
+
} else if (isStatusSync && existingData?.credits !== undefined && existingData.isPremium) {
|
|
124
|
+
// Status sync for existing premium user: preserve current credits
|
|
125
|
+
newCredits = existingData.credits;
|
|
126
|
+
}
|
|
115
127
|
|
|
116
128
|
const creditsData: Record<string, unknown> = {
|
|
117
129
|
isPremium,
|
|
118
130
|
status,
|
|
119
131
|
credits: newCredits,
|
|
120
132
|
creditLimit,
|
|
133
|
+
// Clear free credits flag when user becomes premium
|
|
134
|
+
isFreeCredits: false,
|
|
121
135
|
purchasedAt,
|
|
122
136
|
lastUpdatedAt: now,
|
|
123
|
-
lastPurchaseAt: now,
|
|
137
|
+
lastPurchaseAt: isNewPurchaseOrRenewal ? now : (existingData?.lastPurchaseAt ?? now),
|
|
124
138
|
processedPurchases,
|
|
125
139
|
};
|
|
126
140
|
|
package/src/init/index.ts
CHANGED
|
@@ -6,5 +6,7 @@
|
|
|
6
6
|
export {
|
|
7
7
|
createSubscriptionInitModule,
|
|
8
8
|
type SubscriptionInitModuleConfig,
|
|
9
|
-
type InitModule,
|
|
10
9
|
} from './createSubscriptionInitModule';
|
|
10
|
+
|
|
11
|
+
// Re-export InitModule from design-system for convenience
|
|
12
|
+
export type { InitModule } from '@umituz/react-native-design-system';
|
|
@@ -16,23 +16,40 @@ export interface PurchaseAuthProvider {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
let globalAuthProvider: PurchaseAuthProvider | null = null;
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
|
|
20
|
+
interface SavedPurchaseState {
|
|
21
|
+
pkg: PurchasesPackage;
|
|
22
|
+
source: PurchaseSource;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SAVED_PURCHASE_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
|
|
27
|
+
let savedPurchaseState: SavedPurchaseState | null = null;
|
|
21
28
|
|
|
22
29
|
export const configureAuthProvider = (provider: PurchaseAuthProvider): void => {
|
|
23
30
|
globalAuthProvider = provider;
|
|
24
31
|
};
|
|
25
32
|
|
|
33
|
+
const savePurchase = (pkg: PurchasesPackage, source: PurchaseSource): void => {
|
|
34
|
+
savedPurchaseState = { pkg, source, timestamp: Date.now() };
|
|
35
|
+
};
|
|
36
|
+
|
|
26
37
|
export const getSavedPurchase = (): { pkg: PurchasesPackage; source: PurchaseSource } | null => {
|
|
27
|
-
if (
|
|
28
|
-
return
|
|
38
|
+
if (!savedPurchaseState) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isExpired = Date.now() - savedPurchaseState.timestamp > SAVED_PURCHASE_EXPIRY_MS;
|
|
43
|
+
if (isExpired) {
|
|
44
|
+
savedPurchaseState = null;
|
|
45
|
+
return null;
|
|
29
46
|
}
|
|
30
|
-
|
|
47
|
+
|
|
48
|
+
return { pkg: savedPurchaseState.pkg, source: savedPurchaseState.source };
|
|
31
49
|
};
|
|
32
50
|
|
|
33
51
|
export const clearSavedPurchase = (): void => {
|
|
34
|
-
|
|
35
|
-
savedSource = null;
|
|
52
|
+
savedPurchaseState = null;
|
|
36
53
|
};
|
|
37
54
|
|
|
38
55
|
export interface UseAuthAwarePurchaseParams {
|
|
@@ -69,8 +86,7 @@ export const useAuthAwarePurchase = (
|
|
|
69
86
|
const isAuth = globalAuthProvider.isAuthenticated();
|
|
70
87
|
|
|
71
88
|
if (!isAuth) {
|
|
72
|
-
|
|
73
|
-
savedSource = source || params?.source || "settings";
|
|
89
|
+
savePurchase(pkg, source || params?.source || "settings");
|
|
74
90
|
globalAuthProvider.showAuthModal();
|
|
75
91
|
return false;
|
|
76
92
|
}
|
|
@@ -86,9 +86,9 @@ export const useCredits = (): UseCreditsResult => {
|
|
|
86
86
|
return result.data || null;
|
|
87
87
|
},
|
|
88
88
|
enabled: queryEnabled,
|
|
89
|
-
staleTime:
|
|
90
|
-
gcTime:
|
|
91
|
-
refetchOnMount:
|
|
89
|
+
staleTime: 30 * 1000, // 30 seconds - data considered fresh
|
|
90
|
+
gcTime: 5 * 60 * 1000, // 5 minutes - keep in cache after unmount
|
|
91
|
+
refetchOnMount: "always",
|
|
92
92
|
refetchOnWindowFocus: true,
|
|
93
93
|
refetchOnReconnect: true,
|
|
94
94
|
});
|
|
@@ -63,6 +63,13 @@ async function initializeFreeCreditsForUser(
|
|
|
63
63
|
|
|
64
64
|
const promise = (async () => {
|
|
65
65
|
try {
|
|
66
|
+
if (!isCreditsRepositoryConfigured()) {
|
|
67
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
68
|
+
console.warn("[useFreeCreditsInit] Credits repository not configured");
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
66
73
|
const repository = getCreditsRepository();
|
|
67
74
|
const result = await repository.initializeFreeCredits(userId);
|
|
68
75
|
|
|
@@ -78,6 +85,11 @@ async function initializeFreeCreditsForUser(
|
|
|
78
85
|
}
|
|
79
86
|
return false;
|
|
80
87
|
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
90
|
+
console.error("[useFreeCreditsInit] Unexpected error:", error);
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
81
93
|
} finally {
|
|
82
94
|
freeCreditsInitInProgress.delete(userId);
|
|
83
95
|
initPromises.delete(userId);
|
|
@@ -142,7 +154,11 @@ export function useFreeCreditsInit(params: UseFreeCreditsInitParams): UseFreeCre
|
|
|
142
154
|
if (needsInit) {
|
|
143
155
|
// Double-check inside effect to handle race conditions
|
|
144
156
|
if (!freeCreditsInitAttempted.has(userId)) {
|
|
145
|
-
initializeFreeCreditsForUser(userId, stableOnComplete)
|
|
157
|
+
initializeFreeCreditsForUser(userId, stableOnComplete).catch((error) => {
|
|
158
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
159
|
+
console.error("[useFreeCreditsInit] Init failed:", error);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
146
162
|
}
|
|
147
163
|
} else if (querySuccess && isAnonymous && !hasCredits && isFreeCreditsEnabled) {
|
|
148
164
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -37,9 +37,9 @@ export const useSubscriptionStatus = (): SubscriptionStatusResult => {
|
|
|
37
37
|
return SubscriptionManager.checkPremiumStatus();
|
|
38
38
|
},
|
|
39
39
|
enabled: !!userId && SubscriptionManager.isInitializedForUser(userId),
|
|
40
|
-
staleTime:
|
|
41
|
-
gcTime:
|
|
42
|
-
refetchOnMount:
|
|
40
|
+
staleTime: 30 * 1000, // 30 seconds
|
|
41
|
+
gcTime: 5 * 60 * 1000, // 5 minutes
|
|
42
|
+
refetchOnMount: "always",
|
|
43
43
|
refetchOnWindowFocus: true,
|
|
44
44
|
refetchOnReconnect: true,
|
|
45
45
|
});
|
|
@@ -7,6 +7,8 @@ import type { PurchasesPackage, CustomerInfo } from "react-native-purchases";
|
|
|
7
7
|
import type { IRevenueCatService } from "../../application/ports/IRevenueCatService";
|
|
8
8
|
import { getPremiumEntitlement } from "../../domain/types/RevenueCatTypes";
|
|
9
9
|
|
|
10
|
+
declare const __DEV__: boolean;
|
|
11
|
+
|
|
10
12
|
export interface PremiumStatus {
|
|
11
13
|
isPremium: boolean;
|
|
12
14
|
expirationDate: Date | null;
|
|
@@ -76,7 +76,13 @@ export class CustomerInfoListenerManager {
|
|
|
76
76
|
|
|
77
77
|
this.renewalState = updateRenewalState(this.renewalState, renewalResult);
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
try {
|
|
80
|
+
await syncPremiumStatus(config, this.currentUserId, customerInfo);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (__DEV__) {
|
|
83
|
+
console.error("[CustomerInfoListener] syncPremiumStatus failed:", error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
80
86
|
};
|
|
81
87
|
|
|
82
88
|
Purchases.addCustomerInfoUpdateListener(this.listener);
|
|
@@ -7,6 +7,8 @@ import type { CustomerInfo } from "react-native-purchases";
|
|
|
7
7
|
import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
|
|
8
8
|
import { getPremiumEntitlement } from "../../domain/types/RevenueCatTypes";
|
|
9
9
|
|
|
10
|
+
declare const __DEV__: boolean;
|
|
11
|
+
|
|
10
12
|
export async function syncPremiumStatus(
|
|
11
13
|
config: RevenueCatConfig,
|
|
12
14
|
userId: string,
|
|
@@ -34,13 +36,13 @@ export async function syncPremiumStatus(
|
|
|
34
36
|
} else {
|
|
35
37
|
await config.onPremiumStatusChanged(userId, false, undefined, undefined, undefined, undefined);
|
|
36
38
|
}
|
|
37
|
-
} catch {
|
|
38
|
-
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (__DEV__) {
|
|
41
|
+
console.error('[PremiumStatusSyncer] syncPremiumStatus failed:', error);
|
|
42
|
+
}
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
declare const __DEV__: boolean;
|
|
43
|
-
|
|
44
46
|
export async function notifyPurchaseCompleted(
|
|
45
47
|
config: RevenueCatConfig,
|
|
46
48
|
userId: string,
|
|
@@ -88,7 +90,9 @@ export async function notifyRestoreCompleted(
|
|
|
88
90
|
|
|
89
91
|
try {
|
|
90
92
|
await config.onRestoreCompleted(userId, isPremium, customerInfo);
|
|
91
|
-
} catch {
|
|
92
|
-
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (__DEV__) {
|
|
95
|
+
console.error('[PremiumStatusSyncer] notifyRestoreCompleted failed:', error);
|
|
96
|
+
}
|
|
93
97
|
}
|
|
94
98
|
}
|