@umituz/react-native-subscription 2.43.7 → 2.43.8
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/presentation/deduct-credit/useDeductCredit.ts +15 -24
- package/src/domains/paywall/hooks/usePaywallOrchestrator.ts +7 -24
- package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +2 -9
- package/src/index.ts +20 -22
- package/src/domains/credits/presentation/creditsQueryKeys.ts +0 -5
- package/src/domains/credits/presentation/deduct-credit/mutationConfig.ts +0 -78
- package/src/domains/subscription/presentation/usePremium.ts +0 -33
- package/src/domains/subscription/presentation/usePremium.types.ts +0 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.43.
|
|
3
|
+
"version": "2.43.8",
|
|
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",
|
|
@@ -1,39 +1,32 @@
|
|
|
1
|
-
import { useCallback,
|
|
2
|
-
import { useMutation, useQueryClient } from "@umituz/react-native-design-system/tanstack";
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
3
2
|
import { getCreditsRepository } from "../../infrastructure/CreditsRepositoryManager";
|
|
4
3
|
import type { UseDeductCreditParams, UseDeductCreditResult } from "./types";
|
|
5
|
-
import type { DeductCreditsResult } from "../../core/Credits";
|
|
6
|
-
import { createDeductCreditMutationConfig, type MutationContext } from "./mutationConfig";
|
|
7
4
|
|
|
8
5
|
export const useDeductCredit = ({
|
|
9
6
|
userId,
|
|
10
7
|
onCreditsExhausted,
|
|
11
8
|
}: UseDeductCreditParams): UseDeductCreditResult => {
|
|
12
9
|
const repository = getCreditsRepository();
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const mutation = useMutation<DeductCreditsResult, Error, number, MutationContext>(
|
|
16
|
-
createDeductCreditMutationConfig(userId, repository, queryClient)
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
// Use ref for stable reference to mutateAsync — avoids re-creating callbacks every render
|
|
20
|
-
const mutateAsyncRef = useRef(mutation.mutateAsync);
|
|
21
|
-
mutateAsyncRef.current = mutation.mutateAsync;
|
|
10
|
+
const [isDeducting, setIsDeducting] = useState(false);
|
|
22
11
|
|
|
23
12
|
const deductCredit = useCallback(async (cost: number = 1): Promise<boolean> => {
|
|
24
|
-
if (
|
|
13
|
+
if (!userId) return false;
|
|
14
|
+
|
|
15
|
+
setIsDeducting(true);
|
|
25
16
|
try {
|
|
26
|
-
const res = await
|
|
27
|
-
if (__DEV__) console.log('[useDeductCredit]
|
|
17
|
+
const res = await repository.deductCredit(userId, cost);
|
|
18
|
+
if (__DEV__) console.log('[useDeductCredit] deduction result:', JSON.stringify(res));
|
|
19
|
+
|
|
28
20
|
if (!res.success) {
|
|
29
21
|
if (__DEV__) console.log('[useDeductCredit] deduction FAILED:', res.error?.code, res.error?.message);
|
|
30
|
-
|
|
22
|
+
|
|
31
23
|
if (res.error?.code === "CREDITS_EXHAUSTED" || res.error?.code === "DEDUCT_ERR" || res.error?.code === "NO_CREDITS") {
|
|
32
24
|
if (__DEV__) console.log('[useDeductCredit] Credits exhausted, calling onCreditsExhausted callback');
|
|
33
25
|
onCreditsExhausted?.();
|
|
34
26
|
}
|
|
35
27
|
return false;
|
|
36
28
|
}
|
|
29
|
+
|
|
37
30
|
if (__DEV__) console.log('[useDeductCredit] deduction SUCCESS, remaining:', res.remainingCredits);
|
|
38
31
|
return true;
|
|
39
32
|
} catch (error) {
|
|
@@ -43,8 +36,10 @@ export const useDeductCredit = ({
|
|
|
43
36
|
error: error instanceof Error ? error.message : String(error)
|
|
44
37
|
});
|
|
45
38
|
return false;
|
|
39
|
+
} finally {
|
|
40
|
+
setIsDeducting(false);
|
|
46
41
|
}
|
|
47
|
-
}, [
|
|
42
|
+
}, [userId, repository, onCreditsExhausted]);
|
|
48
43
|
|
|
49
44
|
const checkCredits = useCallback(async (cost: number = 1): Promise<boolean> => {
|
|
50
45
|
if (!userId) return false;
|
|
@@ -55,11 +50,7 @@ export const useDeductCredit = ({
|
|
|
55
50
|
if (!userId) return false;
|
|
56
51
|
try {
|
|
57
52
|
const result = await repository.refundCredit(userId, amount);
|
|
58
|
-
|
|
59
|
-
// Real-time sync (onSnapshot) handles automatic update
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
return false;
|
|
53
|
+
return result.success;
|
|
63
54
|
} catch (error) {
|
|
64
55
|
if (__DEV__) {
|
|
65
56
|
console.error('[useDeductCredit] Unexpected error during credit refund', {
|
|
@@ -76,6 +67,6 @@ export const useDeductCredit = ({
|
|
|
76
67
|
checkCredits,
|
|
77
68
|
deductCredit,
|
|
78
69
|
refundCredits,
|
|
79
|
-
isDeducting
|
|
70
|
+
isDeducting
|
|
80
71
|
};
|
|
81
72
|
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
2
|
import type { NavigationProp } from "@react-navigation/native";
|
|
3
3
|
import type { ImageSourcePropType } from "react-native";
|
|
4
|
-
import {
|
|
4
|
+
import { usePremiumStatus } from "../../subscription/presentation/usePremiumStatus";
|
|
5
|
+
import { usePremiumPackages } from "../../subscription/presentation/usePremiumPackages";
|
|
6
|
+
import { usePremiumActions } from "../../subscription/presentation/usePremiumActions";
|
|
5
7
|
import { useSubscriptionFlowStore } from "../../subscription/presentation/useSubscriptionFlow";
|
|
6
8
|
import { usePaywallVisibility } from "../../subscription/presentation/usePaywallVisibility";
|
|
7
9
|
import { PaywallTranslations, PaywallLegalUrls, SubscriptionFeature } from "../entities/types";
|
|
@@ -18,11 +20,6 @@ export interface PaywallOrchestratorOptions {
|
|
|
18
20
|
creditsLabel?: string;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
/**
|
|
22
|
-
* High-level orchestrator for Paywall navigation.
|
|
23
|
-
* Handles automatic triggers (post-onboarding) and manual triggers (showPaywall state).
|
|
24
|
-
* Centralizes handlers for success, close, and feedback triggers.
|
|
25
|
-
*/
|
|
26
23
|
export function usePaywallOrchestrator({
|
|
27
24
|
navigation,
|
|
28
25
|
translations,
|
|
@@ -34,17 +31,10 @@ export function usePaywallOrchestrator({
|
|
|
34
31
|
bestValueIdentifier = "yearly",
|
|
35
32
|
creditsLabel,
|
|
36
33
|
}: PaywallOrchestratorOptions) {
|
|
37
|
-
|
|
38
|
-
const {
|
|
39
|
-
|
|
40
|
-
packages,
|
|
41
|
-
credits,
|
|
42
|
-
isSyncing,
|
|
43
|
-
purchasePackage,
|
|
44
|
-
restorePurchase,
|
|
45
|
-
} = usePremium();
|
|
34
|
+
const { isPremium, isSyncing, credits } = usePremiumStatus();
|
|
35
|
+
const { packages } = usePremiumPackages();
|
|
36
|
+
const { purchasePackage, restorePurchase } = usePremiumActions();
|
|
46
37
|
|
|
47
|
-
// Selectors for stable references and fine-grained updates
|
|
48
38
|
const isOnboardingComplete = useSubscriptionFlowStore((state) => state.isOnboardingComplete);
|
|
49
39
|
const showPostOnboardingPaywall = useSubscriptionFlowStore((state) => state.showPostOnboardingPaywall);
|
|
50
40
|
const paywallShown = useSubscriptionFlowStore((state) => state.paywallShown);
|
|
@@ -74,7 +64,6 @@ export function usePaywallOrchestrator({
|
|
|
74
64
|
const shouldShowManual = showPaywall && !isPremium && !isAuthModalOpen;
|
|
75
65
|
|
|
76
66
|
if (shouldShowPostOnboarding || shouldShowManual) {
|
|
77
|
-
// Guard against double navigation in same render cycle
|
|
78
67
|
if (hasNavigatedRef.current) return;
|
|
79
68
|
hasNavigatedRef.current = true;
|
|
80
69
|
|
|
@@ -84,7 +73,6 @@ export function usePaywallOrchestrator({
|
|
|
84
73
|
});
|
|
85
74
|
|
|
86
75
|
navigation.navigate("PaywallScreen", {
|
|
87
|
-
// UI Props
|
|
88
76
|
translations,
|
|
89
77
|
legalUrls,
|
|
90
78
|
features,
|
|
@@ -92,14 +80,10 @@ export function usePaywallOrchestrator({
|
|
|
92
80
|
creditsLabel,
|
|
93
81
|
heroImage,
|
|
94
82
|
source: shouldShowPostOnboarding ? "onboarding" : "manual",
|
|
95
|
-
|
|
96
|
-
// Data Props
|
|
97
83
|
packages,
|
|
98
84
|
isPremium,
|
|
99
85
|
credits,
|
|
100
86
|
isSyncing,
|
|
101
|
-
|
|
102
|
-
// Action Props
|
|
103
87
|
onPurchase: purchasePackage,
|
|
104
88
|
onRestore: restorePurchase,
|
|
105
89
|
onClose: handleClose,
|
|
@@ -108,12 +92,11 @@ export function usePaywallOrchestrator({
|
|
|
108
92
|
if (shouldShowPostOnboarding) {
|
|
109
93
|
markPaywallShown();
|
|
110
94
|
}
|
|
111
|
-
|
|
95
|
+
|
|
112
96
|
if (showPaywall) {
|
|
113
97
|
closePaywall();
|
|
114
98
|
}
|
|
115
99
|
} else {
|
|
116
|
-
// Reset navigation flag if conditions no longer met
|
|
117
100
|
hasNavigatedRef.current = false;
|
|
118
101
|
}
|
|
119
102
|
}, [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
3
|
-
import {
|
|
3
|
+
import { usePremiumActions } from "./usePremiumActions";
|
|
4
4
|
import type { PurchaseSource } from "../core/SubscriptionConstants";
|
|
5
5
|
import { authPurchaseStateManager } from "../infrastructure/utils/authPurchaseState";
|
|
6
6
|
import { requireAuthentication } from "./utils/authCheckUtils";
|
|
@@ -27,14 +27,10 @@ interface UseAuthAwarePurchaseResult {
|
|
|
27
27
|
executeSavedPurchase: () => Promise<boolean>;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
* Hook for purchase operations that handle authentication.
|
|
32
|
-
* Automatically saves pending purchases and shows auth modal when needed.
|
|
33
|
-
*/
|
|
34
30
|
export const useAuthAwarePurchase = (
|
|
35
31
|
params?: UseAuthAwarePurchaseParams
|
|
36
32
|
): UseAuthAwarePurchaseResult => {
|
|
37
|
-
const { purchasePackage, restorePurchase } =
|
|
33
|
+
const { purchasePackage, restorePurchase } = usePremiumActions();
|
|
38
34
|
const isExecutingSavedRef = useRef(false);
|
|
39
35
|
|
|
40
36
|
const executeSavedPurchase = useCallback(async (): Promise<boolean> => {
|
|
@@ -55,7 +51,6 @@ export const useAuthAwarePurchase = (
|
|
|
55
51
|
}
|
|
56
52
|
}, [purchasePackage]);
|
|
57
53
|
|
|
58
|
-
// Auto-execute saved purchase when user authenticates
|
|
59
54
|
useEffect(() => {
|
|
60
55
|
const authProvider = authPurchaseStateManager.getProvider();
|
|
61
56
|
const hasUser = authProvider && authProvider.hasFirebaseUser();
|
|
@@ -74,12 +69,10 @@ export const useAuthAwarePurchase = (
|
|
|
74
69
|
const authProvider = authPurchaseStateManager.getProvider();
|
|
75
70
|
|
|
76
71
|
if (!requireAuthentication(authProvider)) {
|
|
77
|
-
// User not authenticated, purchase saved and auth modal shown
|
|
78
72
|
authPurchaseStateManager.savePurchase(pkg, source || params?.source || "settings");
|
|
79
73
|
return false;
|
|
80
74
|
}
|
|
81
75
|
|
|
82
|
-
// User authenticated, proceed with purchase
|
|
83
76
|
const result = await purchasePackage(pkg);
|
|
84
77
|
return result;
|
|
85
78
|
},
|
package/src/index.ts
CHANGED
|
@@ -10,12 +10,6 @@ export {
|
|
|
10
10
|
ANONYMOUS_CACHE_KEY,
|
|
11
11
|
} from "./domains/subscription/core/SubscriptionConstants";
|
|
12
12
|
|
|
13
|
-
// Domain Events
|
|
14
|
-
export { SUBSCRIPTION_EVENTS } from "./domains/subscription/core/events/SubscriptionEvents";
|
|
15
|
-
export type { SubscriptionEventType, SyncStatusChangedEvent } from "./domains/subscription/core/events/SubscriptionEvents";
|
|
16
|
-
export type { PurchaseCompletedEvent, RenewalDetectedEvent, PremiumStatusChangedEvent } from "./domains/subscription/core/SubscriptionEvents";
|
|
17
|
-
export { FLOW_EVENTS } from "./domains/subscription/core/events/FlowEvents";
|
|
18
|
-
export type { FlowEventType, OnboardingCompletedEvent, PaywallShownEvent, PaywallClosedEvent } from "./domains/subscription/core/events/FlowEvents";
|
|
19
13
|
export type {
|
|
20
14
|
UserTierType,
|
|
21
15
|
SubscriptionStatusType,
|
|
@@ -25,9 +19,7 @@ export type {
|
|
|
25
19
|
PurchaseSource,
|
|
26
20
|
PurchaseType,
|
|
27
21
|
} from "./domains/subscription/core/SubscriptionConstants";
|
|
28
|
-
|
|
29
|
-
export type { PremiumStatus as PremiumStatusMetadata } from "./domains/subscription/core/types/PremiumStatus";
|
|
30
|
-
export type { CreditInfo } from "./domains/subscription/core/types/CreditInfo";
|
|
22
|
+
|
|
31
23
|
export {
|
|
32
24
|
createDefaultSubscriptionStatus,
|
|
33
25
|
isSubscriptionValid,
|
|
@@ -35,6 +27,17 @@ export {
|
|
|
35
27
|
} from "./domains/subscription/core/SubscriptionStatus";
|
|
36
28
|
export type { SubscriptionStatus, StatusResolverInput } from "./domains/subscription/core/SubscriptionStatus";
|
|
37
29
|
|
|
30
|
+
// Domain Events
|
|
31
|
+
export { SUBSCRIPTION_EVENTS } from "./domains/subscription/core/events/SubscriptionEvents";
|
|
32
|
+
export type { SubscriptionEventType, SyncStatusChangedEvent } from "./domains/subscription/core/events/SubscriptionEvents";
|
|
33
|
+
export type { PurchaseCompletedEvent, RenewalDetectedEvent, PremiumStatusChangedEvent } from "./domains/subscription/core/SubscriptionEvents";
|
|
34
|
+
export { FLOW_EVENTS } from "./domains/subscription/core/events/FlowEvents";
|
|
35
|
+
export type { FlowEventType, OnboardingCompletedEvent, PaywallShownEvent, PaywallClosedEvent } from "./domains/subscription/core/events/FlowEvents";
|
|
36
|
+
|
|
37
|
+
export type { SubscriptionMetadata } from "./domains/subscription/core/types/SubscriptionMetadata";
|
|
38
|
+
export type { PremiumStatus as PremiumStatusMetadata } from "./domains/subscription/core/types/PremiumStatus";
|
|
39
|
+
export type { CreditInfo } from "./domains/subscription/core/types/CreditInfo";
|
|
40
|
+
|
|
38
41
|
// Application Layer - Ports
|
|
39
42
|
export type { ISubscriptionRepository } from "./shared/application/ports/ISubscriptionRepository";
|
|
40
43
|
export type { IRevenueCatService } from "./shared/application/ports/IRevenueCatService";
|
|
@@ -51,29 +54,29 @@ export {
|
|
|
51
54
|
} from "./shared/utils/Result";
|
|
52
55
|
export type { Result, Success, Failure } from "./shared/utils/Result";
|
|
53
56
|
|
|
54
|
-
// Infrastructure Layer
|
|
57
|
+
// Infrastructure Layer
|
|
55
58
|
export { initializeSubscription } from "./domains/subscription/application/initializer/SubscriptionInitializer";
|
|
56
59
|
export type { SubscriptionInitConfig, CreditPackageConfig } from "./domains/subscription/application/SubscriptionInitializerTypes";
|
|
57
60
|
|
|
58
61
|
export { CreditsRepository } from "./domains/credits/infrastructure/CreditsRepository";
|
|
59
|
-
export {
|
|
60
|
-
configureCreditsRepository,
|
|
61
|
-
getCreditsRepository,
|
|
62
|
-
getCreditsConfig,
|
|
63
|
-
isCreditsRepositoryConfigured
|
|
62
|
+
export {
|
|
63
|
+
configureCreditsRepository,
|
|
64
|
+
getCreditsRepository,
|
|
65
|
+
getCreditsConfig,
|
|
66
|
+
isCreditsRepositoryConfigured
|
|
64
67
|
} from "./domains/credits/infrastructure/CreditsRepositoryManager";
|
|
65
68
|
|
|
69
|
+
export { CreditLimitService, calculateCreditLimit } from "./domains/credits/domain/services/CreditLimitService";
|
|
70
|
+
|
|
66
71
|
// Presentation Layer - Hooks
|
|
67
72
|
export { useAuthAwarePurchase } from "./domains/subscription/presentation/useAuthAwarePurchase";
|
|
68
73
|
export { useCredits } from "./domains/credits/presentation/useCredits";
|
|
69
74
|
export { useDeductCredit } from "./domains/credits/presentation/deduct-credit/useDeductCredit";
|
|
70
75
|
export { useFeatureGate } from "./domains/subscription/presentation/useFeatureGate";
|
|
71
76
|
export { usePaywallVisibility, paywallControl } from "./domains/subscription/presentation/usePaywallVisibility";
|
|
72
|
-
export { usePremium } from "./domains/subscription/presentation/usePremium";
|
|
73
77
|
export { usePremiumStatus } from "./domains/subscription/presentation/usePremiumStatus";
|
|
74
78
|
export { usePremiumPackages } from "./domains/subscription/presentation/usePremiumPackages";
|
|
75
79
|
export { usePremiumActions } from "./domains/subscription/presentation/usePremiumActions";
|
|
76
|
-
export type { UsePremiumResult } from "./domains/subscription/presentation/usePremium.types";
|
|
77
80
|
export type { PremiumStatus } from "./domains/subscription/presentation/usePremiumStatus";
|
|
78
81
|
export type { PremiumPackages } from "./domains/subscription/presentation/usePremiumPackages";
|
|
79
82
|
export type { PremiumActions } from "./domains/subscription/presentation/usePremiumActions";
|
|
@@ -81,7 +84,6 @@ export { useSubscriptionFlowStore } from "./domains/subscription/presentation/us
|
|
|
81
84
|
export type { SubscriptionFlowState, SubscriptionFlowActions, SubscriptionFlowStore } from "./domains/subscription/presentation/useSubscriptionFlow";
|
|
82
85
|
export { useSubscriptionStatus } from "./domains/subscription/presentation/useSubscriptionStatus";
|
|
83
86
|
export type { SubscriptionStatusResult } from "./domains/subscription/presentation/useSubscriptionStatus.types";
|
|
84
|
-
export { useSyncStatusListener } from "./domains/subscription/presentation/useSyncStatusListener";
|
|
85
87
|
export { usePaywallFeedback } from "./presentation/hooks/feedback/usePaywallFeedback";
|
|
86
88
|
export {
|
|
87
89
|
usePaywallFeedbackSubmit,
|
|
@@ -123,7 +125,6 @@ export type {
|
|
|
123
125
|
CreditsResult,
|
|
124
126
|
DeductCreditsResult,
|
|
125
127
|
} from "./domains/credits/core/Credits";
|
|
126
|
-
export { CreditLimitService, calculateCreditLimit } from "./domains/credits/domain/services/CreditLimitService";
|
|
127
128
|
|
|
128
129
|
// Utils
|
|
129
130
|
export {
|
|
@@ -170,9 +171,6 @@ export {
|
|
|
170
171
|
export { getAppVersion, validatePlatform } from "./utils/appUtils";
|
|
171
172
|
export { toDate, toISOString, toTimestamp, getCurrentISOString } from "./shared/utils/dateConverter";
|
|
172
173
|
|
|
173
|
-
// Credits Query Keys
|
|
174
|
-
export { creditsQueryKeys } from "./domains/credits/presentation/creditsQueryKeys";
|
|
175
|
-
|
|
176
174
|
// Paywall Types & Utils
|
|
177
175
|
export type { PaywallTranslations, PaywallLegalUrls, SubscriptionFeature } from "./domains/paywall/entities/types";
|
|
178
176
|
export { createPaywallTranslations, createFeedbackTranslations } from "./domains/paywall/utils/paywallTranslationUtils";
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import type { QueryClient } from "@umituz/react-native-design-system/tanstack";
|
|
2
|
-
import { timezoneService } from "@umituz/react-native-design-system/timezone";
|
|
3
|
-
import type { UserCredits, DeductCreditsResult } from "../../core/Credits";
|
|
4
|
-
import type { CreditsRepository } from "../../infrastructure/CreditsRepository";
|
|
5
|
-
import { creditsQueryKeys } from "../creditsQueryKeys";
|
|
6
|
-
import { calculateRemaining } from "../../../../shared/utils/numberUtils.core";
|
|
7
|
-
|
|
8
|
-
export interface MutationContext {
|
|
9
|
-
previousCredits: UserCredits | null;
|
|
10
|
-
skippedOptimistic: boolean;
|
|
11
|
-
wasInsufficient?: boolean;
|
|
12
|
-
capturedUserId: string | null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function createDeductCreditMutationConfig(
|
|
16
|
-
userId: string | undefined,
|
|
17
|
-
repository: CreditsRepository,
|
|
18
|
-
queryClient: QueryClient
|
|
19
|
-
) {
|
|
20
|
-
return {
|
|
21
|
-
mutationFn: async (cost: number): Promise<DeductCreditsResult> => {
|
|
22
|
-
if (__DEV__) console.log('[deductCreditMutation] mutationFn called', { userId, cost });
|
|
23
|
-
if (!userId) throw new Error("User not authenticated");
|
|
24
|
-
return repository.deductCredit(userId, cost);
|
|
25
|
-
},
|
|
26
|
-
onMutate: async (cost: number): Promise<MutationContext> => {
|
|
27
|
-
if (!userId) {
|
|
28
|
-
return { previousCredits: null, skippedOptimistic: true, capturedUserId: null };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const capturedUserId = userId;
|
|
32
|
-
|
|
33
|
-
await queryClient.cancelQueries({ queryKey: creditsQueryKeys.user(capturedUserId) });
|
|
34
|
-
const previousCredits = queryClient.getQueryData<UserCredits>(
|
|
35
|
-
creditsQueryKeys.user(capturedUserId)
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
if (!previousCredits) {
|
|
39
|
-
return {
|
|
40
|
-
previousCredits: null as UserCredits | null,
|
|
41
|
-
skippedOptimistic: true,
|
|
42
|
-
capturedUserId
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const newCredits = calculateRemaining(previousCredits.credits, cost);
|
|
47
|
-
|
|
48
|
-
queryClient.setQueryData<UserCredits | null>(
|
|
49
|
-
creditsQueryKeys.user(capturedUserId),
|
|
50
|
-
(old) => {
|
|
51
|
-
if (!old) return old;
|
|
52
|
-
return {
|
|
53
|
-
...old,
|
|
54
|
-
credits: newCredits,
|
|
55
|
-
lastUpdatedAt: timezoneService.getNow()
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
previousCredits,
|
|
62
|
-
skippedOptimistic: false,
|
|
63
|
-
wasInsufficient: previousCredits.credits < cost,
|
|
64
|
-
capturedUserId
|
|
65
|
-
};
|
|
66
|
-
},
|
|
67
|
-
onError: (_err: unknown, _cost: number, context: MutationContext | undefined) => {
|
|
68
|
-
if (context?.capturedUserId && context.previousCredits && !context.skippedOptimistic) {
|
|
69
|
-
queryClient.setQueryData(
|
|
70
|
-
creditsQueryKeys.user(context.capturedUserId),
|
|
71
|
-
context.previousCredits
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
// onSuccess removed - real-time sync (onSnapshot) handles automatic updates
|
|
76
|
-
// Optimistic update already applied, real-time listener will confirm actual value
|
|
77
|
-
};
|
|
78
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
2
|
-
import { usePremiumStatus } from './usePremiumStatus';
|
|
3
|
-
import { usePremiumPackages } from './usePremiumPackages';
|
|
4
|
-
import { usePremiumActions } from './usePremiumActions';
|
|
5
|
-
import { UsePremiumResult } from './usePremium.types';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Facade hook that combines status, packages, and actions.
|
|
9
|
-
*
|
|
10
|
-
* Consider using the focused hooks for better performance:
|
|
11
|
-
* - usePremiumStatus() - when you only need premium status
|
|
12
|
-
* - usePremiumPackages() - when you only need package data
|
|
13
|
-
* - usePremiumActions() - when you only need actions
|
|
14
|
-
*
|
|
15
|
-
* This facade re-renders when ANY of the sub-hooks change, whereas focused hooks
|
|
16
|
-
* only re-render when their specific data changes.
|
|
17
|
-
*/
|
|
18
|
-
export const usePremium = (): UsePremiumResult => {
|
|
19
|
-
const status = usePremiumStatus();
|
|
20
|
-
const packages = usePremiumPackages();
|
|
21
|
-
const actions = usePremiumActions();
|
|
22
|
-
|
|
23
|
-
return useMemo(() => ({
|
|
24
|
-
...status,
|
|
25
|
-
...packages,
|
|
26
|
-
...actions,
|
|
27
|
-
isLoading: status.isSyncing || packages.isLoading || actions.isLoading,
|
|
28
|
-
}), [
|
|
29
|
-
status,
|
|
30
|
-
packages,
|
|
31
|
-
actions,
|
|
32
|
-
]);
|
|
33
|
-
};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { PurchasesPackage } from 'react-native-purchases';
|
|
2
|
-
import type { UserCredits } from '../../credits/core/Credits';
|
|
3
|
-
|
|
4
|
-
export interface UsePremiumResult {
|
|
5
|
-
isPremium: boolean;
|
|
6
|
-
isLoading: boolean;
|
|
7
|
-
packages: PurchasesPackage[];
|
|
8
|
-
credits: UserCredits | null;
|
|
9
|
-
showPaywall: boolean;
|
|
10
|
-
isSyncing: boolean;
|
|
11
|
-
purchasePackage: (pkg: PurchasesPackage) => Promise<boolean>;
|
|
12
|
-
restorePurchase: () => Promise<boolean>;
|
|
13
|
-
setShowPaywall: (show: boolean) => void;
|
|
14
|
-
closePaywall: () => void;
|
|
15
|
-
openPaywall: () => void;
|
|
16
|
-
}
|