@umituz/react-native-subscription 2.27.122 → 2.27.124
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/creditOperationUtils.ts +65 -144
- package/src/domains/credits/application/creditOperationUtils.types.ts +19 -0
- package/src/domains/credits/presentation/useCredits.ts +1 -11
- package/src/domains/paywall/components/PaywallModal.tsx +14 -107
- package/src/domains/paywall/components/PaywallModal.types.ts +26 -0
- package/src/domains/paywall/components/PlanCard.tsx +45 -148
- package/src/domains/paywall/components/PlanCard.types.ts +12 -0
- package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +116 -0
- package/src/domains/subscription/application/SubscriptionSyncService.ts +10 -96
- package/src/domains/subscription/application/SubscriptionSyncUtils.ts +0 -2
- package/src/domains/subscription/core/SubscriptionConstants.ts +1 -13
- package/src/domains/subscription/infrastructure/hooks/usePurchasePackage.ts +4 -0
- package/src/domains/subscription/infrastructure/hooks/useRestorePurchase.ts +4 -0
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +7 -13
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.types.ts +15 -0
- package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +20 -5
- package/src/domains/subscription/presentation/screens/SubscriptionDetailScreen.tsx +13 -92
- package/src/domains/subscription/presentation/screens/SubscriptionDetailScreen.types.ts +47 -0
- package/src/domains/subscription/presentation/screens/components/UpgradePrompt.tsx +34 -126
- package/src/domains/subscription/presentation/screens/components/UpgradePrompt.types.ts +12 -0
- package/src/domains/subscription/presentation/usePremium.ts +3 -22
- package/src/domains/subscription/presentation/usePremium.types.ts +16 -0
- package/src/domains/subscription/presentation/useSubscriptionStatus.ts +30 -22
- package/src/domains/subscription/presentation/useSubscriptionStatus.types.ts +7 -0
- package/src/domains/wallet/presentation/hooks/useProductMetadata.ts +3 -16
- package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +1 -13
- package/src/domains/wallet/presentation/screens/WalletScreen.tsx +25 -112
- package/src/domains/wallet/presentation/screens/WalletScreen.types.ts +15 -0
- package/src/shared/utils/appValidators.ts +38 -0
- package/src/shared/utils/validators.ts +4 -122
- package/src/domains/paywall/components/README.md +0 -41
- package/src/domains/subscription/presentation/screens/README.md +0 -52
|
@@ -16,9 +16,10 @@ const configurationState = {
|
|
|
16
16
|
isPurchasesConfigured: false,
|
|
17
17
|
isLogHandlerConfigured: false,
|
|
18
18
|
configurationInProgress: false,
|
|
19
|
-
configurationPromise: null as Promise<
|
|
19
|
+
configurationPromise: null as Promise<InitializeResult> | null,
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
|
|
22
23
|
// Simple lock mechanism to prevent concurrent configurations (implementation deferred)
|
|
23
24
|
|
|
24
25
|
function configureLogHandler(): void {
|
|
@@ -77,9 +78,12 @@ export async function initializeSDK(
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
if (configurationState.configurationInProgress) {
|
|
81
|
+
if (configurationState.configurationPromise) {
|
|
82
|
+
await configurationState.configurationPromise;
|
|
83
|
+
return initializeSDK(deps, userId, apiKey);
|
|
84
|
+
}
|
|
80
85
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
81
|
-
|
|
82
|
-
return { success: false, offering: null, isPremium: false };
|
|
86
|
+
return initializeSDK(deps, userId, apiKey);
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
const key = apiKey || resolveApiKey(deps.config);
|
|
@@ -87,6 +91,11 @@ export async function initializeSDK(
|
|
|
87
91
|
return { success: false, offering: null, isPremium: false };
|
|
88
92
|
}
|
|
89
93
|
|
|
94
|
+
let resolveInProgress: (value: InitializeResult) => void;
|
|
95
|
+
configurationState.configurationPromise = new Promise((resolve) => {
|
|
96
|
+
resolveInProgress = resolve;
|
|
97
|
+
});
|
|
98
|
+
|
|
90
99
|
configurationState.configurationInProgress = true;
|
|
91
100
|
try {
|
|
92
101
|
configureLogHandler();
|
|
@@ -100,10 +109,16 @@ export async function initializeSDK(
|
|
|
100
109
|
Purchases.getOfferings(),
|
|
101
110
|
]);
|
|
102
111
|
|
|
103
|
-
|
|
112
|
+
const result = buildSuccessResult(deps, customerInfo, offerings);
|
|
113
|
+
resolveInProgress!(result);
|
|
114
|
+
return result;
|
|
104
115
|
} catch {
|
|
105
|
-
|
|
116
|
+
const errorResult = { success: false, offering: null, isPremium: false };
|
|
117
|
+
resolveInProgress!(errorResult);
|
|
118
|
+
return errorResult;
|
|
106
119
|
} finally {
|
|
107
120
|
configurationState.configurationInProgress = false;
|
|
121
|
+
configurationState.configurationPromise = null;
|
|
108
122
|
}
|
|
109
123
|
}
|
|
124
|
+
|
|
@@ -1,90 +1,20 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subscription Detail Screen
|
|
3
|
-
* Composition of subscription components
|
|
4
|
-
* No business logic - pure presentation
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
import React, { useMemo } from "react";
|
|
8
|
-
import { StyleSheet, View } from "react-native";
|
|
9
|
-
import {
|
|
10
|
-
useAppDesignTokens,
|
|
11
|
-
NavigationHeader,
|
|
12
|
-
AtomicIcon,
|
|
13
|
-
} from "@umituz/react-native-design-system";
|
|
14
|
-
import { Pressable } from "react-native";
|
|
2
|
+
import { StyleSheet, View, Pressable } from "react-native";
|
|
3
|
+
import { useAppDesignTokens, NavigationHeader, AtomicIcon } from "@umituz/react-native-design-system";
|
|
15
4
|
import { ScreenLayout } from "../../../../shared/presentation";
|
|
16
5
|
import { SubscriptionHeader } from "./components/SubscriptionHeader";
|
|
17
|
-
import { CreditsList
|
|
18
|
-
import { UpgradePrompt
|
|
19
|
-
|
|
20
|
-
export interface SubscriptionDisplayFlags {
|
|
21
|
-
showHeader: boolean;
|
|
22
|
-
showCredits: boolean;
|
|
23
|
-
showUpgradePrompt: boolean;
|
|
24
|
-
showExpirationDate: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface SubscriptionDetailTranslations {
|
|
28
|
-
title: string;
|
|
29
|
-
statusActive: string;
|
|
30
|
-
statusExpired: string;
|
|
31
|
-
statusFree: string;
|
|
32
|
-
statusCanceled: string;
|
|
33
|
-
statusLabel: string;
|
|
34
|
-
lifetimeLabel: string;
|
|
35
|
-
expiresLabel: string;
|
|
36
|
-
purchasedLabel: string;
|
|
37
|
-
usageTitle?: string;
|
|
38
|
-
creditsTitle: string;
|
|
39
|
-
creditsResetInfo?: string;
|
|
40
|
-
remainingLabel?: string;
|
|
41
|
-
upgradeButton: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface UpgradePromptConfig {
|
|
45
|
-
title: string;
|
|
46
|
-
subtitle?: string;
|
|
47
|
-
benefits?: readonly Benefit[];
|
|
48
|
-
onUpgrade?: () => void;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface SubscriptionDetailConfig {
|
|
52
|
-
display: SubscriptionDisplayFlags;
|
|
53
|
-
statusType: "active" | "expired" | "none" | "canceled";
|
|
54
|
-
isLifetime: boolean;
|
|
55
|
-
expirationDate?: string;
|
|
56
|
-
purchaseDate?: string;
|
|
57
|
-
daysRemaining?: number | null;
|
|
58
|
-
credits?: readonly CreditItem[];
|
|
59
|
-
translations: SubscriptionDetailTranslations;
|
|
60
|
-
upgradePrompt?: UpgradePromptConfig;
|
|
61
|
-
onClose?: () => void;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface SubscriptionDetailScreenProps {
|
|
65
|
-
config: SubscriptionDetailConfig;
|
|
66
|
-
}
|
|
6
|
+
import { CreditsList } from "./components/CreditsList";
|
|
7
|
+
import { UpgradePrompt } from "./components/UpgradePrompt";
|
|
8
|
+
import { SubscriptionDetailScreenProps } from "./SubscriptionDetailScreen.types";
|
|
67
9
|
|
|
68
|
-
export const SubscriptionDetailScreen: React.FC<
|
|
69
|
-
SubscriptionDetailScreenProps
|
|
70
|
-
> = ({ config }) => {
|
|
10
|
+
export const SubscriptionDetailScreen: React.FC<SubscriptionDetailScreenProps> = ({ config }) => {
|
|
71
11
|
const tokens = useAppDesignTokens();
|
|
72
12
|
const { showHeader, showCredits, showUpgradePrompt, showExpirationDate } = config.display;
|
|
73
13
|
|
|
74
|
-
const styles = useMemo(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
flexGrow: 1,
|
|
79
|
-
padding: tokens.spacing.lg,
|
|
80
|
-
gap: tokens.spacing.lg,
|
|
81
|
-
},
|
|
82
|
-
cardsContainer: {
|
|
83
|
-
gap: tokens.spacing.xl,
|
|
84
|
-
},
|
|
85
|
-
}),
|
|
86
|
-
[tokens]
|
|
87
|
-
);
|
|
14
|
+
const styles = useMemo(() => StyleSheet.create({
|
|
15
|
+
content: { flexGrow: 1, padding: tokens.spacing.lg, gap: tokens.spacing.lg },
|
|
16
|
+
cardsContainer: { gap: tokens.spacing.xl }
|
|
17
|
+
}), [tokens]);
|
|
88
18
|
|
|
89
19
|
return (
|
|
90
20
|
<ScreenLayout
|
|
@@ -100,10 +30,7 @@ export const SubscriptionDetailScreen: React.FC<
|
|
|
100
30
|
<Pressable
|
|
101
31
|
onPress={config.onClose}
|
|
102
32
|
style={({ pressed }) => ({
|
|
103
|
-
width: 44,
|
|
104
|
-
height: 44,
|
|
105
|
-
justifyContent: "center",
|
|
106
|
-
alignItems: "center",
|
|
33
|
+
width: 44, height: 44, justifyContent: "center", alignItems: "center",
|
|
107
34
|
backgroundColor: pressed ? tokens.colors.surfaceVariant : tokens.colors.surface,
|
|
108
35
|
borderRadius: tokens.radius.full,
|
|
109
36
|
})}
|
|
@@ -126,18 +53,14 @@ export const SubscriptionDetailScreen: React.FC<
|
|
|
126
53
|
translations={config.translations}
|
|
127
54
|
/>
|
|
128
55
|
)}
|
|
129
|
-
|
|
130
56
|
{showCredits && config.credits && (
|
|
131
57
|
<CreditsList
|
|
132
|
-
credits={config.credits}
|
|
133
|
-
title={
|
|
134
|
-
config.translations.usageTitle || config.translations.creditsTitle
|
|
135
|
-
}
|
|
58
|
+
credits={config.credits as any}
|
|
59
|
+
title={config.translations.usageTitle || config.translations.creditsTitle}
|
|
136
60
|
description={config.translations.creditsResetInfo}
|
|
137
61
|
remainingLabel={config.translations.remainingLabel}
|
|
138
62
|
/>
|
|
139
63
|
)}
|
|
140
|
-
|
|
141
64
|
{showUpgradePrompt && config.upgradePrompt && (
|
|
142
65
|
<UpgradePrompt
|
|
143
66
|
title={config.upgradePrompt.title}
|
|
@@ -148,8 +71,6 @@ export const SubscriptionDetailScreen: React.FC<
|
|
|
148
71
|
/>
|
|
149
72
|
)}
|
|
150
73
|
</View>
|
|
151
|
-
|
|
152
|
-
|
|
153
74
|
</ScreenLayout>
|
|
154
75
|
);
|
|
155
76
|
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface SubscriptionDisplayFlags {
|
|
2
|
+
showHeader: boolean;
|
|
3
|
+
showCredits: boolean;
|
|
4
|
+
showUpgradePrompt: boolean;
|
|
5
|
+
showExpirationDate: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SubscriptionDetailTranslations {
|
|
9
|
+
title: string;
|
|
10
|
+
statusActive: string;
|
|
11
|
+
statusExpired: string;
|
|
12
|
+
statusFree: string;
|
|
13
|
+
statusCanceled: string;
|
|
14
|
+
statusLabel: string;
|
|
15
|
+
lifetimeLabel: string;
|
|
16
|
+
expiresLabel: string;
|
|
17
|
+
purchasedLabel: string;
|
|
18
|
+
usageTitle?: string;
|
|
19
|
+
creditsTitle: string;
|
|
20
|
+
creditsResetInfo?: string;
|
|
21
|
+
remainingLabel?: string;
|
|
22
|
+
upgradeButton: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UpgradePromptConfig {
|
|
26
|
+
title: string;
|
|
27
|
+
subtitle?: string;
|
|
28
|
+
benefits?: readonly { icon?: string; text: string }[];
|
|
29
|
+
onUpgrade?: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SubscriptionDetailConfig {
|
|
33
|
+
display: SubscriptionDisplayFlags;
|
|
34
|
+
statusType: "active" | "expired" | "none" | "canceled";
|
|
35
|
+
isLifetime: boolean;
|
|
36
|
+
expirationDate?: string;
|
|
37
|
+
purchaseDate?: string;
|
|
38
|
+
daysRemaining?: number | null;
|
|
39
|
+
credits?: readonly any[];
|
|
40
|
+
translations: SubscriptionDetailTranslations;
|
|
41
|
+
upgradePrompt?: UpgradePromptConfig;
|
|
42
|
+
onClose?: () => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SubscriptionDetailScreenProps {
|
|
46
|
+
config: SubscriptionDetailConfig;
|
|
47
|
+
}
|
|
@@ -1,150 +1,58 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Upgrade Prompt Component
|
|
3
|
-
* Displays premium benefits for free users to encourage upgrade
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import React, { useMemo } from "react";
|
|
7
2
|
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
AtomicText,
|
|
11
|
-
AtomicIcon,
|
|
12
|
-
} from "@umituz/react-native-design-system";
|
|
13
|
-
|
|
14
|
-
export interface Benefit {
|
|
15
|
-
icon?: string;
|
|
16
|
-
text: string;
|
|
17
|
-
}
|
|
3
|
+
import { useAppDesignTokens, AtomicText, AtomicIcon } from "@umituz/react-native-design-system";
|
|
4
|
+
import { UpgradePromptProps } from "./UpgradePrompt.types";
|
|
18
5
|
|
|
19
|
-
export
|
|
20
|
-
title: string;
|
|
21
|
-
subtitle?: string;
|
|
22
|
-
benefits?: readonly Benefit[];
|
|
23
|
-
upgradeButtonLabel: string;
|
|
24
|
-
onUpgrade: () => void;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const UpgradePrompt: React.FC<UpgradePromptProps> = ({
|
|
28
|
-
title,
|
|
29
|
-
subtitle,
|
|
30
|
-
benefits,
|
|
31
|
-
upgradeButtonLabel,
|
|
32
|
-
onUpgrade,
|
|
33
|
-
}) => {
|
|
6
|
+
export const UpgradePrompt: React.FC<UpgradePromptProps> = ({ title, subtitle, benefits, upgradeButtonLabel, onUpgrade }) => {
|
|
34
7
|
const tokens = useAppDesignTokens();
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
textAlign: "center",
|
|
61
|
-
lineHeight: 22,
|
|
62
|
-
},
|
|
63
|
-
benefitsCard: {
|
|
64
|
-
borderRadius: tokens.radius.lg,
|
|
65
|
-
padding: tokens.spacing.lg,
|
|
66
|
-
gap: tokens.spacing.md,
|
|
67
|
-
backgroundColor: tokens.colors.surface,
|
|
68
|
-
},
|
|
69
|
-
benefitItem: {
|
|
70
|
-
flexDirection: "row",
|
|
71
|
-
alignItems: "center",
|
|
72
|
-
gap: tokens.spacing.md,
|
|
73
|
-
},
|
|
74
|
-
benefitIconWrapper: {
|
|
75
|
-
width: 32,
|
|
76
|
-
height: 32,
|
|
77
|
-
borderRadius: 16,
|
|
78
|
-
alignItems: "center",
|
|
79
|
-
justifyContent: "center",
|
|
80
|
-
backgroundColor: tokens.colors.primaryContainer,
|
|
81
|
-
},
|
|
82
|
-
benefitText: {
|
|
83
|
-
flex: 1,
|
|
84
|
-
},
|
|
85
|
-
upgradeButton: {
|
|
86
|
-
paddingVertical: tokens.spacing.lg,
|
|
87
|
-
borderRadius: tokens.radius.lg,
|
|
88
|
-
alignItems: "center",
|
|
89
|
-
backgroundColor: tokens.colors.primary,
|
|
90
|
-
},
|
|
91
|
-
buttonText: {
|
|
92
|
-
color: tokens.colors.onPrimary,
|
|
93
|
-
fontWeight: "700",
|
|
94
|
-
},
|
|
95
|
-
}),
|
|
96
|
-
[tokens]
|
|
97
|
-
);
|
|
8
|
+
const styles = useMemo(() => StyleSheet.create({
|
|
9
|
+
container: { gap: tokens.spacing.lg },
|
|
10
|
+
header: { alignItems: "center", gap: tokens.spacing.md, paddingVertical: tokens.spacing.md },
|
|
11
|
+
iconContainer: {
|
|
12
|
+
width: 64, height: 64, borderRadius: 32, alignItems: "center", justifyContent: "center",
|
|
13
|
+
backgroundColor: tokens.colors.primaryContainer,
|
|
14
|
+
},
|
|
15
|
+
title: { fontWeight: "700", textAlign: "center" },
|
|
16
|
+
subtitle: { textAlign: "center", lineHeight: 22 },
|
|
17
|
+
benefitsCard: {
|
|
18
|
+
borderRadius: tokens.radius.lg, padding: tokens.spacing.lg, gap: tokens.spacing.md,
|
|
19
|
+
backgroundColor: tokens.colors.surface,
|
|
20
|
+
},
|
|
21
|
+
benefitItem: { flexDirection: "row", alignItems: "center", gap: tokens.spacing.md },
|
|
22
|
+
benefitIconWrapper: {
|
|
23
|
+
width: 32, height: 32, borderRadius: 16, alignItems: "center", justifyContent: "center",
|
|
24
|
+
backgroundColor: tokens.colors.primaryContainer,
|
|
25
|
+
},
|
|
26
|
+
benefitText: { flex: 1 },
|
|
27
|
+
upgradeButton: {
|
|
28
|
+
paddingVertical: tokens.spacing.lg, borderRadius: tokens.radius.lg,
|
|
29
|
+
alignItems: "center", backgroundColor: tokens.colors.primary,
|
|
30
|
+
},
|
|
31
|
+
buttonText: { color: tokens.colors.onPrimary, fontWeight: "700" },
|
|
32
|
+
}), [tokens]);
|
|
98
33
|
|
|
99
34
|
return (
|
|
100
35
|
<View style={styles.container}>
|
|
101
36
|
<View style={styles.header}>
|
|
102
|
-
<View style={styles.iconContainer}>
|
|
103
|
-
|
|
104
|
-
</
|
|
105
|
-
<AtomicText
|
|
106
|
-
type="headlineSmall"
|
|
107
|
-
style={[styles.title, { color: tokens.colors.textPrimary }]}
|
|
108
|
-
>
|
|
109
|
-
{title}
|
|
110
|
-
</AtomicText>
|
|
111
|
-
{subtitle && (
|
|
112
|
-
<AtomicText
|
|
113
|
-
type="bodyMedium"
|
|
114
|
-
style={[styles.subtitle, { color: tokens.colors.textSecondary }]}
|
|
115
|
-
>
|
|
116
|
-
{subtitle}
|
|
117
|
-
</AtomicText>
|
|
118
|
-
)}
|
|
37
|
+
<View style={styles.iconContainer}><AtomicIcon name="sparkles" customSize={32} color="primary" /></View>
|
|
38
|
+
<AtomicText type="headlineSmall" style={[styles.title, { color: tokens.colors.textPrimary }]}>{title}</AtomicText>
|
|
39
|
+
{subtitle && <AtomicText type="bodyMedium" style={[styles.subtitle, { color: tokens.colors.textSecondary }]}>{subtitle}</AtomicText>}
|
|
119
40
|
</View>
|
|
120
|
-
|
|
121
41
|
{benefits && benefits.length > 0 && (
|
|
122
42
|
<View style={styles.benefitsCard}>
|
|
123
43
|
{benefits.map((benefit, index) => (
|
|
124
44
|
<View key={index} style={styles.benefitItem}>
|
|
125
45
|
<View style={styles.benefitIconWrapper}>
|
|
126
|
-
<AtomicIcon
|
|
127
|
-
name={benefit.icon || "checkmark-circle-outline"}
|
|
128
|
-
customSize={16}
|
|
129
|
-
color="primary"
|
|
130
|
-
/>
|
|
46
|
+
<AtomicIcon name={benefit.icon || "checkmark-circle-outline"} customSize={16} color="primary" />
|
|
131
47
|
</View>
|
|
132
|
-
<AtomicText
|
|
133
|
-
type="bodyMedium"
|
|
134
|
-
style={[styles.benefitText, { color: tokens.colors.textPrimary }]}
|
|
135
|
-
>
|
|
136
|
-
{benefit.text}
|
|
137
|
-
</AtomicText>
|
|
48
|
+
<AtomicText type="bodyMedium" style={[styles.benefitText, { color: tokens.colors.textPrimary }]}>{benefit.text}</AtomicText>
|
|
138
49
|
</View>
|
|
139
50
|
))}
|
|
140
51
|
</View>
|
|
141
52
|
)}
|
|
142
|
-
|
|
143
53
|
{onUpgrade && upgradeButtonLabel && (
|
|
144
54
|
<TouchableOpacity style={styles.upgradeButton} onPress={onUpgrade}>
|
|
145
|
-
<AtomicText type="titleMedium" style={styles.buttonText}>
|
|
146
|
-
{upgradeButtonLabel}
|
|
147
|
-
</AtomicText>
|
|
55
|
+
<AtomicText type="titleMedium" style={styles.buttonText}>{upgradeButtonLabel}</AtomicText>
|
|
148
56
|
</TouchableOpacity>
|
|
149
57
|
)}
|
|
150
58
|
</View>
|
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* usePremium Hook
|
|
3
|
-
*
|
|
4
|
-
* Complete subscription management.
|
|
5
|
-
* Auth info automatically read from @umituz/react-native-auth.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { useCallback } from 'react';
|
|
9
2
|
import type { PurchasesPackage } from 'react-native-purchases';
|
|
10
|
-
import type { UserCredits } from '../../credits/core/Credits';
|
|
11
3
|
import { useCredits } from '../../credits/presentation/useCredits';
|
|
12
4
|
import { useSubscriptionStatus } from './useSubscriptionStatus';
|
|
13
5
|
import {
|
|
@@ -17,21 +9,10 @@ import {
|
|
|
17
9
|
} from '../infrastructure/hooks/useSubscriptionQueries';
|
|
18
10
|
import { usePaywallVisibility } from './usePaywallVisibility';
|
|
19
11
|
|
|
20
|
-
|
|
21
|
-
isPremium: boolean;
|
|
22
|
-
isLoading: boolean;
|
|
23
|
-
packages: PurchasesPackage[];
|
|
24
|
-
credits: UserCredits | null;
|
|
25
|
-
showPaywall: boolean;
|
|
26
|
-
isSyncing: boolean;
|
|
27
|
-
purchasePackage: (pkg: PurchasesPackage) => Promise<boolean>;
|
|
28
|
-
restorePurchase: () => Promise<boolean>;
|
|
29
|
-
setShowPaywall: (show: boolean) => void;
|
|
30
|
-
closePaywall: () => void;
|
|
31
|
-
openPaywall: () => void;
|
|
32
|
-
}
|
|
12
|
+
import { UsePremiumResult } from './usePremium.types';
|
|
33
13
|
|
|
34
14
|
export const usePremium = (): UsePremiumResult => {
|
|
15
|
+
|
|
35
16
|
const { isPremium: subscriptionActive, isLoading: statusLoading } = useSubscriptionStatus();
|
|
36
17
|
const { credits, isLoading: creditsLoading } = useCredits();
|
|
37
18
|
|
|
@@ -42,7 +23,7 @@ export const usePremium = (): UsePremiumResult => {
|
|
|
42
23
|
|
|
43
24
|
const { showPaywall, setShowPaywall, closePaywall, openPaywall } = usePaywallVisibility();
|
|
44
25
|
|
|
45
|
-
const isPremium = subscriptionActive;
|
|
26
|
+
const isPremium = subscriptionActive || (credits?.isPremium ?? false);
|
|
46
27
|
const isSyncing = subscriptionActive && credits !== null && !credits.isPremium;
|
|
47
28
|
|
|
48
29
|
const handlePurchase = useCallback(
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
}
|
|
@@ -1,34 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Checks real subscription status from RevenueCat.
|
|
5
|
-
* Auth info automatically read from @umituz/react-native-auth.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { useQuery } from "@umituz/react-native-design-system";
|
|
9
|
-
import {
|
|
10
|
-
useAuthStore,
|
|
11
|
-
selectUserId,
|
|
12
|
-
} from "@umituz/react-native-auth";
|
|
1
|
+
import { useQuery, useQueryClient } from "@umituz/react-native-design-system";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
|
|
13
4
|
import { SubscriptionManager } from "../infrastructure/managers/SubscriptionManager";
|
|
5
|
+
import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
|
|
6
|
+
import { SubscriptionStatusResult } from "./useSubscriptionStatus.types";
|
|
14
7
|
|
|
15
8
|
export const subscriptionStatusQueryKeys = {
|
|
16
9
|
all: ["subscriptionStatus"] as const,
|
|
17
10
|
user: (userId: string) => ["subscriptionStatus", userId] as const,
|
|
18
11
|
};
|
|
19
12
|
|
|
20
|
-
export interface SubscriptionStatusResult {
|
|
21
|
-
isPremium: boolean;
|
|
22
|
-
expirationDate: Date | null;
|
|
23
|
-
isLoading: boolean;
|
|
24
|
-
error: Error | null;
|
|
25
|
-
refetch: () => void;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
13
|
export const useSubscriptionStatus = (): SubscriptionStatusResult => {
|
|
29
14
|
const userId = useAuthStore(selectUserId);
|
|
15
|
+
const queryClient = useQueryClient();
|
|
30
16
|
|
|
31
|
-
const { data,
|
|
17
|
+
const { data, status, error, refetch } = useQuery({
|
|
32
18
|
queryKey: subscriptionStatusQueryKeys.user(userId ?? ""),
|
|
33
19
|
queryFn: async () => {
|
|
34
20
|
if (!userId) {
|
|
@@ -43,9 +29,27 @@ export const useSubscriptionStatus = (): SubscriptionStatusResult => {
|
|
|
43
29
|
}
|
|
44
30
|
},
|
|
45
31
|
enabled: !!userId && SubscriptionManager.isInitializedForUser(userId),
|
|
46
|
-
|
|
47
32
|
});
|
|
48
33
|
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!userId) return;
|
|
36
|
+
|
|
37
|
+
const unsubscribe = subscriptionEventBus.on(
|
|
38
|
+
SUBSCRIPTION_EVENTS.PREMIUM_STATUS_CHANGED,
|
|
39
|
+
(event: { userId: string; isPremium: boolean }) => {
|
|
40
|
+
if (event.userId === userId) {
|
|
41
|
+
queryClient.invalidateQueries({
|
|
42
|
+
queryKey: subscriptionStatusQueryKeys.user(userId),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return unsubscribe;
|
|
49
|
+
}, [userId, queryClient]);
|
|
50
|
+
|
|
51
|
+
const isLoading = status === "pending";
|
|
52
|
+
|
|
49
53
|
return {
|
|
50
54
|
isPremium: data?.isPremium ?? false,
|
|
51
55
|
expirationDate: data?.expirationDate ?? null,
|
|
@@ -54,3 +58,7 @@ export const useSubscriptionStatus = (): SubscriptionStatusResult => {
|
|
|
54
58
|
refetch,
|
|
55
59
|
};
|
|
56
60
|
};
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useProductMetadata Hook
|
|
3
|
-
*
|
|
4
|
-
* TanStack Query hook for fetching product metadata.
|
|
5
|
-
* Generic and reusable - uses config from ProductMetadataService.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { useQuery } from "@umituz/react-native-design-system";
|
|
9
2
|
import { useMemo } from "react";
|
|
10
3
|
import type {
|
|
@@ -14,11 +7,6 @@ import type {
|
|
|
14
7
|
} from "../../domain/types/wallet.types";
|
|
15
8
|
import { ProductMetadataService } from "../../infrastructure/services/ProductMetadataService";
|
|
16
9
|
|
|
17
|
-
const CACHE_CONFIG = {
|
|
18
|
-
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
19
|
-
gcTime: 30 * 60 * 1000, // 30 minutes
|
|
20
|
-
};
|
|
21
|
-
|
|
22
10
|
export const productMetadataQueryKeys = {
|
|
23
11
|
all: ["productMetadata"] as const,
|
|
24
12
|
byType: (type: ProductType) => ["productMetadata", type] as const,
|
|
@@ -44,7 +32,6 @@ export function useProductMetadata({
|
|
|
44
32
|
type,
|
|
45
33
|
enabled = true,
|
|
46
34
|
}: UseProductMetadataParams): UseProductMetadataResult {
|
|
47
|
-
// Memoize service to prevent recreation on every render
|
|
48
35
|
const service = useMemo(
|
|
49
36
|
() => new ProductMetadataService(config),
|
|
50
37
|
[config]
|
|
@@ -54,7 +41,7 @@ export function useProductMetadata({
|
|
|
54
41
|
? productMetadataQueryKeys.byType(type)
|
|
55
42
|
: productMetadataQueryKeys.all;
|
|
56
43
|
|
|
57
|
-
const { data,
|
|
44
|
+
const { data, status, error, refetch } = useQuery({
|
|
58
45
|
queryKey,
|
|
59
46
|
queryFn: async () => {
|
|
60
47
|
if (type) {
|
|
@@ -63,11 +50,10 @@ export function useProductMetadata({
|
|
|
63
50
|
return service.getAll();
|
|
64
51
|
},
|
|
65
52
|
enabled,
|
|
66
|
-
staleTime: CACHE_CONFIG.staleTime,
|
|
67
|
-
gcTime: CACHE_CONFIG.gcTime,
|
|
68
53
|
});
|
|
69
54
|
|
|
70
55
|
const products = data ?? [];
|
|
56
|
+
const isLoading = status === "pending";
|
|
71
57
|
|
|
72
58
|
const creditsPackages = products.filter((p) => p.type === "credits");
|
|
73
59
|
const subscriptionPackages = products.filter((p) => p.type === "subscription");
|
|
@@ -81,3 +67,4 @@ export function useProductMetadata({
|
|
|
81
67
|
subscriptionPackages,
|
|
82
68
|
};
|
|
83
69
|
}
|
|
70
|
+
|
|
@@ -1,16 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useTransactionHistory Hook
|
|
3
|
-
*
|
|
4
|
-
* TanStack Query hook for fetching credit transaction history.
|
|
5
|
-
* Auth info automatically read from @umituz/react-native-auth.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { useQuery } from "@umituz/react-native-design-system";
|
|
9
2
|
import { useMemo } from "react";
|
|
10
|
-
import {
|
|
11
|
-
useAuthStore,
|
|
12
|
-
selectUserId,
|
|
13
|
-
} from "@umituz/react-native-auth";
|
|
3
|
+
import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
|
|
14
4
|
import type {
|
|
15
5
|
CreditLog,
|
|
16
6
|
TransactionRepositoryConfig,
|
|
@@ -63,8 +53,6 @@ export function useTransactionHistory({
|
|
|
63
53
|
return result.data ?? [];
|
|
64
54
|
},
|
|
65
55
|
enabled: !!userId,
|
|
66
|
-
staleTime: 0,
|
|
67
|
-
gcTime: 0,
|
|
68
56
|
});
|
|
69
57
|
|
|
70
58
|
const transactions = data ?? [];
|