@umituz/react-native-subscription 2.12.2 → 2.12.4
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 +2 -2
- package/src/domains/index.ts +5 -0
- package/src/domains/paywall/components/CreditCard.tsx +117 -0
- package/src/domains/paywall/components/FeatureItem.tsx +50 -0
- package/src/domains/paywall/components/FeatureList.tsx +34 -0
- package/src/domains/paywall/components/PaywallFooter.tsx +98 -0
- package/src/{presentation/components/paywall/PaywallHeroHeader.tsx → domains/paywall/components/PaywallHeader.tsx} +15 -44
- package/src/domains/paywall/components/PaywallModal.tsx +187 -0
- package/src/domains/paywall/components/PaywallTabBar.tsx +102 -0
- package/src/domains/paywall/components/PlanCard.tsx +124 -0
- package/src/domains/paywall/components/index.ts +14 -0
- package/src/domains/paywall/entities/index.ts +5 -0
- package/src/domains/paywall/entities/types.ts +48 -0
- package/src/domains/paywall/hooks/index.ts +6 -0
- package/src/{presentation → domains/paywall}/hooks/usePaywall.ts +1 -1
- package/src/domains/paywall/index.ts +13 -0
- package/src/index.ts +15 -22
- package/src/presentation/components/details/PremiumDetailsCard.tsx +35 -11
- package/src/domain/entities/paywall/CreditsPackage.ts +0 -16
- package/src/domain/entities/paywall/PaywallMode.ts +0 -6
- package/src/domain/entities/paywall/PaywallTab.ts +0 -11
- package/src/domain/entities/paywall/SubscriptionPlan.ts +0 -27
- package/src/presentation/components/paywall/BestValueBadge.tsx +0 -59
- package/src/presentation/components/paywall/CreditsPackageCard.tsx +0 -161
- package/src/presentation/components/paywall/CreditsTabContent.tsx +0 -123
- package/src/presentation/components/paywall/PaywallFeatureItem.tsx +0 -79
- package/src/presentation/components/paywall/PaywallFeaturesList.tsx +0 -47
- package/src/presentation/components/paywall/PaywallHeader.tsx +0 -82
- package/src/presentation/components/paywall/PaywallLegalFooter.tsx +0 -145
- package/src/presentation/components/paywall/PaywallLegalFooterStyles.ts +0 -53
- package/src/presentation/components/paywall/PaywallLegalFooterTypes.ts +0 -19
- package/src/presentation/components/paywall/PaywallModal.tsx +0 -162
- package/src/presentation/components/paywall/PaywallTabBar.tsx +0 -120
- package/src/presentation/components/paywall/SubscriptionFooter.tsx +0 -116
- package/src/presentation/components/paywall/SubscriptionModal.tsx +0 -168
- package/src/presentation/components/paywall/SubscriptionModalHeader.tsx +0 -78
- package/src/presentation/components/paywall/SubscriptionPackageList.tsx +0 -171
- package/src/presentation/components/paywall/SubscriptionPlanCard.tsx +0 -213
- package/src/presentation/components/paywall/SubscriptionPlanCardStyles.ts +0 -61
- package/src/presentation/components/paywall/SubscriptionPlanCardTypes.ts +0 -15
- package/src/presentation/components/paywall/SubscriptionTabContent.tsx +0 -139
- package/src/presentation/components/paywall/accordion/AccordionPlanCard.tsx +0 -98
- package/src/presentation/components/paywall/accordion/AccordionPlanCardTypes.ts +0 -39
- package/src/presentation/components/paywall/accordion/PlanCardDetails.tsx +0 -107
- package/src/presentation/components/paywall/accordion/PlanCardHeader.tsx +0 -155
- package/src/presentation/components/paywall/accordion/index.ts +0 -12
- /package/src/{presentation → domains/paywall}/hooks/useSubscriptionModal.ts +0 -0
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import React, { useState, useCallback } from "react";
|
|
2
|
-
import { View, StyleSheet, ActivityIndicator } from "react-native";
|
|
3
|
-
import { AtomicText } from "@umituz/react-native-design-system";
|
|
4
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
5
|
-
import type { PurchasesPackage } from "react-native-purchases";
|
|
6
|
-
import { AccordionPlanCard } from "./accordion";
|
|
7
|
-
import { isYearlyPackage, isMonthlyPackage, isWeeklyPackage } from "../../../utils/packagePeriodUtils";
|
|
8
|
-
|
|
9
|
-
interface SubscriptionPackageListProps {
|
|
10
|
-
isLoading: boolean;
|
|
11
|
-
packages: PurchasesPackage[];
|
|
12
|
-
selectedPkg: PurchasesPackage | null;
|
|
13
|
-
loadingText: string;
|
|
14
|
-
emptyText: string;
|
|
15
|
-
onSelect: (pkg: PurchasesPackage) => void;
|
|
16
|
-
/** Optional: Manually specify which package should show "Best Value" badge by identifier */
|
|
17
|
-
bestValueIdentifier?: string;
|
|
18
|
-
/** Optional: Map of product identifier to credit amount (e.g., { "weekly": 6, "monthly": 25, "yearly": 300 }) */
|
|
19
|
-
creditAmounts?: Record<string, number>;
|
|
20
|
-
/** Optional: Text labels for accordion details */
|
|
21
|
-
billingPeriodLabel?: string;
|
|
22
|
-
totalPriceLabel?: string;
|
|
23
|
-
perMonthLabel?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const SubscriptionPackageList: React.FC<SubscriptionPackageListProps> = React.memo(
|
|
27
|
-
({
|
|
28
|
-
isLoading,
|
|
29
|
-
packages,
|
|
30
|
-
selectedPkg,
|
|
31
|
-
loadingText,
|
|
32
|
-
emptyText,
|
|
33
|
-
onSelect,
|
|
34
|
-
bestValueIdentifier,
|
|
35
|
-
creditAmounts,
|
|
36
|
-
billingPeriodLabel,
|
|
37
|
-
totalPriceLabel,
|
|
38
|
-
perMonthLabel,
|
|
39
|
-
}) => {
|
|
40
|
-
const tokens = useAppDesignTokens();
|
|
41
|
-
const hasPackages = packages.length > 0;
|
|
42
|
-
const showLoading = isLoading && !hasPackages;
|
|
43
|
-
|
|
44
|
-
const [expandedPackageId, setExpandedPackageId] = useState<string | null>(null);
|
|
45
|
-
|
|
46
|
-
const handleToggleExpand = useCallback((packageId: string) => {
|
|
47
|
-
setExpandedPackageId((prev) => (prev === packageId ? null : packageId));
|
|
48
|
-
}, []);
|
|
49
|
-
|
|
50
|
-
if (showLoading) {
|
|
51
|
-
return (
|
|
52
|
-
<View style={styles.centerContent}>
|
|
53
|
-
<ActivityIndicator size="large" color={tokens.colors.primary} />
|
|
54
|
-
<AtomicText
|
|
55
|
-
type="bodyMedium"
|
|
56
|
-
style={[styles.loadingText, { color: tokens.colors.textSecondary }]}
|
|
57
|
-
>
|
|
58
|
-
{loadingText}
|
|
59
|
-
</AtomicText>
|
|
60
|
-
</View>
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (!hasPackages) {
|
|
65
|
-
return (
|
|
66
|
-
<View style={styles.centerContent}>
|
|
67
|
-
<AtomicText
|
|
68
|
-
type="bodyMedium"
|
|
69
|
-
style={[styles.emptyText, { color: tokens.colors.textSecondary }]}
|
|
70
|
-
>
|
|
71
|
-
{emptyText}
|
|
72
|
-
</AtomicText>
|
|
73
|
-
</View>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<View style={styles.packagesContainer}>
|
|
79
|
-
{packages.map((pkg) => {
|
|
80
|
-
// Determine if this package should show "Best Value" badge
|
|
81
|
-
let isBestValue = false;
|
|
82
|
-
|
|
83
|
-
if (bestValueIdentifier) {
|
|
84
|
-
// Use manual override if provided
|
|
85
|
-
isBestValue = pkg.product.identifier === bestValueIdentifier;
|
|
86
|
-
} else {
|
|
87
|
-
// Auto-detect: mark yearly packages as best value
|
|
88
|
-
isBestValue = isYearlyPackage(pkg);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Smart matching for credit amounts
|
|
92
|
-
const findCreditAmount = () => {
|
|
93
|
-
if (!creditAmounts) return undefined;
|
|
94
|
-
|
|
95
|
-
const productId = pkg.product.identifier;
|
|
96
|
-
const packageId = pkg.identifier;
|
|
97
|
-
const productTitle = pkg.product.title || "";
|
|
98
|
-
|
|
99
|
-
// 1. Exact match
|
|
100
|
-
if (creditAmounts[productId] !== undefined) return creditAmounts[productId];
|
|
101
|
-
if (creditAmounts[packageId] !== undefined) return creditAmounts[packageId];
|
|
102
|
-
|
|
103
|
-
// 2. Case-insensitive and Title matching
|
|
104
|
-
const lowerProductId = productId.toLowerCase();
|
|
105
|
-
const lowerPackageId = packageId.toLowerCase();
|
|
106
|
-
const lowerTitle = productTitle.toLowerCase();
|
|
107
|
-
|
|
108
|
-
for (const [key, value] of Object.entries(creditAmounts)) {
|
|
109
|
-
const lowerKey = key.toLowerCase();
|
|
110
|
-
if (lowerProductId === lowerKey || lowerPackageId === lowerKey || lowerTitle.includes(lowerKey)) {
|
|
111
|
-
return value;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// 3. Period-based fallback
|
|
116
|
-
const isYearly = isYearlyPackage(pkg);
|
|
117
|
-
const isMonthly = isMonthlyPackage(pkg);
|
|
118
|
-
const isWeekly = isWeeklyPackage(pkg);
|
|
119
|
-
|
|
120
|
-
for (const [key, value] of Object.entries(creditAmounts)) {
|
|
121
|
-
const lowerKey = key.toLowerCase();
|
|
122
|
-
if (isYearly && (lowerKey.includes("year") || lowerKey.includes("annual") || lowerKey === "yearly")) return value;
|
|
123
|
-
if (isMonthly && (lowerKey.includes("month") || lowerKey === "monthly")) return value;
|
|
124
|
-
if (isWeekly && (lowerKey.includes("week") || lowerKey === "weekly")) return value;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return undefined;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const creditAmount = findCreditAmount();
|
|
131
|
-
const packageId = pkg.product.identifier;
|
|
132
|
-
|
|
133
|
-
return (
|
|
134
|
-
<AccordionPlanCard
|
|
135
|
-
key={packageId}
|
|
136
|
-
package={pkg}
|
|
137
|
-
isSelected={selectedPkg?.product.identifier === packageId}
|
|
138
|
-
isExpanded={expandedPackageId === packageId}
|
|
139
|
-
onSelect={() => onSelect(pkg)}
|
|
140
|
-
onToggleExpand={() => handleToggleExpand(packageId)}
|
|
141
|
-
isBestValue={isBestValue}
|
|
142
|
-
creditAmount={creditAmount}
|
|
143
|
-
billingPeriodLabel={billingPeriodLabel}
|
|
144
|
-
totalPriceLabel={totalPriceLabel}
|
|
145
|
-
perMonthLabel={perMonthLabel}
|
|
146
|
-
/>
|
|
147
|
-
);
|
|
148
|
-
})}
|
|
149
|
-
</View>
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
SubscriptionPackageList.displayName = "SubscriptionPackageList";
|
|
155
|
-
|
|
156
|
-
const styles = StyleSheet.create({
|
|
157
|
-
centerContent: {
|
|
158
|
-
alignItems: "center",
|
|
159
|
-
paddingVertical: 40
|
|
160
|
-
},
|
|
161
|
-
loadingText: {
|
|
162
|
-
marginTop: 16
|
|
163
|
-
},
|
|
164
|
-
emptyText: {
|
|
165
|
-
textAlign: "center"
|
|
166
|
-
},
|
|
167
|
-
packagesContainer: {
|
|
168
|
-
gap: 12,
|
|
169
|
-
marginBottom: 20
|
|
170
|
-
},
|
|
171
|
-
});
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subscription Plan Card Component
|
|
3
|
-
* Single Responsibility: Display a subscription plan option
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React, { useMemo } from "react";
|
|
7
|
-
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
-
import { AtomicText } from "@umituz/react-native-design-system";
|
|
9
|
-
import { useAppDesignTokens, withAlpha, useResponsive } from "@umituz/react-native-design-system";
|
|
10
|
-
import { formatPrice } from "../../../utils/priceUtils";
|
|
11
|
-
import { useLocalization } from "@umituz/react-native-localization";
|
|
12
|
-
import { BestValueBadge } from "./BestValueBadge";
|
|
13
|
-
import { getPeriodLabel, isYearlyPackage } from "../../../utils/packagePeriodUtils";
|
|
14
|
-
import { LinearGradient } from "expo-linear-gradient";
|
|
15
|
-
import type { SubscriptionPlanCardProps } from "./SubscriptionPlanCardTypes";
|
|
16
|
-
|
|
17
|
-
export type { SubscriptionPlanCardProps } from "./SubscriptionPlanCardTypes";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Create responsive styles for subscription plan card
|
|
21
|
-
*/
|
|
22
|
-
const createStyles = (spacingMult: number, touchTarget: number) => {
|
|
23
|
-
const basePadding = 18;
|
|
24
|
-
const baseRadius = 16;
|
|
25
|
-
const baseCreditRadius = 12;
|
|
26
|
-
|
|
27
|
-
const radioSize = Math.max(touchTarget * 0.5, 24);
|
|
28
|
-
const radioInnerSize = radioSize * 0.5;
|
|
29
|
-
|
|
30
|
-
return StyleSheet.create({
|
|
31
|
-
container: {
|
|
32
|
-
borderRadius: baseRadius * spacingMult,
|
|
33
|
-
position: "relative",
|
|
34
|
-
overflow: "hidden",
|
|
35
|
-
},
|
|
36
|
-
gradientWrapper: {
|
|
37
|
-
flex: 1,
|
|
38
|
-
padding: basePadding * spacingMult,
|
|
39
|
-
},
|
|
40
|
-
content: {
|
|
41
|
-
flexDirection: "row",
|
|
42
|
-
justifyContent: "space-between",
|
|
43
|
-
alignItems: "center",
|
|
44
|
-
},
|
|
45
|
-
leftSection: {
|
|
46
|
-
flexDirection: "row",
|
|
47
|
-
alignItems: "center",
|
|
48
|
-
flex: 1,
|
|
49
|
-
},
|
|
50
|
-
radio: {
|
|
51
|
-
width: radioSize,
|
|
52
|
-
height: radioSize,
|
|
53
|
-
borderRadius: radioSize / 2,
|
|
54
|
-
borderWidth: 2,
|
|
55
|
-
alignItems: "center",
|
|
56
|
-
justifyContent: "center",
|
|
57
|
-
marginRight: 16 * spacingMult,
|
|
58
|
-
},
|
|
59
|
-
radioInner: {
|
|
60
|
-
width: radioInnerSize,
|
|
61
|
-
height: radioInnerSize,
|
|
62
|
-
borderRadius: radioInnerSize / 2,
|
|
63
|
-
},
|
|
64
|
-
textContainer: {
|
|
65
|
-
flex: 1,
|
|
66
|
-
},
|
|
67
|
-
title: {
|
|
68
|
-
fontWeight: "600",
|
|
69
|
-
marginBottom: 2 * spacingMult,
|
|
70
|
-
},
|
|
71
|
-
creditBadge: {
|
|
72
|
-
paddingHorizontal: 10 * spacingMult,
|
|
73
|
-
paddingVertical: 4 * spacingMult,
|
|
74
|
-
borderRadius: baseCreditRadius * spacingMult,
|
|
75
|
-
marginBottom: 4 * spacingMult,
|
|
76
|
-
},
|
|
77
|
-
rightSection: {
|
|
78
|
-
alignItems: "flex-end",
|
|
79
|
-
},
|
|
80
|
-
price: {
|
|
81
|
-
fontWeight: "700",
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
export const SubscriptionPlanCard: React.FC<SubscriptionPlanCardProps> =
|
|
87
|
-
React.memo(({ package: pkg, isSelected, onSelect, isBestValue = false, creditAmount }) => {
|
|
88
|
-
const tokens = useAppDesignTokens();
|
|
89
|
-
const { t } = useLocalization();
|
|
90
|
-
const { spacingMultiplier, getFontSize, minTouchTarget } = useResponsive();
|
|
91
|
-
|
|
92
|
-
const period = pkg.product.subscriptionPeriod;
|
|
93
|
-
const isYearly = isYearlyPackage(pkg);
|
|
94
|
-
const periodLabel = getPeriodLabel(period);
|
|
95
|
-
const price = formatPrice(pkg.product.price, pkg.product.currencyCode);
|
|
96
|
-
const monthlyEquivalent = isYearly
|
|
97
|
-
? formatPrice(pkg.product.price / 12, pkg.product.currencyCode)
|
|
98
|
-
: null;
|
|
99
|
-
|
|
100
|
-
const title = pkg.product.title || t(`paywall.period.${periodLabel}`);
|
|
101
|
-
|
|
102
|
-
const CardComponent = isSelected ? LinearGradient : View;
|
|
103
|
-
const cardProps = isSelected
|
|
104
|
-
? {
|
|
105
|
-
colors: [withAlpha(tokens.colors.primary, 0.2), tokens.colors.surface],
|
|
106
|
-
start: { x: 0, y: 0 },
|
|
107
|
-
end: { x: 1, y: 1 },
|
|
108
|
-
}
|
|
109
|
-
: {};
|
|
110
|
-
|
|
111
|
-
// Responsive styles
|
|
112
|
-
const styles = useMemo(() => createStyles(spacingMultiplier, minTouchTarget), [spacingMultiplier, minTouchTarget]);
|
|
113
|
-
const secondaryFontSize = getFontSize(11);
|
|
114
|
-
const creditFontSize = getFontSize(11);
|
|
115
|
-
|
|
116
|
-
return (
|
|
117
|
-
<TouchableOpacity
|
|
118
|
-
onPress={onSelect}
|
|
119
|
-
activeOpacity={0.8}
|
|
120
|
-
style={[
|
|
121
|
-
styles.container,
|
|
122
|
-
{
|
|
123
|
-
borderColor: isSelected
|
|
124
|
-
? tokens.colors.primary
|
|
125
|
-
: tokens.colors.borderLight,
|
|
126
|
-
borderWidth: isSelected ? 2 : 1,
|
|
127
|
-
backgroundColor: isSelected ? undefined : tokens.colors.surface,
|
|
128
|
-
},
|
|
129
|
-
]}
|
|
130
|
-
>
|
|
131
|
-
<CardComponent {...(cardProps as any)} style={styles.gradientWrapper}>
|
|
132
|
-
<BestValueBadge text={t("paywall.bestValue")} visible={isBestValue} />
|
|
133
|
-
|
|
134
|
-
<View style={styles.content}>
|
|
135
|
-
<View style={styles.leftSection}>
|
|
136
|
-
<View
|
|
137
|
-
style={[
|
|
138
|
-
styles.radio,
|
|
139
|
-
{
|
|
140
|
-
borderColor: isSelected
|
|
141
|
-
? tokens.colors.primary
|
|
142
|
-
: tokens.colors.border,
|
|
143
|
-
},
|
|
144
|
-
]}
|
|
145
|
-
>
|
|
146
|
-
{isSelected && (
|
|
147
|
-
<View
|
|
148
|
-
style={[
|
|
149
|
-
styles.radioInner,
|
|
150
|
-
{ backgroundColor: tokens.colors.primary },
|
|
151
|
-
]}
|
|
152
|
-
/>
|
|
153
|
-
)}
|
|
154
|
-
</View>
|
|
155
|
-
<View style={styles.textContainer}>
|
|
156
|
-
<AtomicText
|
|
157
|
-
type="titleSmall"
|
|
158
|
-
style={[styles.title, { color: tokens.colors.textPrimary }]}
|
|
159
|
-
>
|
|
160
|
-
{title}
|
|
161
|
-
</AtomicText>
|
|
162
|
-
{isYearly && monthlyEquivalent && (
|
|
163
|
-
<AtomicText
|
|
164
|
-
type="bodySmall"
|
|
165
|
-
style={{ color: tokens.colors.textSecondary, fontSize: secondaryFontSize }}
|
|
166
|
-
>
|
|
167
|
-
{monthlyEquivalent}/mo
|
|
168
|
-
</AtomicText>
|
|
169
|
-
)}
|
|
170
|
-
</View>
|
|
171
|
-
</View>
|
|
172
|
-
|
|
173
|
-
<View style={styles.rightSection}>
|
|
174
|
-
{creditAmount && (
|
|
175
|
-
<View
|
|
176
|
-
style={[
|
|
177
|
-
styles.creditBadge,
|
|
178
|
-
{
|
|
179
|
-
backgroundColor: withAlpha(tokens.colors.primary, 0.25), // Increased alpha
|
|
180
|
-
borderColor: withAlpha(tokens.colors.primary, 0.4),
|
|
181
|
-
borderWidth: 1,
|
|
182
|
-
flexDirection: "row",
|
|
183
|
-
alignItems: "center"
|
|
184
|
-
},
|
|
185
|
-
]}
|
|
186
|
-
>
|
|
187
|
-
<AtomicText
|
|
188
|
-
type="labelSmall"
|
|
189
|
-
style={{
|
|
190
|
-
color: tokens.colors.primary,
|
|
191
|
-
fontWeight: "800",
|
|
192
|
-
fontSize: creditFontSize,
|
|
193
|
-
}}
|
|
194
|
-
>
|
|
195
|
-
{creditAmount} {t("paywall.credits") || "Credits"}
|
|
196
|
-
</AtomicText>
|
|
197
|
-
</View>
|
|
198
|
-
)}
|
|
199
|
-
<AtomicText
|
|
200
|
-
type="titleMedium"
|
|
201
|
-
style={[styles.price, { color: tokens.colors.textPrimary }]}
|
|
202
|
-
>
|
|
203
|
-
{price}
|
|
204
|
-
</AtomicText>
|
|
205
|
-
</View>
|
|
206
|
-
</View>
|
|
207
|
-
</CardComponent>
|
|
208
|
-
</TouchableOpacity>
|
|
209
|
-
);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
SubscriptionPlanCard.displayName = "SubscriptionPlanCard";
|
|
@@ -1,61 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,15 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subscription Tab Content Component
|
|
3
|
-
* Single Responsibility: Display subscription plans list
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React, { useMemo } from "react";
|
|
7
|
-
import { View, StyleSheet, ScrollView } from "react-native";
|
|
8
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
9
|
-
import type { PurchasesPackage } from "react-native-purchases";
|
|
10
|
-
import { PaywallFeaturesList } from "./PaywallFeaturesList";
|
|
11
|
-
import { SubscriptionPackageList } from "./SubscriptionPackageList";
|
|
12
|
-
import { SubscriptionFooter } from "./SubscriptionFooter";
|
|
13
|
-
|
|
14
|
-
interface SubscriptionTabContentProps {
|
|
15
|
-
packages: PurchasesPackage[];
|
|
16
|
-
selectedPackage: PurchasesPackage | null;
|
|
17
|
-
onSelectPackage: (pkg: PurchasesPackage) => void;
|
|
18
|
-
onPurchase: () => void;
|
|
19
|
-
onRestore?: () => void;
|
|
20
|
-
features?: Array<{ icon: string; text: string }>;
|
|
21
|
-
isLoading?: boolean;
|
|
22
|
-
purchaseButtonText: string;
|
|
23
|
-
processingText: string;
|
|
24
|
-
restoreButtonText: string;
|
|
25
|
-
loadingText: string;
|
|
26
|
-
emptyText: string;
|
|
27
|
-
privacyUrl?: string;
|
|
28
|
-
termsUrl?: string;
|
|
29
|
-
privacyText?: string;
|
|
30
|
-
termsOfServiceText?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const isYearlyPackage = (pkg: PurchasesPackage): boolean => {
|
|
34
|
-
const period = pkg.product.subscriptionPeriod;
|
|
35
|
-
return period?.includes("Y") || period?.includes("year") || false;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const sortPackages = (packages: PurchasesPackage[]): PurchasesPackage[] => {
|
|
39
|
-
return [...packages].sort((a, b) => {
|
|
40
|
-
const aIsYearly = isYearlyPackage(a);
|
|
41
|
-
const bIsYearly = isYearlyPackage(b);
|
|
42
|
-
if (aIsYearly && !bIsYearly) return -1;
|
|
43
|
-
if (!aIsYearly && bIsYearly) return 1;
|
|
44
|
-
return b.product.price - a.product.price;
|
|
45
|
-
});
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export const SubscriptionTabContent: React.FC<SubscriptionTabContentProps> =
|
|
49
|
-
React.memo(
|
|
50
|
-
({
|
|
51
|
-
packages,
|
|
52
|
-
selectedPackage,
|
|
53
|
-
onSelectPackage,
|
|
54
|
-
onPurchase,
|
|
55
|
-
onRestore,
|
|
56
|
-
features = [],
|
|
57
|
-
isLoading = false,
|
|
58
|
-
purchaseButtonText,
|
|
59
|
-
processingText,
|
|
60
|
-
restoreButtonText,
|
|
61
|
-
loadingText,
|
|
62
|
-
emptyText,
|
|
63
|
-
privacyUrl,
|
|
64
|
-
termsUrl,
|
|
65
|
-
privacyText,
|
|
66
|
-
termsOfServiceText,
|
|
67
|
-
}) => {
|
|
68
|
-
const tokens = useAppDesignTokens();
|
|
69
|
-
|
|
70
|
-
const sortedPackages = useMemo(() => sortPackages(packages), [packages]);
|
|
71
|
-
|
|
72
|
-
return (
|
|
73
|
-
<View style={styles.container}>
|
|
74
|
-
<ScrollView
|
|
75
|
-
style={styles.scrollView}
|
|
76
|
-
contentContainerStyle={styles.scrollContent}
|
|
77
|
-
showsVerticalScrollIndicator={false}
|
|
78
|
-
>
|
|
79
|
-
<SubscriptionPackageList
|
|
80
|
-
packages={sortedPackages}
|
|
81
|
-
isLoading={isLoading}
|
|
82
|
-
selectedPkg={selectedPackage}
|
|
83
|
-
onSelect={onSelectPackage}
|
|
84
|
-
loadingText={loadingText}
|
|
85
|
-
emptyText={emptyText}
|
|
86
|
-
/>
|
|
87
|
-
|
|
88
|
-
{features.length > 0 && (
|
|
89
|
-
<View
|
|
90
|
-
style={[
|
|
91
|
-
styles.featuresSection,
|
|
92
|
-
{ backgroundColor: tokens.colors.surfaceSecondary },
|
|
93
|
-
]}
|
|
94
|
-
>
|
|
95
|
-
<PaywallFeaturesList features={features} gap={12} />
|
|
96
|
-
</View>
|
|
97
|
-
)}
|
|
98
|
-
</ScrollView>
|
|
99
|
-
|
|
100
|
-
<SubscriptionFooter
|
|
101
|
-
isProcessing={false}
|
|
102
|
-
isLoading={isLoading}
|
|
103
|
-
processingText={processingText}
|
|
104
|
-
purchaseButtonText={purchaseButtonText}
|
|
105
|
-
hasPackages={packages.length > 0}
|
|
106
|
-
selectedPkg={selectedPackage}
|
|
107
|
-
restoreButtonText={restoreButtonText}
|
|
108
|
-
showRestoreButton={!!onRestore}
|
|
109
|
-
onPurchase={onPurchase}
|
|
110
|
-
onRestore={onRestore || (() => { })}
|
|
111
|
-
privacyUrl={privacyUrl}
|
|
112
|
-
termsUrl={termsUrl}
|
|
113
|
-
privacyText={privacyText}
|
|
114
|
-
termsOfServiceText={termsOfServiceText}
|
|
115
|
-
/>
|
|
116
|
-
</View>
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
SubscriptionTabContent.displayName = "SubscriptionTabContent";
|
|
122
|
-
|
|
123
|
-
const styles = StyleSheet.create({
|
|
124
|
-
container: {
|
|
125
|
-
flex: 1,
|
|
126
|
-
},
|
|
127
|
-
scrollView: {
|
|
128
|
-
flex: 1,
|
|
129
|
-
},
|
|
130
|
-
scrollContent: {
|
|
131
|
-
paddingHorizontal: 24,
|
|
132
|
-
paddingBottom: 16,
|
|
133
|
-
},
|
|
134
|
-
featuresSection: {
|
|
135
|
-
borderRadius: 16,
|
|
136
|
-
padding: 16,
|
|
137
|
-
marginTop: 20,
|
|
138
|
-
},
|
|
139
|
-
});
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Accordion Plan Card
|
|
3
|
-
* Expandable subscription plan card
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React, { useCallback, useMemo } from "react";
|
|
7
|
-
import { View, StyleSheet, type StyleProp, type ViewStyle } from "react-native";
|
|
8
|
-
import { useAppDesignTokens, useResponsive } 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
|
-
billingPeriodLabel,
|
|
26
|
-
totalPriceLabel,
|
|
27
|
-
perMonthLabel,
|
|
28
|
-
}) => {
|
|
29
|
-
const tokens = useAppDesignTokens();
|
|
30
|
-
const { t } = useLocalization();
|
|
31
|
-
const { spacingMultiplier } = useResponsive();
|
|
32
|
-
|
|
33
|
-
const styles = useMemo(() => createStyles(spacingMultiplier), [spacingMultiplier]);
|
|
34
|
-
|
|
35
|
-
const period = pkg.product.subscriptionPeriod;
|
|
36
|
-
const isYearly = isYearlyPackage(pkg);
|
|
37
|
-
const periodLabel = getPeriodLabel(period);
|
|
38
|
-
const price = formatPrice(pkg.product.price, pkg.product.currencyCode);
|
|
39
|
-
const monthlyEquivalent = isYearly
|
|
40
|
-
? formatPrice(pkg.product.price / 12, pkg.product.currencyCode)
|
|
41
|
-
: null;
|
|
42
|
-
|
|
43
|
-
const title = pkg.product.title || t(`paywall.period.${periodLabel}`);
|
|
44
|
-
|
|
45
|
-
const handleHeaderPress = useCallback(() => {
|
|
46
|
-
onSelect();
|
|
47
|
-
if (!isExpanded) {
|
|
48
|
-
onToggleExpand();
|
|
49
|
-
}
|
|
50
|
-
}, [onSelect, onToggleExpand, isExpanded]);
|
|
51
|
-
|
|
52
|
-
const containerStyle: StyleProp<ViewStyle> = [
|
|
53
|
-
styles.container,
|
|
54
|
-
{
|
|
55
|
-
backgroundColor: tokens.colors.surface,
|
|
56
|
-
borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
|
|
57
|
-
borderWidth: isSelected ? 2 : 1,
|
|
58
|
-
},
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
return (
|
|
62
|
-
<View style={containerStyle}>
|
|
63
|
-
<PlanCardHeader
|
|
64
|
-
title={title}
|
|
65
|
-
price={price}
|
|
66
|
-
creditAmount={creditAmount}
|
|
67
|
-
isSelected={isSelected}
|
|
68
|
-
isExpanded={isExpanded}
|
|
69
|
-
isBestValue={isBestValue}
|
|
70
|
-
onToggle={handleHeaderPress}
|
|
71
|
-
/>
|
|
72
|
-
|
|
73
|
-
{isExpanded && (
|
|
74
|
-
<PlanCardDetails
|
|
75
|
-
fullPrice={price}
|
|
76
|
-
monthlyEquivalent={monthlyEquivalent}
|
|
77
|
-
periodLabel={periodLabel}
|
|
78
|
-
isYearly={isYearly}
|
|
79
|
-
billingPeriodLabel={billingPeriodLabel}
|
|
80
|
-
totalPriceLabel={totalPriceLabel}
|
|
81
|
-
perMonthLabel={perMonthLabel}
|
|
82
|
-
/>
|
|
83
|
-
)}
|
|
84
|
-
</View>
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
AccordionPlanCard.displayName = "AccordionPlanCard";
|
|
90
|
-
|
|
91
|
-
const createStyles = (spacingMult: number) =>
|
|
92
|
-
StyleSheet.create({
|
|
93
|
-
container: {
|
|
94
|
-
borderRadius: 16 * spacingMult,
|
|
95
|
-
marginBottom: 12 * spacingMult,
|
|
96
|
-
overflow: "hidden",
|
|
97
|
-
},
|
|
98
|
-
});
|