@umituz/react-native-subscription 2.14.8 → 2.14.10

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.14.8",
3
+ "version": "2.14.10",
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",
@@ -62,7 +62,7 @@ export const PaywallTabBar: React.FC<PaywallTabBarProps> = React.memo(
62
62
  return (
63
63
  <View style={[styles.container, { backgroundColor: tokens.colors.surfaceSecondary }]}>
64
64
  <Animated.View
65
- style={[styles.indicator, { backgroundColor: tokens.colors.surface, left: indicatorLeft }]}
65
+ style={[styles.indicator, { backgroundColor: tokens.colors.surface, left: indicatorLeft }] as any}
66
66
  />
67
67
  {renderTab("credits", creditsLabel)}
68
68
  {renderTab("subscription", subscriptionLabel)}
package/src/index.ts CHANGED
@@ -53,8 +53,17 @@ export {
53
53
 
54
54
  export {
55
55
  useFeatureGate,
56
+ useAuthGate,
57
+ useSubscriptionGate,
58
+ useCreditsGate,
56
59
  type UseFeatureGateParams,
57
60
  type UseFeatureGateResult,
61
+ type UseAuthGateParams,
62
+ type UseAuthGateResult,
63
+ type UseSubscriptionGateParams,
64
+ type UseSubscriptionGateResult,
65
+ type UseCreditsGateParams,
66
+ type UseCreditsGateResult,
58
67
  } from "./presentation/hooks/useFeatureGate";
59
68
 
60
69
  export {
@@ -8,7 +8,7 @@ import {
8
8
  View,
9
9
  Modal,
10
10
  TouchableOpacity,
11
- TouchableWithoutFeedback,
11
+ Pressable,
12
12
  TextInput,
13
13
  KeyboardAvoidingView,
14
14
  } from "react-native";
@@ -75,14 +75,13 @@ export const PaywallFeedbackModal: React.FC<PaywallFeedbackModalProps> = React.m
75
75
  animationType="fade"
76
76
  onRequestClose={handleSkip}
77
77
  >
78
- <TouchableWithoutFeedback onPress={handleSkip}>
79
- <View style={styles.overlay}>
80
- <KeyboardAvoidingView
81
- behavior="padding"
82
- style={styles.keyboardView}
83
- >
84
- <TouchableWithoutFeedback>
85
- <View style={styles.container}>
78
+ <Pressable onPress={handleSkip} style={styles.overlay}>
79
+ <KeyboardAvoidingView
80
+ behavior="padding"
81
+ style={styles.keyboardView}
82
+ >
83
+ <Pressable onPress={(e) => e.stopPropagation()}>
84
+ <View style={styles.container}>
86
85
  <View style={styles.header}>
87
86
  <AtomicText type="headlineMedium" style={styles.title}>
88
87
  {displayTitle}
@@ -159,11 +158,10 @@ export const PaywallFeedbackModal: React.FC<PaywallFeedbackModalProps> = React.m
159
158
  {displaySubmitText}
160
159
  </AtomicText>
161
160
  </TouchableOpacity>
162
- </View>
163
- </TouchableWithoutFeedback>
164
- </KeyboardAvoidingView>
165
- </View>
166
- </TouchableWithoutFeedback>
161
+ </View>
162
+ </Pressable>
163
+ </KeyboardAvoidingView>
164
+ </Pressable>
167
165
  </Modal>
168
166
  );
169
167
  });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * useAuthGate Hook
3
+ *
4
+ * Single responsibility: Authentication gating
5
+ * Checks if user is authenticated before allowing actions.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const { requireAuth, isAuthenticated } = useAuthGate({
10
+ * isAuthenticated: !!user && !user.isAnonymous,
11
+ * onAuthRequired: (callback) => showAuthModal(callback),
12
+ * });
13
+ *
14
+ * const handleAction = () => {
15
+ * requireAuth(() => doSomething());
16
+ * };
17
+ * ```
18
+ */
19
+
20
+ import { useCallback } from "react";
21
+
22
+ declare const __DEV__: boolean;
23
+
24
+ export interface UseAuthGateParams {
25
+ /** Whether user is authenticated (not guest/anonymous) */
26
+ isAuthenticated: boolean;
27
+ /** Callback when auth is required - receives pending action callback */
28
+ onAuthRequired: (pendingCallback: () => void | Promise<void>) => void;
29
+ }
30
+
31
+ export interface UseAuthGateResult {
32
+ /** Whether user is authenticated */
33
+ isAuthenticated: boolean;
34
+ /** Gate action behind auth - executes if authenticated, else shows auth modal */
35
+ requireAuth: (action: () => void | Promise<void>) => boolean;
36
+ }
37
+
38
+ export function useAuthGate(params: UseAuthGateParams): UseAuthGateResult {
39
+ const { isAuthenticated, onAuthRequired } = params;
40
+
41
+ const requireAuth = useCallback(
42
+ (action: () => void | Promise<void>): boolean => {
43
+ if (!isAuthenticated) {
44
+ if (__DEV__) {
45
+
46
+ console.log("[useAuthGate] Not authenticated, showing auth modal");
47
+ }
48
+ onAuthRequired(action);
49
+ return false;
50
+ }
51
+
52
+ if (__DEV__) {
53
+
54
+ console.log("[useAuthGate] Authenticated, proceeding");
55
+ }
56
+ return true;
57
+ },
58
+ [isAuthenticated, onAuthRequired]
59
+ );
60
+
61
+ return {
62
+ isAuthenticated,
63
+ requireAuth,
64
+ };
65
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * useCreditsGate Hook
3
+ *
4
+ * Single responsibility: Credits gating
5
+ * Checks if user has enough credits before allowing actions.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const { requireCredits, hasCredits } = useCreditsGate({
10
+ * hasCredits: canAfford(cost),
11
+ * creditBalance: credits,
12
+ * requiredCredits: cost,
13
+ * onCreditsRequired: (required) => showPaywall(required),
14
+ * });
15
+ *
16
+ * const handleGenerate = () => {
17
+ * requireCredits(() => generate());
18
+ * };
19
+ * ```
20
+ */
21
+
22
+ import { useCallback } from "react";
23
+
24
+ declare const __DEV__: boolean;
25
+
26
+ export interface UseCreditsGateParams {
27
+ /** Whether user has enough credits for the action */
28
+ hasCredits: boolean;
29
+ /** Current credit balance */
30
+ creditBalance: number;
31
+ /** Credits required for this action (optional, for display) */
32
+ requiredCredits?: number;
33
+ /** Callback when credits are required - receives required amount */
34
+ onCreditsRequired: (requiredCredits?: number) => void;
35
+ }
36
+
37
+ export interface UseCreditsGateResult {
38
+ /** Whether user has enough credits */
39
+ hasCredits: boolean;
40
+ /** Current credit balance */
41
+ creditBalance: number;
42
+ /** Gate action behind credits - executes if has credits, else shows paywall */
43
+ requireCredits: (action: () => void | Promise<void>) => boolean;
44
+ }
45
+
46
+ export function useCreditsGate(
47
+ params: UseCreditsGateParams
48
+ ): UseCreditsGateResult {
49
+ const { hasCredits, creditBalance, requiredCredits, onCreditsRequired } =
50
+ params;
51
+
52
+ const requireCredits = useCallback(
53
+ (_action: () => void | Promise<void>): boolean => {
54
+ if (!hasCredits) {
55
+ if (__DEV__) {
56
+
57
+ console.log("[useCreditsGate] Insufficient credits", {
58
+ creditBalance,
59
+ requiredCredits,
60
+ });
61
+ }
62
+ onCreditsRequired(requiredCredits);
63
+ return false;
64
+ }
65
+
66
+ if (__DEV__) {
67
+
68
+ console.log("[useCreditsGate] Has credits, proceeding", {
69
+ creditBalance,
70
+ requiredCredits,
71
+ });
72
+ }
73
+ return true;
74
+ },
75
+ [hasCredits, creditBalance, requiredCredits, onCreditsRequired]
76
+ );
77
+
78
+ return {
79
+ hasCredits,
80
+ creditBalance,
81
+ requireCredits,
82
+ };
83
+ }
@@ -1,158 +1,180 @@
1
1
  /**
2
2
  * useFeatureGate Hook
3
3
  *
4
- * Feature gating with TanStack Query for server state.
5
- * Checks auth, premium status, AND credit balance before allowing actions.
4
+ * Combines auth, subscription, and credits gates into a unified feature gate.
5
+ * Uses composition of smaller, single-responsibility hooks.
6
6
  *
7
7
  * Flow:
8
- * 1. NOT authenticatedonShowAuthModal(callback)
9
- * 2. Authenticated but no credits onShowPaywall()
10
- * 3. Authenticated with credits Execute action
8
+ * 1. Auth checkShow auth modal if not authenticated
9
+ * 2. Subscription check If subscribed, bypass credits and execute
10
+ * 3. Credits checkShow paywall if no credits
11
+ * 4. Execute action
11
12
  *
12
13
  * @example
13
14
  * ```typescript
14
15
  * const { requireFeature } = useFeatureGate({
15
- * userId: user?.uid,
16
- * isAuthenticated: !!user,
17
- * onShowAuthModal: (cb) => authModal.show(cb),
18
- * onShowPaywall: () => setShowPaywall(true),
19
- * creditType: 'image', // or 'text'
16
+ * // Auth config
17
+ * isAuthenticated: !!user && !user.isAnonymous,
18
+ * onShowAuthModal: (cb) => showAuthModal(cb),
19
+ *
20
+ * // Subscription config (optional)
21
+ * hasSubscription: isPremium,
22
+ *
23
+ * // Credits config
24
+ * hasCredits: canAfford(cost),
25
+ * creditBalance: credits,
26
+ * requiredCredits: cost,
27
+ * onShowPaywall: (cost) => showPaywall(cost),
20
28
  * });
21
29
  *
22
- * // Gate a premium feature
23
30
  * const handleGenerate = () => {
24
- * requireFeature(() => generateContent());
31
+ * requireFeature(() => generate());
25
32
  * };
26
33
  * ```
27
34
  */
28
35
 
29
36
  import { useCallback } from "react";
30
- import { useCredits } from "./useCredits";
31
- import type { CreditType } from "../../domain/entities/Credits";
37
+ import { useAuthGate } from "./useAuthGate";
38
+ import { useSubscriptionGate } from "./useSubscriptionGate";
39
+ import { useCreditsGate } from "./useCreditsGate";
32
40
 
33
41
  declare const __DEV__: boolean;
34
42
 
35
43
  export interface UseFeatureGateParams {
36
- /** User ID for credits check */
37
- userId: string | undefined;
38
- /** Whether user is authenticated */
44
+ /** Whether user is authenticated (not guest/anonymous) */
39
45
  isAuthenticated: boolean;
40
46
  /** Callback to show auth modal with pending action */
41
47
  onShowAuthModal: (pendingCallback: () => void | Promise<void>) => void;
42
- /** Callback to show paywall */
43
- onShowPaywall: () => void;
44
- /** Credit type to check (default: 'image') */
45
- creditType?: CreditType;
48
+ /** Whether user has active subscription (optional, defaults to false) */
49
+ hasSubscription?: boolean;
50
+ /** Whether user has enough credits for the action */
51
+ hasCredits: boolean;
52
+ /** Current credit balance */
53
+ creditBalance: number;
54
+ /** Credits required for this action (optional, for paywall display) */
55
+ requiredCredits?: number;
56
+ /** Callback to show paywall - receives required credits */
57
+ onShowPaywall: (requiredCredits?: number) => void;
46
58
  }
47
59
 
48
60
  export interface UseFeatureGateResult {
49
- /** Gate a feature - checks auth first, then credits balance */
61
+ /** Gate a feature - checks auth, subscription, then credits */
50
62
  requireFeature: (action: () => void | Promise<void>) => void;
51
63
  /** Whether user is authenticated */
52
64
  isAuthenticated: boolean;
53
- /** Whether user has credits remaining */
65
+ /** Whether user has active subscription */
66
+ hasSubscription: boolean;
67
+ /** Whether user has enough credits */
54
68
  hasCredits: boolean;
69
+ /** Current credit balance */
70
+ creditBalance: number;
55
71
  /** Whether feature access is allowed */
56
72
  canAccess: boolean;
57
- /** Loading state */
58
- isLoading: boolean;
59
- /** Current credit balance for the specified type */
60
- creditBalance: number;
61
73
  }
62
74
 
63
75
  export function useFeatureGate(
64
76
  params: UseFeatureGateParams
65
77
  ): UseFeatureGateResult {
66
78
  const {
67
- userId,
68
79
  isAuthenticated,
69
80
  onShowAuthModal,
81
+ hasSubscription = false,
82
+ hasCredits,
83
+ creditBalance,
84
+ requiredCredits,
70
85
  onShowPaywall,
71
- creditType = "image",
72
86
  } = params;
73
87
 
74
- // Use TanStack Query to get credits (server state)
75
- const { credits, isLoading, hasImageCredits, hasTextCredits } = useCredits({
76
- userId,
77
- enabled: isAuthenticated && !!userId,
88
+ // Compose individual gates
89
+ const authGate = useAuthGate({
90
+ isAuthenticated,
91
+ onAuthRequired: onShowAuthModal,
78
92
  });
79
93
 
80
- // Check actual credit balance, not just existence
81
- const hasCredits = creditType === "image" ? hasImageCredits : hasTextCredits;
82
- const creditBalance =
83
- creditType === "image"
84
- ? credits?.imageCredits ?? 0
85
- : credits?.textCredits ?? 0;
94
+ const subscriptionGate = useSubscriptionGate({
95
+ hasSubscription,
96
+ onSubscriptionRequired: () => onShowPaywall(requiredCredits),
97
+ });
86
98
 
87
- if (__DEV__) {
88
- console.log("[useFeatureGate] Hook state", {
89
- userId,
90
- isAuthenticated,
91
- creditType,
92
- hasCredits,
93
- creditBalance,
94
- isLoading,
95
- });
96
- }
99
+ const creditsGate = useCreditsGate({
100
+ hasCredits,
101
+ creditBalance,
102
+ requiredCredits,
103
+ onCreditsRequired: onShowPaywall,
104
+ });
97
105
 
98
106
  const requireFeature = useCallback(
99
107
  (action: () => void | Promise<void>) => {
100
108
  if (__DEV__) {
101
- console.log("[useFeatureGate] requireFeature called", {
109
+
110
+ console.log("[useFeatureGate] Checking gates", {
102
111
  isAuthenticated,
112
+ hasSubscription,
103
113
  hasCredits,
104
114
  creditBalance,
105
- creditType,
106
115
  });
107
116
  }
108
117
 
109
- // Step 1: Check authentication
110
- if (!isAuthenticated) {
111
- if (__DEV__) {
112
- console.log("[useFeatureGate] Not authenticated, showing auth modal");
113
- }
114
- onShowAuthModal(() => {
115
- // We NO LONGER call action() blindly here.
116
- // The component will re-render with the new auth state,
117
- // and the user should be allowed to try the action again.
118
- });
118
+ // Step 1: Auth check
119
+ if (!authGate.requireAuth(() => {})) {
120
+ onShowAuthModal(action);
119
121
  return;
120
122
  }
121
123
 
122
- // Step 2: Check credit balance (not just existence)
123
- if (!hasCredits) {
124
+ // Step 2: Subscription check (bypasses credits if subscribed)
125
+ if (hasSubscription) {
124
126
  if (__DEV__) {
125
- console.log("[useFeatureGate] No credits, showing paywall", {
126
- creditBalance,
127
- creditType,
128
- });
127
+
128
+ console.log("[useFeatureGate] Has subscription, executing action");
129
129
  }
130
- onShowPaywall();
130
+ action();
131
131
  return;
132
132
  }
133
133
 
134
- // Step 3: User is authenticated with credits - execute action
134
+ // Step 3: Credits check
135
+ if (!creditsGate.requireCredits(() => {})) {
136
+ return;
137
+ }
138
+
139
+ // Step 4: All checks passed, execute action
135
140
  if (__DEV__) {
136
- console.log("[useFeatureGate] Access granted, executing action");
141
+
142
+ console.log("[useFeatureGate] All gates passed, executing action");
137
143
  }
138
144
  action();
139
145
  },
140
146
  [
147
+ authGate,
148
+ creditsGate,
149
+ hasSubscription,
141
150
  isAuthenticated,
142
151
  hasCredits,
143
152
  creditBalance,
144
- creditType,
145
153
  onShowAuthModal,
146
- onShowPaywall,
147
154
  ]
148
155
  );
149
156
 
150
157
  return {
151
158
  requireFeature,
152
- isAuthenticated,
153
- hasCredits,
154
- canAccess: isAuthenticated && hasCredits,
155
- isLoading,
156
- creditBalance,
159
+ isAuthenticated: authGate.isAuthenticated,
160
+ hasSubscription: subscriptionGate.hasSubscription,
161
+ hasCredits: creditsGate.hasCredits,
162
+ creditBalance: creditsGate.creditBalance,
163
+ canAccess: isAuthenticated && (hasSubscription || hasCredits),
157
164
  };
158
165
  }
166
+
167
+ // Re-export individual gates for standalone use
168
+ export { useAuthGate, useSubscriptionGate, useCreditsGate };
169
+ export type {
170
+ UseAuthGateParams,
171
+ UseAuthGateResult,
172
+ } from "./useAuthGate";
173
+ export type {
174
+ UseSubscriptionGateParams,
175
+ UseSubscriptionGateResult,
176
+ } from "./useSubscriptionGate";
177
+ export type {
178
+ UseCreditsGateParams,
179
+ UseCreditsGateResult,
180
+ } from "./useCreditsGate";
@@ -0,0 +1,67 @@
1
+ /**
2
+ * useSubscriptionGate Hook
3
+ *
4
+ * Single responsibility: Subscription/Premium gating
5
+ * Checks if user has active subscription before allowing actions.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const { requireSubscription, hasSubscription } = useSubscriptionGate({
10
+ * hasSubscription: isPremium,
11
+ * onSubscriptionRequired: () => showPaywall(),
12
+ * });
13
+ *
14
+ * const handlePremiumAction = () => {
15
+ * requireSubscription(() => doPremiumThing());
16
+ * };
17
+ * ```
18
+ */
19
+
20
+ import { useCallback } from "react";
21
+
22
+ declare const __DEV__: boolean;
23
+
24
+ export interface UseSubscriptionGateParams {
25
+ /** Whether user has active subscription */
26
+ hasSubscription: boolean;
27
+ /** Callback when subscription is required */
28
+ onSubscriptionRequired: () => void;
29
+ }
30
+
31
+ export interface UseSubscriptionGateResult {
32
+ /** Whether user has active subscription */
33
+ hasSubscription: boolean;
34
+ /** Gate action behind subscription - executes if subscribed, else shows paywall */
35
+ requireSubscription: (action: () => void | Promise<void>) => boolean;
36
+ }
37
+
38
+ export function useSubscriptionGate(
39
+ params: UseSubscriptionGateParams
40
+ ): UseSubscriptionGateResult {
41
+ const { hasSubscription, onSubscriptionRequired } = params;
42
+
43
+ const requireSubscription = useCallback(
44
+ (_action: () => void | Promise<void>): boolean => {
45
+ if (!hasSubscription) {
46
+ if (__DEV__) {
47
+
48
+ console.log("[useSubscriptionGate] No subscription, showing paywall");
49
+ }
50
+ onSubscriptionRequired();
51
+ return false;
52
+ }
53
+
54
+ if (__DEV__) {
55
+
56
+ console.log("[useSubscriptionGate] Has subscription, proceeding");
57
+ }
58
+ return true;
59
+ },
60
+ [hasSubscription, onSubscriptionRequired]
61
+ );
62
+
63
+ return {
64
+ hasSubscription,
65
+ requireSubscription,
66
+ };
67
+ }