@umituz/react-native-subscription 1.4.0 → 1.5.1
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 +42 -39
- 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/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.1",
|
|
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,74 +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";
|
|
74
|
+
|
|
75
|
+
// =============================================================================
|
|
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";
|
|
81
88
|
|
|
82
89
|
// =============================================================================
|
|
83
|
-
// UTILS
|
|
90
|
+
// UTILS - Date & Price
|
|
84
91
|
// =============================================================================
|
|
85
92
|
|
|
86
|
-
// Date utilities
|
|
87
93
|
export {
|
|
88
94
|
isSubscriptionExpired,
|
|
89
95
|
getDaysUntilExpiration,
|
|
90
|
-
} from
|
|
91
|
-
|
|
92
|
-
// Price utilities
|
|
93
|
-
export { formatPrice } from './utils/priceUtils';
|
|
96
|
+
} from "./utils/dateValidationUtils";
|
|
94
97
|
|
|
98
|
+
export { formatPrice } from "./utils/priceUtils";
|
|
95
99
|
|
|
96
100
|
// =============================================================================
|
|
97
|
-
//
|
|
101
|
+
// UTILS - User Tier
|
|
98
102
|
// =============================================================================
|
|
99
103
|
|
|
100
104
|
export type {
|
|
101
105
|
UserTier,
|
|
102
106
|
UserTierInfo,
|
|
103
107
|
PremiumStatusFetcher,
|
|
104
|
-
} from
|
|
108
|
+
} from "./utils/types";
|
|
105
109
|
|
|
106
110
|
export {
|
|
107
111
|
getUserTierInfo,
|
|
108
112
|
checkPremiumAccess,
|
|
109
|
-
} from
|
|
113
|
+
} from "./utils/tierUtils";
|
|
110
114
|
|
|
111
115
|
export {
|
|
112
116
|
hasTierAccess,
|
|
113
117
|
isTierPremium,
|
|
114
118
|
isTierFreemium,
|
|
115
119
|
isTierGuest,
|
|
116
|
-
} from
|
|
120
|
+
} from "./utils/userTierUtils";
|
|
117
121
|
|
|
118
122
|
export {
|
|
119
123
|
isAuthenticated,
|
|
120
124
|
isGuest,
|
|
121
|
-
} from
|
|
125
|
+
} from "./utils/authUtils";
|
|
122
126
|
|
|
123
127
|
export {
|
|
124
128
|
isValidUserTier,
|
|
@@ -127,5 +131,4 @@ export {
|
|
|
127
131
|
validateIsGuest,
|
|
128
132
|
validateIsPremium,
|
|
129
133
|
validateFetcher,
|
|
130
|
-
} from
|
|
131
|
-
|
|
134
|
+
} from "./utils/validation";
|
|
@@ -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
|
+
|