@umituz/react-native-subscription 2.0.0 → 2.1.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 +1 -1
- package/src/index.ts +12 -0
- package/src/presentation/components/sections/SubscriptionSection.tsx +30 -16
- package/src/presentation/hooks/useFeatureGate.ts +37 -0
- package/src/presentation/screens/SubscriptionDetailScreen.tsx +101 -0
- package/src/presentation/screens/components/CreditItem.tsx +103 -0
- package/src/presentation/screens/components/CreditsList.tsx +71 -0
- package/src/presentation/screens/components/SubscriptionActions.tsx +89 -0
- package/src/presentation/screens/components/SubscriptionHeader.tsx +156 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
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",
|
package/src/index.ts
CHANGED
|
@@ -124,6 +124,18 @@ export {
|
|
|
124
124
|
type SubscriptionSectionConfig,
|
|
125
125
|
} from "./presentation/components/sections/SubscriptionSection";
|
|
126
126
|
|
|
127
|
+
// =============================================================================
|
|
128
|
+
// PRESENTATION LAYER - Subscription Detail Screen
|
|
129
|
+
// =============================================================================
|
|
130
|
+
|
|
131
|
+
export {
|
|
132
|
+
SubscriptionDetailScreen,
|
|
133
|
+
type SubscriptionDetailScreenProps,
|
|
134
|
+
type SubscriptionDetailConfig,
|
|
135
|
+
type SubscriptionDetailTranslations,
|
|
136
|
+
} from "./presentation/screens/SubscriptionDetailScreen";
|
|
137
|
+
|
|
138
|
+
|
|
127
139
|
// =============================================================================
|
|
128
140
|
// UTILS - Date & Price
|
|
129
141
|
// =============================================================================
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import React from "react";
|
|
8
|
-
import { View } from "react-native";
|
|
8
|
+
import { View, TouchableOpacity } from "react-native";
|
|
9
9
|
import type { StyleProp, ViewStyle } from "react-native";
|
|
10
10
|
import {
|
|
11
11
|
PremiumDetailsCard,
|
|
@@ -35,6 +35,8 @@ export interface SubscriptionSectionConfig {
|
|
|
35
35
|
onManageSubscription?: () => void;
|
|
36
36
|
/** Handler for upgrade button */
|
|
37
37
|
onUpgrade?: () => void;
|
|
38
|
+
/** Handler for when section is tapped (navigate to detail screen) */
|
|
39
|
+
onPress?: () => void;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export interface SubscriptionSectionProps {
|
|
@@ -46,20 +48,32 @@ export const SubscriptionSection: React.FC<SubscriptionSectionProps> = ({
|
|
|
46
48
|
config,
|
|
47
49
|
containerStyle,
|
|
48
50
|
}) => {
|
|
49
|
-
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
/>
|
|
63
|
-
</View>
|
|
51
|
+
const content = (
|
|
52
|
+
<PremiumDetailsCard
|
|
53
|
+
statusType={config.statusType}
|
|
54
|
+
isPremium={config.isPremium}
|
|
55
|
+
expirationDate={config.expirationDate}
|
|
56
|
+
purchaseDate={config.purchaseDate}
|
|
57
|
+
isLifetime={config.isLifetime}
|
|
58
|
+
daysRemaining={config.daysRemaining}
|
|
59
|
+
credits={config.credits}
|
|
60
|
+
translations={config.translations}
|
|
61
|
+
onManageSubscription={config.onManageSubscription}
|
|
62
|
+
onUpgrade={config.onUpgrade}
|
|
63
|
+
/>
|
|
64
64
|
);
|
|
65
|
+
|
|
66
|
+
if (config.onPress) {
|
|
67
|
+
return (
|
|
68
|
+
<TouchableOpacity
|
|
69
|
+
style={containerStyle}
|
|
70
|
+
onPress={config.onPress}
|
|
71
|
+
activeOpacity={0.7}
|
|
72
|
+
>
|
|
73
|
+
{content}
|
|
74
|
+
</TouchableOpacity>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return <View style={containerStyle}>{content}</View>;
|
|
65
79
|
};
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
import { useCallback } from "react";
|
|
29
29
|
import { useCredits } from "./useCredits";
|
|
30
30
|
|
|
31
|
+
declare const __DEV__: boolean;
|
|
32
|
+
|
|
31
33
|
export interface UseFeatureGateParams {
|
|
32
34
|
/** User ID for credits check */
|
|
33
35
|
userId: string | undefined;
|
|
@@ -66,12 +68,39 @@ export function useFeatureGate(
|
|
|
66
68
|
// User is premium if they have credits
|
|
67
69
|
const isPremium = credits !== null;
|
|
68
70
|
|
|
71
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
72
|
+
// eslint-disable-next-line no-console
|
|
73
|
+
console.log("[useFeatureGate] Hook state", {
|
|
74
|
+
userId,
|
|
75
|
+
isAuthenticated,
|
|
76
|
+
isPremium,
|
|
77
|
+
hasCredits: credits !== null,
|
|
78
|
+
isLoading,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
69
82
|
const requireFeature = useCallback(
|
|
70
83
|
(action: () => void | Promise<void>) => {
|
|
84
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
85
|
+
// eslint-disable-next-line no-console
|
|
86
|
+
console.log("[useFeatureGate] requireFeature() called", {
|
|
87
|
+
isAuthenticated,
|
|
88
|
+
isPremium,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
71
92
|
// Step 1: Check authentication
|
|
72
93
|
if (!isAuthenticated) {
|
|
94
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
95
|
+
// eslint-disable-next-line no-console
|
|
96
|
+
console.log("[useFeatureGate] NOT authenticated → showing auth modal");
|
|
97
|
+
}
|
|
73
98
|
// After auth, re-check premium before executing
|
|
74
99
|
onShowAuthModal(() => {
|
|
100
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
101
|
+
// eslint-disable-next-line no-console
|
|
102
|
+
console.log("[useFeatureGate] Auth modal callback → executing action");
|
|
103
|
+
}
|
|
75
104
|
// This callback runs after successful auth
|
|
76
105
|
// The component will re-render with new auth state
|
|
77
106
|
// and user can try the action again
|
|
@@ -82,11 +111,19 @@ export function useFeatureGate(
|
|
|
82
111
|
|
|
83
112
|
// Step 2: Check premium (has credits from TanStack Query)
|
|
84
113
|
if (!isPremium) {
|
|
114
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
115
|
+
// eslint-disable-next-line no-console
|
|
116
|
+
console.log("[useFeatureGate] NOT premium → showing paywall");
|
|
117
|
+
}
|
|
85
118
|
onShowPaywall();
|
|
86
119
|
return;
|
|
87
120
|
}
|
|
88
121
|
|
|
89
122
|
// Step 3: User is authenticated and premium - execute action
|
|
123
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
124
|
+
// eslint-disable-next-line no-console
|
|
125
|
+
console.log("[useFeatureGate] PREMIUM user → executing action");
|
|
126
|
+
}
|
|
90
127
|
action();
|
|
91
128
|
},
|
|
92
129
|
[isAuthenticated, isPremium, onShowAuthModal, onShowPaywall]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Detail Screen
|
|
3
|
+
* Composition of subscription components
|
|
4
|
+
* No business logic - pure presentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { ScrollView, StyleSheet } from "react-native";
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
10
|
+
import { SubscriptionHeader } from "./components/SubscriptionHeader";
|
|
11
|
+
import { CreditsList } from "./components/CreditsList";
|
|
12
|
+
import { SubscriptionActions } from "./components/SubscriptionActions";
|
|
13
|
+
import type { SubscriptionStatusType } from "../components/details/PremiumStatusBadge";
|
|
14
|
+
import type { CreditInfo } from "../components/details/PremiumDetailsCard";
|
|
15
|
+
|
|
16
|
+
export interface SubscriptionDetailTranslations {
|
|
17
|
+
title: string;
|
|
18
|
+
statusLabel?: string;
|
|
19
|
+
statusActive?: string;
|
|
20
|
+
statusExpired?: string;
|
|
21
|
+
statusFree?: string;
|
|
22
|
+
expiresLabel?: string;
|
|
23
|
+
purchasedLabel?: string;
|
|
24
|
+
lifetimeLabel?: string;
|
|
25
|
+
creditsTitle?: string;
|
|
26
|
+
remainingLabel?: string;
|
|
27
|
+
usageTitle?: string;
|
|
28
|
+
manageButton?: string;
|
|
29
|
+
upgradeButton?: string;
|
|
30
|
+
creditsResetInfo?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SubscriptionDetailConfig {
|
|
34
|
+
statusType: SubscriptionStatusType;
|
|
35
|
+
isPremium: boolean;
|
|
36
|
+
expirationDate?: string | null;
|
|
37
|
+
purchaseDate?: string | null;
|
|
38
|
+
isLifetime?: boolean;
|
|
39
|
+
daysRemaining?: number | null;
|
|
40
|
+
credits?: CreditInfo[];
|
|
41
|
+
translations: SubscriptionDetailTranslations;
|
|
42
|
+
onManageSubscription?: () => void;
|
|
43
|
+
onUpgrade?: () => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SubscriptionDetailScreenProps {
|
|
47
|
+
config: SubscriptionDetailConfig;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const SubscriptionDetailScreen: React.FC<
|
|
51
|
+
SubscriptionDetailScreenProps
|
|
52
|
+
> = ({ config }) => {
|
|
53
|
+
const tokens = useAppDesignTokens();
|
|
54
|
+
const showCredits = config.credits && config.credits.length > 0;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<ScrollView
|
|
58
|
+
style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}
|
|
59
|
+
contentContainerStyle={styles.content}
|
|
60
|
+
>
|
|
61
|
+
<SubscriptionHeader
|
|
62
|
+
statusType={config.statusType}
|
|
63
|
+
isPremium={config.isPremium}
|
|
64
|
+
isLifetime={config.isLifetime}
|
|
65
|
+
expirationDate={config.expirationDate}
|
|
66
|
+
purchaseDate={config.purchaseDate}
|
|
67
|
+
daysRemaining={config.daysRemaining}
|
|
68
|
+
translations={config.translations}
|
|
69
|
+
/>
|
|
70
|
+
|
|
71
|
+
{showCredits && (
|
|
72
|
+
<CreditsList
|
|
73
|
+
credits={config.credits!}
|
|
74
|
+
title={
|
|
75
|
+
config.translations.usageTitle || config.translations.creditsTitle
|
|
76
|
+
}
|
|
77
|
+
description={config.translations.creditsResetInfo}
|
|
78
|
+
remainingLabel={config.translations.remainingLabel}
|
|
79
|
+
/>
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
<SubscriptionActions
|
|
83
|
+
isPremium={config.isPremium}
|
|
84
|
+
manageButtonLabel={config.translations.manageButton}
|
|
85
|
+
upgradeButtonLabel={config.translations.upgradeButton}
|
|
86
|
+
onManage={config.onManageSubscription}
|
|
87
|
+
onUpgrade={config.onUpgrade}
|
|
88
|
+
/>
|
|
89
|
+
</ScrollView>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const styles = StyleSheet.create({
|
|
94
|
+
container: {
|
|
95
|
+
flex: 1,
|
|
96
|
+
},
|
|
97
|
+
content: {
|
|
98
|
+
padding: 16,
|
|
99
|
+
gap: 16,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit Item Component
|
|
3
|
+
* Displays individual credit usage with progress bar
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, Text, StyleSheet } from "react-native";
|
|
8
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
9
|
+
|
|
10
|
+
interface CreditItemProps {
|
|
11
|
+
label: string;
|
|
12
|
+
current: number;
|
|
13
|
+
total: number;
|
|
14
|
+
remainingLabel?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const CreditItem: React.FC<CreditItemProps> = ({
|
|
18
|
+
label,
|
|
19
|
+
current,
|
|
20
|
+
total,
|
|
21
|
+
remainingLabel = "remaining",
|
|
22
|
+
}) => {
|
|
23
|
+
const tokens = useAppDesignTokens();
|
|
24
|
+
const percentage = total > 0 ? (current / total) * 100 : 0;
|
|
25
|
+
const isLow = percentage <= 20;
|
|
26
|
+
const isMedium = percentage > 20 && percentage <= 50;
|
|
27
|
+
|
|
28
|
+
const getColor = () => {
|
|
29
|
+
if (isLow) return tokens.colors.error;
|
|
30
|
+
if (isMedium) return tokens.colors.warning;
|
|
31
|
+
return tokens.colors.success;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<View style={styles.container}>
|
|
36
|
+
<View style={styles.header}>
|
|
37
|
+
<Text style={[styles.label, { color: tokens.colors.textPrimary }]}>
|
|
38
|
+
{label}
|
|
39
|
+
</Text>
|
|
40
|
+
<View style={[styles.badge, { backgroundColor: tokens.colors.surfaceSecondary }]}>
|
|
41
|
+
<Text style={[styles.count, { color: getColor() }]}>
|
|
42
|
+
{current} / {total}
|
|
43
|
+
</Text>
|
|
44
|
+
</View>
|
|
45
|
+
</View>
|
|
46
|
+
<View
|
|
47
|
+
style={[
|
|
48
|
+
styles.progressBar,
|
|
49
|
+
{ backgroundColor: tokens.colors.surfaceSecondary },
|
|
50
|
+
]}
|
|
51
|
+
>
|
|
52
|
+
<View
|
|
53
|
+
style={[
|
|
54
|
+
styles.progressFill,
|
|
55
|
+
{
|
|
56
|
+
width: `${percentage}%`,
|
|
57
|
+
backgroundColor: getColor(),
|
|
58
|
+
},
|
|
59
|
+
]}
|
|
60
|
+
/>
|
|
61
|
+
</View>
|
|
62
|
+
<Text style={[styles.remaining, { color: tokens.colors.textSecondary }]}>
|
|
63
|
+
{current} {remainingLabel}
|
|
64
|
+
</Text>
|
|
65
|
+
</View>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const styles = StyleSheet.create({
|
|
70
|
+
container: {
|
|
71
|
+
gap: 8,
|
|
72
|
+
},
|
|
73
|
+
header: {
|
|
74
|
+
flexDirection: "row",
|
|
75
|
+
justifyContent: "space-between",
|
|
76
|
+
alignItems: "center",
|
|
77
|
+
},
|
|
78
|
+
label: {
|
|
79
|
+
fontSize: 15,
|
|
80
|
+
fontWeight: "500",
|
|
81
|
+
},
|
|
82
|
+
badge: {
|
|
83
|
+
paddingHorizontal: 12,
|
|
84
|
+
paddingVertical: 4,
|
|
85
|
+
borderRadius: 12,
|
|
86
|
+
},
|
|
87
|
+
count: {
|
|
88
|
+
fontSize: 13,
|
|
89
|
+
fontWeight: "600",
|
|
90
|
+
},
|
|
91
|
+
progressBar: {
|
|
92
|
+
height: 8,
|
|
93
|
+
borderRadius: 4,
|
|
94
|
+
overflow: "hidden",
|
|
95
|
+
},
|
|
96
|
+
progressFill: {
|
|
97
|
+
height: "100%",
|
|
98
|
+
borderRadius: 4,
|
|
99
|
+
},
|
|
100
|
+
remaining: {
|
|
101
|
+
fontSize: 12,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credits List Component
|
|
3
|
+
* Displays list of credit usages
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, Text, StyleSheet } from "react-native";
|
|
8
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
9
|
+
import { CreditItem } from "./CreditItem";
|
|
10
|
+
import type { CreditInfo } from "../../components/details/PremiumDetailsCard";
|
|
11
|
+
|
|
12
|
+
interface CreditsListProps {
|
|
13
|
+
credits: CreditInfo[];
|
|
14
|
+
title?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
remainingLabel?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const CreditsList: React.FC<CreditsListProps> = ({
|
|
20
|
+
credits,
|
|
21
|
+
title,
|
|
22
|
+
description,
|
|
23
|
+
remainingLabel,
|
|
24
|
+
}) => {
|
|
25
|
+
const tokens = useAppDesignTokens();
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<View style={[styles.container, { backgroundColor: tokens.colors.surface }]}>
|
|
29
|
+
{title && (
|
|
30
|
+
<Text style={[styles.title, { color: tokens.colors.textPrimary }]}>
|
|
31
|
+
{title}
|
|
32
|
+
</Text>
|
|
33
|
+
)}
|
|
34
|
+
{description && (
|
|
35
|
+
<Text style={[styles.description, { color: tokens.colors.textSecondary }]}>
|
|
36
|
+
{description}
|
|
37
|
+
</Text>
|
|
38
|
+
)}
|
|
39
|
+
<View style={styles.list}>
|
|
40
|
+
{credits.map((credit) => (
|
|
41
|
+
<CreditItem
|
|
42
|
+
key={credit.id}
|
|
43
|
+
label={credit.label}
|
|
44
|
+
current={credit.current}
|
|
45
|
+
total={credit.total}
|
|
46
|
+
remainingLabel={remainingLabel}
|
|
47
|
+
/>
|
|
48
|
+
))}
|
|
49
|
+
</View>
|
|
50
|
+
</View>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const styles = StyleSheet.create({
|
|
55
|
+
container: {
|
|
56
|
+
borderRadius: 16,
|
|
57
|
+
padding: 20,
|
|
58
|
+
gap: 16,
|
|
59
|
+
},
|
|
60
|
+
title: {
|
|
61
|
+
fontSize: 18,
|
|
62
|
+
fontWeight: "700",
|
|
63
|
+
},
|
|
64
|
+
description: {
|
|
65
|
+
fontSize: 14,
|
|
66
|
+
marginTop: -8,
|
|
67
|
+
},
|
|
68
|
+
list: {
|
|
69
|
+
gap: 16,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Actions Component
|
|
3
|
+
* Displays action buttons for subscription management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
|
|
8
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
9
|
+
|
|
10
|
+
interface SubscriptionActionsProps {
|
|
11
|
+
isPremium: boolean;
|
|
12
|
+
manageButtonLabel?: string;
|
|
13
|
+
upgradeButtonLabel?: string;
|
|
14
|
+
onManage?: () => void;
|
|
15
|
+
onUpgrade?: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const SubscriptionActions: React.FC<SubscriptionActionsProps> = ({
|
|
19
|
+
isPremium,
|
|
20
|
+
manageButtonLabel,
|
|
21
|
+
upgradeButtonLabel,
|
|
22
|
+
onManage,
|
|
23
|
+
onUpgrade,
|
|
24
|
+
}) => {
|
|
25
|
+
const tokens = useAppDesignTokens();
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<View style={styles.container}>
|
|
29
|
+
{isPremium && onManage && manageButtonLabel && (
|
|
30
|
+
<TouchableOpacity
|
|
31
|
+
style={[
|
|
32
|
+
styles.secondaryButton,
|
|
33
|
+
{ backgroundColor: tokens.colors.surfaceSecondary },
|
|
34
|
+
]}
|
|
35
|
+
onPress={onManage}
|
|
36
|
+
>
|
|
37
|
+
<Text
|
|
38
|
+
style={[
|
|
39
|
+
styles.secondaryButtonText,
|
|
40
|
+
{ color: tokens.colors.textPrimary },
|
|
41
|
+
]}
|
|
42
|
+
>
|
|
43
|
+
{manageButtonLabel}
|
|
44
|
+
</Text>
|
|
45
|
+
</TouchableOpacity>
|
|
46
|
+
)}
|
|
47
|
+
{!isPremium && onUpgrade && upgradeButtonLabel && (
|
|
48
|
+
<TouchableOpacity
|
|
49
|
+
style={[styles.primaryButton, { backgroundColor: tokens.colors.primary }]}
|
|
50
|
+
onPress={onUpgrade}
|
|
51
|
+
>
|
|
52
|
+
<Text
|
|
53
|
+
style={[
|
|
54
|
+
styles.primaryButtonText,
|
|
55
|
+
{ color: tokens.colors.onPrimary },
|
|
56
|
+
]}
|
|
57
|
+
>
|
|
58
|
+
{upgradeButtonLabel}
|
|
59
|
+
</Text>
|
|
60
|
+
</TouchableOpacity>
|
|
61
|
+
)}
|
|
62
|
+
</View>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const styles = StyleSheet.create({
|
|
67
|
+
container: {
|
|
68
|
+
gap: 12,
|
|
69
|
+
paddingBottom: 32,
|
|
70
|
+
},
|
|
71
|
+
primaryButton: {
|
|
72
|
+
paddingVertical: 16,
|
|
73
|
+
borderRadius: 12,
|
|
74
|
+
alignItems: "center",
|
|
75
|
+
},
|
|
76
|
+
primaryButtonText: {
|
|
77
|
+
fontSize: 16,
|
|
78
|
+
fontWeight: "700",
|
|
79
|
+
},
|
|
80
|
+
secondaryButton: {
|
|
81
|
+
paddingVertical: 16,
|
|
82
|
+
borderRadius: 12,
|
|
83
|
+
alignItems: "center",
|
|
84
|
+
},
|
|
85
|
+
secondaryButtonText: {
|
|
86
|
+
fontSize: 16,
|
|
87
|
+
fontWeight: "600",
|
|
88
|
+
},
|
|
89
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Header Component
|
|
3
|
+
* Displays status badge and subscription details
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, Text, StyleSheet } from "react-native";
|
|
8
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
9
|
+
import {
|
|
10
|
+
PremiumStatusBadge,
|
|
11
|
+
type SubscriptionStatusType,
|
|
12
|
+
} from "../../components/details/PremiumStatusBadge";
|
|
13
|
+
|
|
14
|
+
interface SubscriptionHeaderTranslations {
|
|
15
|
+
title: string;
|
|
16
|
+
statusLabel?: string;
|
|
17
|
+
statusActive?: string;
|
|
18
|
+
statusExpired?: string;
|
|
19
|
+
statusFree?: string;
|
|
20
|
+
expiresLabel?: string;
|
|
21
|
+
purchasedLabel?: string;
|
|
22
|
+
lifetimeLabel?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SubscriptionHeaderProps {
|
|
26
|
+
statusType: SubscriptionStatusType;
|
|
27
|
+
isPremium: boolean;
|
|
28
|
+
isLifetime?: boolean;
|
|
29
|
+
expirationDate?: string | null;
|
|
30
|
+
purchaseDate?: string | null;
|
|
31
|
+
daysRemaining?: number | null;
|
|
32
|
+
translations: SubscriptionHeaderTranslations;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const SubscriptionHeader: React.FC<SubscriptionHeaderProps> = ({
|
|
36
|
+
statusType,
|
|
37
|
+
isPremium,
|
|
38
|
+
isLifetime,
|
|
39
|
+
expirationDate,
|
|
40
|
+
purchaseDate,
|
|
41
|
+
daysRemaining,
|
|
42
|
+
translations,
|
|
43
|
+
}) => {
|
|
44
|
+
const tokens = useAppDesignTokens();
|
|
45
|
+
const showExpiring =
|
|
46
|
+
daysRemaining !== null &&
|
|
47
|
+
daysRemaining !== undefined &&
|
|
48
|
+
daysRemaining <= 7;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<View style={[styles.container, { backgroundColor: tokens.colors.surface }]}>
|
|
52
|
+
<View style={styles.header}>
|
|
53
|
+
<Text style={[styles.title, { color: tokens.colors.textPrimary }]}>
|
|
54
|
+
{translations.title}
|
|
55
|
+
</Text>
|
|
56
|
+
<PremiumStatusBadge
|
|
57
|
+
status={statusType}
|
|
58
|
+
activeLabel={translations.statusActive}
|
|
59
|
+
expiredLabel={translations.statusExpired}
|
|
60
|
+
noneLabel={translations.statusFree}
|
|
61
|
+
/>
|
|
62
|
+
</View>
|
|
63
|
+
|
|
64
|
+
{isPremium && (
|
|
65
|
+
<View style={styles.details}>
|
|
66
|
+
{isLifetime ? (
|
|
67
|
+
<DetailRow
|
|
68
|
+
label={translations.statusLabel || "Subscription"}
|
|
69
|
+
value={translations.lifetimeLabel || "Lifetime Access"}
|
|
70
|
+
tokens={tokens}
|
|
71
|
+
/>
|
|
72
|
+
) : (
|
|
73
|
+
<>
|
|
74
|
+
{expirationDate && (
|
|
75
|
+
<DetailRow
|
|
76
|
+
label={translations.expiresLabel || "Expires"}
|
|
77
|
+
value={expirationDate}
|
|
78
|
+
highlight={showExpiring}
|
|
79
|
+
tokens={tokens}
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
82
|
+
{purchaseDate && (
|
|
83
|
+
<DetailRow
|
|
84
|
+
label={translations.purchasedLabel || "Purchased"}
|
|
85
|
+
value={purchaseDate}
|
|
86
|
+
tokens={tokens}
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
</>
|
|
90
|
+
)}
|
|
91
|
+
</View>
|
|
92
|
+
)}
|
|
93
|
+
</View>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
interface DetailRowProps {
|
|
98
|
+
label: string;
|
|
99
|
+
value: string;
|
|
100
|
+
highlight?: boolean;
|
|
101
|
+
tokens: ReturnType<typeof useAppDesignTokens>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const DetailRow: React.FC<DetailRowProps> = ({
|
|
105
|
+
label,
|
|
106
|
+
value,
|
|
107
|
+
highlight,
|
|
108
|
+
tokens,
|
|
109
|
+
}) => (
|
|
110
|
+
<View style={styles.row}>
|
|
111
|
+
<Text style={[styles.label, { color: tokens.colors.textSecondary }]}>
|
|
112
|
+
{label}
|
|
113
|
+
</Text>
|
|
114
|
+
<Text
|
|
115
|
+
style={[
|
|
116
|
+
styles.value,
|
|
117
|
+
{ color: highlight ? tokens.colors.warning : tokens.colors.textPrimary },
|
|
118
|
+
]}
|
|
119
|
+
>
|
|
120
|
+
{value}
|
|
121
|
+
</Text>
|
|
122
|
+
</View>
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const styles = StyleSheet.create({
|
|
126
|
+
container: {
|
|
127
|
+
borderRadius: 16,
|
|
128
|
+
padding: 20,
|
|
129
|
+
gap: 16,
|
|
130
|
+
},
|
|
131
|
+
header: {
|
|
132
|
+
flexDirection: "row",
|
|
133
|
+
justifyContent: "space-between",
|
|
134
|
+
alignItems: "center",
|
|
135
|
+
},
|
|
136
|
+
title: {
|
|
137
|
+
fontSize: 24,
|
|
138
|
+
fontWeight: "700",
|
|
139
|
+
},
|
|
140
|
+
details: {
|
|
141
|
+
gap: 12,
|
|
142
|
+
paddingTop: 12,
|
|
143
|
+
},
|
|
144
|
+
row: {
|
|
145
|
+
flexDirection: "row",
|
|
146
|
+
justifyContent: "space-between",
|
|
147
|
+
alignItems: "center",
|
|
148
|
+
},
|
|
149
|
+
label: {
|
|
150
|
+
fontSize: 15,
|
|
151
|
+
},
|
|
152
|
+
value: {
|
|
153
|
+
fontSize: 15,
|
|
154
|
+
fontWeight: "600",
|
|
155
|
+
},
|
|
156
|
+
});
|