@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.
Files changed (47) hide show
  1. package/package.json +2 -2
  2. package/src/domains/index.ts +5 -0
  3. package/src/domains/paywall/components/CreditCard.tsx +117 -0
  4. package/src/domains/paywall/components/FeatureItem.tsx +50 -0
  5. package/src/domains/paywall/components/FeatureList.tsx +34 -0
  6. package/src/domains/paywall/components/PaywallFooter.tsx +98 -0
  7. package/src/{presentation/components/paywall/PaywallHeroHeader.tsx → domains/paywall/components/PaywallHeader.tsx} +15 -44
  8. package/src/domains/paywall/components/PaywallModal.tsx +187 -0
  9. package/src/domains/paywall/components/PaywallTabBar.tsx +102 -0
  10. package/src/domains/paywall/components/PlanCard.tsx +124 -0
  11. package/src/domains/paywall/components/index.ts +14 -0
  12. package/src/domains/paywall/entities/index.ts +5 -0
  13. package/src/domains/paywall/entities/types.ts +48 -0
  14. package/src/domains/paywall/hooks/index.ts +6 -0
  15. package/src/{presentation → domains/paywall}/hooks/usePaywall.ts +1 -1
  16. package/src/domains/paywall/index.ts +13 -0
  17. package/src/index.ts +15 -22
  18. package/src/presentation/components/details/PremiumDetailsCard.tsx +35 -11
  19. package/src/domain/entities/paywall/CreditsPackage.ts +0 -16
  20. package/src/domain/entities/paywall/PaywallMode.ts +0 -6
  21. package/src/domain/entities/paywall/PaywallTab.ts +0 -11
  22. package/src/domain/entities/paywall/SubscriptionPlan.ts +0 -27
  23. package/src/presentation/components/paywall/BestValueBadge.tsx +0 -59
  24. package/src/presentation/components/paywall/CreditsPackageCard.tsx +0 -161
  25. package/src/presentation/components/paywall/CreditsTabContent.tsx +0 -123
  26. package/src/presentation/components/paywall/PaywallFeatureItem.tsx +0 -79
  27. package/src/presentation/components/paywall/PaywallFeaturesList.tsx +0 -47
  28. package/src/presentation/components/paywall/PaywallHeader.tsx +0 -82
  29. package/src/presentation/components/paywall/PaywallLegalFooter.tsx +0 -145
  30. package/src/presentation/components/paywall/PaywallLegalFooterStyles.ts +0 -53
  31. package/src/presentation/components/paywall/PaywallLegalFooterTypes.ts +0 -19
  32. package/src/presentation/components/paywall/PaywallModal.tsx +0 -162
  33. package/src/presentation/components/paywall/PaywallTabBar.tsx +0 -120
  34. package/src/presentation/components/paywall/SubscriptionFooter.tsx +0 -116
  35. package/src/presentation/components/paywall/SubscriptionModal.tsx +0 -168
  36. package/src/presentation/components/paywall/SubscriptionModalHeader.tsx +0 -78
  37. package/src/presentation/components/paywall/SubscriptionPackageList.tsx +0 -171
  38. package/src/presentation/components/paywall/SubscriptionPlanCard.tsx +0 -213
  39. package/src/presentation/components/paywall/SubscriptionPlanCardStyles.ts +0 -61
  40. package/src/presentation/components/paywall/SubscriptionPlanCardTypes.ts +0 -15
  41. package/src/presentation/components/paywall/SubscriptionTabContent.tsx +0 -139
  42. package/src/presentation/components/paywall/accordion/AccordionPlanCard.tsx +0 -98
  43. package/src/presentation/components/paywall/accordion/AccordionPlanCardTypes.ts +0 -39
  44. package/src/presentation/components/paywall/accordion/PlanCardDetails.tsx +0 -107
  45. package/src/presentation/components/paywall/accordion/PlanCardHeader.tsx +0 -155
  46. package/src/presentation/components/paywall/accordion/index.ts +0 -12
  47. /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
- });