@umituz/react-native-subscription 2.34.0 → 2.35.0
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/DeductCreditsCommand.ts +11 -0
- package/src/domains/credits/application/creditOperationUtils.ts +5 -1
- package/src/domains/credits/infrastructure/operations/CreditsInitializer.ts +6 -1
- package/src/domains/credits/presentation/deduct-credit/useDeductCredit.ts +11 -4
- package/src/domains/credits/presentation/useCredits.ts +2 -7
- package/src/domains/paywall/hooks/usePaywallActions.ts +50 -16
- package/src/domains/revenuecat/infrastructure/services/ConfigurationStateManager.ts +16 -4
- package/src/domains/revenuecat/infrastructure/services/RevenueCatInitializer.ts +17 -4
- package/src/domains/revenuecat/infrastructure/services/userSwitchHandler.ts +8 -1
- package/src/domains/subscription/application/SubscriptionSyncService.ts +24 -6
- package/src/domains/subscription/application/SubscriptionSyncUtils.ts +32 -9
- package/src/domains/subscription/application/initializer/BackgroundInitializer.ts +13 -3
- package/src/domains/subscription/application/initializer/ServiceConfigurator.ts +6 -2
- package/src/domains/subscription/application/initializer/SubscriptionInitializer.ts +3 -2
- package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +9 -1
- package/src/domains/subscription/infrastructure/handlers/package-operations/PackagePurchaser.ts +10 -0
- package/src/domains/subscription/infrastructure/hooks/useSubscriptionPackages.ts +6 -0
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +12 -1
- package/src/domains/subscription/infrastructure/managers/managerOperations.ts +10 -1
- package/src/domains/subscription/infrastructure/managers/subscriptionManagerUtils.ts +2 -2
- package/src/domains/subscription/infrastructure/services/CustomerInfoListenerManager.ts +13 -5
- package/src/domains/subscription/infrastructure/services/PurchaseHandler.ts +17 -1
- package/src/domains/subscription/infrastructure/services/RevenueCatService.types.ts +10 -1
- package/src/domains/subscription/infrastructure/services/purchase/PurchaseExecutor.ts +23 -4
- package/src/domains/subscription/infrastructure/utils/InitializationCache.ts +2 -7
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +7 -3
- package/src/domains/subscription/infrastructure/utils/renewal/RenewalDetector.ts +2 -2
- package/src/domains/subscription/presentation/stores/purchaseLoadingStore.ts +4 -4
- package/src/init/createSubscriptionInitModule.ts +1 -1
- package/src/shared/infrastructure/SubscriptionEventBus.ts +10 -5
- package/src/shared/presentation/hooks/useServiceCall.ts +15 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.35.0",
|
|
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",
|
|
@@ -13,6 +13,17 @@ export async function deductCreditsOperation(
|
|
|
13
13
|
cost: number,
|
|
14
14
|
userId: string
|
|
15
15
|
): Promise<DeductCreditsResult> {
|
|
16
|
+
if (!userId || userId.trim().length === 0) {
|
|
17
|
+
return {
|
|
18
|
+
success: false,
|
|
19
|
+
remainingCredits: null,
|
|
20
|
+
error: {
|
|
21
|
+
message: 'Valid userId is required for credit deduction',
|
|
22
|
+
code: 'INVALID_USER'
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
try {
|
|
17
28
|
const remaining = await runTransaction(async (tx: Transaction) => {
|
|
18
29
|
const docSnap = await tx.get(creditsRef);
|
|
@@ -90,5 +90,9 @@ export function shouldSkipStatusSyncWrite(
|
|
|
90
90
|
existingData.status === newCreditsData.status &&
|
|
91
91
|
existingData.credits === newCreditsData.credits &&
|
|
92
92
|
existingData.creditLimit === newCreditsData.creditLimit &&
|
|
93
|
-
existingData.productId === newCreditsData.productId
|
|
93
|
+
existingData.productId === newCreditsData.productId &&
|
|
94
|
+
existingData.willRenew === newCreditsData.willRenew &&
|
|
95
|
+
existingData.expirationDate === newCreditsData.expirationDate &&
|
|
96
|
+
existingData.canceledAt === newCreditsData.canceledAt &&
|
|
97
|
+
existingData.billingIssueDetectedAt === newCreditsData.billingIssueDetectedAt;
|
|
94
98
|
}
|
|
@@ -72,7 +72,12 @@ export async function initializeCreditsWithRetry(params: InitializeCreditsParams
|
|
|
72
72
|
lastError = error;
|
|
73
73
|
|
|
74
74
|
if (isTransientError(error) && attempt < maxRetries - 1) {
|
|
75
|
-
|
|
75
|
+
const baseDelay = 100;
|
|
76
|
+
const maxDelay = 5000;
|
|
77
|
+
const exponentialDelay = baseDelay * Math.pow(2, attempt);
|
|
78
|
+
const jitter = Math.random() * baseDelay;
|
|
79
|
+
const delay = Math.min(exponentialDelay + jitter, maxDelay);
|
|
80
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
76
81
|
continue;
|
|
77
82
|
}
|
|
78
83
|
break;
|
|
@@ -20,14 +20,21 @@ export const useDeductCredit = ({
|
|
|
20
20
|
try {
|
|
21
21
|
const res = await mutation.mutateAsync(cost);
|
|
22
22
|
if (!res.success) {
|
|
23
|
-
if (res.error?.code === "CREDITS_EXHAUSTED")
|
|
23
|
+
if (res.error?.code === "CREDITS_EXHAUSTED") {
|
|
24
|
+
onCreditsExhausted?.();
|
|
25
|
+
}
|
|
24
26
|
return false;
|
|
25
27
|
}
|
|
26
28
|
return true;
|
|
27
|
-
} catch {
|
|
28
|
-
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('[useDeductCredit] Unexpected error during credit deduction', {
|
|
31
|
+
cost,
|
|
32
|
+
userId,
|
|
33
|
+
error
|
|
34
|
+
});
|
|
35
|
+
throw error;
|
|
29
36
|
}
|
|
30
|
-
}, [mutation, onCreditsExhausted]);
|
|
37
|
+
}, [mutation, onCreditsExhausted, userId]);
|
|
31
38
|
|
|
32
39
|
const deductCredits = useCallback(async (cost: number): Promise<boolean> => {
|
|
33
40
|
return await deductCredit(cost);
|
|
@@ -52,23 +52,18 @@ export const useCredits = (): UseCreditsResult => {
|
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
const queryClient = useQueryClient();
|
|
55
|
-
const queryClientRef = useRef(queryClient);
|
|
56
|
-
|
|
57
|
-
useEffect(() => {
|
|
58
|
-
queryClientRef.current = queryClient;
|
|
59
|
-
}, [queryClient]);
|
|
60
55
|
|
|
61
56
|
useEffect(() => {
|
|
62
57
|
if (!isAuthenticated(userId)) return undefined;
|
|
63
58
|
|
|
64
59
|
const unsubscribe = subscriptionEventBus.on(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, (updatedUserId) => {
|
|
65
60
|
if (updatedUserId === userId) {
|
|
66
|
-
|
|
61
|
+
queryClient.invalidateQueries({ queryKey: creditsQueryKeys.user(userId) });
|
|
67
62
|
}
|
|
68
63
|
});
|
|
69
64
|
|
|
70
65
|
return unsubscribe;
|
|
71
|
-
}, [userId]);
|
|
66
|
+
}, [userId, queryClient]);
|
|
72
67
|
|
|
73
68
|
const credits = data ?? null;
|
|
74
69
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* usePaywallActions Hook
|
|
3
3
|
* Encapsulates purchase and restore flow for the paywall.
|
|
4
4
|
*/
|
|
5
|
-
import { useState, useCallback } from "react";
|
|
5
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
6
6
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
7
7
|
import { usePurchaseLoadingStore } from "../../subscription/presentation/stores";
|
|
8
8
|
import type { PurchaseSource } from "../../subscription/core/SubscriptionConstants";
|
|
@@ -35,53 +35,87 @@ export function usePaywallActions({
|
|
|
35
35
|
|
|
36
36
|
const isProcessing = isLocalProcessing || isGlobalPurchasing;
|
|
37
37
|
|
|
38
|
+
const onPurchaseRef = useRef(onPurchase);
|
|
39
|
+
const onRestoreRef = useRef(onRestore);
|
|
40
|
+
const onPurchaseSuccessRef = useRef(onPurchaseSuccess);
|
|
41
|
+
const onPurchaseErrorRef = useRef(onPurchaseError);
|
|
42
|
+
const onAuthRequiredRef = useRef(onAuthRequired);
|
|
43
|
+
const onCloseRef = useRef(onClose);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
onPurchaseRef.current = onPurchase;
|
|
47
|
+
onRestoreRef.current = onRestore;
|
|
48
|
+
onPurchaseSuccessRef.current = onPurchaseSuccess;
|
|
49
|
+
onPurchaseErrorRef.current = onPurchaseError;
|
|
50
|
+
onAuthRequiredRef.current = onAuthRequired;
|
|
51
|
+
onCloseRef.current = onClose;
|
|
52
|
+
});
|
|
53
|
+
|
|
38
54
|
const handlePurchase = useCallback(async () => {
|
|
39
|
-
|
|
55
|
+
console.log('🔵 [usePaywallActions] handlePurchase called', {
|
|
56
|
+
selectedPlanId,
|
|
57
|
+
packagesCount: packages.length,
|
|
58
|
+
isProcessing,
|
|
59
|
+
hasOnPurchase: !!onPurchaseRef.current
|
|
60
|
+
});
|
|
61
|
+
|
|
40
62
|
const planId = selectedPlanId || (packages.length > 0 ? packages[0]?.product.identifier : null);
|
|
41
|
-
|
|
42
|
-
if (!planId || !
|
|
43
|
-
|
|
63
|
+
|
|
64
|
+
if (!planId || !onPurchaseRef.current || isProcessing) {
|
|
65
|
+
console.log('⚠️ [usePaywallActions] Purchase blocked', {
|
|
66
|
+
noPlanId: !planId,
|
|
67
|
+
noCallback: !onPurchaseRef.current,
|
|
68
|
+
isProcessing
|
|
69
|
+
});
|
|
70
|
+
if (!planId && onAuthRequiredRef.current) onAuthRequiredRef.current();
|
|
44
71
|
return;
|
|
45
72
|
}
|
|
46
73
|
|
|
74
|
+
console.log('🟢 [usePaywallActions] Starting purchase', { planId });
|
|
47
75
|
setIsLocalProcessing(true);
|
|
48
|
-
// Map PurchaseSource to store's expected "manual" | "auto-execution"
|
|
49
76
|
startPurchase(planId, "manual");
|
|
50
77
|
|
|
51
78
|
try {
|
|
52
79
|
const pkg = packages.find((p) => p.product.identifier === planId);
|
|
80
|
+
console.log('📦 [usePaywallActions] Package found:', !!pkg);
|
|
81
|
+
|
|
53
82
|
if (pkg) {
|
|
54
|
-
|
|
83
|
+
console.log('🚀 [usePaywallActions] Calling onPurchase callback');
|
|
84
|
+
const success = await onPurchaseRef.current(pkg);
|
|
85
|
+
console.log('✅ [usePaywallActions] onPurchase completed', { success });
|
|
86
|
+
|
|
55
87
|
if (success !== false) {
|
|
56
|
-
|
|
57
|
-
|
|
88
|
+
onPurchaseSuccessRef.current?.();
|
|
89
|
+
onCloseRef.current?.();
|
|
58
90
|
}
|
|
59
91
|
}
|
|
60
92
|
} catch (error) {
|
|
93
|
+
console.error('❌ [usePaywallActions] Purchase error:', error);
|
|
61
94
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
62
|
-
|
|
95
|
+
onPurchaseErrorRef.current?.(err);
|
|
63
96
|
} finally {
|
|
97
|
+
console.log('🏁 [usePaywallActions] Purchase flow finished');
|
|
64
98
|
setIsLocalProcessing(false);
|
|
65
99
|
endPurchase(planId);
|
|
66
100
|
}
|
|
67
|
-
}, [selectedPlanId, packages,
|
|
101
|
+
}, [selectedPlanId, packages, isProcessing, startPurchase, endPurchase]);
|
|
68
102
|
|
|
69
103
|
const handleRestore = useCallback(async () => {
|
|
70
|
-
if (!
|
|
104
|
+
if (!onRestoreRef.current || isProcessing) return;
|
|
71
105
|
|
|
72
106
|
setIsLocalProcessing(true);
|
|
73
107
|
try {
|
|
74
|
-
const success = await
|
|
108
|
+
const success = await onRestoreRef.current();
|
|
75
109
|
if (success !== false) {
|
|
76
|
-
|
|
110
|
+
onPurchaseSuccessRef.current?.();
|
|
77
111
|
}
|
|
78
112
|
} catch (error) {
|
|
79
113
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
80
|
-
|
|
114
|
+
onPurchaseErrorRef.current?.(err);
|
|
81
115
|
} finally {
|
|
82
116
|
setIsLocalProcessing(false);
|
|
83
117
|
}
|
|
84
|
-
}, [
|
|
118
|
+
}, [isProcessing]);
|
|
85
119
|
|
|
86
120
|
const resetState = useCallback(() => {
|
|
87
121
|
setSelectedPlanId(null);
|
|
@@ -22,21 +22,33 @@ export class ConfigurationStateManager {
|
|
|
22
22
|
throw new Error('Configuration already in progress');
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
let capturedResolve: ((value: InitializeResult) => void) | null = null;
|
|
26
|
+
|
|
25
27
|
this._configurationPromise = new Promise((resolve) => {
|
|
28
|
+
capturedResolve = resolve;
|
|
26
29
|
this._resolveConfiguration = resolve;
|
|
27
30
|
});
|
|
28
31
|
|
|
29
32
|
return (value: InitializeResult) => {
|
|
30
|
-
if (
|
|
31
|
-
|
|
33
|
+
if (capturedResolve) {
|
|
34
|
+
capturedResolve(value);
|
|
35
|
+
capturedResolve = null;
|
|
32
36
|
}
|
|
33
37
|
};
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
completeConfiguration(success: boolean): void {
|
|
37
41
|
this._isPurchasesConfigured = success;
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
|
|
43
|
+
if (success) {
|
|
44
|
+
this._configurationPromise = null;
|
|
45
|
+
this._resolveConfiguration = null;
|
|
46
|
+
} else {
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
this._configurationPromise = null;
|
|
49
|
+
this._resolveConfiguration = null;
|
|
50
|
+
}, 1000);
|
|
51
|
+
}
|
|
40
52
|
}
|
|
41
53
|
|
|
42
54
|
reset(): void {
|
|
@@ -7,10 +7,13 @@ import { handleUserSwitch, handleInitialConfiguration, fetchCurrentUserData } fr
|
|
|
7
7
|
|
|
8
8
|
export type { InitializerDeps } from "./RevenueCatInitializer.types";
|
|
9
9
|
|
|
10
|
+
const MAX_CONFIG_START_RETRIES = 3;
|
|
11
|
+
|
|
10
12
|
export async function initializeSDK(
|
|
11
13
|
deps: InitializerDeps,
|
|
12
14
|
userId: string,
|
|
13
|
-
apiKey?: string
|
|
15
|
+
apiKey?: string,
|
|
16
|
+
configStartRetryCount: number = 0
|
|
14
17
|
): Promise<InitializeResult> {
|
|
15
18
|
if (deps.isInitialized() && deps.getCurrentUserId() === userId) {
|
|
16
19
|
return fetchCurrentUserData(deps);
|
|
@@ -36,7 +39,7 @@ export async function initializeSDK(
|
|
|
36
39
|
return FAILED_INITIALIZATION_RESULT;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
return initializeSDK(deps, userId, apiKey);
|
|
42
|
+
return initializeSDK(deps, userId, apiKey, configStartRetryCount);
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
const key = apiKey || resolveApiKey(deps.config);
|
|
@@ -48,12 +51,22 @@ export async function initializeSDK(
|
|
|
48
51
|
try {
|
|
49
52
|
resolveConfig = configState.startConfiguration();
|
|
50
53
|
} catch (error) {
|
|
51
|
-
|
|
54
|
+
if (configStartRetryCount >= MAX_CONFIG_START_RETRIES) {
|
|
55
|
+
console.error('[RevenueCatInitializer] Max configuration start retries reached', {
|
|
56
|
+
userId,
|
|
57
|
+
retryCount: configStartRetryCount,
|
|
58
|
+
error
|
|
59
|
+
});
|
|
60
|
+
return FAILED_INITIALIZATION_RESULT;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.error('[RevenueCatInitializer] Failed to start configuration, retrying', {
|
|
52
64
|
userId,
|
|
65
|
+
retryCount: configStartRetryCount,
|
|
53
66
|
error
|
|
54
67
|
});
|
|
55
68
|
await new Promise(resolve => setTimeout(resolve, CONFIGURATION_RETRY_DELAY_MS));
|
|
56
|
-
return initializeSDK(deps, userId, apiKey);
|
|
69
|
+
return initializeSDK(deps, userId, apiKey, configStartRetryCount + 1);
|
|
57
70
|
}
|
|
58
71
|
|
|
59
72
|
const result = await handleInitialConfiguration(deps, userId, key);
|
|
@@ -72,9 +72,16 @@ export async function handleUserSwitch(
|
|
|
72
72
|
|
|
73
73
|
return buildSuccessResult(deps, customerInfo, offerings);
|
|
74
74
|
} catch (error) {
|
|
75
|
+
let currentAppUserId = 'unknown';
|
|
76
|
+
try {
|
|
77
|
+
currentAppUserId = await Purchases.getAppUserID();
|
|
78
|
+
} catch {
|
|
79
|
+
// Ignore error in error handler
|
|
80
|
+
}
|
|
81
|
+
|
|
75
82
|
console.error('[UserSwitchHandler] Failed during user switch or fetch', {
|
|
76
83
|
userId,
|
|
77
|
-
currentAppUserId
|
|
84
|
+
currentAppUserId,
|
|
78
85
|
error
|
|
79
86
|
});
|
|
80
87
|
return FAILED_INITIALIZATION_RESULT;
|
|
@@ -14,8 +14,13 @@ export class SubscriptionSyncService {
|
|
|
14
14
|
try {
|
|
15
15
|
await this.processor.processPurchase(userId, productId, customerInfo, source);
|
|
16
16
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.PURCHASE_COMPLETED, { userId, productId });
|
|
17
|
-
} catch (
|
|
18
|
-
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error('[SubscriptionSyncService] Purchase processing failed', {
|
|
19
|
+
userId,
|
|
20
|
+
productId,
|
|
21
|
+
source,
|
|
22
|
+
error
|
|
23
|
+
});
|
|
19
24
|
}
|
|
20
25
|
}
|
|
21
26
|
|
|
@@ -23,8 +28,13 @@ export class SubscriptionSyncService {
|
|
|
23
28
|
try {
|
|
24
29
|
await this.processor.processRenewal(userId, productId, newExpirationDate, customerInfo);
|
|
25
30
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.RENEWAL_DETECTED, { userId, productId });
|
|
26
|
-
} catch (
|
|
27
|
-
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('[SubscriptionSyncService] Renewal processing failed', {
|
|
33
|
+
userId,
|
|
34
|
+
productId,
|
|
35
|
+
newExpirationDate,
|
|
36
|
+
error
|
|
37
|
+
});
|
|
28
38
|
}
|
|
29
39
|
}
|
|
30
40
|
|
|
@@ -39,8 +49,16 @@ export class SubscriptionSyncService {
|
|
|
39
49
|
try {
|
|
40
50
|
await this.processor.processStatusChange(userId, isPremium, productId, expiresAt, willRenew, periodType);
|
|
41
51
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.PREMIUM_STATUS_CHANGED, { userId, isPremium });
|
|
42
|
-
} catch (
|
|
43
|
-
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('[SubscriptionSyncService] Status change processing failed', {
|
|
54
|
+
userId,
|
|
55
|
+
isPremium,
|
|
56
|
+
productId,
|
|
57
|
+
expiresAt,
|
|
58
|
+
willRenew,
|
|
59
|
+
periodType,
|
|
60
|
+
error
|
|
61
|
+
});
|
|
44
62
|
}
|
|
45
63
|
}
|
|
46
64
|
}
|
|
@@ -10,18 +10,41 @@ function validatePeriodType(periodType: string | undefined): PeriodType | null {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export const extractRevenueCatData = (customerInfo: CustomerInfo, entitlementId: string): RevenueCatData => {
|
|
13
|
+
if (!customerInfo) {
|
|
14
|
+
throw new Error('[extractRevenueCatData] customerInfo is required');
|
|
15
|
+
}
|
|
16
|
+
if (!entitlementId) {
|
|
17
|
+
throw new Error('[extractRevenueCatData] entitlementId is required');
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
const entitlement = customerInfo.entitlements.active[entitlementId]
|
|
14
21
|
?? customerInfo.entitlements.all[entitlementId];
|
|
15
22
|
|
|
23
|
+
const isPremium = !!customerInfo.entitlements.active[entitlementId];
|
|
24
|
+
|
|
25
|
+
if (!entitlement) {
|
|
26
|
+
return {
|
|
27
|
+
expirationDate: null,
|
|
28
|
+
willRenew: null,
|
|
29
|
+
originalTransactionId: null,
|
|
30
|
+
periodType: null,
|
|
31
|
+
isPremium: false,
|
|
32
|
+
unsubscribeDetectedAt: null,
|
|
33
|
+
billingIssueDetectedAt: null,
|
|
34
|
+
store: null,
|
|
35
|
+
ownershipType: null,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
16
39
|
return {
|
|
17
|
-
expirationDate: entitlement
|
|
18
|
-
willRenew: entitlement
|
|
19
|
-
originalTransactionId:
|
|
20
|
-
periodType: validatePeriodType(entitlement
|
|
21
|
-
isPremium
|
|
22
|
-
unsubscribeDetectedAt: entitlement
|
|
23
|
-
billingIssueDetectedAt: entitlement
|
|
24
|
-
store: entitlement
|
|
25
|
-
ownershipType: entitlement
|
|
40
|
+
expirationDate: entitlement.expirationDate ?? null,
|
|
41
|
+
willRenew: entitlement.willRenew ?? null,
|
|
42
|
+
originalTransactionId: entitlement.originalPurchaseDate ?? null,
|
|
43
|
+
periodType: validatePeriodType(entitlement.periodType),
|
|
44
|
+
isPremium,
|
|
45
|
+
unsubscribeDetectedAt: entitlement.unsubscribeDetectedAt ?? null,
|
|
46
|
+
billingIssueDetectedAt: entitlement.billingIssueDetectedAt ?? null,
|
|
47
|
+
store: entitlement.store ?? null,
|
|
48
|
+
ownershipType: entitlement.ownershipType ?? null,
|
|
26
49
|
};
|
|
27
50
|
};
|
|
@@ -4,7 +4,7 @@ import type { SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
|
|
|
4
4
|
|
|
5
5
|
declare const __DEV__: boolean;
|
|
6
6
|
|
|
7
|
-
export async function startBackgroundInitialization(config: SubscriptionInitConfig): Promise<void> {
|
|
7
|
+
export async function startBackgroundInitialization(config: SubscriptionInitConfig): Promise<() => void> {
|
|
8
8
|
const initializeInBackground = async (userId?: string): Promise<void> => {
|
|
9
9
|
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
10
10
|
console.log('[BackgroundInitializer] initializeInBackground called with userId:', userId || '(undefined - anonymous)');
|
|
@@ -29,10 +29,20 @@ export async function startBackgroundInitialization(config: SubscriptionInitConf
|
|
|
29
29
|
|
|
30
30
|
await initializeInBackground(initialUserId);
|
|
31
31
|
|
|
32
|
-
setupAuthStateListener(() => auth, (newUserId) => {
|
|
32
|
+
const unsubscribe = setupAuthStateListener(() => auth, async (newUserId) => {
|
|
33
33
|
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
34
34
|
console.log('[BackgroundInitializer] Auth state listener triggered, reinitializing with userId:', newUserId || '(undefined - anonymous)');
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
try {
|
|
37
|
+
await initializeInBackground(newUserId);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('[BackgroundInitializer] Failed to reinitialize on auth change', { userId: newUserId, error });
|
|
40
|
+
}
|
|
37
41
|
});
|
|
42
|
+
|
|
43
|
+
return () => {
|
|
44
|
+
if (unsubscribe) {
|
|
45
|
+
unsubscribe();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
38
48
|
}
|
|
@@ -7,9 +7,13 @@ import type { SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
|
|
|
7
7
|
export function configureServices(config: SubscriptionInitConfig, apiKey: string): SubscriptionSyncService {
|
|
8
8
|
const { entitlementId, credits, creditPackages, getFirebaseAuth, showAuthModal, onCreditsUpdated } = config;
|
|
9
9
|
|
|
10
|
+
if (!creditPackages) {
|
|
11
|
+
throw new Error('[ServiceConfigurator] creditPackages configuration is required');
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
configureCreditsRepository({
|
|
11
15
|
...credits,
|
|
12
|
-
creditPackageAmounts: creditPackages
|
|
16
|
+
creditPackageAmounts: creditPackages.amounts
|
|
13
17
|
});
|
|
14
18
|
|
|
15
19
|
const syncService = new SubscriptionSyncService(entitlementId);
|
|
@@ -18,7 +22,7 @@ export function configureServices(config: SubscriptionInitConfig, apiKey: string
|
|
|
18
22
|
config: {
|
|
19
23
|
apiKey,
|
|
20
24
|
entitlementIdentifier: entitlementId,
|
|
21
|
-
consumableProductIdentifiers: [creditPackages
|
|
25
|
+
consumableProductIdentifiers: [creditPackages.identifierPattern],
|
|
22
26
|
onPurchaseCompleted: (u: string, p: string, c: any, s: any) => syncService.handlePurchase(u, p, c, s),
|
|
23
27
|
onRenewalDetected: (u: string, p: string, expires: string, c: any) => syncService.handleRenewal(u, p, expires, c),
|
|
24
28
|
onPremiumStatusChanged: (u: string, isP: boolean, pId: any, exp: any, willR: any, pt: any) => syncService.handlePremiumStatusChanged(u, isP, pId, exp, willR, pt),
|
|
@@ -3,9 +3,10 @@ import { getApiKey, validateConfig } from "./ConfigValidator";
|
|
|
3
3
|
import { configureServices } from "./ServiceConfigurator";
|
|
4
4
|
import { startBackgroundInitialization } from "./BackgroundInitializer";
|
|
5
5
|
|
|
6
|
-
export const initializeSubscription = async (config: SubscriptionInitConfig): Promise<void> => {
|
|
6
|
+
export const initializeSubscription = async (config: SubscriptionInitConfig): Promise<() => void> => {
|
|
7
7
|
const apiKey = getApiKey(config);
|
|
8
8
|
validateConfig(config);
|
|
9
9
|
configureServices(config, apiKey);
|
|
10
|
-
await startBackgroundInitialization(config);
|
|
10
|
+
const cleanup = await startBackgroundInitialization(config);
|
|
11
|
+
return cleanup;
|
|
11
12
|
};
|
|
@@ -24,7 +24,15 @@ export class PackageHandler {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
async purchase(pkg: PurchasesPackage, userId: string): Promise<boolean> {
|
|
27
|
-
|
|
27
|
+
console.log('🔵 [PackageHandler] purchase called', {
|
|
28
|
+
productId: pkg.product.identifier,
|
|
29
|
+
userId
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const result = await executePurchase(this.service, pkg, userId);
|
|
33
|
+
console.log('✅ [PackageHandler] purchase completed', { result });
|
|
34
|
+
|
|
35
|
+
return result;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
async restore(userId: string): Promise<RestoreResultInfo> {
|
package/src/domains/subscription/infrastructure/handlers/package-operations/PackagePurchaser.ts
CHANGED
|
@@ -6,10 +6,20 @@ export async function executePurchase(
|
|
|
6
6
|
pkg: PurchasesPackage,
|
|
7
7
|
userId: string
|
|
8
8
|
): Promise<boolean> {
|
|
9
|
+
console.log('🔵 [executePurchase] Starting', {
|
|
10
|
+
productId: pkg.product.identifier,
|
|
11
|
+
userId,
|
|
12
|
+
isInitialized: service.isInitialized()
|
|
13
|
+
});
|
|
14
|
+
|
|
9
15
|
if (!service.isInitialized()) {
|
|
16
|
+
console.error('❌ [executePurchase] Service not initialized!');
|
|
10
17
|
throw new Error("Service not initialized");
|
|
11
18
|
}
|
|
12
19
|
|
|
20
|
+
console.log('🚀 [executePurchase] Calling service.purchasePackage');
|
|
13
21
|
const result = await service.purchasePackage(pkg, userId);
|
|
22
|
+
console.log('✅ [executePurchase] Completed', { success: result.success });
|
|
23
|
+
|
|
14
24
|
return result.success;
|
|
15
25
|
}
|
|
@@ -55,10 +55,16 @@ export const useSubscriptionPackages = () => {
|
|
|
55
55
|
|
|
56
56
|
if (prevUserId !== userId) {
|
|
57
57
|
if (prevUserId) {
|
|
58
|
+
queryClient.cancelQueries({
|
|
59
|
+
queryKey: [...SUBSCRIPTION_QUERY_KEYS.packages, prevUserId],
|
|
60
|
+
});
|
|
58
61
|
queryClient.removeQueries({
|
|
59
62
|
queryKey: [...SUBSCRIPTION_QUERY_KEYS.packages, prevUserId],
|
|
60
63
|
});
|
|
61
64
|
} else {
|
|
65
|
+
queryClient.cancelQueries({
|
|
66
|
+
queryKey: [...SUBSCRIPTION_QUERY_KEYS.packages, "anonymous"],
|
|
67
|
+
});
|
|
62
68
|
queryClient.removeQueries({
|
|
63
69
|
queryKey: [...SUBSCRIPTION_QUERY_KEYS.packages, "anonymous"],
|
|
64
70
|
});
|
|
@@ -87,9 +87,20 @@ class SubscriptionManagerImpl {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
async purchasePackage(pkg: PurchasesPackage): Promise<boolean> {
|
|
90
|
+
console.log('🔵 [SubscriptionManager] purchasePackage called', {
|
|
91
|
+
productId: pkg.product.identifier,
|
|
92
|
+
isConfigured: !!this.managerConfig,
|
|
93
|
+
hasPackageHandler: !!this.packageHandler
|
|
94
|
+
});
|
|
95
|
+
|
|
90
96
|
this.ensureConfigured();
|
|
91
97
|
this.ensurePackageHandlerInitialized();
|
|
92
|
-
|
|
98
|
+
|
|
99
|
+
console.log('🚀 [SubscriptionManager] Calling purchasePackageOperation');
|
|
100
|
+
const result = await purchasePackageOperation(pkg, this.managerConfig, this.state, this.packageHandler!);
|
|
101
|
+
console.log('✅ [SubscriptionManager] purchasePackageOperation completed', { result });
|
|
102
|
+
|
|
103
|
+
return result;
|
|
93
104
|
}
|
|
94
105
|
|
|
95
106
|
async restore(): Promise<RestoreResultInfo> {
|
|
@@ -22,9 +22,18 @@ export const purchasePackageOperation = async (
|
|
|
22
22
|
state: SubscriptionInternalState,
|
|
23
23
|
packageHandler: PackageHandler
|
|
24
24
|
): Promise<boolean> => {
|
|
25
|
+
console.log('🔵 [purchasePackageOperation] Starting', {
|
|
26
|
+
productId: pkg.product.identifier
|
|
27
|
+
});
|
|
28
|
+
|
|
25
29
|
ensureConfigured(managerConfig);
|
|
26
30
|
const userId = getCurrentUserIdOrThrow(state);
|
|
27
|
-
|
|
31
|
+
|
|
32
|
+
console.log('🚀 [purchasePackageOperation] Calling packageHandler.purchase', { userId });
|
|
33
|
+
const result = await packageHandler.purchase(pkg, userId);
|
|
34
|
+
console.log('✅ [purchasePackageOperation] Completed', { result });
|
|
35
|
+
|
|
36
|
+
return result;
|
|
28
37
|
};
|
|
29
38
|
|
|
30
39
|
export const restoreOperation = async (
|
|
@@ -22,8 +22,8 @@ export function ensureConfigured(config: SubscriptionManagerConfig | null): void
|
|
|
22
22
|
*/
|
|
23
23
|
export function getCurrentUserIdOrThrow(state: SubscriptionInternalState): string {
|
|
24
24
|
const userId = state.initCache.getCurrentUserId();
|
|
25
|
-
if (
|
|
26
|
-
throw new Error("
|
|
25
|
+
if (userId === null || userId === undefined) {
|
|
26
|
+
throw new Error("SubscriptionManager not initialized - no current user ID available");
|
|
27
27
|
}
|
|
28
28
|
return userId;
|
|
29
29
|
}
|
|
@@ -55,22 +55,30 @@ export class CustomerInfoListenerManager {
|
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
const capturedUserId = this.state.currentUserId;
|
|
59
|
+
if (!capturedUserId) {
|
|
59
60
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
60
61
|
console.log("[CustomerInfoListener] No userId - skipping");
|
|
61
62
|
}
|
|
62
63
|
return;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
const newRenewalState = await processCustomerInfo(
|
|
66
67
|
customerInfo,
|
|
67
|
-
|
|
68
|
+
capturedUserId,
|
|
68
69
|
this.state.renewalState,
|
|
69
70
|
config
|
|
70
71
|
);
|
|
71
72
|
|
|
72
|
-
if (
|
|
73
|
-
|
|
73
|
+
if (this.state.currentUserId === capturedUserId) {
|
|
74
|
+
this.state.renewalState = newRenewalState;
|
|
75
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
76
|
+
console.log("[CustomerInfoListener] processCustomerInfo completed");
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
80
|
+
console.log("[CustomerInfoListener] User changed during processing - discarding result");
|
|
81
|
+
}
|
|
74
82
|
}
|
|
75
83
|
};
|
|
76
84
|
|
|
@@ -16,22 +16,38 @@ export async function handlePurchase(
|
|
|
16
16
|
pkg: PurchasesPackage,
|
|
17
17
|
userId: string
|
|
18
18
|
): Promise<PurchaseResult> {
|
|
19
|
+
console.log('🔵 [PurchaseHandler] handlePurchase called', {
|
|
20
|
+
productId: pkg.product.identifier,
|
|
21
|
+
userId,
|
|
22
|
+
isInitialized: deps.isInitialized()
|
|
23
|
+
});
|
|
24
|
+
|
|
19
25
|
validatePurchaseReady(deps.isInitialized());
|
|
20
26
|
|
|
21
27
|
const consumableIds = deps.config.consumableProductIdentifiers || [];
|
|
22
28
|
const isConsumable = isConsumableProduct(pkg, consumableIds);
|
|
23
29
|
|
|
30
|
+
console.log('📦 [PurchaseHandler] Product type', { isConsumable });
|
|
31
|
+
|
|
24
32
|
try {
|
|
25
|
-
|
|
33
|
+
console.log('🚀 [PurchaseHandler] Calling executePurchase');
|
|
34
|
+
const result = await executePurchase(deps.config, userId, pkg, isConsumable);
|
|
35
|
+
console.log('✅ [PurchaseHandler] executePurchase completed', { success: result.success });
|
|
36
|
+
return result;
|
|
26
37
|
} catch (error) {
|
|
38
|
+
console.error('❌ [PurchaseHandler] Purchase failed', { error });
|
|
39
|
+
|
|
27
40
|
if (isUserCancelledError(error)) {
|
|
41
|
+
console.log('⚠️ [PurchaseHandler] User cancelled');
|
|
28
42
|
return { success: false, isPremium: false, productId: pkg.product.identifier };
|
|
29
43
|
}
|
|
30
44
|
|
|
31
45
|
if (isAlreadyPurchasedError(error)) {
|
|
46
|
+
console.log('⚠️ [PurchaseHandler] Already purchased');
|
|
32
47
|
return await handleAlreadyPurchasedError(deps, userId, pkg, error);
|
|
33
48
|
}
|
|
34
49
|
|
|
50
|
+
console.error('❌ [PurchaseHandler] Unhandled error');
|
|
35
51
|
return handlePurchaseError(error, pkg, userId);
|
|
36
52
|
}
|
|
37
53
|
}
|
|
@@ -60,7 +60,16 @@ export class RevenueCatService implements IRevenueCatService {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
async purchasePackage(pkg: PurchasesPackage, userId: string): Promise<PurchaseResult> {
|
|
63
|
-
|
|
63
|
+
console.log('🔵 [RevenueCatService] purchasePackage called', {
|
|
64
|
+
productId: pkg.product.identifier,
|
|
65
|
+
userId,
|
|
66
|
+
isInitialized: this.isInitialized()
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const result = await handlePurchase(this.getSDKParams(), pkg, userId);
|
|
70
|
+
console.log('✅ [RevenueCatService] purchasePackage completed', { success: result.success });
|
|
71
|
+
|
|
72
|
+
return result;
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
async restorePurchases(userId: string): Promise<RestoreResult> {
|
|
@@ -10,9 +10,14 @@ async function executeConsumablePurchase(
|
|
|
10
10
|
productId: string,
|
|
11
11
|
customerInfo: CustomerInfo
|
|
12
12
|
): Promise<PurchaseResult> {
|
|
13
|
-
const
|
|
13
|
+
const savedPurchase = getSavedPurchase();
|
|
14
|
+
const source = savedPurchase?.source;
|
|
15
|
+
if (savedPurchase) {
|
|
16
|
+
clearSavedPurchase();
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
await notifyPurchaseCompleted(config, userId, productId, customerInfo, source);
|
|
15
|
-
|
|
20
|
+
|
|
16
21
|
return {
|
|
17
22
|
success: true,
|
|
18
23
|
isPremium: false,
|
|
@@ -32,7 +37,11 @@ async function executeSubscriptionPurchase(
|
|
|
32
37
|
entitlementIdentifier: string
|
|
33
38
|
): Promise<PurchaseResult> {
|
|
34
39
|
const isPremium = !!customerInfo.entitlements.active[entitlementIdentifier];
|
|
35
|
-
const
|
|
40
|
+
const savedPurchase = getSavedPurchase();
|
|
41
|
+
const source = savedPurchase?.source;
|
|
42
|
+
if (savedPurchase) {
|
|
43
|
+
clearSavedPurchase();
|
|
44
|
+
}
|
|
36
45
|
|
|
37
46
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
38
47
|
console.log("[PurchaseExecutor] executeSubscriptionPurchase:", {
|
|
@@ -52,7 +61,6 @@ async function executeSubscriptionPurchase(
|
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
await notifyPurchaseCompleted(config, userId, productId, customerInfo, source);
|
|
55
|
-
clearSavedPurchase();
|
|
56
64
|
|
|
57
65
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
58
66
|
console.log("[PurchaseExecutor] Purchase flow completed successfully");
|
|
@@ -72,13 +80,24 @@ export async function executePurchase(
|
|
|
72
80
|
pkg: PurchasesPackage,
|
|
73
81
|
isConsumable: boolean
|
|
74
82
|
): Promise<PurchaseResult> {
|
|
83
|
+
console.log('🔵 [PurchaseExecutor] executePurchase called', {
|
|
84
|
+
productId: pkg.product.identifier,
|
|
85
|
+
userId,
|
|
86
|
+
isConsumable
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
console.log('🚀 [PurchaseExecutor] Calling Purchases.purchasePackage (RevenueCat SDK)');
|
|
75
90
|
const { customerInfo } = await Purchases.purchasePackage(pkg);
|
|
91
|
+
console.log('✅ [PurchaseExecutor] Purchases.purchasePackage completed');
|
|
92
|
+
|
|
76
93
|
const productId = pkg.product.identifier;
|
|
77
94
|
|
|
78
95
|
if (isConsumable) {
|
|
96
|
+
console.log('💰 [PurchaseExecutor] Processing as consumable purchase');
|
|
79
97
|
return executeConsumablePurchase(config, userId, productId, customerInfo);
|
|
80
98
|
}
|
|
81
99
|
|
|
100
|
+
console.log('📅 [PurchaseExecutor] Processing as subscription purchase');
|
|
82
101
|
return executeSubscriptionPurchase(
|
|
83
102
|
config,
|
|
84
103
|
userId,
|
|
@@ -48,10 +48,8 @@ export class InitializationCache {
|
|
|
48
48
|
this.initPromise = promise;
|
|
49
49
|
this.promiseUserId = userId;
|
|
50
50
|
|
|
51
|
-
// Capture userId to prevent stale reference after catch clears promiseUserId
|
|
52
51
|
const targetUserId = userId;
|
|
53
52
|
|
|
54
|
-
// Chain to mark completion and set currentUserId only on success
|
|
55
53
|
promise
|
|
56
54
|
.then((result) => {
|
|
57
55
|
if (result && this.promiseUserId === targetUserId) {
|
|
@@ -61,7 +59,6 @@ export class InitializationCache {
|
|
|
61
59
|
return result;
|
|
62
60
|
})
|
|
63
61
|
.catch((error) => {
|
|
64
|
-
// On failure, clear the promise so retry is possible
|
|
65
62
|
if (this.promiseUserId === targetUserId) {
|
|
66
63
|
this.initPromise = null;
|
|
67
64
|
this.promiseUserId = null;
|
|
@@ -69,12 +66,10 @@ export class InitializationCache {
|
|
|
69
66
|
}
|
|
70
67
|
this.promiseCompleted = true;
|
|
71
68
|
console.error('[InitializationCache] Initialization failed', { userId: targetUserId, error });
|
|
72
|
-
|
|
73
|
-
throw error;
|
|
69
|
+
return false;
|
|
74
70
|
})
|
|
75
71
|
.finally(() => {
|
|
76
|
-
|
|
77
|
-
if (this.promiseUserId === targetUserId) {
|
|
72
|
+
if (this.initializationInProgress) {
|
|
78
73
|
this.initializationInProgress = false;
|
|
79
74
|
}
|
|
80
75
|
});
|
|
@@ -14,7 +14,7 @@ export async function syncPremiumStatus(
|
|
|
14
14
|
config: RevenueCatConfig,
|
|
15
15
|
userId: string,
|
|
16
16
|
customerInfo: CustomerInfo
|
|
17
|
-
): Promise<
|
|
17
|
+
): Promise<{ success: boolean; error?: Error }> {
|
|
18
18
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
19
19
|
console.log("[PremiumStatusSyncer] syncPremiumStatus called:", {
|
|
20
20
|
userId,
|
|
@@ -28,7 +28,7 @@ export async function syncPremiumStatus(
|
|
|
28
28
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
29
29
|
console.log("[PremiumStatusSyncer] No onPremiumStatusChanged callback - skipping");
|
|
30
30
|
}
|
|
31
|
-
return;
|
|
31
|
+
return { success: true };
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const premiumEntitlement = getPremiumEntitlement(
|
|
@@ -68,6 +68,7 @@ export async function syncPremiumStatus(
|
|
|
68
68
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
69
69
|
console.log("[PremiumStatusSyncer] onPremiumStatusChanged completed successfully");
|
|
70
70
|
}
|
|
71
|
+
return { success: true };
|
|
71
72
|
} catch (error) {
|
|
72
73
|
console.error('[PremiumStatusSyncer] Premium status change callback failed', {
|
|
73
74
|
userId,
|
|
@@ -75,7 +76,10 @@ export async function syncPremiumStatus(
|
|
|
75
76
|
productId: premiumEntitlement?.productIdentifier,
|
|
76
77
|
error
|
|
77
78
|
});
|
|
78
|
-
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
82
|
+
};
|
|
79
83
|
}
|
|
80
84
|
}
|
|
81
85
|
|
|
@@ -42,8 +42,8 @@ export function detectRenewal(
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
const newExpiration = new Date(newExpirationDate);
|
|
46
|
-
const previousExpiration = new Date(state.previousExpirationDate);
|
|
45
|
+
const newExpiration = new Date(newExpirationDate).getTime();
|
|
46
|
+
const previousExpiration = new Date(state.previousExpirationDate).getTime();
|
|
47
47
|
const productChanged = productId !== state.previousProductId;
|
|
48
48
|
const expirationExtended = newExpiration > previousExpiration;
|
|
49
49
|
|
|
@@ -25,12 +25,12 @@ export interface PurchaseLoadingActions {
|
|
|
25
25
|
|
|
26
26
|
export type PurchaseLoadingStore = PurchaseLoadingState & PurchaseLoadingActions;
|
|
27
27
|
|
|
28
|
-
const
|
|
28
|
+
const createInitialState = (): PurchaseLoadingState => ({
|
|
29
29
|
activePurchases: new Map(),
|
|
30
|
-
};
|
|
30
|
+
});
|
|
31
31
|
|
|
32
32
|
export const usePurchaseLoadingStore = create<PurchaseLoadingStore>((set, get) => ({
|
|
33
|
-
...
|
|
33
|
+
...createInitialState(),
|
|
34
34
|
|
|
35
35
|
startPurchase: (productId, source) => {
|
|
36
36
|
set((state) => {
|
|
@@ -55,7 +55,7 @@ export const usePurchaseLoadingStore = create<PurchaseLoadingStore>((set, get) =
|
|
|
55
55
|
},
|
|
56
56
|
|
|
57
57
|
reset: () => {
|
|
58
|
-
set(
|
|
58
|
+
set(createInitialState());
|
|
59
59
|
},
|
|
60
60
|
}));
|
|
61
61
|
|
|
@@ -21,7 +21,7 @@ export function createSubscriptionInitModule(config: SubscriptionInitModuleConfi
|
|
|
21
21
|
return true;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
await initializeSubscription({ apiKey, ...subscriptionConfig });
|
|
24
|
+
const cleanup = await initializeSubscription({ apiKey, ...subscriptionConfig });
|
|
25
25
|
return true;
|
|
26
26
|
} catch {
|
|
27
27
|
return false;
|
|
@@ -39,12 +39,17 @@ export class SubscriptionEventBus {
|
|
|
39
39
|
|
|
40
40
|
emit<T>(event: string, data: T): void {
|
|
41
41
|
if (!this.listeners[event]) return;
|
|
42
|
+
|
|
42
43
|
this.listeners[event].forEach(callback => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
Promise.resolve().then(() => {
|
|
45
|
+
try {
|
|
46
|
+
callback(data);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('[SubscriptionEventBus] Listener error for event:', event, { error });
|
|
49
|
+
}
|
|
50
|
+
}).catch(error => {
|
|
51
|
+
console.error('[SubscriptionEventBus] Async listener error for event:', event, { error });
|
|
52
|
+
});
|
|
48
53
|
});
|
|
49
54
|
}
|
|
50
55
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Shared hook for handling service calls with loading, error, and success states
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useState, useCallback } from "react";
|
|
6
|
+
import { useState, useCallback, useRef, useEffect } from "react";
|
|
7
7
|
|
|
8
8
|
export interface ServiceCallState<T> {
|
|
9
9
|
data: T | null;
|
|
@@ -36,21 +36,31 @@ export function useServiceCall<T>(
|
|
|
36
36
|
error: null,
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
+
const onSuccessRef = useRef(onSuccess);
|
|
40
|
+
const onErrorRef = useRef(onError);
|
|
41
|
+
const onCompleteRef = useRef(onComplete);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
onSuccessRef.current = onSuccess;
|
|
45
|
+
onErrorRef.current = onError;
|
|
46
|
+
onCompleteRef.current = onComplete;
|
|
47
|
+
});
|
|
48
|
+
|
|
39
49
|
const execute = useCallback(async () => {
|
|
40
50
|
setState({ data: null, isLoading: true, error: null });
|
|
41
51
|
|
|
42
52
|
try {
|
|
43
53
|
const data = await serviceFn();
|
|
44
54
|
setState({ data, isLoading: false, error: null });
|
|
45
|
-
|
|
55
|
+
onSuccessRef.current?.(data);
|
|
46
56
|
} catch (error) {
|
|
47
57
|
const errorObj = error instanceof Error ? error : new Error("Service call failed");
|
|
48
58
|
setState({ data: null, isLoading: false, error: errorObj });
|
|
49
|
-
|
|
59
|
+
onErrorRef.current?.(errorObj);
|
|
50
60
|
} finally {
|
|
51
|
-
|
|
61
|
+
onCompleteRef.current?.();
|
|
52
62
|
}
|
|
53
|
-
}, [serviceFn
|
|
63
|
+
}, [serviceFn]);
|
|
54
64
|
|
|
55
65
|
const reset = useCallback(() => {
|
|
56
66
|
setState({ data: null, isLoading: false, error: null });
|