@umituz/react-native-subscription 2.12.0 → 2.12.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.12.0",
3
+ "version": "2.12.2",
4
4
  "description": "Complete subscription management with RevenueCat, paywall UI, and credits system for React Native apps",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -47,7 +47,7 @@
47
47
  "devDependencies": {
48
48
  "@tanstack/react-query": "^5.0.0",
49
49
  "@types/react": "~19.1.10",
50
- "@umituz/react-native-design-system": "^2.2.0",
50
+ "@umituz/react-native-design-system": "^2.3.1",
51
51
  "@umituz/react-native-firebase": "*",
52
52
  "@umituz/react-native-legal": "*",
53
53
  "@umituz/react-native-localization": "*",
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Paywall Mode Entity
3
+ * Defines paywall display modes
4
+ */
5
+
6
+ export type PaywallMode = "subscription" | "credits" | "hybrid";
package/src/index.ts CHANGED
@@ -85,7 +85,10 @@ export { PaywallLegalFooter } from "./presentation/components/paywall/PaywallLeg
85
85
  export {
86
86
  PaywallModal,
87
87
  type PaywallModalProps,
88
+ type PaywallTranslations,
89
+ type PaywallLegalUrls,
88
90
  } from "./presentation/components/paywall/PaywallModal";
91
+ export type { PaywallMode } from "./domain/entities/paywall/PaywallMode";
89
92
 
90
93
  // =============================================================================
91
94
  // PRESENTATION LAYER - Premium Details Components
@@ -4,8 +4,8 @@
4
4
  */
5
5
 
6
6
  import React from "react";
7
- import { View, Text, StyleSheet } from "react-native";
8
- import { useAppDesignTokens } from "@umituz/react-native-design-system";
7
+ import { View, StyleSheet } from "react-native";
8
+ import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
9
9
 
10
10
  export interface CreditRowProps {
11
11
  label: string;
@@ -27,17 +27,17 @@ export const CreditRow: React.FC<CreditRowProps> = ({
27
27
  return (
28
28
  <View style={styles.container}>
29
29
  <View style={styles.header}>
30
- <Text style={[styles.label, { color: tokens.colors.text }]}>
30
+ <AtomicText type="bodySmall" style={{ color: tokens.colors.text }}>
31
31
  {label}
32
- </Text>
33
- <Text
34
- style={[
35
- styles.count,
36
- { color: isLow ? tokens.colors.warning : tokens.colors.textSecondary },
37
- ]}
32
+ </AtomicText>
33
+ <AtomicText
34
+ type="bodySmall"
35
+ style={{
36
+ color: isLow ? tokens.colors.warning : tokens.colors.textSecondary,
37
+ }}
38
38
  >
39
39
  {current} / {total} {remainingLabel}
40
- </Text>
40
+ </AtomicText>
41
41
  </View>
42
42
  <View
43
43
  style={[styles.progressBar, { backgroundColor: tokens.colors.surfaceSecondary }]}
@@ -65,12 +65,6 @@ const styles = StyleSheet.create({
65
65
  justifyContent: "space-between",
66
66
  alignItems: "center",
67
67
  },
68
- label: {
69
- fontSize: 13,
70
- },
71
- count: {
72
- fontSize: 12,
73
- },
74
68
  progressBar: {
75
69
  height: 6,
76
70
  borderRadius: 3,
@@ -4,8 +4,8 @@
4
4
  */
5
5
 
6
6
  import React from "react";
7
- import { View, Text, StyleSheet } from "react-native";
8
- import { useAppDesignTokens } from "@umituz/react-native-design-system";
7
+ import { View, StyleSheet } from "react-native";
8
+ import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
9
9
 
10
10
  export interface DetailRowProps {
11
11
  label: string;
@@ -22,17 +22,18 @@ export const DetailRow: React.FC<DetailRowProps> = ({
22
22
 
23
23
  return (
24
24
  <View style={styles.container}>
25
- <Text style={[styles.label, { color: tokens.colors.textSecondary }]}>
25
+ <AtomicText type="bodyMedium" style={{ color: tokens.colors.textSecondary }}>
26
26
  {label}
27
- </Text>
28
- <Text
29
- style={[
30
- styles.value,
31
- { color: highlight ? tokens.colors.warning : tokens.colors.text },
32
- ]}
27
+ </AtomicText>
28
+ <AtomicText
29
+ type="bodyMedium"
30
+ style={{
31
+ color: highlight ? tokens.colors.warning : tokens.colors.text,
32
+ fontWeight: "500",
33
+ }}
33
34
  >
34
35
  {value}
35
- </Text>
36
+ </AtomicText>
36
37
  </View>
37
38
  );
38
39
  };
@@ -43,11 +44,4 @@ const styles = StyleSheet.create({
43
44
  justifyContent: "space-between",
44
45
  alignItems: "center",
45
46
  },
46
- label: {
47
- fontSize: 14,
48
- },
49
- value: {
50
- fontSize: 14,
51
- fontWeight: "500",
52
- },
53
47
  });
@@ -4,8 +4,8 @@
4
4
  */
5
5
 
6
6
  import React from "react";
7
- import { View, Text, StyleSheet } from "react-native";
8
- import { useAppDesignTokens } from "@umituz/react-native-design-system";
7
+ import { View, StyleSheet } from "react-native";
8
+ import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
9
9
  import { SubscriptionStatusType } from "../../../domain/entities/SubscriptionStatus";
10
10
  export type { SubscriptionStatusType };
11
11
 
@@ -48,9 +48,9 @@ export const PremiumStatusBadge: React.FC<PremiumStatusBadgeProps> = ({
48
48
 
49
49
  return (
50
50
  <View style={[styles.badge, { backgroundColor }]}>
51
- <Text style={[styles.badgeText, { color: tokens.colors.onPrimary }]}>
51
+ <AtomicText type="labelSmall" style={[styles.badgeText, { color: tokens.colors.onPrimary }]}>
52
52
  {label}
53
- </Text>
53
+ </AtomicText>
54
54
  </View>
55
55
  );
56
56
  };
@@ -62,7 +62,6 @@ const styles = StyleSheet.create({
62
62
  borderRadius: 4,
63
63
  },
64
64
  badgeText: {
65
- fontSize: 12,
66
65
  fontWeight: "600",
67
66
  },
68
67
  });
@@ -1,12 +1,16 @@
1
1
  /**
2
2
  * Credits Package Card Component
3
- * Single Responsibility: Display a single credits package option
3
+ * Selectable card for credit packages
4
4
  */
5
5
 
6
6
  import React from "react";
7
7
  import { View, StyleSheet, TouchableOpacity } from "react-native";
8
- import { AtomicText } from "@umituz/react-native-design-system";
9
- import { useAppDesignTokens } from "@umituz/react-native-design-system";
8
+ import {
9
+ AtomicText,
10
+ AtomicIcon,
11
+ AtomicBadge,
12
+ useAppDesignTokens,
13
+ } from "@umituz/react-native-design-system";
10
14
  import type { CreditsPackage } from "../../../domain/entities/paywall/CreditsPackage";
11
15
 
12
16
  interface CreditsPackageCardProps {
@@ -15,94 +19,110 @@ interface CreditsPackageCardProps {
15
19
  onSelect: () => void;
16
20
  }
17
21
 
18
- export const CreditsPackageCard: React.FC<CreditsPackageCardProps> =
19
- React.memo(({ package: pkg, isSelected, onSelect }) => {
22
+ export const CreditsPackageCard: React.FC<CreditsPackageCardProps> = React.memo(
23
+ ({ package: pkg, isSelected, onSelect }) => {
20
24
  const tokens = useAppDesignTokens();
21
-
22
25
  const totalCredits = pkg.credits + (pkg.bonus || 0);
23
26
 
24
27
  return (
25
28
  <TouchableOpacity
26
- style={[
27
- styles.container,
28
- {
29
- backgroundColor: isSelected
30
- ? tokens.colors.primaryLight
31
- : tokens.colors.surface,
32
- borderColor: isSelected
33
- ? tokens.colors.primary
34
- : tokens.colors.border,
35
- borderWidth: isSelected ? 2 : 1,
36
- },
37
- ]}
38
29
  onPress={onSelect}
39
- activeOpacity={0.8}
30
+ activeOpacity={0.7}
31
+ style={styles.touchable}
40
32
  >
41
- {pkg.badge && (
42
- <View
43
- style={[styles.badge, { backgroundColor: tokens.colors.warning }]}
44
- >
45
- <AtomicText
46
- type="labelSmall"
47
- style={{ color: tokens.colors.onPrimary, fontWeight: "700" }}
48
- >
49
- {pkg.badge}
50
- </AtomicText>
51
- </View>
52
- )}
53
- <View style={styles.content}>
54
- <View style={styles.leftSection}>
55
- <AtomicText
56
- type="headlineMedium"
57
- style={[styles.credits, { color: tokens.colors.textPrimary }]}
58
- >
59
- {totalCredits.toLocaleString()} Credits
60
- </AtomicText>
61
- {(pkg.bonus ?? 0) > 0 && (
62
- <AtomicText
63
- type="bodySmall"
64
- style={[styles.bonus, { color: tokens.colors.success }]}
65
- >
66
- +{pkg.bonus} bonus
67
- </AtomicText>
68
- )}
69
- {pkg.description && (
33
+ <View
34
+ style={[
35
+ styles.container,
36
+ {
37
+ backgroundColor: tokens.colors.surface,
38
+ borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
39
+ borderWidth: isSelected ? 2 : 1,
40
+ },
41
+ ]}
42
+ >
43
+ {pkg.badge && (
44
+ <View style={styles.badgeContainer}>
45
+ <AtomicBadge text={pkg.badge} variant="warning" size="sm" />
46
+ </View>
47
+ )}
48
+
49
+ <View style={styles.content}>
50
+ <View style={styles.leftSection}>
51
+ <View style={styles.creditsRow}>
52
+ <AtomicIcon
53
+ name="flash"
54
+ size="md"
55
+ color={isSelected ? "primary" : "secondary"}
56
+ />
57
+ <AtomicText
58
+ type="headlineSmall"
59
+ style={[
60
+ styles.credits,
61
+ { color: isSelected ? tokens.colors.primary : tokens.colors.textPrimary },
62
+ ]}
63
+ >
64
+ {totalCredits.toLocaleString()}
65
+ </AtomicText>
66
+ </View>
67
+
68
+ {(pkg.bonus ?? 0) > 0 && (
69
+ <View style={styles.bonusContainer}>
70
+ <AtomicIcon name="gift-outline" size="sm" color="success" />
71
+ <AtomicText
72
+ type="bodySmall"
73
+ style={[styles.bonus, { color: tokens.colors.success }]}
74
+ >
75
+ +{pkg.bonus}
76
+ </AtomicText>
77
+ </View>
78
+ )}
79
+
80
+ {pkg.description && (
81
+ <AtomicText
82
+ type="bodySmall"
83
+ style={{ color: tokens.colors.textSecondary }}
84
+ >
85
+ {pkg.description}
86
+ </AtomicText>
87
+ )}
88
+ </View>
89
+
90
+ <View style={styles.rightSection}>
70
91
  <AtomicText
71
- type="bodySmall"
72
- style={{ color: tokens.colors.textSecondary }}
92
+ type="titleLarge"
93
+ style={[
94
+ styles.price,
95
+ { color: isSelected ? tokens.colors.primary : tokens.colors.textPrimary },
96
+ ]}
73
97
  >
74
- {pkg.description}
98
+ {pkg.currency}{pkg.price.toFixed(2)}
75
99
  </AtomicText>
76
- )}
77
- </View>
78
- <View style={styles.rightSection}>
79
- <AtomicText
80
- type="titleLarge"
81
- style={[styles.price, { color: tokens.colors.primary }]}
82
- >
83
- {pkg.currency} {pkg.price.toFixed(2)}
84
- </AtomicText>
100
+ {isSelected && (
101
+ <AtomicIcon name="checkmark-circle" size="md" color="primary" />
102
+ )}
103
+ </View>
85
104
  </View>
86
105
  </View>
87
106
  </TouchableOpacity>
88
107
  );
89
- });
108
+ }
109
+ );
90
110
 
91
111
  CreditsPackageCard.displayName = "CreditsPackageCard";
92
112
 
93
113
  const styles = StyleSheet.create({
114
+ touchable: {
115
+ marginBottom: 12,
116
+ },
94
117
  container: {
95
118
  borderRadius: 16,
96
- padding: 20,
119
+ padding: 16,
97
120
  position: "relative",
98
121
  },
99
- badge: {
122
+ badgeContainer: {
100
123
  position: "absolute",
101
124
  top: -10,
102
- right: 20,
103
- paddingHorizontal: 10,
104
- paddingVertical: 4,
105
- borderRadius: 8,
125
+ right: 16,
106
126
  },
107
127
  content: {
108
128
  flexDirection: "row",
@@ -111,19 +131,31 @@ const styles = StyleSheet.create({
111
131
  },
112
132
  leftSection: {
113
133
  flex: 1,
134
+ marginRight: 16,
135
+ },
136
+ creditsRow: {
137
+ flexDirection: "row",
138
+ alignItems: "center",
139
+ marginBottom: 4,
114
140
  },
115
141
  credits: {
116
142
  fontWeight: "700",
143
+ marginLeft: 8,
144
+ },
145
+ bonusContainer: {
146
+ flexDirection: "row",
147
+ alignItems: "center",
117
148
  marginBottom: 4,
118
149
  },
119
150
  bonus: {
120
151
  fontWeight: "600",
121
- marginBottom: 4,
152
+ marginLeft: 4,
122
153
  },
123
154
  rightSection: {
124
155
  alignItems: "flex-end",
125
156
  },
126
157
  price: {
127
158
  fontWeight: "700",
159
+ marginBottom: 4,
128
160
  },
129
161
  });
@@ -1,11 +1,16 @@
1
1
  /**
2
2
  * Paywall Feature Item Component
3
- * Single Responsibility: Display a single feature in the features list
3
+ * Single feature in the features list
4
4
  */
5
5
 
6
6
  import React, { useMemo } from "react";
7
7
  import { View, StyleSheet } from "react-native";
8
- import { AtomicText, AtomicIcon, useAppDesignTokens, useResponsive } from "@umituz/react-native-design-system";
8
+ import {
9
+ AtomicText,
10
+ AtomicIcon,
11
+ useAppDesignTokens,
12
+ useResponsive,
13
+ } from "@umituz/react-native-design-system";
9
14
 
10
15
  interface PaywallFeatureItemProps {
11
16
  icon: string;
@@ -19,33 +24,36 @@ export const PaywallFeatureItem: React.FC<PaywallFeatureItemProps> = React.memo(
19
24
 
20
25
  const styles = useMemo(() => createStyles(spacingMultiplier), [spacingMultiplier]);
21
26
  const fontSize = getFontSize(15);
22
- const lineHeight = getFontSize(22);
23
- const iconSize = getFontSize(20);
27
+ const iconSize = getFontSize(18);
24
28
 
25
- // Pass icon name directly to AtomicIcon (which uses Ionicons)
26
- // Do NOT capitalize, as Ionicons names are lowercase/kebab-case.
27
29
  const iconName = useMemo(() => {
28
- if (!icon) return "help-circle-outline";
30
+ if (!icon) return "checkmark-circle";
29
31
  return icon;
30
32
  }, [icon]);
31
33
 
32
34
  return (
33
35
  <View style={styles.featureItem}>
34
- <AtomicIcon
35
- name={iconName}
36
- customSize={iconSize}
37
- customColor={tokens.colors.primary}
38
- style={styles.featureIcon}
39
- />
36
+ <View
37
+ style={[
38
+ styles.iconContainer,
39
+ { backgroundColor: tokens.colors.primaryLight },
40
+ ]}
41
+ >
42
+ <AtomicIcon
43
+ name={iconName}
44
+ customSize={iconSize}
45
+ customColor={tokens.colors.primary}
46
+ />
47
+ </View>
40
48
  <AtomicText
41
49
  type="bodyMedium"
42
- style={[styles.featureText, { color: tokens.colors.textPrimary, fontSize, lineHeight }]}
50
+ style={[styles.featureText, { color: tokens.colors.textPrimary, fontSize }]}
43
51
  >
44
52
  {text}
45
53
  </AtomicText>
46
54
  </View>
47
55
  );
48
- },
56
+ }
49
57
  );
50
58
 
51
59
  PaywallFeatureItem.displayName = "PaywallFeatureItem";
@@ -56,10 +64,16 @@ const createStyles = (spacingMult: number) =>
56
64
  flexDirection: "row",
57
65
  alignItems: "center",
58
66
  },
59
- featureIcon: {
67
+ iconContainer: {
68
+ width: 32 * spacingMult,
69
+ height: 32 * spacingMult,
70
+ borderRadius: 16 * spacingMult,
71
+ justifyContent: "center",
72
+ alignItems: "center",
60
73
  marginRight: 12 * spacingMult,
61
74
  },
62
75
  featureText: {
63
76
  flex: 1,
77
+ fontWeight: "500",
64
78
  },
65
79
  });
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Paywall Hero Header Component
3
+ * Header with gradient background - theme aware
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, StyleSheet, TouchableOpacity } from "react-native";
8
+ import { LinearGradient } from "expo-linear-gradient";
9
+ import {
10
+ AtomicText,
11
+ AtomicIcon,
12
+ useDesignSystemTheme,
13
+ useAppDesignTokens,
14
+ } from "@umituz/react-native-design-system";
15
+
16
+ interface PaywallHeroHeaderProps {
17
+ title: string;
18
+ subtitle?: string;
19
+ onClose: () => void;
20
+ }
21
+
22
+ export const PaywallHeroHeader: React.FC<PaywallHeroHeaderProps> = React.memo(
23
+ ({ title, subtitle, onClose }) => {
24
+ const tokens = useAppDesignTokens();
25
+ const { themeMode } = useDesignSystemTheme();
26
+ const isDark = themeMode === "dark";
27
+
28
+ const gradientColors: readonly [string, string, string] = isDark
29
+ ? [tokens.colors.background, tokens.colors.surfaceSecondary, tokens.colors.surface]
30
+ : [tokens.colors.primary, tokens.colors.primaryDark, tokens.colors.primary];
31
+
32
+ return (
33
+ <LinearGradient
34
+ colors={gradientColors}
35
+ start={{ x: 0, y: 0 }}
36
+ end={{ x: 1, y: 1 }}
37
+ style={styles.container}
38
+ >
39
+ <View style={[styles.decorativeCircle, styles.circle1]} />
40
+ <View style={[styles.decorativeCircle, styles.circle2]} />
41
+
42
+ <TouchableOpacity
43
+ onPress={onClose}
44
+ style={[
45
+ styles.closeButton,
46
+ {
47
+ backgroundColor: isDark
48
+ ? tokens.colors.surfaceSecondary
49
+ : tokens.colors.onPrimary,
50
+ },
51
+ ]}
52
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
53
+ >
54
+ <AtomicIcon
55
+ name="close-outline"
56
+ size="md"
57
+ customColor={isDark ? tokens.colors.textPrimary : tokens.colors.primary}
58
+ />
59
+ </TouchableOpacity>
60
+
61
+ <View style={styles.content}>
62
+ <AtomicText
63
+ type="headlineLarge"
64
+ style={[styles.title, { color: tokens.colors.onPrimary }]}
65
+ >
66
+ {title}
67
+ </AtomicText>
68
+ {subtitle && (
69
+ <AtomicText
70
+ type="bodyLarge"
71
+ style={[styles.subtitle, { color: tokens.colors.onPrimary }]}
72
+ >
73
+ {subtitle}
74
+ </AtomicText>
75
+ )}
76
+ </View>
77
+
78
+ <View style={[styles.wave, { backgroundColor: tokens.colors.background }]} />
79
+ </LinearGradient>
80
+ );
81
+ }
82
+ );
83
+
84
+ PaywallHeroHeader.displayName = "PaywallHeroHeader";
85
+
86
+ const styles = StyleSheet.create({
87
+ container: {
88
+ paddingTop: 60,
89
+ paddingBottom: 40,
90
+ paddingHorizontal: 24,
91
+ position: "relative",
92
+ overflow: "hidden",
93
+ },
94
+ decorativeCircle: {
95
+ position: "absolute",
96
+ borderRadius: 9999,
97
+ backgroundColor: "rgba(255, 255, 255, 0.1)",
98
+ },
99
+ circle1: {
100
+ width: 200,
101
+ height: 200,
102
+ top: -100,
103
+ right: -50,
104
+ },
105
+ circle2: {
106
+ width: 150,
107
+ height: 150,
108
+ bottom: -75,
109
+ left: -40,
110
+ },
111
+ closeButton: {
112
+ position: "absolute",
113
+ top: 50,
114
+ right: 20,
115
+ width: 36,
116
+ height: 36,
117
+ borderRadius: 18,
118
+ justifyContent: "center",
119
+ alignItems: "center",
120
+ zIndex: 10,
121
+ },
122
+ content: {
123
+ alignItems: "center",
124
+ zIndex: 1,
125
+ },
126
+ title: {
127
+ fontWeight: "700",
128
+ textAlign: "center",
129
+ marginBottom: 8,
130
+ },
131
+ subtitle: {
132
+ textAlign: "center",
133
+ opacity: 0.9,
134
+ },
135
+ wave: {
136
+ position: "absolute",
137
+ bottom: -1,
138
+ left: 0,
139
+ right: 0,
140
+ height: 30,
141
+ borderTopLeftRadius: 30,
142
+ borderTopRightRadius: 30,
143
+ },
144
+ });
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Paywall Modal Component
3
- * Displays paywall with Credits and Subscription tabs
3
+ * Mode-based paywall: subscription, credits, or hybrid
4
4
  */
5
5
 
6
6
  import React from "react";
@@ -8,76 +8,69 @@ import { View, StyleSheet } from "react-native";
8
8
  import { BaseModal } from "@umituz/react-native-design-system";
9
9
  import type { PurchasesPackage } from "react-native-purchases";
10
10
  import { usePaywall } from "../../hooks/usePaywall";
11
- import { PaywallHeader } from "./PaywallHeader";
11
+ import { PaywallHeroHeader } from "./PaywallHeroHeader";
12
12
  import { PaywallTabBar } from "./PaywallTabBar";
13
13
  import { CreditsTabContent } from "./CreditsTabContent";
14
14
  import { SubscriptionTabContent } from "./SubscriptionTabContent";
15
15
  import type { PaywallTabType } from "../../../domain/entities/paywall/PaywallTab";
16
+ import type { PaywallMode } from "../../../domain/entities/paywall/PaywallMode";
16
17
  import type { CreditsPackage } from "../../../domain/entities/paywall/CreditsPackage";
17
18
 
18
- export interface PaywallModalStyles {
19
- headerTopPadding?: number;
20
- contentHorizontalPadding?: number;
21
- contentBottomPadding?: number;
22
- }
23
-
24
19
  export interface PaywallModalProps {
25
20
  visible: boolean;
26
21
  onClose: () => void;
22
+ mode: PaywallMode;
27
23
  initialTab?: PaywallTabType;
28
- creditsPackages: CreditsPackage[];
29
- subscriptionPackages: PurchasesPackage[];
30
- currentCredits: number;
24
+ creditsPackages?: CreditsPackage[];
25
+ subscriptionPackages?: PurchasesPackage[];
26
+ currentCredits?: number;
31
27
  requiredCredits?: number;
32
- onCreditsPurchase: (packageId: string) => Promise<void>;
33
- onSubscriptionPurchase: (pkg: PurchasesPackage) => Promise<void>;
28
+ onCreditsPurchase?: (packageId: string) => Promise<void>;
29
+ onSubscriptionPurchase?: (pkg: PurchasesPackage) => Promise<void>;
34
30
  onRestore?: () => Promise<void>;
35
31
  subscriptionFeatures?: Array<{ icon: string; text: string }>;
36
32
  isLoading?: boolean;
33
+ translations: PaywallTranslations;
34
+ legalUrls?: PaywallLegalUrls;
35
+ }
36
+
37
+ export interface PaywallTranslations {
37
38
  title: string;
38
39
  subtitle: string;
39
- creditsTabLabel: string;
40
- subscriptionTabLabel: string;
40
+ creditsTabLabel?: string;
41
+ subscriptionTabLabel?: string;
41
42
  purchaseButtonText: string;
42
- subscribeButtonText: string;
43
+ subscribeButtonText?: string;
43
44
  restoreButtonText: string;
44
45
  loadingText: string;
45
46
  emptyText: string;
46
47
  processingText: string;
47
- privacyUrl?: string;
48
- termsUrl?: string;
49
48
  privacyText?: string;
50
49
  termsOfServiceText?: string;
51
50
  }
52
51
 
52
+ export interface PaywallLegalUrls {
53
+ privacyUrl?: string;
54
+ termsUrl?: string;
55
+ }
56
+
53
57
  export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
54
58
  const {
55
59
  visible,
56
60
  onClose,
57
- initialTab = "credits",
58
- creditsPackages,
59
- subscriptionPackages,
60
- currentCredits,
61
+ mode,
62
+ initialTab = mode === "credits" ? "credits" : "subscription",
63
+ creditsPackages = [],
64
+ subscriptionPackages = [],
65
+ currentCredits = 0,
61
66
  requiredCredits,
62
67
  onCreditsPurchase,
63
68
  onSubscriptionPurchase,
64
69
  onRestore,
65
70
  subscriptionFeatures = [],
66
71
  isLoading = false,
67
- title,
68
- subtitle,
69
- creditsTabLabel,
70
- subscriptionTabLabel,
71
- purchaseButtonText,
72
- subscribeButtonText,
73
- privacyUrl,
74
- termsUrl,
75
- privacyText,
76
- termsOfServiceText,
77
- restoreButtonText,
78
- loadingText,
79
- emptyText,
80
- processingText,
72
+ translations,
73
+ legalUrls = {},
81
74
  } = props;
82
75
 
83
76
  const {
@@ -91,28 +84,34 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
91
84
  handleSubscriptionPurchase,
92
85
  } = usePaywall({
93
86
  initialTab,
94
- onCreditsPurchase,
95
- onSubscriptionPurchase,
87
+ onCreditsPurchase: onCreditsPurchase ?? (() => Promise.resolve()),
88
+ onSubscriptionPurchase: onSubscriptionPurchase ?? (() => Promise.resolve()),
96
89
  });
97
90
 
91
+ const showTabs = mode === "hybrid";
92
+ const showCredits = mode === "credits" || (mode === "hybrid" && activeTab === "credits");
93
+ const showSubscription = mode === "subscription" || (mode === "hybrid" && activeTab === "subscription");
94
+
98
95
  return (
99
96
  <BaseModal visible={visible} onClose={onClose}>
100
97
  <View style={styles.container}>
101
- <PaywallHeader
102
- title={title}
103
- subtitle={subtitle}
98
+ <PaywallHeroHeader
99
+ title={translations.title}
100
+ subtitle={translations.subtitle}
104
101
  onClose={onClose}
105
102
  />
106
103
 
107
- <PaywallTabBar
108
- activeTab={activeTab}
109
- onTabChange={handleTabChange}
110
- creditsLabel={creditsTabLabel}
111
- subscriptionLabel={subscriptionTabLabel}
112
- />
104
+ {showTabs && (
105
+ <PaywallTabBar
106
+ activeTab={activeTab}
107
+ onTabChange={handleTabChange}
108
+ creditsLabel={translations.creditsTabLabel}
109
+ subscriptionLabel={translations.subscriptionTabLabel}
110
+ />
111
+ )}
113
112
 
114
113
  <View style={styles.tabContent}>
115
- {activeTab === "credits" ? (
114
+ {showCredits && (
116
115
  <CreditsTabContent
117
116
  packages={creditsPackages}
118
117
  selectedPackageId={selectedCreditsPackageId}
@@ -121,9 +120,10 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
121
120
  currentCredits={currentCredits}
122
121
  requiredCredits={requiredCredits}
123
122
  isLoading={isLoading}
124
- purchaseButtonText={purchaseButtonText}
123
+ purchaseButtonText={translations.purchaseButtonText}
125
124
  />
126
- ) : (
125
+ )}
126
+ {showSubscription && (
127
127
  <SubscriptionTabContent
128
128
  packages={subscriptionPackages}
129
129
  selectedPackage={selectedSubscriptionPkg}
@@ -131,16 +131,16 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
131
131
  onPurchase={handleSubscriptionPurchase}
132
132
  features={subscriptionFeatures}
133
133
  isLoading={isLoading}
134
- purchaseButtonText={subscribeButtonText}
135
- processingText={processingText}
136
- restoreButtonText={restoreButtonText}
137
- loadingText={loadingText}
138
- emptyText={emptyText}
134
+ purchaseButtonText={translations.subscribeButtonText ?? translations.purchaseButtonText}
135
+ processingText={translations.processingText}
136
+ restoreButtonText={translations.restoreButtonText}
137
+ loadingText={translations.loadingText}
138
+ emptyText={translations.emptyText}
139
139
  onRestore={onRestore}
140
- privacyUrl={privacyUrl}
141
- termsUrl={termsUrl}
142
- privacyText={privacyText}
143
- termsOfServiceText={termsOfServiceText}
140
+ privacyUrl={legalUrls.privacyUrl}
141
+ termsUrl={legalUrls.termsUrl}
142
+ privacyText={translations.privacyText}
143
+ termsOfServiceText={translations.termsOfServiceText}
144
144
  />
145
145
  )}
146
146
  </View>
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * Paywall Tab Bar Component
3
- * Single Responsibility: Display and handle tab selection
3
+ * Segmented control for paywall tabs
4
4
  */
5
5
 
6
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";
7
+ import { View, TouchableOpacity, StyleSheet, Animated } from "react-native";
8
+ import { AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
10
9
  import type { PaywallTabType } from "../../../domain/entities/paywall/PaywallTab";
11
10
 
12
11
  interface PaywallTabBarProps {
@@ -24,6 +23,18 @@ export const PaywallTabBar: React.FC<PaywallTabBarProps> = React.memo(
24
23
  subscriptionLabel = "Subscription",
25
24
  }) => {
26
25
  const tokens = useAppDesignTokens();
26
+ const animatedValue = React.useRef(
27
+ new Animated.Value(activeTab === "credits" ? 0 : 1)
28
+ ).current;
29
+
30
+ React.useEffect(() => {
31
+ Animated.spring(animatedValue, {
32
+ toValue: activeTab === "credits" ? 0 : 1,
33
+ useNativeDriver: false,
34
+ tension: 68,
35
+ friction: 12,
36
+ }).start();
37
+ }, [activeTab, animatedValue]);
27
38
 
28
39
  const renderTab = (tab: PaywallTabType, label: string) => {
29
40
  const isActive = activeTab === tab;
@@ -31,25 +42,16 @@ export const PaywallTabBar: React.FC<PaywallTabBarProps> = React.memo(
31
42
  return (
32
43
  <TouchableOpacity
33
44
  key={tab}
34
- style={[
35
- styles.tab,
36
- {
37
- backgroundColor: isActive
38
- ? tokens.colors.primary
39
- : tokens.colors.surfaceSecondary,
40
- },
41
- ]}
45
+ style={styles.tab}
42
46
  onPress={() => onTabChange(tab)}
43
- activeOpacity={0.8}
47
+ activeOpacity={0.7}
44
48
  >
45
49
  <AtomicText
46
50
  type="labelLarge"
47
51
  style={[
48
52
  styles.tabText,
49
53
  {
50
- color: isActive
51
- ? tokens.colors.onPrimary
52
- : tokens.colors.textSecondary,
54
+ color: isActive ? tokens.colors.primary : tokens.colors.textSecondary,
53
55
  },
54
56
  ]}
55
57
  >
@@ -59,6 +61,11 @@ export const PaywallTabBar: React.FC<PaywallTabBarProps> = React.memo(
59
61
  );
60
62
  };
61
63
 
64
+ const indicatorTranslateX = animatedValue.interpolate({
65
+ inputRange: [0, 1],
66
+ outputRange: ["0%", "50%"],
67
+ });
68
+
62
69
  return (
63
70
  <View
64
71
  style={[
@@ -66,11 +73,20 @@ export const PaywallTabBar: React.FC<PaywallTabBarProps> = React.memo(
66
73
  { backgroundColor: tokens.colors.surfaceSecondary },
67
74
  ]}
68
75
  >
76
+ <Animated.View
77
+ style={[
78
+ styles.indicator,
79
+ {
80
+ backgroundColor: tokens.colors.surface,
81
+ left: indicatorTranslateX,
82
+ },
83
+ ]}
84
+ />
69
85
  {renderTab("credits", creditsLabel)}
70
86
  {renderTab("subscription", subscriptionLabel)}
71
87
  </View>
72
88
  );
73
- },
89
+ }
74
90
  );
75
91
 
76
92
  PaywallTabBar.displayName = "PaywallTabBar";
@@ -82,13 +98,21 @@ const styles = StyleSheet.create({
82
98
  padding: 4,
83
99
  marginHorizontal: 24,
84
100
  marginBottom: 16,
101
+ position: "relative",
102
+ height: 44,
103
+ },
104
+ indicator: {
105
+ position: "absolute",
106
+ top: 4,
107
+ bottom: 4,
108
+ width: "48%",
109
+ borderRadius: 8,
85
110
  },
86
111
  tab: {
87
112
  flex: 1,
88
- paddingVertical: 12,
89
- borderRadius: 8,
90
113
  alignItems: "center",
91
114
  justifyContent: "center",
115
+ zIndex: 1,
92
116
  },
93
117
  tabText: {
94
118
  fontWeight: "600",
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Accordion Plan Card
3
- * Expandable subscription plan card with credit display
3
+ * Expandable subscription plan card
4
4
  */
5
5
 
6
6
  import React, { useCallback, useMemo } from "react";
@@ -41,7 +41,6 @@ export const AccordionPlanCard: React.FC<AccordionPlanCardProps> = React.memo(
41
41
  : null;
42
42
 
43
43
  const title = pkg.product.title || t(`paywall.period.${periodLabel}`);
44
- const displayPrice = price;
45
44
 
46
45
  const handleHeaderPress = useCallback(() => {
47
46
  onSelect();
@@ -53,11 +52,9 @@ export const AccordionPlanCard: React.FC<AccordionPlanCardProps> = React.memo(
53
52
  const containerStyle: StyleProp<ViewStyle> = [
54
53
  styles.container,
55
54
  {
56
- borderColor: isSelected
57
- ? tokens.colors.primary
58
- : tokens.colors.borderLight,
59
- borderWidth: isSelected ? 2 : 1,
60
55
  backgroundColor: tokens.colors.surface,
56
+ borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
57
+ borderWidth: isSelected ? 2 : 1,
61
58
  },
62
59
  ];
63
60
 
@@ -65,7 +62,7 @@ export const AccordionPlanCard: React.FC<AccordionPlanCardProps> = React.memo(
65
62
  <View style={containerStyle}>
66
63
  <PlanCardHeader
67
64
  title={title}
68
- price={displayPrice}
65
+ price={price}
69
66
  creditAmount={creditAmount}
70
67
  isSelected={isSelected}
71
68
  isExpanded={isExpanded}
@@ -96,5 +93,6 @@ const createStyles = (spacingMult: number) =>
96
93
  container: {
97
94
  borderRadius: 16 * spacingMult,
98
95
  marginBottom: 12 * spacingMult,
96
+ overflow: "hidden",
99
97
  },
100
98
  });
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Plan Card Header
3
- * Collapsed state of accordion subscription card
3
+ * Header for accordion subscription card
4
4
  */
5
5
 
6
6
  import React, { useMemo } from "react";
7
7
  import { View, TouchableOpacity, StyleSheet } from "react-native";
8
8
  import {
9
9
  AtomicText,
10
+ AtomicIcon,
10
11
  useAppDesignTokens,
11
12
  withAlpha,
12
13
  useResponsive,
@@ -47,19 +48,13 @@ export const PlanCardHeader: React.FC<PlanCardHeaderProps> = ({
47
48
  style={[
48
49
  styles.radio,
49
50
  {
50
- borderColor: isSelected
51
- ? tokens.colors.primary
52
- : tokens.colors.border,
51
+ borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
52
+ backgroundColor: isSelected ? tokens.colors.primary : "transparent",
53
53
  },
54
54
  ]}
55
55
  >
56
56
  {isSelected && (
57
- <View
58
- style={[
59
- styles.radioInner,
60
- { backgroundColor: tokens.colors.primary },
61
- ]}
62
- />
57
+ <AtomicIcon name="checkmark" customSize={14} customColor={tokens.colors.onPrimary} />
63
58
  )}
64
59
  </View>
65
60
 
@@ -80,15 +75,17 @@ export const PlanCardHeader: React.FC<PlanCardHeaderProps> = ({
80
75
  },
81
76
  ]}
82
77
  >
78
+ <AtomicIcon name="flash" customSize={creditFontSize} customColor={tokens.colors.primary} />
83
79
  <AtomicText
84
80
  type="labelSmall"
85
81
  style={{
86
82
  color: tokens.colors.primary,
87
83
  fontWeight: "700",
88
84
  fontSize: creditFontSize,
85
+ marginLeft: 4,
89
86
  }}
90
87
  >
91
- {creditAmount} {t("paywall.credits") || "Credits"}
88
+ {creditAmount} {t("paywall.credits")}
92
89
  </AtomicText>
93
90
  </View>
94
91
  )}
@@ -98,10 +95,7 @@ export const PlanCardHeader: React.FC<PlanCardHeaderProps> = ({
98
95
  <View style={styles.rightSection}>
99
96
  <AtomicText
100
97
  type="titleMedium"
101
- style={{
102
- color: tokens.colors.textPrimary,
103
- fontWeight: "700",
104
- }}
98
+ style={{ color: tokens.colors.textPrimary, fontWeight: "700" }}
105
99
  >
106
100
  {price}
107
101
  </AtomicText>
@@ -113,7 +107,6 @@ export const PlanCardHeader: React.FC<PlanCardHeaderProps> = ({
113
107
 
114
108
  const createStyles = (spacingMult: number, touchTarget: number) => {
115
109
  const radioSize = Math.max(touchTarget * 0.4, 22);
116
- const radioInnerSize = radioSize * 0.55;
117
110
 
118
111
  return StyleSheet.create({
119
112
  container: {
@@ -140,17 +133,14 @@ const createStyles = (spacingMult: number, touchTarget: number) => {
140
133
  justifyContent: "center",
141
134
  marginRight: 12 * spacingMult,
142
135
  },
143
- radioInner: {
144
- width: radioInnerSize,
145
- height: radioInnerSize,
146
- borderRadius: radioInnerSize / 2,
147
- },
148
136
  textContainer: {
149
137
  flex: 1,
150
138
  gap: 6 * spacingMult,
151
139
  },
152
140
  creditBadge: {
153
- paddingHorizontal: 10 * spacingMult,
141
+ flexDirection: "row",
142
+ alignItems: "center",
143
+ paddingHorizontal: 8 * spacingMult,
154
144
  paddingVertical: 4 * spacingMult,
155
145
  borderRadius: 12 * spacingMult,
156
146
  borderWidth: 1,