@umituz/react-native-subscription 1.3.1 → 1.5.0
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 +11 -10
- package/src/domain/entities/paywall/CreditsPackage.ts +16 -0
- package/src/domain/entities/paywall/PaywallTab.ts +11 -0
- package/src/domain/entities/paywall/SubscriptionPlan.ts +27 -0
- package/src/index.ts +47 -35
- package/src/presentation/components/paywall/CreditsPackageCard.tsx +129 -0
- package/src/presentation/components/paywall/CreditsTabContent.tsx +130 -0
- package/src/presentation/components/paywall/PaywallFeatureItem.tsx +66 -0
- package/src/presentation/components/paywall/PaywallFeaturesList.tsx +43 -0
- package/src/presentation/components/paywall/PaywallHeader.tsx +82 -0
- package/src/presentation/components/paywall/PaywallLegalFooter.tsx +54 -0
- package/src/presentation/components/paywall/PaywallModal.tsx +161 -0
- package/src/presentation/components/paywall/PaywallTabBar.tsx +96 -0
- package/src/presentation/components/paywall/SubscriptionModal.tsx +206 -0
- package/src/presentation/components/paywall/SubscriptionPlanCard.tsx +186 -0
- package/src/presentation/components/paywall/SubscriptionTabContent.tsx +142 -0
- package/src/utils/priceUtils.ts +20 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Subscription management
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "Subscription management and paywall UI for React Native apps",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
7
7
|
"scripts": {
|
|
@@ -12,16 +12,12 @@
|
|
|
12
12
|
"react-native",
|
|
13
13
|
"subscription",
|
|
14
14
|
"premium",
|
|
15
|
+
"paywall",
|
|
15
16
|
"in-app-purchase",
|
|
16
17
|
"iap",
|
|
17
|
-
"
|
|
18
|
+
"revenuecat",
|
|
18
19
|
"ddd",
|
|
19
|
-
"domain-driven-design"
|
|
20
|
-
"type-safe",
|
|
21
|
-
"solid",
|
|
22
|
-
"dry",
|
|
23
|
-
"kiss",
|
|
24
|
-
"database-first"
|
|
20
|
+
"domain-driven-design"
|
|
25
21
|
],
|
|
26
22
|
"author": "Ümit UZ <umit@umituz.com>",
|
|
27
23
|
"license": "MIT",
|
|
@@ -31,7 +27,12 @@
|
|
|
31
27
|
},
|
|
32
28
|
"peerDependencies": {
|
|
33
29
|
"react": ">=18.2.0",
|
|
34
|
-
"react-native": ">=0.74.0"
|
|
30
|
+
"react-native": ">=0.74.0",
|
|
31
|
+
"react-native-purchases": ">=8.0.0",
|
|
32
|
+
"react-native-safe-area-context": ">=4.0.0",
|
|
33
|
+
"@umituz/react-native-design-system-atoms": "*",
|
|
34
|
+
"@umituz/react-native-design-system-theme": "*",
|
|
35
|
+
"@umituz/react-native-legal": "*"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@types/react": "~19.1.0",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credits Package Entity
|
|
3
|
+
* Represents a credit package for purchase
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface CreditsPackage {
|
|
7
|
+
id: string;
|
|
8
|
+
credits: number;
|
|
9
|
+
price: number;
|
|
10
|
+
priceString?: string;
|
|
11
|
+
currency: string;
|
|
12
|
+
bonus?: number;
|
|
13
|
+
popular?: boolean;
|
|
14
|
+
badge?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Plan Entity
|
|
3
|
+
* Represents a subscription plan for purchase
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface SubscriptionPlan {
|
|
7
|
+
/** Plan ID */
|
|
8
|
+
id: string;
|
|
9
|
+
|
|
10
|
+
/** Plan type */
|
|
11
|
+
type: "monthly" | "yearly";
|
|
12
|
+
|
|
13
|
+
/** Price */
|
|
14
|
+
price: number;
|
|
15
|
+
|
|
16
|
+
/** Currency code */
|
|
17
|
+
currency: string;
|
|
18
|
+
|
|
19
|
+
/** Whether this is the best value option */
|
|
20
|
+
isBestValue?: boolean;
|
|
21
|
+
|
|
22
|
+
/** Optional discount percentage */
|
|
23
|
+
discountPercentage?: number;
|
|
24
|
+
|
|
25
|
+
/** Optional features list */
|
|
26
|
+
features?: string[];
|
|
27
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,23 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* React Native Subscription - Public API
|
|
3
3
|
*
|
|
4
|
+
* Subscription management and paywall UI for React Native apps
|
|
4
5
|
* Domain-Driven Design (DDD) Architecture
|
|
5
|
-
*
|
|
6
|
-
* This is the SINGLE SOURCE OF TRUTH for all subscription operations.
|
|
7
|
-
* ALL imports from the Subscription package MUST go through this file.
|
|
8
|
-
*
|
|
9
|
-
* Architecture:
|
|
10
|
-
* - domain: Entities, value objects, errors (business logic)
|
|
11
|
-
* - application: Ports (interfaces)
|
|
12
|
-
* - infrastructure: Subscription service implementation
|
|
13
|
-
* - presentation: Hooks (React integration)
|
|
14
|
-
*
|
|
15
|
-
* Usage:
|
|
16
|
-
* import { initializeSubscriptionService, useSubscription } from '@umituz/react-native-subscription';
|
|
17
6
|
*/
|
|
18
7
|
|
|
19
8
|
// =============================================================================
|
|
20
|
-
// DOMAIN LAYER -
|
|
9
|
+
// DOMAIN LAYER - Errors
|
|
21
10
|
// =============================================================================
|
|
22
11
|
|
|
23
12
|
export {
|
|
@@ -25,25 +14,29 @@ export {
|
|
|
25
14
|
SubscriptionRepositoryError,
|
|
26
15
|
SubscriptionValidationError,
|
|
27
16
|
SubscriptionConfigurationError,
|
|
28
|
-
} from
|
|
17
|
+
} from "./domain/errors/SubscriptionError";
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// DOMAIN LAYER - Entities
|
|
21
|
+
// =============================================================================
|
|
29
22
|
|
|
30
23
|
export {
|
|
31
24
|
createDefaultSubscriptionStatus,
|
|
32
25
|
isSubscriptionValid,
|
|
33
|
-
} from
|
|
34
|
-
export type { SubscriptionStatus } from
|
|
26
|
+
} from "./domain/entities/SubscriptionStatus";
|
|
27
|
+
export type { SubscriptionStatus } from "./domain/entities/SubscriptionStatus";
|
|
35
28
|
|
|
36
|
-
export type { SubscriptionConfig } from
|
|
29
|
+
export type { SubscriptionConfig } from "./domain/value-objects/SubscriptionConfig";
|
|
37
30
|
|
|
38
31
|
// =============================================================================
|
|
39
32
|
// APPLICATION LAYER - Ports
|
|
40
33
|
// =============================================================================
|
|
41
34
|
|
|
42
|
-
export type { ISubscriptionRepository } from
|
|
43
|
-
export type { ISubscriptionService } from
|
|
35
|
+
export type { ISubscriptionRepository } from "./application/ports/ISubscriptionRepository";
|
|
36
|
+
export type { ISubscriptionService } from "./application/ports/ISubscriptionService";
|
|
44
37
|
|
|
45
38
|
// =============================================================================
|
|
46
|
-
// INFRASTRUCTURE LAYER -
|
|
39
|
+
// INFRASTRUCTURE LAYER - Services
|
|
47
40
|
// =============================================================================
|
|
48
41
|
|
|
49
42
|
export {
|
|
@@ -51,71 +44,85 @@ export {
|
|
|
51
44
|
initializeSubscriptionService,
|
|
52
45
|
getSubscriptionService,
|
|
53
46
|
resetSubscriptionService,
|
|
54
|
-
} from
|
|
47
|
+
} from "./infrastructure/services/SubscriptionService";
|
|
55
48
|
|
|
56
49
|
// =============================================================================
|
|
57
50
|
// PRESENTATION LAYER - Hooks
|
|
58
51
|
// =============================================================================
|
|
59
52
|
|
|
60
|
-
export { useSubscription } from
|
|
61
|
-
export type { UseSubscriptionResult } from
|
|
53
|
+
export { useSubscription } from "./presentation/hooks/useSubscription";
|
|
54
|
+
export type { UseSubscriptionResult } from "./presentation/hooks/useSubscription";
|
|
62
55
|
|
|
63
56
|
export {
|
|
64
57
|
usePremiumGate,
|
|
65
58
|
type UsePremiumGateParams,
|
|
66
59
|
type UsePremiumGateResult,
|
|
67
|
-
} from
|
|
60
|
+
} from "./presentation/hooks/usePremiumGate";
|
|
68
61
|
|
|
69
62
|
export {
|
|
70
63
|
useUserTier,
|
|
71
64
|
type UseUserTierParams,
|
|
72
65
|
type UseUserTierResult,
|
|
73
|
-
} from
|
|
66
|
+
} from "./presentation/hooks/useUserTier";
|
|
74
67
|
|
|
75
68
|
export {
|
|
76
69
|
useUserTierWithRepository,
|
|
77
70
|
type UseUserTierWithRepositoryParams,
|
|
78
71
|
type UseUserTierWithRepositoryResult,
|
|
79
72
|
type AuthProvider,
|
|
80
|
-
} from
|
|
73
|
+
} from "./presentation/hooks/useUserTierWithRepository";
|
|
81
74
|
|
|
82
75
|
// =============================================================================
|
|
83
|
-
//
|
|
76
|
+
// PRESENTATION LAYER - Paywall Components
|
|
77
|
+
// =============================================================================
|
|
78
|
+
|
|
79
|
+
export {
|
|
80
|
+
SubscriptionModal,
|
|
81
|
+
type SubscriptionModalProps,
|
|
82
|
+
} from "./presentation/components/paywall/SubscriptionModal";
|
|
83
|
+
|
|
84
|
+
export { SubscriptionPlanCard } from "./presentation/components/paywall/SubscriptionPlanCard";
|
|
85
|
+
export { PaywallFeaturesList } from "./presentation/components/paywall/PaywallFeaturesList";
|
|
86
|
+
export { PaywallFeatureItem } from "./presentation/components/paywall/PaywallFeatureItem";
|
|
87
|
+
export { PaywallLegalFooter } from "./presentation/components/paywall/PaywallLegalFooter";
|
|
88
|
+
|
|
89
|
+
// =============================================================================
|
|
90
|
+
// UTILS - Date & Price
|
|
84
91
|
// =============================================================================
|
|
85
92
|
|
|
86
|
-
// Date utilities
|
|
87
93
|
export {
|
|
88
94
|
isSubscriptionExpired,
|
|
89
95
|
getDaysUntilExpiration,
|
|
90
|
-
} from
|
|
96
|
+
} from "./utils/dateValidationUtils";
|
|
91
97
|
|
|
98
|
+
export { formatPrice } from "./utils/priceUtils";
|
|
92
99
|
|
|
93
100
|
// =============================================================================
|
|
94
|
-
//
|
|
101
|
+
// UTILS - User Tier
|
|
95
102
|
// =============================================================================
|
|
96
103
|
|
|
97
104
|
export type {
|
|
98
105
|
UserTier,
|
|
99
106
|
UserTierInfo,
|
|
100
107
|
PremiumStatusFetcher,
|
|
101
|
-
} from
|
|
108
|
+
} from "./utils/types";
|
|
102
109
|
|
|
103
110
|
export {
|
|
104
111
|
getUserTierInfo,
|
|
105
112
|
checkPremiumAccess,
|
|
106
|
-
} from
|
|
113
|
+
} from "./utils/tierUtils";
|
|
107
114
|
|
|
108
115
|
export {
|
|
109
116
|
hasTierAccess,
|
|
110
117
|
isTierPremium,
|
|
111
118
|
isTierFreemium,
|
|
112
119
|
isTierGuest,
|
|
113
|
-
} from
|
|
120
|
+
} from "./utils/userTierUtils";
|
|
114
121
|
|
|
115
122
|
export {
|
|
116
123
|
isAuthenticated,
|
|
117
124
|
isGuest,
|
|
118
|
-
} from
|
|
125
|
+
} from "./utils/authUtils";
|
|
119
126
|
|
|
120
127
|
export {
|
|
121
128
|
isValidUserTier,
|
|
@@ -124,5 +131,10 @@ export {
|
|
|
124
131
|
validateIsGuest,
|
|
125
132
|
validateIsPremium,
|
|
126
133
|
validateFetcher,
|
|
127
|
-
} from
|
|
134
|
+
} from "./utils/validation";
|
|
135
|
+
|
|
136
|
+
// =============================================================================
|
|
137
|
+
// TYPES - Re-export from peer dependencies
|
|
138
|
+
// =============================================================================
|
|
128
139
|
|
|
140
|
+
export type { PurchasesPackage } from "react-native-purchases";
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credits Package Card Component
|
|
3
|
+
* Single Responsibility: Display a single credits package option
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
8
|
+
import { AtomicText } from "@umituz/react-native-design-system-atoms";
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
10
|
+
import type { CreditsPackage } from "../../domain/entities/CreditsPackage";
|
|
11
|
+
|
|
12
|
+
interface CreditsPackageCardProps {
|
|
13
|
+
package: CreditsPackage;
|
|
14
|
+
isSelected: boolean;
|
|
15
|
+
onSelect: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const CreditsPackageCard: React.FC<CreditsPackageCardProps> =
|
|
19
|
+
React.memo(({ package: pkg, isSelected, onSelect }) => {
|
|
20
|
+
const tokens = useAppDesignTokens();
|
|
21
|
+
|
|
22
|
+
const totalCredits = pkg.credits + (pkg.bonus || 0);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<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
|
+
onPress={onSelect}
|
|
39
|
+
activeOpacity={0.8}
|
|
40
|
+
>
|
|
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 && (
|
|
70
|
+
<AtomicText
|
|
71
|
+
type="bodySmall"
|
|
72
|
+
style={{ color: tokens.colors.textSecondary }}
|
|
73
|
+
>
|
|
74
|
+
{pkg.description}
|
|
75
|
+
</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>
|
|
85
|
+
</View>
|
|
86
|
+
</View>
|
|
87
|
+
</TouchableOpacity>
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
CreditsPackageCard.displayName = "CreditsPackageCard";
|
|
92
|
+
|
|
93
|
+
const styles = StyleSheet.create({
|
|
94
|
+
container: {
|
|
95
|
+
borderRadius: 16,
|
|
96
|
+
padding: 20,
|
|
97
|
+
position: "relative",
|
|
98
|
+
},
|
|
99
|
+
badge: {
|
|
100
|
+
position: "absolute",
|
|
101
|
+
top: -10,
|
|
102
|
+
right: 20,
|
|
103
|
+
paddingHorizontal: 10,
|
|
104
|
+
paddingVertical: 4,
|
|
105
|
+
borderRadius: 8,
|
|
106
|
+
},
|
|
107
|
+
content: {
|
|
108
|
+
flexDirection: "row",
|
|
109
|
+
justifyContent: "space-between",
|
|
110
|
+
alignItems: "center",
|
|
111
|
+
},
|
|
112
|
+
leftSection: {
|
|
113
|
+
flex: 1,
|
|
114
|
+
},
|
|
115
|
+
credits: {
|
|
116
|
+
fontWeight: "700",
|
|
117
|
+
marginBottom: 4,
|
|
118
|
+
},
|
|
119
|
+
bonus: {
|
|
120
|
+
fontWeight: "600",
|
|
121
|
+
marginBottom: 4,
|
|
122
|
+
},
|
|
123
|
+
rightSection: {
|
|
124
|
+
alignItems: "flex-end",
|
|
125
|
+
},
|
|
126
|
+
price: {
|
|
127
|
+
fontWeight: "700",
|
|
128
|
+
},
|
|
129
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credits Tab Content Component
|
|
3
|
+
* Single Responsibility: Display credits packages list
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, StyleSheet, ScrollView } from "react-native";
|
|
8
|
+
import { AtomicText, AtomicButton } from "@umituz/react-native-design-system-atoms";
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
10
|
+
import { useLocalization } from "@umituz/react-native-localization";
|
|
11
|
+
import { CreditsPackageCard } from "./CreditsPackageCard";
|
|
12
|
+
import { PaywallLegalFooter } from "./PaywallLegalFooter";
|
|
13
|
+
import type { CreditsPackage } from "../../domain/entities/CreditsPackage";
|
|
14
|
+
|
|
15
|
+
interface CreditsTabContentProps {
|
|
16
|
+
packages: CreditsPackage[];
|
|
17
|
+
selectedPackageId: string | null;
|
|
18
|
+
onSelectPackage: (packageId: string) => void;
|
|
19
|
+
onPurchase: () => void;
|
|
20
|
+
currentCredits: number;
|
|
21
|
+
requiredCredits?: number;
|
|
22
|
+
isLoading?: boolean;
|
|
23
|
+
purchaseButtonText?: string;
|
|
24
|
+
creditsInfoText?: string;
|
|
25
|
+
processingText?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const CreditsTabContent: React.FC<CreditsTabContentProps> = React.memo(
|
|
29
|
+
({
|
|
30
|
+
packages,
|
|
31
|
+
selectedPackageId,
|
|
32
|
+
onSelectPackage,
|
|
33
|
+
onPurchase,
|
|
34
|
+
currentCredits,
|
|
35
|
+
requiredCredits,
|
|
36
|
+
isLoading = false,
|
|
37
|
+
purchaseButtonText,
|
|
38
|
+
creditsInfoText,
|
|
39
|
+
processingText,
|
|
40
|
+
}) => {
|
|
41
|
+
const tokens = useAppDesignTokens();
|
|
42
|
+
const { t } = useLocalization();
|
|
43
|
+
|
|
44
|
+
const needsCredits = requiredCredits && requiredCredits > currentCredits;
|
|
45
|
+
|
|
46
|
+
const displayPurchaseButtonText = purchaseButtonText ||
|
|
47
|
+
t("paywall.purchase", { defaultValue: "Purchase" });
|
|
48
|
+
const displayProcessingText = processingText ||
|
|
49
|
+
t("paywall.processing", { defaultValue: "Processing..." });
|
|
50
|
+
const displayCreditsInfoText = creditsInfoText ||
|
|
51
|
+
t("paywall.creditsInfo", { defaultValue: "You need {required} credits. You have {current}." });
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<View style={styles.container}>
|
|
55
|
+
{needsCredits && (
|
|
56
|
+
<View
|
|
57
|
+
style={[
|
|
58
|
+
styles.infoCard,
|
|
59
|
+
{ backgroundColor: tokens.colors.surfaceSecondary },
|
|
60
|
+
]}
|
|
61
|
+
>
|
|
62
|
+
<AtomicText
|
|
63
|
+
type="bodyMedium"
|
|
64
|
+
style={{ color: tokens.colors.textSecondary }}
|
|
65
|
+
>
|
|
66
|
+
{displayCreditsInfoText
|
|
67
|
+
.replace("{required}", String(requiredCredits))
|
|
68
|
+
.replace("{current}", String(currentCredits))}
|
|
69
|
+
</AtomicText>
|
|
70
|
+
</View>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
<ScrollView
|
|
74
|
+
style={styles.scrollView}
|
|
75
|
+
contentContainerStyle={styles.scrollContent}
|
|
76
|
+
showsVerticalScrollIndicator={false}
|
|
77
|
+
>
|
|
78
|
+
<View style={styles.packagesContainer}>
|
|
79
|
+
{packages.map((pkg) => (
|
|
80
|
+
<CreditsPackageCard
|
|
81
|
+
key={pkg.id}
|
|
82
|
+
package={pkg}
|
|
83
|
+
isSelected={selectedPackageId === pkg.id}
|
|
84
|
+
onSelect={() => onSelectPackage(pkg.id)}
|
|
85
|
+
/>
|
|
86
|
+
))}
|
|
87
|
+
</View>
|
|
88
|
+
</ScrollView>
|
|
89
|
+
|
|
90
|
+
<View style={styles.footer}>
|
|
91
|
+
<AtomicButton
|
|
92
|
+
title={isLoading ? displayProcessingText : displayPurchaseButtonText}
|
|
93
|
+
onPress={onPurchase}
|
|
94
|
+
disabled={!selectedPackageId || isLoading}
|
|
95
|
+
/>
|
|
96
|
+
</View>
|
|
97
|
+
|
|
98
|
+
<PaywallLegalFooter />
|
|
99
|
+
</View>
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
CreditsTabContent.displayName = "CreditsTabContent";
|
|
105
|
+
|
|
106
|
+
const styles = StyleSheet.create({
|
|
107
|
+
container: {
|
|
108
|
+
flex: 1,
|
|
109
|
+
},
|
|
110
|
+
infoCard: {
|
|
111
|
+
marginHorizontal: 24,
|
|
112
|
+
marginBottom: 16,
|
|
113
|
+
padding: 12,
|
|
114
|
+
borderRadius: 12,
|
|
115
|
+
},
|
|
116
|
+
scrollView: {
|
|
117
|
+
flex: 1,
|
|
118
|
+
},
|
|
119
|
+
scrollContent: {
|
|
120
|
+
paddingHorizontal: 24,
|
|
121
|
+
paddingBottom: 16,
|
|
122
|
+
},
|
|
123
|
+
packagesContainer: {
|
|
124
|
+
gap: 12,
|
|
125
|
+
},
|
|
126
|
+
footer: {
|
|
127
|
+
padding: 24,
|
|
128
|
+
paddingTop: 16,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall Feature Item Component
|
|
3
|
+
* Single Responsibility: Display a single feature in the features list
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system-atoms";
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
10
|
+
|
|
11
|
+
interface PaywallFeatureItemProps {
|
|
12
|
+
icon: string;
|
|
13
|
+
text: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const PaywallFeatureItem: React.FC<PaywallFeatureItemProps> = React.memo(
|
|
17
|
+
({ icon, text }) => {
|
|
18
|
+
const tokens = useAppDesignTokens();
|
|
19
|
+
|
|
20
|
+
// Icon name should already be PascalCase (e.g., "Sparkles", "Image", "Wand")
|
|
21
|
+
// If not, convert first letter to uppercase
|
|
22
|
+
const iconName = useMemo(() => {
|
|
23
|
+
if (!icon) return "Circle";
|
|
24
|
+
// If already PascalCase, return as is
|
|
25
|
+
if (/^[A-Z]/.test(icon)) {
|
|
26
|
+
return icon;
|
|
27
|
+
}
|
|
28
|
+
// Convert first letter to uppercase
|
|
29
|
+
return icon.charAt(0).toUpperCase() + icon.slice(1);
|
|
30
|
+
}, [icon]);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<View style={styles.featureItem}>
|
|
34
|
+
<AtomicIcon
|
|
35
|
+
name={iconName}
|
|
36
|
+
customSize={20}
|
|
37
|
+
customColor={tokens.colors.primary}
|
|
38
|
+
style={styles.featureIcon}
|
|
39
|
+
/>
|
|
40
|
+
<AtomicText
|
|
41
|
+
type="bodyMedium"
|
|
42
|
+
style={[styles.featureText, { color: tokens.colors.text }]}
|
|
43
|
+
>
|
|
44
|
+
{text}
|
|
45
|
+
</AtomicText>
|
|
46
|
+
</View>
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
PaywallFeatureItem.displayName = "PaywallFeatureItem";
|
|
52
|
+
|
|
53
|
+
const styles = StyleSheet.create({
|
|
54
|
+
featureItem: {
|
|
55
|
+
flexDirection: "row",
|
|
56
|
+
alignItems: "center",
|
|
57
|
+
},
|
|
58
|
+
featureIcon: {
|
|
59
|
+
marginRight: 12,
|
|
60
|
+
},
|
|
61
|
+
featureText: {
|
|
62
|
+
fontSize: 15,
|
|
63
|
+
flex: 1,
|
|
64
|
+
lineHeight: 22,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall Features List Component
|
|
3
|
+
* Displays premium features list
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import { PaywallFeatureItem } from "./PaywallFeatureItem";
|
|
9
|
+
|
|
10
|
+
interface PaywallFeaturesListProps {
|
|
11
|
+
/** Features list */
|
|
12
|
+
features: Array<{ icon: string; text: string }>;
|
|
13
|
+
/** Optional custom container style */
|
|
14
|
+
containerStyle?: object;
|
|
15
|
+
/** Optional gap between items (default: 12) */
|
|
16
|
+
gap?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const PaywallFeaturesList: React.FC<PaywallFeaturesListProps> = React.memo(
|
|
20
|
+
({ features, containerStyle, gap = 12 }) => {
|
|
21
|
+
return (
|
|
22
|
+
<View style={[styles.container, containerStyle]}>
|
|
23
|
+
{features.map((feature, index) => (
|
|
24
|
+
<View
|
|
25
|
+
key={`${feature.icon}-${feature.text}-${index}`}
|
|
26
|
+
style={{ marginBottom: index < features.length - 1 ? gap : 0 }}
|
|
27
|
+
>
|
|
28
|
+
<PaywallFeatureItem icon={feature.icon} text={feature.text} />
|
|
29
|
+
</View>
|
|
30
|
+
))}
|
|
31
|
+
</View>
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
PaywallFeaturesList.displayName = "PaywallFeaturesList";
|
|
37
|
+
|
|
38
|
+
const styles = StyleSheet.create({
|
|
39
|
+
container: {
|
|
40
|
+
width: "100%",
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|