@umituz/react-native-subscription 2.14.82 → 2.14.84
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/paywall/components/PaywallContainer.tsx +26 -159
- package/src/domains/paywall/components/PaywallFeatures.tsx +25 -0
- package/src/domains/paywall/components/PaywallFooter.tsx +46 -93
- package/src/domains/paywall/components/PaywallModal.tsx +14 -99
- package/src/domains/paywall/hooks/usePaywallActions.ts +63 -0
- package/src/domains/wallet/domain/errors/WalletError.ts +12 -20
- package/src/domains/wallet/domain/errors/WalletErrorMessages.ts +17 -0
- package/src/index.ts +23 -461
- package/src/infrastructure/repositories/CreditsRepository.ts +43 -177
- package/src/infrastructure/services/SubscriptionInitializer.ts +32 -186
- package/src/presentation/components/details/PremiumDetailsCard.styles.ts +54 -0
- package/src/presentation/components/details/PremiumDetailsCard.tsx +2 -49
- package/src/presentation/hooks/index.ts +23 -0
- package/src/presentation/hooks/paywall/usePaywallOperations.ts +2 -2
- package/src/presentation/hooks/useDeductCredit.ts +22 -148
- package/src/presentation/hooks/useInitializeCredits.ts +57 -0
- package/src/presentation/hooks/usePremiumWithCredits.ts +1 -1
- package/src/presentation/hooks/useSubscription.ts +59 -79
- package/src/presentation/hooks/useSubscription.utils.ts +78 -0
- package/src/presentation/hooks/useSubscriptionSettingsConfig.ts +3 -15
- package/src/presentation/hooks/useSubscriptionSettingsConfig.utils.ts +48 -0
- package/src/revenuecat/index.ts +12 -0
- package/src/utils/index.ts +15 -0
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useDeductCredit Hook
|
|
3
|
-
*
|
|
4
3
|
* TanStack Query mutation hook for deducting credits.
|
|
5
|
-
* Generic and reusable - uses module-level repository.
|
|
6
4
|
*/
|
|
7
5
|
|
|
8
6
|
import { useCallback } from "react";
|
|
@@ -17,9 +15,7 @@ export interface UseDeductCreditParams {
|
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
export interface UseDeductCreditResult {
|
|
20
|
-
/** Deduct a single credit */
|
|
21
18
|
deductCredit: (creditType: CreditType) => Promise<boolean>;
|
|
22
|
-
/** Deduct multiple credits (loops internally) */
|
|
23
19
|
deductCredits: (cost: number, creditType?: CreditType) => Promise<boolean>;
|
|
24
20
|
isDeducting: boolean;
|
|
25
21
|
}
|
|
@@ -33,169 +29,47 @@ export const useDeductCredit = ({
|
|
|
33
29
|
|
|
34
30
|
const mutation = useMutation({
|
|
35
31
|
mutationFn: async (creditType: CreditType) => {
|
|
36
|
-
if (!userId)
|
|
37
|
-
throw new Error("User not authenticated");
|
|
38
|
-
}
|
|
32
|
+
if (!userId) throw new Error("User not authenticated");
|
|
39
33
|
return repository.deductCredit(userId, creditType);
|
|
40
34
|
},
|
|
41
35
|
onMutate: async (creditType: CreditType) => {
|
|
42
36
|
if (!userId) return;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
await queryClient.cancelQueries({ queryKey: creditsQueryKeys.user(userId) });
|
|
38
|
+
const previousCredits = queryClient.getQueryData<UserCredits>(creditsQueryKeys.user(userId));
|
|
39
|
+
queryClient.setQueryData<UserCredits | null>(creditsQueryKeys.user(userId), (old) => {
|
|
40
|
+
if (!old) return old;
|
|
41
|
+
const field = creditType === "text" ? "textCredits" : "imageCredits";
|
|
42
|
+
return { ...old, [field]: Math.max(0, old[field] - 1), lastUpdatedAt: new Date() };
|
|
46
43
|
});
|
|
47
|
-
|
|
48
|
-
const previousCredits = queryClient.getQueryData<UserCredits>(
|
|
49
|
-
creditsQueryKeys.user(userId)
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
queryClient.setQueryData<UserCredits | null>(
|
|
53
|
-
creditsQueryKeys.user(userId),
|
|
54
|
-
(old) => {
|
|
55
|
-
if (!old) return old;
|
|
56
|
-
const fieldName =
|
|
57
|
-
creditType === "text" ? "textCredits" : "imageCredits";
|
|
58
|
-
return {
|
|
59
|
-
...old,
|
|
60
|
-
[fieldName]: Math.max(0, old[fieldName] - 1),
|
|
61
|
-
lastUpdatedAt: new Date(),
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
);
|
|
65
|
-
|
|
66
44
|
return { previousCredits };
|
|
67
45
|
},
|
|
68
|
-
onError: (_err,
|
|
46
|
+
onError: (_err, _type, context) => {
|
|
69
47
|
if (userId && context?.previousCredits) {
|
|
70
|
-
queryClient.setQueryData(
|
|
71
|
-
creditsQueryKeys.user(userId),
|
|
72
|
-
context.previousCredits
|
|
73
|
-
);
|
|
48
|
+
queryClient.setQueryData(creditsQueryKeys.user(userId), context.previousCredits);
|
|
74
49
|
}
|
|
75
50
|
},
|
|
76
51
|
onSettled: () => {
|
|
77
|
-
if (userId) {
|
|
78
|
-
queryClient.invalidateQueries({
|
|
79
|
-
queryKey: creditsQueryKeys.user(userId),
|
|
80
|
-
});
|
|
81
|
-
}
|
|
52
|
+
if (userId) queryClient.invalidateQueries({ queryKey: creditsQueryKeys.user(userId) });
|
|
82
53
|
},
|
|
83
54
|
});
|
|
84
55
|
|
|
85
|
-
const deductCredit = useCallback(async (
|
|
56
|
+
const deductCredit = useCallback(async (type: CreditType): Promise<boolean> => {
|
|
86
57
|
try {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (result.error?.code === "CREDITS_EXHAUSTED") {
|
|
91
|
-
onCreditsExhausted?.();
|
|
92
|
-
}
|
|
58
|
+
const res = await mutation.mutateAsync(type);
|
|
59
|
+
if (!res.success) {
|
|
60
|
+
if (res.error?.code === "CREDITS_EXHAUSTED") onCreditsExhausted?.();
|
|
93
61
|
return false;
|
|
94
62
|
}
|
|
95
|
-
|
|
96
63
|
return true;
|
|
97
|
-
} catch {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
64
|
+
} catch { return false; }
|
|
100
65
|
}, [mutation, onCreditsExhausted]);
|
|
101
66
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
for (let i = 0; i < cost; i++) {
|
|
109
|
-
const success = await deductCredit(creditType);
|
|
110
|
-
if (!success) return false;
|
|
111
|
-
}
|
|
112
|
-
return true;
|
|
113
|
-
},
|
|
114
|
-
[deductCredit],
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
deductCredit,
|
|
119
|
-
deductCredits,
|
|
120
|
-
isDeducting: mutation.isPending,
|
|
121
|
-
};
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
export interface UseInitializeCreditsParams {
|
|
125
|
-
userId: string | undefined;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export interface InitializeCreditsOptions {
|
|
129
|
-
purchaseId?: string;
|
|
130
|
-
productId?: string;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export interface UseInitializeCreditsResult {
|
|
134
|
-
initializeCredits: (options?: InitializeCreditsOptions) => Promise<boolean>;
|
|
135
|
-
isInitializing: boolean;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export const useInitializeCredits = ({
|
|
139
|
-
userId,
|
|
140
|
-
}: UseInitializeCreditsParams): UseInitializeCreditsResult => {
|
|
141
|
-
const repository = getCreditsRepository();
|
|
142
|
-
const queryClient = useQueryClient();
|
|
143
|
-
|
|
144
|
-
const mutation = useMutation({
|
|
145
|
-
mutationFn: async (options?: InitializeCreditsOptions) => {
|
|
146
|
-
if (!userId) {
|
|
147
|
-
throw new Error("User not authenticated");
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (__DEV__) {
|
|
151
|
-
console.log("[useInitializeCredits] Initializing credits:", {
|
|
152
|
-
userId,
|
|
153
|
-
purchaseId: options?.purchaseId,
|
|
154
|
-
productId: options?.productId,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return repository.initializeCredits(
|
|
159
|
-
userId,
|
|
160
|
-
options?.purchaseId,
|
|
161
|
-
options?.productId
|
|
162
|
-
);
|
|
163
|
-
},
|
|
164
|
-
onSuccess: (result) => {
|
|
165
|
-
if (userId && result.success && result.data) {
|
|
166
|
-
if (__DEV__) {
|
|
167
|
-
console.log("[useInitializeCredits] Success, updating cache:", result.data);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Set the data immediately for optimistic UI
|
|
171
|
-
queryClient.setQueryData(creditsQueryKeys.user(userId), result.data);
|
|
172
|
-
// Also invalidate to ensure all subscribers get the update
|
|
173
|
-
queryClient.invalidateQueries({
|
|
174
|
-
queryKey: creditsQueryKeys.user(userId),
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
},
|
|
178
|
-
onError: (error) => {
|
|
179
|
-
if (__DEV__) {
|
|
180
|
-
console.error("[useInitializeCredits] Error:", error);
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
const initializeCredits = useCallback(
|
|
186
|
-
async (options?: InitializeCreditsOptions): Promise<boolean> => {
|
|
187
|
-
try {
|
|
188
|
-
const result = await mutation.mutateAsync(options);
|
|
189
|
-
return result.success;
|
|
190
|
-
} catch {
|
|
191
|
-
return false;
|
|
192
|
-
}
|
|
193
|
-
},
|
|
194
|
-
[mutation]
|
|
195
|
-
);
|
|
67
|
+
const deductCredits = useCallback(async (cost: number, type: CreditType = "image"): Promise<boolean> => {
|
|
68
|
+
for (let i = 0; i < cost; i++) {
|
|
69
|
+
if (!(await deductCredit(type))) return false;
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}, [deductCredit]);
|
|
196
73
|
|
|
197
|
-
return {
|
|
198
|
-
initializeCredits,
|
|
199
|
-
isInitializing: mutation.isPending,
|
|
200
|
-
};
|
|
74
|
+
return { deductCredit, deductCredits, isDeducting: mutation.isPending };
|
|
201
75
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useInitializeCredits Hook
|
|
3
|
+
* TanStack Query mutation hook for initializing credits after purchase.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback } from "react";
|
|
7
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
8
|
+
import { getCreditsRepository } from "../../infrastructure/repositories/CreditsRepositoryProvider";
|
|
9
|
+
import { creditsQueryKeys } from "./useCredits";
|
|
10
|
+
|
|
11
|
+
declare const __DEV__: boolean;
|
|
12
|
+
|
|
13
|
+
export interface UseInitializeCreditsParams {
|
|
14
|
+
userId: string | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface InitializeCreditsOptions {
|
|
18
|
+
purchaseId?: string;
|
|
19
|
+
productId?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UseInitializeCreditsResult {
|
|
23
|
+
initializeCredits: (options?: InitializeCreditsOptions) => Promise<boolean>;
|
|
24
|
+
isInitializing: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const useInitializeCredits = ({
|
|
28
|
+
userId,
|
|
29
|
+
}: UseInitializeCreditsParams): UseInitializeCreditsResult => {
|
|
30
|
+
const repository = getCreditsRepository();
|
|
31
|
+
const queryClient = useQueryClient();
|
|
32
|
+
|
|
33
|
+
const mutation = useMutation({
|
|
34
|
+
mutationFn: async (options?: InitializeCreditsOptions) => {
|
|
35
|
+
if (!userId) throw new Error("User not authenticated");
|
|
36
|
+
if (__DEV__) console.log("[useInitializeCredits] Initializing:", { userId, ...options });
|
|
37
|
+
return repository.initializeCredits(userId, options?.purchaseId, options?.productId);
|
|
38
|
+
},
|
|
39
|
+
onSuccess: (result) => {
|
|
40
|
+
if (userId && result.success && result.data) {
|
|
41
|
+
if (__DEV__) console.log("[useInitializeCredits] Success:", result.data);
|
|
42
|
+
queryClient.setQueryData(creditsQueryKeys.user(userId), result.data);
|
|
43
|
+
queryClient.invalidateQueries({ queryKey: creditsQueryKeys.user(userId) });
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
onError: (error) => { if (__DEV__) console.error("[useInitializeCredits] Error:", error); },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const initializeCredits = useCallback(async (opts?: InitializeCreditsOptions): Promise<boolean> => {
|
|
50
|
+
try {
|
|
51
|
+
const res = await mutation.mutateAsync(opts);
|
|
52
|
+
return res.success;
|
|
53
|
+
} catch { return false; }
|
|
54
|
+
}, [mutation]);
|
|
55
|
+
|
|
56
|
+
return { initializeCredits, isInitializing: mutation.isPending };
|
|
57
|
+
};
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { useEffect, useCallback } from "react";
|
|
9
9
|
import { useCredits, type UseCreditsResult } from "./useCredits";
|
|
10
|
-
import { useInitializeCredits } from "./
|
|
10
|
+
import { useInitializeCredits } from "./useInitializeCredits";
|
|
11
11
|
|
|
12
12
|
export interface UsePremiumWithCreditsParams {
|
|
13
13
|
userId: string | undefined;
|
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useState, useCallback } from 'react';
|
|
7
|
-
import { getSubscriptionService } from '../../infrastructure/services/SubscriptionService';
|
|
8
7
|
import type { SubscriptionStatus } from '../../domain/entities/SubscriptionStatus';
|
|
9
8
|
import { isSubscriptionValid } from '../../domain/entities/SubscriptionStatus';
|
|
9
|
+
import {
|
|
10
|
+
checkSubscriptionService,
|
|
11
|
+
validateUserId,
|
|
12
|
+
executeSubscriptionOperation,
|
|
13
|
+
} from './useSubscription.utils';
|
|
10
14
|
|
|
11
15
|
export interface UseSubscriptionResult {
|
|
12
16
|
/** Current subscription status */
|
|
@@ -45,120 +49,96 @@ export function useSubscription(): UseSubscriptionResult {
|
|
|
45
49
|
const [error, setError] = useState<string | null>(null);
|
|
46
50
|
|
|
47
51
|
const loadStatus = useCallback(async (userId: string) => {
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
const validationError = validateUserId(userId);
|
|
53
|
+
if (validationError) {
|
|
54
|
+
setError(validationError);
|
|
50
55
|
return;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
const
|
|
54
|
-
if (!
|
|
55
|
-
setError(
|
|
58
|
+
const serviceCheck = checkSubscriptionService();
|
|
59
|
+
if (!serviceCheck.success) {
|
|
60
|
+
setError(serviceCheck.error || "Service error");
|
|
56
61
|
return;
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
} catch (err) {
|
|
66
|
-
const errorMessage =
|
|
67
|
-
err instanceof Error ? err.message : 'Failed to load subscription status';
|
|
68
|
-
setError(errorMessage);
|
|
69
|
-
} finally {
|
|
70
|
-
setLoading(false);
|
|
71
|
-
}
|
|
64
|
+
await executeSubscriptionOperation(
|
|
65
|
+
() => serviceCheck.service!.getSubscriptionStatus(userId),
|
|
66
|
+
setLoading,
|
|
67
|
+
setError,
|
|
68
|
+
(result) => setStatus(result)
|
|
69
|
+
);
|
|
72
70
|
}, []);
|
|
73
71
|
|
|
74
72
|
const refreshStatus = useCallback(async (userId: string) => {
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
const validationError = validateUserId(userId);
|
|
74
|
+
if (validationError) {
|
|
75
|
+
setError(validationError);
|
|
77
76
|
return;
|
|
78
77
|
}
|
|
79
78
|
|
|
80
|
-
const
|
|
81
|
-
if (!
|
|
82
|
-
setError(
|
|
79
|
+
const serviceCheck = checkSubscriptionService();
|
|
80
|
+
if (!serviceCheck.success) {
|
|
81
|
+
setError(serviceCheck.error || "Service error");
|
|
83
82
|
return;
|
|
84
83
|
}
|
|
85
84
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
} catch (err) {
|
|
93
|
-
const errorMessage =
|
|
94
|
-
err instanceof Error ? err.message : 'Failed to refresh subscription status';
|
|
95
|
-
setError(errorMessage);
|
|
96
|
-
} finally {
|
|
97
|
-
setLoading(false);
|
|
98
|
-
}
|
|
85
|
+
await executeSubscriptionOperation(
|
|
86
|
+
() => serviceCheck.service!.getSubscriptionStatus(userId),
|
|
87
|
+
setLoading,
|
|
88
|
+
setError,
|
|
89
|
+
(result) => setStatus(result)
|
|
90
|
+
);
|
|
99
91
|
}, []);
|
|
100
92
|
|
|
101
93
|
const activateSubscription = useCallback(
|
|
102
94
|
async (userId: string, productId: string, expiresAt: string | null) => {
|
|
103
|
-
|
|
104
|
-
|
|
95
|
+
const validationError = validateUserId(userId);
|
|
96
|
+
if (validationError) {
|
|
97
|
+
setError(validationError);
|
|
105
98
|
return;
|
|
106
99
|
}
|
|
107
100
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
setError('Subscription service is not initialized');
|
|
101
|
+
if (!productId) {
|
|
102
|
+
setError("Product ID is required");
|
|
111
103
|
return;
|
|
112
104
|
}
|
|
113
105
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const updatedStatus = await service.activateSubscription(
|
|
119
|
-
userId,
|
|
120
|
-
productId,
|
|
121
|
-
expiresAt,
|
|
122
|
-
);
|
|
123
|
-
setStatus(updatedStatus);
|
|
124
|
-
} catch (err) {
|
|
125
|
-
const errorMessage =
|
|
126
|
-
err instanceof Error ? err.message : 'Failed to activate subscription';
|
|
127
|
-
setError(errorMessage);
|
|
128
|
-
throw err;
|
|
129
|
-
} finally {
|
|
130
|
-
setLoading(false);
|
|
106
|
+
const serviceCheck = checkSubscriptionService();
|
|
107
|
+
if (!serviceCheck.success) {
|
|
108
|
+
setError(serviceCheck.error || "Service error");
|
|
109
|
+
return;
|
|
131
110
|
}
|
|
111
|
+
|
|
112
|
+
await executeSubscriptionOperation(
|
|
113
|
+
() =>
|
|
114
|
+
serviceCheck.service!.activateSubscription(userId, productId, expiresAt),
|
|
115
|
+
setLoading,
|
|
116
|
+
setError,
|
|
117
|
+
(result) => setStatus(result)
|
|
118
|
+
);
|
|
132
119
|
},
|
|
133
|
-
[]
|
|
120
|
+
[]
|
|
134
121
|
);
|
|
135
122
|
|
|
136
123
|
const deactivateSubscription = useCallback(async (userId: string) => {
|
|
137
|
-
|
|
138
|
-
|
|
124
|
+
const validationError = validateUserId(userId);
|
|
125
|
+
if (validationError) {
|
|
126
|
+
setError(validationError);
|
|
139
127
|
return;
|
|
140
128
|
}
|
|
141
129
|
|
|
142
|
-
const
|
|
143
|
-
if (!
|
|
144
|
-
setError(
|
|
130
|
+
const serviceCheck = checkSubscriptionService();
|
|
131
|
+
if (!serviceCheck.success) {
|
|
132
|
+
setError(serviceCheck.error || "Service error");
|
|
145
133
|
return;
|
|
146
134
|
}
|
|
147
135
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
} catch (err) {
|
|
155
|
-
const errorMessage =
|
|
156
|
-
err instanceof Error ? err.message : 'Failed to deactivate subscription';
|
|
157
|
-
setError(errorMessage);
|
|
158
|
-
throw err;
|
|
159
|
-
} finally {
|
|
160
|
-
setLoading(false);
|
|
161
|
-
}
|
|
136
|
+
await executeSubscriptionOperation(
|
|
137
|
+
() => serviceCheck.service!.deactivateSubscription(userId),
|
|
138
|
+
setLoading,
|
|
139
|
+
setError,
|
|
140
|
+
(result) => setStatus(result)
|
|
141
|
+
);
|
|
162
142
|
}, []);
|
|
163
143
|
|
|
164
144
|
const isPremium = isSubscriptionValid(status);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSubscription Utilities
|
|
3
|
+
* Shared utilities for subscription hook operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type AsyncSubscriptionOperation<T> = () => Promise<T>;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Result of a subscription service initialization check
|
|
10
|
+
*/
|
|
11
|
+
export interface ServiceCheckResult {
|
|
12
|
+
success: boolean;
|
|
13
|
+
service: ReturnType<typeof import("../../infrastructure/services/SubscriptionService").getSubscriptionService> | null;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Checks if subscription service is initialized
|
|
19
|
+
* Returns service instance or error
|
|
20
|
+
*/
|
|
21
|
+
export function checkSubscriptionService(): ServiceCheckResult {
|
|
22
|
+
const { getSubscriptionService } = require("../../infrastructure/services/SubscriptionService");
|
|
23
|
+
const service = getSubscriptionService();
|
|
24
|
+
|
|
25
|
+
if (!service) {
|
|
26
|
+
return {
|
|
27
|
+
success: false,
|
|
28
|
+
service: null,
|
|
29
|
+
error: "Subscription service is not initialized",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { success: true, service, error: undefined };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validates user ID
|
|
38
|
+
*/
|
|
39
|
+
export function validateUserId(userId: string): string | null {
|
|
40
|
+
if (!userId) {
|
|
41
|
+
return "User ID is required";
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Wraps async subscription operations with loading state, error handling, and state updates
|
|
48
|
+
*/
|
|
49
|
+
export async function executeSubscriptionOperation<T>(
|
|
50
|
+
operation: AsyncSubscriptionOperation<T>,
|
|
51
|
+
setLoading: (loading: boolean) => void,
|
|
52
|
+
setError: (error: string | null) => void,
|
|
53
|
+
onSuccess?: (result: T) => void
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
setLoading(true);
|
|
56
|
+
setError(null);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const result = await operation();
|
|
60
|
+
if (onSuccess) {
|
|
61
|
+
onSuccess(result);
|
|
62
|
+
}
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const errorMessage =
|
|
65
|
+
err instanceof Error ? err.message : "Operation failed";
|
|
66
|
+
setError(errorMessage);
|
|
67
|
+
throw err;
|
|
68
|
+
} finally {
|
|
69
|
+
setLoading(false);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Formats error message from unknown error
|
|
75
|
+
*/
|
|
76
|
+
export function formatErrorMessage(err: unknown, fallbackMessage: string): string {
|
|
77
|
+
return err instanceof Error ? err.message : fallbackMessage;
|
|
78
|
+
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
formatDateForLocale,
|
|
16
16
|
calculateDaysRemaining,
|
|
17
17
|
} from "../utils/subscriptionDateUtils";
|
|
18
|
+
import { useCreditsArray, getSubscriptionStatusType } from "./useSubscriptionSettingsConfig.utils";
|
|
18
19
|
import type {
|
|
19
20
|
SubscriptionSettingsConfig,
|
|
20
21
|
SubscriptionStatusType,
|
|
@@ -104,23 +105,10 @@ export const useSubscriptionSettingsConfig = (
|
|
|
104
105
|
);
|
|
105
106
|
|
|
106
107
|
// Status type
|
|
107
|
-
const statusType: SubscriptionStatusType = isPremium
|
|
108
|
+
const statusType: SubscriptionStatusType = getSubscriptionStatusType(isPremium);
|
|
108
109
|
|
|
109
110
|
// Credits array
|
|
110
|
-
const creditsArray =
|
|
111
|
-
if (!credits) return [];
|
|
112
|
-
const total = getCreditLimit
|
|
113
|
-
? getCreditLimit(credits.imageCredits)
|
|
114
|
-
: credits.imageCredits;
|
|
115
|
-
return [
|
|
116
|
-
{
|
|
117
|
-
id: "image",
|
|
118
|
-
label: translations.imageCreditsLabel || "Image Credits",
|
|
119
|
-
current: credits.imageCredits,
|
|
120
|
-
total,
|
|
121
|
-
},
|
|
122
|
-
];
|
|
123
|
-
}, [credits, getCreditLimit, translations.imageCreditsLabel]);
|
|
111
|
+
const creditsArray = useCreditsArray(credits, getCreditLimit, translations);
|
|
124
112
|
|
|
125
113
|
// Build config
|
|
126
114
|
const config = useMemo(
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSubscriptionSettingsConfig Utilities
|
|
3
|
+
* Helper functions for subscription settings config
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
import type { UserCredits } from "../../domain/entities/Credits";
|
|
8
|
+
import type { SubscriptionSettingsTranslations } from "../types/SubscriptionSettingsTypes";
|
|
9
|
+
|
|
10
|
+
export interface CreditsInfo {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
current: number;
|
|
14
|
+
total: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Builds credits array for display
|
|
19
|
+
*/
|
|
20
|
+
export function useCreditsArray(
|
|
21
|
+
credits: UserCredits | null | undefined,
|
|
22
|
+
getCreditLimit: ((credits: number) => number) | undefined,
|
|
23
|
+
translations: SubscriptionSettingsTranslations
|
|
24
|
+
): CreditsInfo[] {
|
|
25
|
+
return useMemo(() => {
|
|
26
|
+
if (!credits) return [];
|
|
27
|
+
const total = getCreditLimit
|
|
28
|
+
? getCreditLimit(credits.imageCredits)
|
|
29
|
+
: credits.imageCredits;
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
id: "image",
|
|
33
|
+
label: translations.imageCreditsLabel || "Image Credits",
|
|
34
|
+
current: credits.imageCredits,
|
|
35
|
+
total,
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
}, [credits, getCreditLimit, translations.imageCreditsLabel]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Calculates subscription status type
|
|
43
|
+
*/
|
|
44
|
+
export function getSubscriptionStatusType(
|
|
45
|
+
isPremium: boolean
|
|
46
|
+
): "active" | "none" {
|
|
47
|
+
return isPremium ? "active" : "none";
|
|
48
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./domain/errors/RevenueCatError";
|
|
2
|
+
export * from "./domain/value-objects/RevenueCatConfig";
|
|
3
|
+
export * from "./domain/types/RevenueCatTypes";
|
|
4
|
+
export * from "./domain/constants/RevenueCatConstants";
|
|
5
|
+
export * from "./application/ports/IRevenueCatService";
|
|
6
|
+
export * from "./infrastructure/services/RevenueCatService";
|
|
7
|
+
export * from "./infrastructure/managers/SubscriptionManager";
|
|
8
|
+
export * from "./infrastructure/handlers/PackageHandler";
|
|
9
|
+
export * from "./presentation/hooks/useRevenueCat";
|
|
10
|
+
export * from "./presentation/hooks/useCustomerInfo";
|
|
11
|
+
export * from "./presentation/hooks/usePaywallFlow";
|
|
12
|
+
export * from "./presentation/hooks/useSubscriptionQueries";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from "./aiCreditHelpers";
|
|
2
|
+
export * from "./authUtils";
|
|
3
|
+
export * from "./creditChecker";
|
|
4
|
+
export * from "./creditMapper";
|
|
5
|
+
export * from "./dateValidationUtils";
|
|
6
|
+
export * from "./packageFilter";
|
|
7
|
+
export * from "./packagePeriodUtils";
|
|
8
|
+
export * from "./packageTypeDetector";
|
|
9
|
+
export * from "./premiumAsyncUtils";
|
|
10
|
+
export * from "./premiumStatusUtils";
|
|
11
|
+
export * from "./priceUtils";
|
|
12
|
+
export * from "./tierUtils";
|
|
13
|
+
export * from "./types";
|
|
14
|
+
export * from "./userTierUtils";
|
|
15
|
+
export * from "./validation";
|