@umituz/react-native-subscription 2.11.7 → 2.11.9
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/index.ts +8 -0
- package/src/presentation/components/details/CreditRow.tsx +83 -0
- package/src/presentation/components/details/DetailRow.tsx +53 -0
- package/src/presentation/components/details/PremiumDetailsCard.tsx +5 -155
- package/src/presentation/components/details/PremiumDetailsCardTypes.ts +41 -0
- package/src/presentation/components/paywall/PaywallLegalFooter.tsx +5 -62
- package/src/presentation/components/paywall/PaywallLegalFooterStyles.ts +53 -0
- package/src/presentation/components/paywall/PaywallLegalFooterTypes.ts +19 -0
- package/src/presentation/components/paywall/PaywallModal.tsx +1 -1
- package/src/presentation/components/paywall/SubscriptionFooter.tsx +0 -4
- package/src/presentation/components/paywall/SubscriptionModal.tsx +1 -26
- package/src/presentation/components/paywall/SubscriptionPackageList.tsx +14 -5
- package/src/presentation/components/paywall/SubscriptionPlanCard.tsx +5 -67
- package/src/presentation/components/paywall/SubscriptionPlanCardStyles.ts +61 -0
- package/src/presentation/components/paywall/SubscriptionPlanCardTypes.ts +15 -0
- package/src/presentation/components/paywall/accordion/AccordionPlanCard.tsx +93 -0
- package/src/presentation/components/paywall/accordion/AccordionPlanCardTypes.ts +33 -0
- package/src/presentation/components/paywall/accordion/PlanCardDetails.tsx +101 -0
- package/src/presentation/components/paywall/accordion/PlanCardHeader.tsx +159 -0
- package/src/presentation/components/paywall/accordion/index.ts +12 -0
- package/src/revenuecat/presentation/hooks/subscriptionQueryKeys.ts +16 -0
- package/src/revenuecat/presentation/hooks/useInitializeSubscription.ts +56 -0
- package/src/revenuecat/presentation/hooks/usePurchasePackage.ts +79 -0
- package/src/revenuecat/presentation/hooks/useRestorePurchase.ts +68 -0
- package/src/revenuecat/presentation/hooks/useSubscriptionPackages.ts +44 -0
- package/src/revenuecat/presentation/hooks/useSubscriptionQueries.ts +9 -216
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useState, useCallback } from "react";
|
|
2
2
|
import { View, StyleSheet, ActivityIndicator } from "react-native";
|
|
3
3
|
import { AtomicText } from "@umituz/react-native-design-system";
|
|
4
4
|
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
5
5
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
6
|
-
import {
|
|
6
|
+
import { AccordionPlanCard } from "./accordion";
|
|
7
7
|
import { isYearlyPackage, isMonthlyPackage, isWeeklyPackage } from "../../../utils/packagePeriodUtils";
|
|
8
8
|
|
|
9
9
|
interface SubscriptionPackageListProps {
|
|
@@ -34,6 +34,12 @@ export const SubscriptionPackageList: React.FC<SubscriptionPackageListProps> = R
|
|
|
34
34
|
const hasPackages = packages.length > 0;
|
|
35
35
|
const showLoading = isLoading && !hasPackages;
|
|
36
36
|
|
|
37
|
+
const [expandedPackageId, setExpandedPackageId] = useState<string | null>(null);
|
|
38
|
+
|
|
39
|
+
const handleToggleExpand = useCallback((packageId: string) => {
|
|
40
|
+
setExpandedPackageId((prev) => (prev === packageId ? null : packageId));
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
37
43
|
if (showLoading) {
|
|
38
44
|
return (
|
|
39
45
|
<View style={styles.centerContent}>
|
|
@@ -115,13 +121,16 @@ export const SubscriptionPackageList: React.FC<SubscriptionPackageListProps> = R
|
|
|
115
121
|
};
|
|
116
122
|
|
|
117
123
|
const creditAmount = findCreditAmount();
|
|
124
|
+
const packageId = pkg.product.identifier;
|
|
118
125
|
|
|
119
126
|
return (
|
|
120
|
-
<
|
|
121
|
-
key={
|
|
127
|
+
<AccordionPlanCard
|
|
128
|
+
key={packageId}
|
|
122
129
|
package={pkg}
|
|
123
|
-
isSelected={selectedPkg?.product.identifier ===
|
|
130
|
+
isSelected={selectedPkg?.product.identifier === packageId}
|
|
131
|
+
isExpanded={expandedPackageId === packageId}
|
|
124
132
|
onSelect={() => onSelect(pkg)}
|
|
133
|
+
onToggleExpand={() => handleToggleExpand(packageId)}
|
|
125
134
|
isBestValue={isBestValue}
|
|
126
135
|
creditAmount={creditAmount}
|
|
127
136
|
/>
|
|
@@ -4,25 +4,18 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React from "react";
|
|
7
|
-
import { View, TouchableOpacity
|
|
8
|
-
import type { PurchasesPackage } from "react-native-purchases";
|
|
7
|
+
import { View, TouchableOpacity } from "react-native";
|
|
9
8
|
import { AtomicText } from "@umituz/react-native-design-system";
|
|
10
9
|
import { useAppDesignTokens, withAlpha } from "@umituz/react-native-design-system";
|
|
11
10
|
import { formatPrice } from "../../../utils/priceUtils";
|
|
12
11
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
13
12
|
import { BestValueBadge } from "./BestValueBadge";
|
|
14
|
-
|
|
15
13
|
import { getPeriodLabel, isYearlyPackage } from "../../../utils/packagePeriodUtils";
|
|
16
14
|
import { LinearGradient } from "expo-linear-gradient";
|
|
15
|
+
import type { SubscriptionPlanCardProps } from "./SubscriptionPlanCardTypes";
|
|
16
|
+
import { styles } from "./SubscriptionPlanCardStyles";
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
package: PurchasesPackage;
|
|
20
|
-
isSelected: boolean;
|
|
21
|
-
onSelect: () => void;
|
|
22
|
-
isBestValue?: boolean;
|
|
23
|
-
/** Optional: Number of credits/generations included with this package */
|
|
24
|
-
creditAmount?: number;
|
|
25
|
-
}
|
|
18
|
+
export type { SubscriptionPlanCardProps } from "./SubscriptionPlanCardTypes";
|
|
26
19
|
|
|
27
20
|
export const SubscriptionPlanCard: React.FC<SubscriptionPlanCardProps> =
|
|
28
21
|
React.memo(({ package: pkg, isSelected, onSelect, isBestValue = false, creditAmount }) => {
|
|
@@ -147,59 +140,4 @@ export const SubscriptionPlanCard: React.FC<SubscriptionPlanCardProps> =
|
|
|
147
140
|
});
|
|
148
141
|
|
|
149
142
|
|
|
150
|
-
SubscriptionPlanCard.displayName = "SubscriptionPlanCard";
|
|
151
|
-
|
|
152
|
-
const styles = StyleSheet.create({
|
|
153
|
-
container: {
|
|
154
|
-
borderRadius: 16,
|
|
155
|
-
position: "relative",
|
|
156
|
-
overflow: "hidden", // Important for gradient borders/corners
|
|
157
|
-
},
|
|
158
|
-
gradientWrapper: {
|
|
159
|
-
flex: 1,
|
|
160
|
-
padding: 18,
|
|
161
|
-
},
|
|
162
|
-
content: {
|
|
163
|
-
flexDirection: "row",
|
|
164
|
-
justifyContent: "space-between",
|
|
165
|
-
alignItems: "center",
|
|
166
|
-
},
|
|
167
|
-
leftSection: {
|
|
168
|
-
flexDirection: "row",
|
|
169
|
-
alignItems: "center",
|
|
170
|
-
flex: 1,
|
|
171
|
-
},
|
|
172
|
-
radio: {
|
|
173
|
-
width: 24,
|
|
174
|
-
height: 24,
|
|
175
|
-
borderRadius: 12,
|
|
176
|
-
borderWidth: 2,
|
|
177
|
-
alignItems: "center",
|
|
178
|
-
justifyContent: "center",
|
|
179
|
-
marginRight: 16,
|
|
180
|
-
},
|
|
181
|
-
radioInner: {
|
|
182
|
-
width: 12,
|
|
183
|
-
height: 12,
|
|
184
|
-
borderRadius: 6,
|
|
185
|
-
},
|
|
186
|
-
textContainer: {
|
|
187
|
-
flex: 1,
|
|
188
|
-
},
|
|
189
|
-
title: {
|
|
190
|
-
fontWeight: "600",
|
|
191
|
-
marginBottom: 2,
|
|
192
|
-
},
|
|
193
|
-
creditBadge: {
|
|
194
|
-
paddingHorizontal: 10,
|
|
195
|
-
paddingVertical: 4,
|
|
196
|
-
borderRadius: 12,
|
|
197
|
-
marginBottom: 4,
|
|
198
|
-
},
|
|
199
|
-
rightSection: {
|
|
200
|
-
alignItems: "flex-end",
|
|
201
|
-
},
|
|
202
|
-
price: {
|
|
203
|
-
fontWeight: "700",
|
|
204
|
-
},
|
|
205
|
-
});
|
|
143
|
+
SubscriptionPlanCard.displayName = "SubscriptionPlanCard";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Plan Card Styles
|
|
3
|
+
* StyleSheet definitions for subscription plan card
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { StyleSheet } from "react-native";
|
|
7
|
+
|
|
8
|
+
export const styles = StyleSheet.create({
|
|
9
|
+
container: {
|
|
10
|
+
borderRadius: 16,
|
|
11
|
+
position: "relative",
|
|
12
|
+
overflow: "hidden",
|
|
13
|
+
},
|
|
14
|
+
gradientWrapper: {
|
|
15
|
+
flex: 1,
|
|
16
|
+
padding: 18,
|
|
17
|
+
},
|
|
18
|
+
content: {
|
|
19
|
+
flexDirection: "row",
|
|
20
|
+
justifyContent: "space-between",
|
|
21
|
+
alignItems: "center",
|
|
22
|
+
},
|
|
23
|
+
leftSection: {
|
|
24
|
+
flexDirection: "row",
|
|
25
|
+
alignItems: "center",
|
|
26
|
+
flex: 1,
|
|
27
|
+
},
|
|
28
|
+
radio: {
|
|
29
|
+
width: 24,
|
|
30
|
+
height: 24,
|
|
31
|
+
borderRadius: 12,
|
|
32
|
+
borderWidth: 2,
|
|
33
|
+
alignItems: "center",
|
|
34
|
+
justifyContent: "center",
|
|
35
|
+
marginRight: 16,
|
|
36
|
+
},
|
|
37
|
+
radioInner: {
|
|
38
|
+
width: 12,
|
|
39
|
+
height: 12,
|
|
40
|
+
borderRadius: 6,
|
|
41
|
+
},
|
|
42
|
+
textContainer: {
|
|
43
|
+
flex: 1,
|
|
44
|
+
},
|
|
45
|
+
title: {
|
|
46
|
+
fontWeight: "600",
|
|
47
|
+
marginBottom: 2,
|
|
48
|
+
},
|
|
49
|
+
creditBadge: {
|
|
50
|
+
paddingHorizontal: 10,
|
|
51
|
+
paddingVertical: 4,
|
|
52
|
+
borderRadius: 12,
|
|
53
|
+
marginBottom: 4,
|
|
54
|
+
},
|
|
55
|
+
rightSection: {
|
|
56
|
+
alignItems: "flex-end",
|
|
57
|
+
},
|
|
58
|
+
price: {
|
|
59
|
+
fontWeight: "700",
|
|
60
|
+
},
|
|
61
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Plan Card Types
|
|
3
|
+
* Type definitions for subscription plan display
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
7
|
+
|
|
8
|
+
export interface SubscriptionPlanCardProps {
|
|
9
|
+
package: PurchasesPackage;
|
|
10
|
+
isSelected: boolean;
|
|
11
|
+
onSelect: () => void;
|
|
12
|
+
isBestValue?: boolean;
|
|
13
|
+
/** Optional: Number of credits/generations included with this package */
|
|
14
|
+
creditAmount?: number;
|
|
15
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accordion Plan Card
|
|
3
|
+
* Expandable subscription plan card with credit display
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useCallback } from "react";
|
|
7
|
+
import { View, StyleSheet, type StyleProp, type ViewStyle } from "react-native";
|
|
8
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
9
|
+
import { formatPrice } from "../../../../utils/priceUtils";
|
|
10
|
+
import { getPeriodLabel, isYearlyPackage } from "../../../../utils/packagePeriodUtils";
|
|
11
|
+
import { useLocalization } from "@umituz/react-native-localization";
|
|
12
|
+
import { PlanCardHeader } from "./PlanCardHeader";
|
|
13
|
+
import { PlanCardDetails } from "./PlanCardDetails";
|
|
14
|
+
import type { AccordionPlanCardProps } from "./AccordionPlanCardTypes";
|
|
15
|
+
|
|
16
|
+
export const AccordionPlanCard: React.FC<AccordionPlanCardProps> = React.memo(
|
|
17
|
+
({
|
|
18
|
+
package: pkg,
|
|
19
|
+
isSelected,
|
|
20
|
+
isExpanded,
|
|
21
|
+
onSelect,
|
|
22
|
+
onToggleExpand,
|
|
23
|
+
isBestValue = false,
|
|
24
|
+
creditAmount,
|
|
25
|
+
}) => {
|
|
26
|
+
const tokens = useAppDesignTokens();
|
|
27
|
+
const { t } = useLocalization();
|
|
28
|
+
|
|
29
|
+
const period = pkg.product.subscriptionPeriod;
|
|
30
|
+
const isYearly = isYearlyPackage(pkg);
|
|
31
|
+
const periodLabel = getPeriodLabel(period);
|
|
32
|
+
const price = formatPrice(pkg.product.price, pkg.product.currencyCode);
|
|
33
|
+
const monthlyEquivalent = isYearly
|
|
34
|
+
? formatPrice(pkg.product.price / 12, pkg.product.currencyCode)
|
|
35
|
+
: null;
|
|
36
|
+
|
|
37
|
+
const title = pkg.product.title || t(`paywall.period.${periodLabel}`);
|
|
38
|
+
const displayPrice = isYearly && monthlyEquivalent
|
|
39
|
+
? `${monthlyEquivalent}/mo`
|
|
40
|
+
: price;
|
|
41
|
+
|
|
42
|
+
const handleHeaderPress = useCallback(() => {
|
|
43
|
+
onSelect();
|
|
44
|
+
if (!isExpanded) {
|
|
45
|
+
onToggleExpand();
|
|
46
|
+
}
|
|
47
|
+
}, [onSelect, onToggleExpand, isExpanded]);
|
|
48
|
+
|
|
49
|
+
const containerStyle: StyleProp<ViewStyle> = [
|
|
50
|
+
styles.container,
|
|
51
|
+
{
|
|
52
|
+
borderColor: isSelected
|
|
53
|
+
? tokens.colors.primary
|
|
54
|
+
: tokens.colors.borderLight,
|
|
55
|
+
borderWidth: isSelected ? 2 : 1,
|
|
56
|
+
backgroundColor: tokens.colors.surface,
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<View style={containerStyle}>
|
|
62
|
+
<PlanCardHeader
|
|
63
|
+
title={title}
|
|
64
|
+
price={displayPrice}
|
|
65
|
+
creditAmount={creditAmount}
|
|
66
|
+
isSelected={isSelected}
|
|
67
|
+
isExpanded={isExpanded}
|
|
68
|
+
isBestValue={isBestValue}
|
|
69
|
+
onToggle={handleHeaderPress}
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
{isExpanded && (
|
|
73
|
+
<PlanCardDetails
|
|
74
|
+
fullPrice={price}
|
|
75
|
+
monthlyEquivalent={monthlyEquivalent}
|
|
76
|
+
periodLabel={periodLabel}
|
|
77
|
+
isYearly={isYearly}
|
|
78
|
+
/>
|
|
79
|
+
)}
|
|
80
|
+
</View>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
AccordionPlanCard.displayName = "AccordionPlanCard";
|
|
86
|
+
|
|
87
|
+
const styles = StyleSheet.create({
|
|
88
|
+
container: {
|
|
89
|
+
borderRadius: 16,
|
|
90
|
+
overflow: "hidden",
|
|
91
|
+
marginBottom: 12,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accordion Plan Card Types
|
|
3
|
+
* Type definitions for accordion-style subscription cards
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
7
|
+
|
|
8
|
+
export interface AccordionPlanCardProps {
|
|
9
|
+
package: PurchasesPackage;
|
|
10
|
+
isSelected: boolean;
|
|
11
|
+
isExpanded: boolean;
|
|
12
|
+
onSelect: () => void;
|
|
13
|
+
onToggleExpand: () => void;
|
|
14
|
+
isBestValue?: boolean;
|
|
15
|
+
creditAmount?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PlanCardHeaderProps {
|
|
19
|
+
title: string;
|
|
20
|
+
price: string;
|
|
21
|
+
creditAmount?: number;
|
|
22
|
+
isSelected: boolean;
|
|
23
|
+
isExpanded: boolean;
|
|
24
|
+
isBestValue?: boolean;
|
|
25
|
+
onToggle: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PlanCardDetailsProps {
|
|
29
|
+
fullPrice: string;
|
|
30
|
+
monthlyEquivalent: string | null;
|
|
31
|
+
periodLabel: string;
|
|
32
|
+
isYearly: boolean;
|
|
33
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Card Details
|
|
3
|
+
* Expanded state of accordion subscription card
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
useAppDesignTokens,
|
|
11
|
+
} from "@umituz/react-native-design-system";
|
|
12
|
+
import { useLocalization } from "@umituz/react-native-localization";
|
|
13
|
+
import type { PlanCardDetailsProps } from "./AccordionPlanCardTypes";
|
|
14
|
+
|
|
15
|
+
export const PlanCardDetails: React.FC<PlanCardDetailsProps> = ({
|
|
16
|
+
fullPrice,
|
|
17
|
+
monthlyEquivalent,
|
|
18
|
+
periodLabel,
|
|
19
|
+
isYearly,
|
|
20
|
+
}) => {
|
|
21
|
+
const tokens = useAppDesignTokens();
|
|
22
|
+
const { t } = useLocalization();
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<View
|
|
26
|
+
style={[
|
|
27
|
+
styles.container,
|
|
28
|
+
{
|
|
29
|
+
backgroundColor: tokens.colors.surfaceSecondary,
|
|
30
|
+
borderTopColor: tokens.colors.border,
|
|
31
|
+
},
|
|
32
|
+
]}
|
|
33
|
+
>
|
|
34
|
+
<View style={styles.row}>
|
|
35
|
+
<AtomicText
|
|
36
|
+
type="bodyMedium"
|
|
37
|
+
style={{ color: tokens.colors.textSecondary }}
|
|
38
|
+
>
|
|
39
|
+
{t("paywall.billingPeriod") || "Billing Period"}
|
|
40
|
+
</AtomicText>
|
|
41
|
+
<AtomicText
|
|
42
|
+
type="bodyMedium"
|
|
43
|
+
style={{ color: tokens.colors.textPrimary, fontWeight: "600" }}
|
|
44
|
+
>
|
|
45
|
+
{t(`paywall.period.${periodLabel}`)}
|
|
46
|
+
</AtomicText>
|
|
47
|
+
</View>
|
|
48
|
+
|
|
49
|
+
{isYearly && (
|
|
50
|
+
<View style={styles.row}>
|
|
51
|
+
<AtomicText
|
|
52
|
+
type="bodyMedium"
|
|
53
|
+
style={{ color: tokens.colors.textSecondary }}
|
|
54
|
+
>
|
|
55
|
+
{t("paywall.totalPrice") || "Total Price"}
|
|
56
|
+
</AtomicText>
|
|
57
|
+
<AtomicText
|
|
58
|
+
type="bodyMedium"
|
|
59
|
+
style={{ color: tokens.colors.textPrimary, fontWeight: "600" }}
|
|
60
|
+
>
|
|
61
|
+
{fullPrice}
|
|
62
|
+
</AtomicText>
|
|
63
|
+
</View>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
{monthlyEquivalent && (
|
|
67
|
+
<View style={styles.row}>
|
|
68
|
+
<AtomicText
|
|
69
|
+
type="bodyMedium"
|
|
70
|
+
style={{ color: tokens.colors.textSecondary }}
|
|
71
|
+
>
|
|
72
|
+
{t("paywall.perMonth") || "Per Month"}
|
|
73
|
+
</AtomicText>
|
|
74
|
+
<AtomicText
|
|
75
|
+
type="bodyMedium"
|
|
76
|
+
style={{
|
|
77
|
+
color: tokens.colors.primary,
|
|
78
|
+
fontWeight: "700",
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{monthlyEquivalent}
|
|
82
|
+
</AtomicText>
|
|
83
|
+
</View>
|
|
84
|
+
)}
|
|
85
|
+
</View>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const styles = StyleSheet.create({
|
|
90
|
+
container: {
|
|
91
|
+
paddingHorizontal: 16,
|
|
92
|
+
paddingVertical: 12,
|
|
93
|
+
borderTopWidth: 1,
|
|
94
|
+
gap: 10,
|
|
95
|
+
},
|
|
96
|
+
row: {
|
|
97
|
+
flexDirection: "row",
|
|
98
|
+
justifyContent: "space-between",
|
|
99
|
+
alignItems: "center",
|
|
100
|
+
},
|
|
101
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Card Header
|
|
3
|
+
* Collapsed state of accordion subscription card
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
AtomicText,
|
|
10
|
+
AtomicIcon,
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
withAlpha,
|
|
13
|
+
} from "@umituz/react-native-design-system";
|
|
14
|
+
import { BestValueBadge } from "../BestValueBadge";
|
|
15
|
+
import { useLocalization } from "@umituz/react-native-localization";
|
|
16
|
+
import type { PlanCardHeaderProps } from "./AccordionPlanCardTypes";
|
|
17
|
+
|
|
18
|
+
export const PlanCardHeader: React.FC<PlanCardHeaderProps> = ({
|
|
19
|
+
title,
|
|
20
|
+
price,
|
|
21
|
+
creditAmount,
|
|
22
|
+
isSelected,
|
|
23
|
+
isExpanded,
|
|
24
|
+
isBestValue,
|
|
25
|
+
onToggle,
|
|
26
|
+
}) => {
|
|
27
|
+
const tokens = useAppDesignTokens();
|
|
28
|
+
const { t } = useLocalization();
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<TouchableOpacity
|
|
32
|
+
onPress={onToggle}
|
|
33
|
+
activeOpacity={0.7}
|
|
34
|
+
style={styles.container}
|
|
35
|
+
>
|
|
36
|
+
<BestValueBadge text={t("paywall.bestValue")} visible={isBestValue} />
|
|
37
|
+
|
|
38
|
+
<View style={styles.content}>
|
|
39
|
+
<View style={styles.leftSection}>
|
|
40
|
+
<View
|
|
41
|
+
style={[
|
|
42
|
+
styles.radio,
|
|
43
|
+
{
|
|
44
|
+
borderColor: isSelected
|
|
45
|
+
? tokens.colors.primary
|
|
46
|
+
: tokens.colors.border,
|
|
47
|
+
},
|
|
48
|
+
]}
|
|
49
|
+
>
|
|
50
|
+
{isSelected && (
|
|
51
|
+
<View
|
|
52
|
+
style={[
|
|
53
|
+
styles.radioInner,
|
|
54
|
+
{ backgroundColor: tokens.colors.primary },
|
|
55
|
+
]}
|
|
56
|
+
/>
|
|
57
|
+
)}
|
|
58
|
+
</View>
|
|
59
|
+
|
|
60
|
+
<View style={styles.textContainer}>
|
|
61
|
+
<AtomicText
|
|
62
|
+
type="titleSmall"
|
|
63
|
+
style={{ color: tokens.colors.textPrimary, fontWeight: "600" }}
|
|
64
|
+
>
|
|
65
|
+
{title}
|
|
66
|
+
</AtomicText>
|
|
67
|
+
{creditAmount && (
|
|
68
|
+
<View
|
|
69
|
+
style={[
|
|
70
|
+
styles.creditBadge,
|
|
71
|
+
{
|
|
72
|
+
backgroundColor: withAlpha(tokens.colors.primary, 0.15),
|
|
73
|
+
borderColor: withAlpha(tokens.colors.primary, 0.3),
|
|
74
|
+
},
|
|
75
|
+
]}
|
|
76
|
+
>
|
|
77
|
+
<AtomicText
|
|
78
|
+
type="labelSmall"
|
|
79
|
+
style={{
|
|
80
|
+
color: tokens.colors.primary,
|
|
81
|
+
fontWeight: "700",
|
|
82
|
+
fontSize: 11,
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
{creditAmount} {t("paywall.credits") || "Credits"}
|
|
86
|
+
</AtomicText>
|
|
87
|
+
</View>
|
|
88
|
+
)}
|
|
89
|
+
</View>
|
|
90
|
+
</View>
|
|
91
|
+
|
|
92
|
+
<View style={styles.rightSection}>
|
|
93
|
+
<AtomicText
|
|
94
|
+
type="titleMedium"
|
|
95
|
+
style={{
|
|
96
|
+
color: tokens.colors.textPrimary,
|
|
97
|
+
fontWeight: "700",
|
|
98
|
+
marginRight: 8,
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{price}
|
|
102
|
+
</AtomicText>
|
|
103
|
+
<AtomicIcon
|
|
104
|
+
name={isExpanded ? "ChevronUp" : "ChevronDown"}
|
|
105
|
+
size={20}
|
|
106
|
+
color={tokens.colors.textSecondary}
|
|
107
|
+
/>
|
|
108
|
+
</View>
|
|
109
|
+
</View>
|
|
110
|
+
</TouchableOpacity>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const styles = StyleSheet.create({
|
|
115
|
+
container: {
|
|
116
|
+
width: "100%",
|
|
117
|
+
},
|
|
118
|
+
content: {
|
|
119
|
+
flexDirection: "row",
|
|
120
|
+
alignItems: "center",
|
|
121
|
+
justifyContent: "space-between",
|
|
122
|
+
paddingVertical: 16,
|
|
123
|
+
paddingHorizontal: 16,
|
|
124
|
+
},
|
|
125
|
+
leftSection: {
|
|
126
|
+
flexDirection: "row",
|
|
127
|
+
alignItems: "center",
|
|
128
|
+
flex: 1,
|
|
129
|
+
},
|
|
130
|
+
radio: {
|
|
131
|
+
width: 22,
|
|
132
|
+
height: 22,
|
|
133
|
+
borderRadius: 11,
|
|
134
|
+
borderWidth: 2,
|
|
135
|
+
alignItems: "center",
|
|
136
|
+
justifyContent: "center",
|
|
137
|
+
marginRight: 12,
|
|
138
|
+
},
|
|
139
|
+
radioInner: {
|
|
140
|
+
width: 12,
|
|
141
|
+
height: 12,
|
|
142
|
+
borderRadius: 6,
|
|
143
|
+
},
|
|
144
|
+
textContainer: {
|
|
145
|
+
flex: 1,
|
|
146
|
+
gap: 6,
|
|
147
|
+
},
|
|
148
|
+
creditBadge: {
|
|
149
|
+
paddingHorizontal: 10,
|
|
150
|
+
paddingVertical: 4,
|
|
151
|
+
borderRadius: 12,
|
|
152
|
+
borderWidth: 1,
|
|
153
|
+
alignSelf: "flex-start",
|
|
154
|
+
},
|
|
155
|
+
rightSection: {
|
|
156
|
+
flexDirection: "row",
|
|
157
|
+
alignItems: "center",
|
|
158
|
+
},
|
|
159
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accordion Components Barrel Export
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { AccordionPlanCard } from "./AccordionPlanCard";
|
|
6
|
+
export { PlanCardHeader } from "./PlanCardHeader";
|
|
7
|
+
export { PlanCardDetails } from "./PlanCardDetails";
|
|
8
|
+
export type {
|
|
9
|
+
AccordionPlanCardProps,
|
|
10
|
+
PlanCardHeaderProps,
|
|
11
|
+
PlanCardDetailsProps,
|
|
12
|
+
} from "./AccordionPlanCardTypes";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Query Keys
|
|
3
|
+
* TanStack Query keys and constants for subscription state
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Query keys for TanStack Query
|
|
8
|
+
*/
|
|
9
|
+
export const SUBSCRIPTION_QUERY_KEYS = {
|
|
10
|
+
packages: ["subscription", "packages"] as const,
|
|
11
|
+
initialized: (userId: string) =>
|
|
12
|
+
["subscription", "initialized", userId] as const,
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export const STALE_TIME = 5 * 60 * 1000; // 5 minutes
|
|
16
|
+
export const GC_TIME = 30 * 60 * 1000; // 30 minutes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initialize Subscription Hook
|
|
3
|
+
* TanStack mutation for initializing RevenueCat
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
7
|
+
import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
|
|
8
|
+
import {
|
|
9
|
+
trackPackageError,
|
|
10
|
+
addPackageBreadcrumb,
|
|
11
|
+
} from "@umituz/react-native-sentry";
|
|
12
|
+
import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Initialize subscription with RevenueCat
|
|
16
|
+
*/
|
|
17
|
+
export const useInitializeSubscription = (userId: string | undefined) => {
|
|
18
|
+
const queryClient = useQueryClient();
|
|
19
|
+
|
|
20
|
+
return useMutation({
|
|
21
|
+
mutationFn: async () => {
|
|
22
|
+
if (!userId) {
|
|
23
|
+
throw new Error("User not authenticated");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
addPackageBreadcrumb("subscription", "Initialize mutation started", {
|
|
27
|
+
userId,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return SubscriptionManager.initialize(userId);
|
|
31
|
+
},
|
|
32
|
+
onSuccess: () => {
|
|
33
|
+
if (userId) {
|
|
34
|
+
queryClient.invalidateQueries({
|
|
35
|
+
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
addPackageBreadcrumb(
|
|
39
|
+
"subscription",
|
|
40
|
+
"Initialize mutation success - packages invalidated",
|
|
41
|
+
{ userId }
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
onError: (error) => {
|
|
46
|
+
trackPackageError(
|
|
47
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
48
|
+
{
|
|
49
|
+
packageName: "subscription",
|
|
50
|
+
operation: "initialize_mutation",
|
|
51
|
+
userId: userId ?? "NO_USER",
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
};
|