@umituz/react-native-subscription 2.14.83 → 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/wallet/domain/errors/WalletError.ts +12 -20
- package/src/domains/wallet/domain/errors/WalletErrorMessages.ts +17 -0
- package/src/presentation/components/details/PremiumDetailsCard.styles.ts +54 -0
- package/src/presentation/components/details/PremiumDetailsCard.tsx +2 -49
- package/src/presentation/hooks/paywall/usePaywallOperations.ts +2 -2
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.84",
|
|
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",
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Follows the same pattern as SubscriptionError.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { WALLET_ERROR_MESSAGES } from "./WalletErrorMessages";
|
|
9
|
+
|
|
8
10
|
export type WalletErrorCategory =
|
|
9
11
|
| "PAYMENT"
|
|
10
12
|
| "VALIDATION"
|
|
@@ -39,110 +41,100 @@ export abstract class WalletError extends Error {
|
|
|
39
41
|
export class PaymentValidationError extends WalletError {
|
|
40
42
|
readonly code = "PAYMENT_VALIDATION_ERROR";
|
|
41
43
|
readonly category = "PAYMENT" as const;
|
|
42
|
-
readonly userMessage
|
|
44
|
+
readonly userMessage = WALLET_ERROR_MESSAGES.PAYMENT_VALIDATION_FAILED;
|
|
43
45
|
|
|
44
46
|
constructor(message: string, cause?: Error) {
|
|
45
47
|
super(message, cause);
|
|
46
|
-
this.userMessage = "Payment validation failed. Please try again.";
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
export class PaymentProviderError extends WalletError {
|
|
51
52
|
readonly code = "PAYMENT_PROVIDER_ERROR";
|
|
52
53
|
readonly category = "PAYMENT" as const;
|
|
53
|
-
readonly userMessage
|
|
54
|
+
readonly userMessage = WALLET_ERROR_MESSAGES.PAYMENT_PROVIDER_ERROR;
|
|
54
55
|
|
|
55
56
|
constructor(message: string, cause?: Error) {
|
|
56
57
|
super(message, cause);
|
|
57
|
-
this.userMessage = "Payment provider error. Please try again.";
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export class DuplicatePaymentError extends WalletError {
|
|
62
62
|
readonly code = "DUPLICATE_PAYMENT";
|
|
63
63
|
readonly category = "PAYMENT" as const;
|
|
64
|
-
readonly userMessage
|
|
64
|
+
readonly userMessage = WALLET_ERROR_MESSAGES.DUPLICATE_PAYMENT;
|
|
65
65
|
|
|
66
66
|
constructor(message: string) {
|
|
67
67
|
super(message);
|
|
68
|
-
this.userMessage = "This payment has already been processed.";
|
|
69
68
|
}
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
export class UserValidationError extends WalletError {
|
|
73
72
|
readonly code = "USER_VALIDATION_ERROR";
|
|
74
73
|
readonly category = "VALIDATION" as const;
|
|
75
|
-
readonly userMessage
|
|
74
|
+
readonly userMessage = WALLET_ERROR_MESSAGES.USER_VALIDATION_FAILED;
|
|
76
75
|
|
|
77
76
|
constructor(message: string) {
|
|
78
77
|
super(message);
|
|
79
|
-
this.userMessage = "Invalid user information. Please log in again.";
|
|
80
78
|
}
|
|
81
79
|
}
|
|
82
80
|
|
|
83
81
|
export class PackageValidationError extends WalletError {
|
|
84
82
|
readonly code = "PACKAGE_VALIDATION_ERROR";
|
|
85
83
|
readonly category = "VALIDATION" as const;
|
|
86
|
-
readonly userMessage
|
|
84
|
+
readonly userMessage = WALLET_ERROR_MESSAGES.PACKAGE_VALIDATION_FAILED;
|
|
87
85
|
|
|
88
86
|
constructor(message: string) {
|
|
89
87
|
super(message);
|
|
90
|
-
this.userMessage = "Invalid credit package. Please select a valid package.";
|
|
91
88
|
}
|
|
92
89
|
}
|
|
93
90
|
|
|
94
91
|
export class ReceiptValidationError extends WalletError {
|
|
95
92
|
readonly code = "RECEIPT_VALIDATION_ERROR";
|
|
96
93
|
readonly category = "VALIDATION" as const;
|
|
97
|
-
readonly userMessage
|
|
94
|
+
readonly userMessage = WALLET_ERROR_MESSAGES.RECEIPT_VALIDATION_FAILED;
|
|
98
95
|
|
|
99
96
|
constructor(message: string) {
|
|
100
97
|
super(message);
|
|
101
|
-
this.userMessage = "Invalid payment receipt. Please contact support.";
|
|
102
98
|
}
|
|
103
99
|
}
|
|
104
100
|
|
|
105
101
|
export class TransactionError extends WalletError {
|
|
106
102
|
readonly code = "TRANSACTION_ERROR";
|
|
107
103
|
readonly category = "INFRASTRUCTURE" as const;
|
|
108
|
-
readonly userMessage
|
|
104
|
+
readonly userMessage = WALLET_ERROR_MESSAGES.TRANSACTION_FAILED;
|
|
109
105
|
|
|
110
106
|
constructor(message: string, cause?: Error) {
|
|
111
107
|
super(message, cause);
|
|
112
|
-
this.userMessage = "Transaction failed. Please try again.";
|
|
113
108
|
}
|
|
114
109
|
}
|
|
115
110
|
|
|
116
111
|
export class NetworkError extends WalletError {
|
|
117
112
|
readonly code = "NETWORK_ERROR";
|
|
118
113
|
readonly category = "INFRASTRUCTURE" as const;
|
|
119
|
-
readonly userMessage
|
|
114
|
+
readonly userMessage = WALLET_ERROR_MESSAGES.NETWORK_ERROR;
|
|
120
115
|
|
|
121
116
|
constructor(message: string, cause?: Error) {
|
|
122
117
|
super(message, cause);
|
|
123
|
-
this.userMessage = "Network error. Please check your connection.";
|
|
124
118
|
}
|
|
125
119
|
}
|
|
126
120
|
|
|
127
121
|
export class CreditLimitError extends WalletError {
|
|
128
122
|
readonly code = "CREDIT_LIMIT_ERROR";
|
|
129
123
|
readonly category = "BUSINESS" as const;
|
|
130
|
-
readonly userMessage
|
|
124
|
+
readonly userMessage = WALLET_ERROR_MESSAGES.CREDIT_LIMIT_EXCEEDED;
|
|
131
125
|
|
|
132
126
|
constructor(message: string) {
|
|
133
127
|
super(message);
|
|
134
|
-
this.userMessage = "Credit limit exceeded. Please contact support.";
|
|
135
128
|
}
|
|
136
129
|
}
|
|
137
130
|
|
|
138
131
|
export class RefundError extends WalletError {
|
|
139
132
|
readonly code = "REFUND_ERROR";
|
|
140
133
|
readonly category = "BUSINESS" as const;
|
|
141
|
-
readonly userMessage
|
|
134
|
+
readonly userMessage = WALLET_ERROR_MESSAGES.REFUND_FAILED;
|
|
142
135
|
|
|
143
136
|
constructor(message: string, cause?: Error) {
|
|
144
137
|
super(message, cause);
|
|
145
|
-
this.userMessage = "Refund failed. Please contact support.";
|
|
146
138
|
}
|
|
147
139
|
}
|
|
148
140
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wallet Error Messages
|
|
3
|
+
* Centralized error user messages for wallet operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const WALLET_ERROR_MESSAGES = {
|
|
7
|
+
PAYMENT_VALIDATION_FAILED: "Payment validation failed. Please try again.",
|
|
8
|
+
PAYMENT_PROVIDER_ERROR: "Payment provider error. Please try again.",
|
|
9
|
+
DUPLICATE_PAYMENT: "This payment has already been processed.",
|
|
10
|
+
USER_VALIDATION_FAILED: "Invalid user information. Please log in again.",
|
|
11
|
+
PACKAGE_VALIDATION_FAILED: "Invalid credit package. Please select a valid package.",
|
|
12
|
+
RECEIPT_VALIDATION_FAILED: "Invalid payment receipt. Please contact support.",
|
|
13
|
+
TRANSACTION_FAILED: "Transaction failed. Please try again.",
|
|
14
|
+
NETWORK_ERROR: "Network error. Please check your connection.",
|
|
15
|
+
CREDIT_LIMIT_EXCEEDED: "Credit limit exceeded. Please contact support.",
|
|
16
|
+
REFUND_FAILED: "Refund failed. Please contact support.",
|
|
17
|
+
} as const;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Premium Details Card Styles
|
|
3
|
+
* StyleSheet for PremiumDetailsCard component
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { StyleSheet } from "react-native";
|
|
7
|
+
|
|
8
|
+
export const styles = StyleSheet.create({
|
|
9
|
+
card: {
|
|
10
|
+
borderRadius: 12,
|
|
11
|
+
padding: 16,
|
|
12
|
+
gap: 12,
|
|
13
|
+
},
|
|
14
|
+
header: {
|
|
15
|
+
flexDirection: "row",
|
|
16
|
+
justifyContent: "space-between",
|
|
17
|
+
alignItems: "center",
|
|
18
|
+
},
|
|
19
|
+
headerTitleContainer: {
|
|
20
|
+
flex: 1,
|
|
21
|
+
marginRight: 12,
|
|
22
|
+
},
|
|
23
|
+
freeUserHeader: {
|
|
24
|
+
marginBottom: 4,
|
|
25
|
+
},
|
|
26
|
+
freeUserTextContainer: {
|
|
27
|
+
gap: 6,
|
|
28
|
+
},
|
|
29
|
+
premiumButton: {
|
|
30
|
+
paddingVertical: 16,
|
|
31
|
+
borderRadius: 12,
|
|
32
|
+
alignItems: "center",
|
|
33
|
+
},
|
|
34
|
+
detailsSection: {
|
|
35
|
+
gap: 8,
|
|
36
|
+
},
|
|
37
|
+
sectionTitle: {
|
|
38
|
+
marginBottom: 4,
|
|
39
|
+
fontWeight: "600",
|
|
40
|
+
},
|
|
41
|
+
creditsSection: {
|
|
42
|
+
gap: 8,
|
|
43
|
+
paddingTop: 12,
|
|
44
|
+
borderTopWidth: 1,
|
|
45
|
+
},
|
|
46
|
+
actionsSection: {
|
|
47
|
+
gap: 8,
|
|
48
|
+
},
|
|
49
|
+
secondaryButton: {
|
|
50
|
+
paddingVertical: 12,
|
|
51
|
+
borderRadius: 8,
|
|
52
|
+
alignItems: "center",
|
|
53
|
+
},
|
|
54
|
+
});
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import React from "react";
|
|
8
|
-
import { View,
|
|
8
|
+
import { View, TouchableOpacity } from "react-native";
|
|
9
9
|
import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
|
|
10
10
|
import { PremiumStatusBadge } from "./PremiumStatusBadge";
|
|
11
11
|
import { DetailRow } from "./DetailRow";
|
|
12
12
|
import { CreditRow } from "./CreditRow";
|
|
13
|
+
import { styles } from "./PremiumDetailsCard.styles";
|
|
13
14
|
import type { PremiumDetailsCardProps } from "./PremiumDetailsCardTypes";
|
|
14
15
|
|
|
15
16
|
export type { CreditInfo, PremiumDetailsTranslations, PremiumDetailsCardProps } from "./PremiumDetailsCardTypes";
|
|
@@ -144,51 +145,3 @@ export const PremiumDetailsCard: React.FC<PremiumDetailsCardProps> = ({
|
|
|
144
145
|
</View>
|
|
145
146
|
);
|
|
146
147
|
};
|
|
147
|
-
|
|
148
|
-
const styles = StyleSheet.create({
|
|
149
|
-
card: {
|
|
150
|
-
borderRadius: 12,
|
|
151
|
-
padding: 16,
|
|
152
|
-
gap: 12,
|
|
153
|
-
},
|
|
154
|
-
header: {
|
|
155
|
-
flexDirection: "row",
|
|
156
|
-
justifyContent: "space-between",
|
|
157
|
-
alignItems: "center",
|
|
158
|
-
},
|
|
159
|
-
headerTitleContainer: {
|
|
160
|
-
flex: 1,
|
|
161
|
-
marginRight: 12,
|
|
162
|
-
},
|
|
163
|
-
freeUserHeader: {
|
|
164
|
-
marginBottom: 4,
|
|
165
|
-
},
|
|
166
|
-
freeUserTextContainer: {
|
|
167
|
-
gap: 6,
|
|
168
|
-
},
|
|
169
|
-
premiumButton: {
|
|
170
|
-
paddingVertical: 16,
|
|
171
|
-
borderRadius: 12,
|
|
172
|
-
alignItems: "center",
|
|
173
|
-
},
|
|
174
|
-
detailsSection: {
|
|
175
|
-
gap: 8,
|
|
176
|
-
},
|
|
177
|
-
sectionTitle: {
|
|
178
|
-
marginBottom: 4,
|
|
179
|
-
fontWeight: "600",
|
|
180
|
-
},
|
|
181
|
-
creditsSection: {
|
|
182
|
-
gap: 8,
|
|
183
|
-
paddingTop: 12,
|
|
184
|
-
borderTopWidth: 1,
|
|
185
|
-
},
|
|
186
|
-
actionsSection: {
|
|
187
|
-
gap: 8,
|
|
188
|
-
},
|
|
189
|
-
secondaryButton: {
|
|
190
|
-
paddingVertical: 12,
|
|
191
|
-
borderRadius: 8,
|
|
192
|
-
alignItems: "center",
|
|
193
|
-
},
|
|
194
|
-
});
|
|
@@ -15,7 +15,7 @@ const RERENDER_DELAY_MS = 100;
|
|
|
15
15
|
export function usePaywallOperations({
|
|
16
16
|
userId,
|
|
17
17
|
isAnonymous,
|
|
18
|
-
onPaywallClose,
|
|
18
|
+
onPaywallClose: _onPaywallClose,
|
|
19
19
|
onPurchaseSuccess,
|
|
20
20
|
onAuthRequired,
|
|
21
21
|
}: PaywallOperationsProps): PaywallOperationsResult {
|
|
@@ -54,7 +54,7 @@ export function usePaywallOperations({
|
|
|
54
54
|
else showError();
|
|
55
55
|
return success;
|
|
56
56
|
},
|
|
57
|
-
[isAuthenticated, purchasePackage, onPurchaseSuccess,
|
|
57
|
+
[isAuthenticated, purchasePackage, onPurchaseSuccess, onAuthRequired, showError]
|
|
58
58
|
);
|
|
59
59
|
|
|
60
60
|
const handleRestore = useCallback(async (): Promise<boolean> => {
|
|
@@ -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
|
+
}
|