@umituz/react-native-subscription 2.12.1 → 2.12.3

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 (45) hide show
  1. package/package.json +3 -3
  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/domains/paywall/components/PaywallHeader.tsx +115 -0
  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 +17 -21
  18. package/src/domain/entities/paywall/CreditsPackage.ts +0 -16
  19. package/src/domain/entities/paywall/PaywallTab.ts +0 -11
  20. package/src/domain/entities/paywall/SubscriptionPlan.ts +0 -27
  21. package/src/presentation/components/paywall/BestValueBadge.tsx +0 -59
  22. package/src/presentation/components/paywall/CreditsPackageCard.tsx +0 -129
  23. package/src/presentation/components/paywall/CreditsTabContent.tsx +0 -123
  24. package/src/presentation/components/paywall/PaywallFeatureItem.tsx +0 -65
  25. package/src/presentation/components/paywall/PaywallFeaturesList.tsx +0 -47
  26. package/src/presentation/components/paywall/PaywallHeader.tsx +0 -82
  27. package/src/presentation/components/paywall/PaywallLegalFooter.tsx +0 -145
  28. package/src/presentation/components/paywall/PaywallLegalFooterStyles.ts +0 -53
  29. package/src/presentation/components/paywall/PaywallLegalFooterTypes.ts +0 -19
  30. package/src/presentation/components/paywall/PaywallModal.tsx +0 -162
  31. package/src/presentation/components/paywall/PaywallTabBar.tsx +0 -96
  32. package/src/presentation/components/paywall/SubscriptionFooter.tsx +0 -116
  33. package/src/presentation/components/paywall/SubscriptionModal.tsx +0 -168
  34. package/src/presentation/components/paywall/SubscriptionModalHeader.tsx +0 -78
  35. package/src/presentation/components/paywall/SubscriptionPackageList.tsx +0 -171
  36. package/src/presentation/components/paywall/SubscriptionPlanCard.tsx +0 -213
  37. package/src/presentation/components/paywall/SubscriptionPlanCardStyles.ts +0 -61
  38. package/src/presentation/components/paywall/SubscriptionPlanCardTypes.ts +0 -15
  39. package/src/presentation/components/paywall/SubscriptionTabContent.tsx +0 -139
  40. package/src/presentation/components/paywall/accordion/AccordionPlanCard.tsx +0 -100
  41. package/src/presentation/components/paywall/accordion/AccordionPlanCardTypes.ts +0 -39
  42. package/src/presentation/components/paywall/accordion/PlanCardDetails.tsx +0 -107
  43. package/src/presentation/components/paywall/accordion/PlanCardHeader.tsx +0 -165
  44. package/src/presentation/components/paywall/accordion/index.ts +0 -12
  45. /package/src/{presentation → domains/paywall}/hooks/useSubscriptionModal.ts +0 -0
@@ -1,96 +0,0 @@
1
- /**
2
- * Paywall Tab Bar Component
3
- * Single Responsibility: Display and handle tab selection
4
- */
5
-
6
- import React from "react";
7
- import { View, TouchableOpacity, StyleSheet } from "react-native";
8
- import { AtomicText } from "@umituz/react-native-design-system";
9
- import { useAppDesignTokens } from "@umituz/react-native-design-system";
10
- import type { PaywallTabType } from "../../../domain/entities/paywall/PaywallTab";
11
-
12
- interface PaywallTabBarProps {
13
- activeTab: PaywallTabType;
14
- onTabChange: (tab: PaywallTabType) => void;
15
- creditsLabel?: string;
16
- subscriptionLabel?: string;
17
- }
18
-
19
- export const PaywallTabBar: React.FC<PaywallTabBarProps> = React.memo(
20
- ({
21
- activeTab,
22
- onTabChange,
23
- creditsLabel = "Credits",
24
- subscriptionLabel = "Subscription",
25
- }) => {
26
- const tokens = useAppDesignTokens();
27
-
28
- const renderTab = (tab: PaywallTabType, label: string) => {
29
- const isActive = activeTab === tab;
30
-
31
- return (
32
- <TouchableOpacity
33
- key={tab}
34
- style={[
35
- styles.tab,
36
- {
37
- backgroundColor: isActive
38
- ? tokens.colors.primary
39
- : tokens.colors.surfaceSecondary,
40
- },
41
- ]}
42
- onPress={() => onTabChange(tab)}
43
- activeOpacity={0.8}
44
- >
45
- <AtomicText
46
- type="labelLarge"
47
- style={[
48
- styles.tabText,
49
- {
50
- color: isActive
51
- ? tokens.colors.onPrimary
52
- : tokens.colors.textSecondary,
53
- },
54
- ]}
55
- >
56
- {label}
57
- </AtomicText>
58
- </TouchableOpacity>
59
- );
60
- };
61
-
62
- return (
63
- <View
64
- style={[
65
- styles.container,
66
- { backgroundColor: tokens.colors.surfaceSecondary },
67
- ]}
68
- >
69
- {renderTab("credits", creditsLabel)}
70
- {renderTab("subscription", subscriptionLabel)}
71
- </View>
72
- );
73
- },
74
- );
75
-
76
- PaywallTabBar.displayName = "PaywallTabBar";
77
-
78
- const styles = StyleSheet.create({
79
- container: {
80
- flexDirection: "row",
81
- borderRadius: 12,
82
- padding: 4,
83
- marginHorizontal: 24,
84
- marginBottom: 16,
85
- },
86
- tab: {
87
- flex: 1,
88
- paddingVertical: 12,
89
- borderRadius: 8,
90
- alignItems: "center",
91
- justifyContent: "center",
92
- },
93
- tabText: {
94
- fontWeight: "600",
95
- },
96
- });
@@ -1,116 +0,0 @@
1
- import React, { useMemo } from "react";
2
- import { View, StyleSheet, TouchableOpacity } from "react-native";
3
- import type { PurchasesPackage } from "react-native-purchases";
4
- import { AtomicText, useAppDesignTokens, useResponsive } from "@umituz/react-native-design-system";
5
- import { PaywallLegalFooter } from "./PaywallLegalFooter";
6
-
7
- interface SubscriptionFooterProps {
8
- isProcessing: boolean;
9
- isLoading: boolean;
10
- processingText: string;
11
- purchaseButtonText: string;
12
- hasPackages: boolean;
13
- selectedPkg: PurchasesPackage | null;
14
- restoreButtonText: string;
15
- showRestoreButton: boolean;
16
- privacyUrl?: string;
17
- termsUrl?: string;
18
- privacyText?: string;
19
- termsOfServiceText?: string;
20
- onPurchase: () => void;
21
- onRestore: () => void;
22
- }
23
-
24
- import { LinearGradient } from "expo-linear-gradient";
25
-
26
- export const SubscriptionFooter: React.FC<SubscriptionFooterProps> = React.memo(
27
- ({
28
- isProcessing,
29
- isLoading,
30
- processingText,
31
- purchaseButtonText,
32
- hasPackages,
33
- selectedPkg,
34
- restoreButtonText,
35
- showRestoreButton,
36
- privacyUrl,
37
- termsUrl,
38
- privacyText,
39
- termsOfServiceText,
40
- onPurchase,
41
- onRestore,
42
- }) => {
43
- const tokens = useAppDesignTokens();
44
- const { spacingMultiplier, getFontSize } = useResponsive();
45
-
46
- const styles = useMemo(() => createStyles(spacingMultiplier), [spacingMultiplier]);
47
- const buttonFontSize = getFontSize(16);
48
-
49
- const isDisabled = !selectedPkg || isProcessing || isLoading;
50
-
51
- return (
52
- <View style={styles.container}>
53
- <View style={styles.actions}>
54
- {hasPackages && (
55
- <TouchableOpacity
56
- onPress={onPurchase}
57
- disabled={isDisabled}
58
- activeOpacity={0.8}
59
- >
60
- <LinearGradient
61
- colors={[tokens.colors.primary, tokens.colors.secondary]}
62
- start={{ x: 0, y: 0 }}
63
- end={{ x: 1, y: 0 }}
64
- style={[styles.gradientButton, isDisabled && { opacity: 0.5 }]}
65
- >
66
- <AtomicText
67
- type="titleSmall"
68
- style={{
69
- color: tokens.colors.onPrimary,
70
- fontWeight: "800",
71
- fontSize: buttonFontSize,
72
- }}
73
- >
74
- {isProcessing ? processingText : purchaseButtonText}
75
- </AtomicText>
76
- </LinearGradient>
77
- </TouchableOpacity>
78
- )}
79
- </View>
80
-
81
- <PaywallLegalFooter
82
- privacyUrl={privacyUrl}
83
- termsUrl={termsUrl}
84
- privacyText={privacyText}
85
- termsOfServiceText={termsOfServiceText}
86
- showRestoreButton={showRestoreButton}
87
- restoreButtonText={restoreButtonText}
88
- onRestore={onRestore}
89
- isProcessing={isProcessing || isLoading}
90
- />
91
- </View>
92
- );
93
- }
94
- );
95
-
96
- SubscriptionFooter.displayName = "SubscriptionFooter";
97
-
98
- const createStyles = (spacingMult: number) =>
99
- StyleSheet.create({
100
- container: {},
101
- actions: {
102
- paddingHorizontal: 24 * spacingMult,
103
- paddingVertical: 16 * spacingMult,
104
- gap: 12 * spacingMult,
105
- },
106
- gradientButton: {
107
- paddingVertical: 16 * spacingMult,
108
- borderRadius: 16 * spacingMult,
109
- alignItems: "center",
110
- justifyContent: "center",
111
- },
112
- restoreButton: {
113
- alignItems: "center",
114
- paddingVertical: 8 * spacingMult,
115
- },
116
- });
@@ -1,168 +0,0 @@
1
- /**
2
- * Subscription Modal Component
3
- * Fullscreen subscription flow using BaseModal from design system
4
- */
5
-
6
- import React, { useMemo } from "react";
7
- import { View, StyleSheet, ScrollView } from "react-native";
8
- import { BaseModal, useResponsive } from "@umituz/react-native-design-system";
9
- import type { PurchasesPackage } from "react-native-purchases";
10
-
11
- import { SubscriptionModalHeader } from "./SubscriptionModalHeader";
12
- import { SubscriptionPackageList } from "./SubscriptionPackageList";
13
- import { SubscriptionFooter } from "./SubscriptionFooter";
14
- import { PaywallFeaturesList } from "./PaywallFeaturesList";
15
- import { useSubscriptionModal } from "../../hooks/useSubscriptionModal";
16
-
17
- export interface SubscriptionModalProps {
18
- visible: boolean;
19
- onClose: () => void;
20
- packages: PurchasesPackage[];
21
- onPurchase: (pkg: PurchasesPackage) => Promise<boolean>;
22
- onRestore: () => Promise<boolean>;
23
- title: string;
24
- subtitle?: string;
25
- isLoading?: boolean;
26
- purchaseButtonText: string;
27
- restoreButtonText: string;
28
- loadingText: string;
29
- emptyText: string;
30
- processingText: string;
31
- privacyUrl?: string;
32
- termsUrl?: string;
33
- privacyText?: string;
34
- termsOfServiceText?: string;
35
- showRestoreButton?: boolean;
36
- /** Optional: Map of product identifier to credit amount */
37
- creditAmounts?: Record<string, number>;
38
- /** Optional: Manually specify which package should show "Best Value" badge */
39
- bestValueIdentifier?: string;
40
- /** Optional: Text labels for accordion details */
41
- billingPeriodLabel?: string;
42
- totalPriceLabel?: string;
43
- perMonthLabel?: string;
44
- /** Optional: List of premium features to display */
45
- features?: Array<{ icon: string; text: string }>;
46
- }
47
-
48
- export const SubscriptionModal: React.FC<SubscriptionModalProps> = React.memo((props) => {
49
- const {
50
- visible,
51
- onClose,
52
- packages,
53
- onPurchase,
54
- onRestore,
55
- title,
56
- subtitle,
57
- isLoading = false,
58
- purchaseButtonText,
59
- restoreButtonText,
60
- loadingText,
61
- emptyText,
62
- processingText,
63
- privacyUrl,
64
- termsUrl,
65
- privacyText,
66
- termsOfServiceText,
67
- showRestoreButton = true,
68
- creditAmounts,
69
- bestValueIdentifier,
70
- billingPeriodLabel,
71
- totalPriceLabel,
72
- perMonthLabel,
73
- features = [],
74
- } = props;
75
-
76
- const {
77
- selectedPkg,
78
- setSelectedPkg,
79
- isProcessing,
80
- handlePurchase,
81
- handleRestore,
82
- } = useSubscriptionModal({
83
- onPurchase,
84
- onRestore,
85
- onClose,
86
- });
87
-
88
- const { spacingMultiplier } = useResponsive();
89
- const styles = useMemo(() => createStyles(spacingMultiplier), [spacingMultiplier]);
90
-
91
- return (
92
- <BaseModal visible={visible} onClose={onClose}>
93
- <View style={styles.container}>
94
- <SubscriptionModalHeader
95
- title={title}
96
- subtitle={subtitle}
97
- onClose={onClose}
98
- />
99
-
100
- <ScrollView
101
- style={styles.scrollView}
102
- contentContainerStyle={styles.scrollContent}
103
- showsVerticalScrollIndicator={false}
104
- bounces={false}
105
- >
106
- {features.length > 0 && (
107
- <PaywallFeaturesList
108
- features={features}
109
- containerStyle={styles.featuresList}
110
- />
111
- )}
112
-
113
- <SubscriptionPackageList
114
- packages={packages}
115
- isLoading={isLoading}
116
- selectedPkg={selectedPkg}
117
- onSelect={setSelectedPkg}
118
- loadingText={loadingText}
119
- emptyText={emptyText}
120
- creditAmounts={creditAmounts}
121
- bestValueIdentifier={bestValueIdentifier}
122
- billingPeriodLabel={billingPeriodLabel}
123
- totalPriceLabel={totalPriceLabel}
124
- perMonthLabel={perMonthLabel}
125
- />
126
- </ScrollView>
127
-
128
- <SubscriptionFooter
129
- isProcessing={isProcessing}
130
- isLoading={isLoading}
131
- processingText={processingText}
132
- purchaseButtonText={purchaseButtonText}
133
- hasPackages={packages.length > 0}
134
- selectedPkg={selectedPkg}
135
- restoreButtonText={restoreButtonText}
136
- showRestoreButton={showRestoreButton}
137
- privacyUrl={privacyUrl}
138
- termsUrl={termsUrl}
139
- privacyText={privacyText}
140
- termsOfServiceText={termsOfServiceText}
141
- onPurchase={handlePurchase}
142
- onRestore={handleRestore}
143
- />
144
- </View>
145
- </BaseModal>
146
- );
147
- });
148
-
149
- SubscriptionModal.displayName = "SubscriptionModal";
150
-
151
- const createStyles = (spacingMult: number) =>
152
- StyleSheet.create({
153
- container: {
154
- flex: 1,
155
- width: "100%",
156
- },
157
- scrollView: {
158
- flex: 1,
159
- },
160
- scrollContent: {
161
- flexGrow: 1,
162
- paddingBottom: 32 * spacingMult,
163
- },
164
- featuresList: {
165
- paddingHorizontal: 24 * spacingMult,
166
- marginBottom: 24 * spacingMult,
167
- },
168
- });
@@ -1,78 +0,0 @@
1
- /**
2
- * Subscription Modal Header Component
3
- */
4
-
5
- import React from "react";
6
- import { View, StyleSheet, TouchableOpacity } from "react-native";
7
- import { AtomicText } from "@umituz/react-native-design-system";
8
- import { useAppDesignTokens } from "@umituz/react-native-design-system";
9
-
10
- interface SubscriptionModalHeaderProps {
11
- title: string;
12
- subtitle?: string;
13
- onClose: () => void;
14
- }
15
-
16
- export const SubscriptionModalHeader: React.FC<SubscriptionModalHeaderProps> = ({
17
- title,
18
- subtitle,
19
- onClose,
20
- }) => {
21
- const tokens = useAppDesignTokens();
22
-
23
- return (
24
- <View style={styles.header}>
25
- <TouchableOpacity
26
- style={styles.closeButton}
27
- onPress={onClose}
28
- testID="subscription-modal-close-button"
29
- >
30
- <AtomicText style={[styles.closeIcon, { color: tokens.colors.textSecondary }]}>
31
- ×
32
- </AtomicText>
33
- </TouchableOpacity>
34
- <AtomicText
35
- type="headlineMedium"
36
- style={[styles.title, { color: tokens.colors.textPrimary }]}
37
- >
38
- {title}
39
- </AtomicText>
40
- {subtitle && (
41
- <AtomicText
42
- type="bodyMedium"
43
- style={[styles.subtitle, { color: tokens.colors.textSecondary }]}
44
- >
45
- {subtitle}
46
- </AtomicText>
47
- )}
48
- </View>
49
- );
50
- };
51
-
52
- const styles = StyleSheet.create({
53
- header: {
54
- alignItems: "center",
55
- paddingHorizontal: 24,
56
- paddingTop: 16,
57
- paddingBottom: 16,
58
- },
59
- closeButton: {
60
- position: "absolute",
61
- top: 8,
62
- right: 16,
63
- padding: 8,
64
- zIndex: 1,
65
- },
66
- closeIcon: {
67
- fontSize: 28,
68
- fontWeight: "300",
69
- },
70
- title: {
71
- marginBottom: 8,
72
- textAlign: "center",
73
- },
74
- subtitle: {
75
- textAlign: "center",
76
- paddingHorizontal: 20,
77
- },
78
- });
@@ -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
- });