@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.0.0",
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
- return (
50
- <View style={containerStyle}>
51
- <PremiumDetailsCard
52
- statusType={config.statusType}
53
- isPremium={config.isPremium}
54
- expirationDate={config.expirationDate}
55
- purchaseDate={config.purchaseDate}
56
- isLifetime={config.isLifetime}
57
- daysRemaining={config.daysRemaining}
58
- credits={config.credits}
59
- translations={config.translations}
60
- onManageSubscription={config.onManageSubscription}
61
- onUpgrade={config.onUpgrade}
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
+ });