@umituz/react-native-subscription 1.5.0 → 1.6.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Subscription management and paywall UI for React Native apps",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/react": "~19.1.0",
|
|
39
|
-
"typescript": "~5.9.2"
|
|
39
|
+
"typescript": "~5.9.2",
|
|
40
|
+
"react-native": "~0.76.0",
|
|
41
|
+
"@umituz/react-native-design-system-theme": "latest"
|
|
40
42
|
},
|
|
41
43
|
"publishConfig": {
|
|
42
44
|
"access": "public"
|
package/src/index.ts
CHANGED
|
@@ -53,6 +53,11 @@ export {
|
|
|
53
53
|
export { useSubscription } from "./presentation/hooks/useSubscription";
|
|
54
54
|
export type { UseSubscriptionResult } from "./presentation/hooks/useSubscription";
|
|
55
55
|
|
|
56
|
+
export {
|
|
57
|
+
useSubscriptionDetails,
|
|
58
|
+
type SubscriptionDetails,
|
|
59
|
+
} from "./presentation/hooks/useSubscriptionDetails";
|
|
60
|
+
|
|
56
61
|
export {
|
|
57
62
|
usePremiumGate,
|
|
58
63
|
type UsePremiumGateParams,
|
|
@@ -86,6 +91,23 @@ export { PaywallFeaturesList } from "./presentation/components/paywall/PaywallFe
|
|
|
86
91
|
export { PaywallFeatureItem } from "./presentation/components/paywall/PaywallFeatureItem";
|
|
87
92
|
export { PaywallLegalFooter } from "./presentation/components/paywall/PaywallLegalFooter";
|
|
88
93
|
|
|
94
|
+
// =============================================================================
|
|
95
|
+
// PRESENTATION LAYER - Premium Details Components
|
|
96
|
+
// =============================================================================
|
|
97
|
+
|
|
98
|
+
export {
|
|
99
|
+
PremiumDetailsCard,
|
|
100
|
+
type PremiumDetailsCardProps,
|
|
101
|
+
type PremiumDetailsTranslations,
|
|
102
|
+
type CreditInfo,
|
|
103
|
+
} from "./presentation/components/details/PremiumDetailsCard";
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
PremiumStatusBadge,
|
|
107
|
+
type PremiumStatusBadgeProps,
|
|
108
|
+
type SubscriptionStatusType,
|
|
109
|
+
} from "./presentation/components/details/PremiumStatusBadge";
|
|
110
|
+
|
|
89
111
|
// =============================================================================
|
|
90
112
|
// UTILS - Date & Price
|
|
91
113
|
// =============================================================================
|
|
@@ -132,9 +154,3 @@ export {
|
|
|
132
154
|
validateIsPremium,
|
|
133
155
|
validateFetcher,
|
|
134
156
|
} from "./utils/validation";
|
|
135
|
-
|
|
136
|
-
// =============================================================================
|
|
137
|
-
// TYPES - Re-export from peer dependencies
|
|
138
|
-
// =============================================================================
|
|
139
|
-
|
|
140
|
-
export type { PurchasesPackage } from "react-native-purchases";
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Premium Details Card
|
|
3
|
+
* Generic component for displaying subscription details
|
|
4
|
+
* Accepts credits via props for app-specific customization
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
10
|
+
import {
|
|
11
|
+
PremiumStatusBadge,
|
|
12
|
+
type SubscriptionStatusType,
|
|
13
|
+
} from "./PremiumStatusBadge";
|
|
14
|
+
|
|
15
|
+
export interface CreditInfo {
|
|
16
|
+
id: string;
|
|
17
|
+
label: string;
|
|
18
|
+
current: number;
|
|
19
|
+
total: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PremiumDetailsTranslations {
|
|
23
|
+
title: string;
|
|
24
|
+
statusLabel: string;
|
|
25
|
+
expiresLabel: string;
|
|
26
|
+
purchasedLabel: string;
|
|
27
|
+
creditsTitle?: string;
|
|
28
|
+
remainingLabel?: string;
|
|
29
|
+
manageButton?: string;
|
|
30
|
+
upgradeButton?: string;
|
|
31
|
+
lifetimeLabel?: string;
|
|
32
|
+
statusActive?: string;
|
|
33
|
+
statusExpired?: string;
|
|
34
|
+
statusFree?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PremiumDetailsCardProps {
|
|
38
|
+
statusType: SubscriptionStatusType;
|
|
39
|
+
isPremium: boolean;
|
|
40
|
+
expirationDate?: string | null;
|
|
41
|
+
purchaseDate?: string | null;
|
|
42
|
+
isLifetime?: boolean;
|
|
43
|
+
daysRemaining?: number | null;
|
|
44
|
+
credits?: CreditInfo[];
|
|
45
|
+
translations: PremiumDetailsTranslations;
|
|
46
|
+
onManageSubscription?: () => void;
|
|
47
|
+
onUpgrade?: () => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const PremiumDetailsCard: React.FC<PremiumDetailsCardProps> = ({
|
|
51
|
+
statusType,
|
|
52
|
+
isPremium,
|
|
53
|
+
expirationDate,
|
|
54
|
+
purchaseDate,
|
|
55
|
+
isLifetime = false,
|
|
56
|
+
daysRemaining,
|
|
57
|
+
credits,
|
|
58
|
+
translations,
|
|
59
|
+
onManageSubscription,
|
|
60
|
+
onUpgrade,
|
|
61
|
+
}) => {
|
|
62
|
+
const tokens = useAppDesignTokens();
|
|
63
|
+
const showCredits = credits && credits.length > 0;
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<View style={[styles.card, { backgroundColor: tokens.colors.surface }]}>
|
|
67
|
+
<View style={styles.header}>
|
|
68
|
+
<Text style={[styles.title, { color: tokens.colors.text }]}>
|
|
69
|
+
{translations.title}
|
|
70
|
+
</Text>
|
|
71
|
+
<PremiumStatusBadge
|
|
72
|
+
status={statusType}
|
|
73
|
+
activeLabel={translations.statusActive}
|
|
74
|
+
expiredLabel={translations.statusExpired}
|
|
75
|
+
noneLabel={translations.statusFree}
|
|
76
|
+
/>
|
|
77
|
+
</View>
|
|
78
|
+
|
|
79
|
+
{isPremium && (
|
|
80
|
+
<View style={styles.detailsSection}>
|
|
81
|
+
{isLifetime ? (
|
|
82
|
+
<DetailRow
|
|
83
|
+
label={translations.statusLabel}
|
|
84
|
+
value={translations.lifetimeLabel || "Lifetime"}
|
|
85
|
+
tokens={tokens}
|
|
86
|
+
/>
|
|
87
|
+
) : (
|
|
88
|
+
<>
|
|
89
|
+
{expirationDate && (
|
|
90
|
+
<DetailRow
|
|
91
|
+
label={translations.expiresLabel}
|
|
92
|
+
value={expirationDate}
|
|
93
|
+
highlight={
|
|
94
|
+
daysRemaining !== null &&
|
|
95
|
+
daysRemaining !== undefined &&
|
|
96
|
+
daysRemaining <= 7
|
|
97
|
+
}
|
|
98
|
+
tokens={tokens}
|
|
99
|
+
/>
|
|
100
|
+
)}
|
|
101
|
+
{purchaseDate && (
|
|
102
|
+
<DetailRow
|
|
103
|
+
label={translations.purchasedLabel}
|
|
104
|
+
value={purchaseDate}
|
|
105
|
+
tokens={tokens}
|
|
106
|
+
/>
|
|
107
|
+
)}
|
|
108
|
+
</>
|
|
109
|
+
)}
|
|
110
|
+
</View>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{showCredits && (
|
|
114
|
+
<View
|
|
115
|
+
style={[styles.creditsSection, { borderTopColor: tokens.colors.border }]}
|
|
116
|
+
>
|
|
117
|
+
{translations.creditsTitle && (
|
|
118
|
+
<Text style={[styles.sectionTitle, { color: tokens.colors.text }]}>
|
|
119
|
+
{translations.creditsTitle}
|
|
120
|
+
</Text>
|
|
121
|
+
)}
|
|
122
|
+
{credits.map((credit) => (
|
|
123
|
+
<CreditRow
|
|
124
|
+
key={credit.id}
|
|
125
|
+
label={credit.label}
|
|
126
|
+
current={credit.current}
|
|
127
|
+
total={credit.total}
|
|
128
|
+
remainingLabel={translations.remainingLabel}
|
|
129
|
+
tokens={tokens}
|
|
130
|
+
/>
|
|
131
|
+
))}
|
|
132
|
+
</View>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
<View style={styles.actionsSection}>
|
|
136
|
+
{isPremium && onManageSubscription && translations.manageButton && (
|
|
137
|
+
<TouchableOpacity
|
|
138
|
+
style={[
|
|
139
|
+
styles.secondaryButton,
|
|
140
|
+
{ backgroundColor: tokens.colors.surfaceSecondary },
|
|
141
|
+
]}
|
|
142
|
+
onPress={onManageSubscription}
|
|
143
|
+
>
|
|
144
|
+
<Text style={[styles.secondaryButtonText, { color: tokens.colors.text }]}>
|
|
145
|
+
{translations.manageButton}
|
|
146
|
+
</Text>
|
|
147
|
+
</TouchableOpacity>
|
|
148
|
+
)}
|
|
149
|
+
{!isPremium && onUpgrade && translations.upgradeButton && (
|
|
150
|
+
<TouchableOpacity
|
|
151
|
+
style={[styles.primaryButton, { backgroundColor: tokens.colors.primary }]}
|
|
152
|
+
onPress={onUpgrade}
|
|
153
|
+
>
|
|
154
|
+
<Text
|
|
155
|
+
style={[styles.primaryButtonText, { color: tokens.colors.onPrimary }]}
|
|
156
|
+
>
|
|
157
|
+
{translations.upgradeButton}
|
|
158
|
+
</Text>
|
|
159
|
+
</TouchableOpacity>
|
|
160
|
+
)}
|
|
161
|
+
</View>
|
|
162
|
+
</View>
|
|
163
|
+
);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
interface DetailRowProps {
|
|
167
|
+
label: string;
|
|
168
|
+
value: string;
|
|
169
|
+
highlight?: boolean;
|
|
170
|
+
tokens: ReturnType<typeof useAppDesignTokens>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const DetailRow: React.FC<DetailRowProps> = ({
|
|
174
|
+
label,
|
|
175
|
+
value,
|
|
176
|
+
highlight = false,
|
|
177
|
+
tokens,
|
|
178
|
+
}) => (
|
|
179
|
+
<View style={styles.detailRow}>
|
|
180
|
+
<Text style={[styles.detailLabel, { color: tokens.colors.textSecondary }]}>
|
|
181
|
+
{label}
|
|
182
|
+
</Text>
|
|
183
|
+
<Text
|
|
184
|
+
style={[
|
|
185
|
+
styles.detailValue,
|
|
186
|
+
{ color: highlight ? tokens.colors.warning : tokens.colors.text },
|
|
187
|
+
]}
|
|
188
|
+
>
|
|
189
|
+
{value}
|
|
190
|
+
</Text>
|
|
191
|
+
</View>
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
interface CreditRowProps {
|
|
195
|
+
label: string;
|
|
196
|
+
current: number;
|
|
197
|
+
total: number;
|
|
198
|
+
remainingLabel?: string;
|
|
199
|
+
tokens: ReturnType<typeof useAppDesignTokens>;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const CreditRow: React.FC<CreditRowProps> = ({
|
|
203
|
+
label,
|
|
204
|
+
current,
|
|
205
|
+
total,
|
|
206
|
+
remainingLabel = "remaining",
|
|
207
|
+
tokens,
|
|
208
|
+
}) => {
|
|
209
|
+
const percentage = total > 0 ? (current / total) * 100 : 0;
|
|
210
|
+
const isLow = percentage <= 20;
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<View style={styles.creditRow}>
|
|
214
|
+
<View style={styles.creditHeader}>
|
|
215
|
+
<Text style={[styles.creditLabel, { color: tokens.colors.text }]}>
|
|
216
|
+
{label}
|
|
217
|
+
</Text>
|
|
218
|
+
<Text
|
|
219
|
+
style={[
|
|
220
|
+
styles.creditCount,
|
|
221
|
+
{ color: isLow ? tokens.colors.warning : tokens.colors.textSecondary },
|
|
222
|
+
]}
|
|
223
|
+
>
|
|
224
|
+
{current} / {total} {remainingLabel}
|
|
225
|
+
</Text>
|
|
226
|
+
</View>
|
|
227
|
+
<View
|
|
228
|
+
style={[styles.progressBar, { backgroundColor: tokens.colors.surfaceSecondary }]}
|
|
229
|
+
>
|
|
230
|
+
<View
|
|
231
|
+
style={[
|
|
232
|
+
styles.progressFill,
|
|
233
|
+
{
|
|
234
|
+
width: `${percentage}%`,
|
|
235
|
+
backgroundColor: isLow ? tokens.colors.warning : tokens.colors.primary,
|
|
236
|
+
},
|
|
237
|
+
]}
|
|
238
|
+
/>
|
|
239
|
+
</View>
|
|
240
|
+
</View>
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const styles = StyleSheet.create({
|
|
245
|
+
card: {
|
|
246
|
+
borderRadius: 12,
|
|
247
|
+
padding: 16,
|
|
248
|
+
gap: 12,
|
|
249
|
+
},
|
|
250
|
+
header: {
|
|
251
|
+
flexDirection: "row",
|
|
252
|
+
justifyContent: "space-between",
|
|
253
|
+
alignItems: "center",
|
|
254
|
+
},
|
|
255
|
+
title: {
|
|
256
|
+
fontSize: 18,
|
|
257
|
+
fontWeight: "600",
|
|
258
|
+
},
|
|
259
|
+
detailsSection: {
|
|
260
|
+
gap: 8,
|
|
261
|
+
},
|
|
262
|
+
sectionTitle: {
|
|
263
|
+
fontSize: 14,
|
|
264
|
+
fontWeight: "600",
|
|
265
|
+
marginBottom: 4,
|
|
266
|
+
},
|
|
267
|
+
detailRow: {
|
|
268
|
+
flexDirection: "row",
|
|
269
|
+
justifyContent: "space-between",
|
|
270
|
+
alignItems: "center",
|
|
271
|
+
},
|
|
272
|
+
detailLabel: {
|
|
273
|
+
fontSize: 14,
|
|
274
|
+
},
|
|
275
|
+
detailValue: {
|
|
276
|
+
fontSize: 14,
|
|
277
|
+
fontWeight: "500",
|
|
278
|
+
},
|
|
279
|
+
creditsSection: {
|
|
280
|
+
gap: 8,
|
|
281
|
+
paddingTop: 12,
|
|
282
|
+
borderTopWidth: 1,
|
|
283
|
+
},
|
|
284
|
+
creditRow: {
|
|
285
|
+
gap: 4,
|
|
286
|
+
},
|
|
287
|
+
creditHeader: {
|
|
288
|
+
flexDirection: "row",
|
|
289
|
+
justifyContent: "space-between",
|
|
290
|
+
alignItems: "center",
|
|
291
|
+
},
|
|
292
|
+
creditLabel: {
|
|
293
|
+
fontSize: 13,
|
|
294
|
+
},
|
|
295
|
+
creditCount: {
|
|
296
|
+
fontSize: 12,
|
|
297
|
+
},
|
|
298
|
+
progressBar: {
|
|
299
|
+
height: 6,
|
|
300
|
+
borderRadius: 3,
|
|
301
|
+
overflow: "hidden",
|
|
302
|
+
},
|
|
303
|
+
progressFill: {
|
|
304
|
+
height: "100%",
|
|
305
|
+
borderRadius: 3,
|
|
306
|
+
},
|
|
307
|
+
actionsSection: {
|
|
308
|
+
gap: 8,
|
|
309
|
+
},
|
|
310
|
+
primaryButton: {
|
|
311
|
+
paddingVertical: 12,
|
|
312
|
+
borderRadius: 8,
|
|
313
|
+
alignItems: "center",
|
|
314
|
+
},
|
|
315
|
+
primaryButtonText: {
|
|
316
|
+
fontSize: 14,
|
|
317
|
+
fontWeight: "600",
|
|
318
|
+
},
|
|
319
|
+
secondaryButton: {
|
|
320
|
+
paddingVertical: 12,
|
|
321
|
+
borderRadius: 8,
|
|
322
|
+
alignItems: "center",
|
|
323
|
+
},
|
|
324
|
+
secondaryButtonText: {
|
|
325
|
+
fontSize: 14,
|
|
326
|
+
fontWeight: "500",
|
|
327
|
+
},
|
|
328
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Premium Status Badge
|
|
3
|
+
* Displays subscription status as a colored badge
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, Text, StyleSheet } from "react-native";
|
|
8
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
9
|
+
|
|
10
|
+
export type SubscriptionStatusType = "active" | "expired" | "none";
|
|
11
|
+
|
|
12
|
+
export interface PremiumStatusBadgeProps {
|
|
13
|
+
status: SubscriptionStatusType;
|
|
14
|
+
activeLabel?: string;
|
|
15
|
+
expiredLabel?: string;
|
|
16
|
+
noneLabel?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Badge component showing subscription status
|
|
21
|
+
*/
|
|
22
|
+
export const PremiumStatusBadge: React.FC<PremiumStatusBadgeProps> = ({
|
|
23
|
+
status,
|
|
24
|
+
activeLabel = "Active",
|
|
25
|
+
expiredLabel = "Expired",
|
|
26
|
+
noneLabel = "Free",
|
|
27
|
+
}) => {
|
|
28
|
+
const tokens = useAppDesignTokens();
|
|
29
|
+
|
|
30
|
+
const labels: Record<SubscriptionStatusType, string> = {
|
|
31
|
+
active: activeLabel,
|
|
32
|
+
expired: expiredLabel,
|
|
33
|
+
none: noneLabel,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const colors: Record<SubscriptionStatusType, string> = {
|
|
37
|
+
active: tokens.colors.success,
|
|
38
|
+
expired: tokens.colors.error,
|
|
39
|
+
none: tokens.colors.textTertiary,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const backgroundColor = colors[status];
|
|
43
|
+
const label = labels[status];
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<View style={[styles.badge, { backgroundColor }]}>
|
|
47
|
+
<Text style={[styles.badgeText, { color: tokens.colors.onPrimary }]}>
|
|
48
|
+
{label}
|
|
49
|
+
</Text>
|
|
50
|
+
</View>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const styles = StyleSheet.create({
|
|
55
|
+
badge: {
|
|
56
|
+
paddingHorizontal: 8,
|
|
57
|
+
paddingVertical: 4,
|
|
58
|
+
borderRadius: 4,
|
|
59
|
+
},
|
|
60
|
+
badgeText: {
|
|
61
|
+
fontSize: 12,
|
|
62
|
+
fontWeight: "600",
|
|
63
|
+
},
|
|
64
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSubscriptionDetails Hook
|
|
3
|
+
* Provides formatted subscription details for display
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
import type { SubscriptionStatus } from "../../domain/entities/SubscriptionStatus";
|
|
8
|
+
import {
|
|
9
|
+
getDaysUntilExpiration,
|
|
10
|
+
isSubscriptionExpired,
|
|
11
|
+
} from "../../utils/dateValidationUtils";
|
|
12
|
+
|
|
13
|
+
export interface SubscriptionDetails {
|
|
14
|
+
/** Raw subscription status */
|
|
15
|
+
status: SubscriptionStatus | null;
|
|
16
|
+
/** Whether user has active premium */
|
|
17
|
+
isPremium: boolean;
|
|
18
|
+
/** Whether subscription is expired */
|
|
19
|
+
isExpired: boolean;
|
|
20
|
+
/** Whether this is a lifetime subscription */
|
|
21
|
+
isLifetime: boolean;
|
|
22
|
+
/** Days remaining until expiration (null for lifetime) */
|
|
23
|
+
daysRemaining: number | null;
|
|
24
|
+
/** Formatted expiration date (null if lifetime) */
|
|
25
|
+
formattedExpirationDate: string | null;
|
|
26
|
+
/** Formatted purchase date */
|
|
27
|
+
formattedPurchaseDate: string | null;
|
|
28
|
+
/** Status text key for localization */
|
|
29
|
+
statusKey: "active" | "expired" | "none";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface UseSubscriptionDetailsParams {
|
|
33
|
+
status: SubscriptionStatus | null;
|
|
34
|
+
locale?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Format date to localized string
|
|
39
|
+
*/
|
|
40
|
+
function formatDate(dateString: string | null, locale: string): string | null {
|
|
41
|
+
if (!dateString) return null;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const date = new Date(dateString);
|
|
45
|
+
return date.toLocaleDateString(locale, {
|
|
46
|
+
year: "numeric",
|
|
47
|
+
month: "long",
|
|
48
|
+
day: "numeric",
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Hook for formatted subscription details
|
|
57
|
+
*/
|
|
58
|
+
export function useSubscriptionDetails(
|
|
59
|
+
params: UseSubscriptionDetailsParams,
|
|
60
|
+
): SubscriptionDetails {
|
|
61
|
+
const { status, locale = "en-US" } = params;
|
|
62
|
+
|
|
63
|
+
return useMemo(() => {
|
|
64
|
+
if (!status) {
|
|
65
|
+
return {
|
|
66
|
+
status: null,
|
|
67
|
+
isPremium: false,
|
|
68
|
+
isExpired: false,
|
|
69
|
+
isLifetime: false,
|
|
70
|
+
daysRemaining: null,
|
|
71
|
+
formattedExpirationDate: null,
|
|
72
|
+
formattedPurchaseDate: null,
|
|
73
|
+
statusKey: "none",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const isExpired = isSubscriptionExpired(status);
|
|
78
|
+
const isLifetime = status.isPremium && !status.expiresAt;
|
|
79
|
+
const daysRemaining = getDaysUntilExpiration(status);
|
|
80
|
+
const isPremium = status.isPremium && !isExpired;
|
|
81
|
+
|
|
82
|
+
let statusKey: "active" | "expired" | "none" = "none";
|
|
83
|
+
if (status.isPremium) {
|
|
84
|
+
statusKey = isExpired ? "expired" : "active";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
status,
|
|
89
|
+
isPremium,
|
|
90
|
+
isExpired,
|
|
91
|
+
isLifetime,
|
|
92
|
+
daysRemaining,
|
|
93
|
+
formattedExpirationDate: formatDate(status.expiresAt, locale),
|
|
94
|
+
formattedPurchaseDate: formatDate(status.purchasedAt, locale),
|
|
95
|
+
statusKey,
|
|
96
|
+
};
|
|
97
|
+
}, [status, locale]);
|
|
98
|
+
}
|