@umituz/react-native-subscription 2.26.14 → 2.26.16
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/domains/paywall/components/PaywallModal.tsx +36 -13
- package/src/domains/paywall/components/PlanCard.tsx +16 -3
- package/src/domains/paywall/entities/types.ts +4 -0
- package/src/domains/paywall/hooks/usePaywallTranslations.ts +8 -0
- package/src/presentation/hooks/index.ts +0 -13
- package/src/presentation/hooks/useFeatureGate.ts +12 -33
- package/src/revenuecat/domain/types/RevenueCatTypes.ts +32 -0
- package/src/revenuecat/index.ts +1 -0
- package/src/revenuecat/presentation/hooks/useRevenueCatTrialEligibility.ts +179 -0
- package/src/presentation/hooks/useAuthAwarePurchase.md +0 -92
- package/src/presentation/hooks/useAuthGate.md +0 -89
- package/src/presentation/hooks/useAuthGate.ts +0 -65
- package/src/presentation/hooks/useCreditChecker.md +0 -102
- package/src/presentation/hooks/useCreditChecker.ts +0 -41
- package/src/presentation/hooks/useCreditsGate.md +0 -94
- package/src/presentation/hooks/useCreditsGate.ts +0 -67
- package/src/presentation/hooks/useDevTestCallbacks.md +0 -91
- package/src/presentation/hooks/useDevTestCallbacks.ts +0 -142
- package/src/presentation/hooks/useInitializeCredits.md +0 -92
- package/src/presentation/hooks/useInitializeCredits.ts +0 -57
- package/src/presentation/hooks/usePremiumGate.md +0 -88
- package/src/presentation/hooks/usePremiumGate.ts +0 -116
- package/src/presentation/hooks/usePremiumWithCredits.md +0 -92
- package/src/presentation/hooks/usePremiumWithCredits.ts +0 -48
- package/src/presentation/hooks/useSubscription.md +0 -94
- package/src/presentation/hooks/useSubscription.ts +0 -119
- package/src/presentation/hooks/useSubscriptionDetails.md +0 -93
- package/src/presentation/hooks/useSubscriptionDetails.ts +0 -85
- package/src/presentation/hooks/useSubscriptionGate.md +0 -84
- package/src/presentation/hooks/useSubscriptionGate.ts +0 -67
- package/src/presentation/hooks/useSubscriptionStatus.md +0 -94
- package/src/presentation/hooks/useTrialEligibility.ts +0 -66
- package/src/presentation/hooks/useUserTier.md +0 -91
- package/src/presentation/hooks/useUserTier.ts +0 -78
- package/src/presentation/hooks/useUserTierWithRepository.md +0 -92
- package/src/presentation/hooks/useUserTierWithRepository.ts +0 -151
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.26.
|
|
3
|
+
"version": "2.26.16",
|
|
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",
|
|
@@ -16,6 +16,14 @@ import { usePurchaseLoadingStore, selectIsPurchasing } from "../../../presentati
|
|
|
16
16
|
|
|
17
17
|
declare const __DEV__: boolean;
|
|
18
18
|
|
|
19
|
+
/** Trial eligibility info per product */
|
|
20
|
+
export interface TrialEligibilityInfo {
|
|
21
|
+
/** Whether eligible for trial */
|
|
22
|
+
eligible: boolean;
|
|
23
|
+
/** Trial duration in days */
|
|
24
|
+
durationDays?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
export interface PaywallModalProps {
|
|
20
28
|
visible: boolean;
|
|
21
29
|
onClose: () => void;
|
|
@@ -30,10 +38,12 @@ export interface PaywallModalProps {
|
|
|
30
38
|
heroImage?: ImageSourcePropType;
|
|
31
39
|
onPurchase?: (pkg: PurchasesPackage) => Promise<void | boolean>;
|
|
32
40
|
onRestore?: () => Promise<void | boolean>;
|
|
41
|
+
/** Trial eligibility map per product ID */
|
|
42
|
+
trialEligibility?: Record<string, TrialEligibilityInfo>;
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
36
|
-
const { visible, onClose, translations, packages = [], features = [], isLoading = false, legalUrls = {}, bestValueIdentifier, creditAmounts, creditsLabel, heroImage, onPurchase, onRestore } = props;
|
|
46
|
+
const { visible, onClose, translations, packages = [], features = [], isLoading = false, legalUrls = {}, bestValueIdentifier, creditAmounts, creditsLabel, heroImage, onPurchase, onRestore, trialEligibility = {} } = props;
|
|
37
47
|
const tokens = useAppDesignTokens();
|
|
38
48
|
const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);
|
|
39
49
|
const [isLocalProcessing, setIsLocalProcessing] = useState(false);
|
|
@@ -114,7 +124,11 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
|
114
124
|
|
|
115
125
|
<View style={styles.header}>
|
|
116
126
|
<AtomicText type="headlineMedium" style={[styles.title, { color: tokens.colors.textPrimary }]}>{translations.title}</AtomicText>
|
|
117
|
-
{translations.
|
|
127
|
+
{(translations.trialSubtitle || translations.subtitle) && (
|
|
128
|
+
<AtomicText type="bodyMedium" style={[styles.subtitle, { color: tokens.colors.textSecondary }]}>
|
|
129
|
+
{translations.trialSubtitle ?? translations.subtitle}
|
|
130
|
+
</AtomicText>
|
|
131
|
+
)}
|
|
118
132
|
</View>
|
|
119
133
|
|
|
120
134
|
<PaywallFeatures features={features} />
|
|
@@ -123,17 +137,26 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
|
123
137
|
<View style={styles.loading}><AtomicSpinner size="lg" color="primary" text={translations.loadingText} /></View>
|
|
124
138
|
) : (
|
|
125
139
|
<View style={styles.plans}>
|
|
126
|
-
{packages.map((pkg) =>
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
{packages.map((pkg) => {
|
|
141
|
+
const productId = pkg.product.identifier;
|
|
142
|
+
const eligibility = trialEligibility[productId];
|
|
143
|
+
const hasFreeTrial = eligibility?.eligible ?? false;
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<PlanCard
|
|
147
|
+
key={productId}
|
|
148
|
+
pkg={pkg}
|
|
149
|
+
isSelected={selectedPlanId === productId}
|
|
150
|
+
onSelect={() => setSelectedPlanId(productId)}
|
|
151
|
+
badge={productId === bestValueIdentifier ? translations.bestValueBadgeText : undefined}
|
|
152
|
+
creditAmount={creditAmounts?.[productId]}
|
|
153
|
+
creditsLabel={creditsLabel}
|
|
154
|
+
hasFreeTrial={hasFreeTrial}
|
|
155
|
+
trialDurationDays={eligibility?.durationDays}
|
|
156
|
+
trialBadgeText={hasFreeTrial ? translations.trialBadgeText : undefined}
|
|
157
|
+
/>
|
|
158
|
+
);
|
|
159
|
+
})}
|
|
137
160
|
</View>
|
|
138
161
|
)}
|
|
139
162
|
|
|
@@ -17,14 +17,23 @@ interface PlanCardProps {
|
|
|
17
17
|
badge?: string;
|
|
18
18
|
creditAmount?: number;
|
|
19
19
|
creditsLabel?: string;
|
|
20
|
+
/** Whether this plan has a free trial */
|
|
21
|
+
hasFreeTrial?: boolean;
|
|
22
|
+
/** Trial duration in days */
|
|
23
|
+
trialDurationDays?: number;
|
|
24
|
+
/** Trial badge text (e.g., "7 days free") */
|
|
25
|
+
trialBadgeText?: string;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
export const PlanCard: React.FC<PlanCardProps> = React.memo(
|
|
23
|
-
({ pkg, isSelected, onSelect, badge, creditAmount, creditsLabel }) => {
|
|
29
|
+
({ pkg, isSelected, onSelect, badge, creditAmount, creditsLabel, hasFreeTrial, trialBadgeText }) => {
|
|
24
30
|
const tokens = useAppDesignTokens();
|
|
25
31
|
const title = pkg.product.title;
|
|
26
32
|
const price = formatPrice(pkg.product.price, pkg.product.currencyCode);
|
|
27
33
|
|
|
34
|
+
// Determine which badge to show (trial badge takes priority if eligible)
|
|
35
|
+
const displayBadge = hasFreeTrial && trialBadgeText ? trialBadgeText : badge;
|
|
36
|
+
|
|
28
37
|
return (
|
|
29
38
|
<TouchableOpacity onPress={onSelect} activeOpacity={0.7} style={styles.touchable}>
|
|
30
39
|
<View
|
|
@@ -37,9 +46,13 @@ export const PlanCard: React.FC<PlanCardProps> = React.memo(
|
|
|
37
46
|
},
|
|
38
47
|
]}
|
|
39
48
|
>
|
|
40
|
-
{
|
|
49
|
+
{displayBadge && (
|
|
41
50
|
<View style={styles.badgeContainer}>
|
|
42
|
-
<AtomicBadge
|
|
51
|
+
<AtomicBadge
|
|
52
|
+
text={displayBadge}
|
|
53
|
+
variant={hasFreeTrial ? "success" : "primary"}
|
|
54
|
+
size="sm"
|
|
55
|
+
/>
|
|
43
56
|
</View>
|
|
44
57
|
)}
|
|
45
58
|
|
|
@@ -41,6 +41,10 @@ export interface PaywallTranslations {
|
|
|
41
41
|
privacyText?: string;
|
|
42
42
|
termsOfServiceText?: string;
|
|
43
43
|
bestValueBadgeText?: string;
|
|
44
|
+
/** Trial-related translations */
|
|
45
|
+
trialBadgeText?: string;
|
|
46
|
+
/** Trial subtitle (e.g., "Try free for 7 days, then $X/year") */
|
|
47
|
+
trialSubtitle?: string;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
export interface PaywallLegalUrls {
|
|
@@ -13,6 +13,10 @@ interface PaywallTranslationKeys {
|
|
|
13
13
|
processingText: string;
|
|
14
14
|
privacyText: string;
|
|
15
15
|
termsOfServiceText: string;
|
|
16
|
+
/** Trial badge text key */
|
|
17
|
+
trialBadgeText?: string;
|
|
18
|
+
/** Trial subtitle key */
|
|
19
|
+
trialSubtitle?: string;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
interface UsePaywallTranslationsParams {
|
|
@@ -38,6 +42,8 @@ const DEFAULT_KEYS: PaywallTranslationKeys = {
|
|
|
38
42
|
processingText: "paywall.processing",
|
|
39
43
|
privacyText: "auth.privacyPolicy",
|
|
40
44
|
termsOfServiceText: "auth.termsOfService",
|
|
45
|
+
trialBadgeText: "paywall.trial.badge",
|
|
46
|
+
trialSubtitle: "paywall.trial.subtitle",
|
|
41
47
|
};
|
|
42
48
|
|
|
43
49
|
export const usePaywallTranslations = ({
|
|
@@ -62,6 +68,8 @@ export const usePaywallTranslations = ({
|
|
|
62
68
|
processingText: t(mergedKeys.processingText),
|
|
63
69
|
privacyText: t(mergedKeys.privacyText),
|
|
64
70
|
termsOfServiceText: t(mergedKeys.termsOfServiceText),
|
|
71
|
+
trialBadgeText: mergedKeys.trialBadgeText ? t(mergedKeys.trialBadgeText) : undefined,
|
|
72
|
+
trialSubtitle: mergedKeys.trialSubtitle ? t(mergedKeys.trialSubtitle) : undefined,
|
|
65
73
|
}),
|
|
66
74
|
[t, mergedKeys],
|
|
67
75
|
);
|
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
export * from "./useAuthAwarePurchase";
|
|
2
|
-
export * from "./useAuthGate";
|
|
3
2
|
export * from "./useAuthSubscriptionSync";
|
|
4
3
|
export * from "./useSavedPurchaseAutoExecution";
|
|
5
|
-
export * from "./useCreditChecker";
|
|
6
4
|
export * from "./useCredits";
|
|
7
|
-
export * from "./useCreditsGate";
|
|
8
5
|
export * from "./useDeductCredit";
|
|
9
|
-
export * from "./useInitializeCredits";
|
|
10
|
-
export * from "./useDevTestCallbacks";
|
|
11
6
|
export * from "./useFeatureGate";
|
|
12
7
|
export * from "./usePaywallVisibility";
|
|
13
8
|
export * from "./usePremium";
|
|
14
|
-
export * from "./usePremiumGate";
|
|
15
|
-
export * from "./usePremiumWithCredits";
|
|
16
|
-
export * from "./useSubscription";
|
|
17
|
-
export * from "./useSubscriptionDetails";
|
|
18
|
-
export * from "./useSubscriptionGate";
|
|
19
9
|
export * from "./useSubscriptionSettingsConfig";
|
|
20
10
|
export * from "./useSubscriptionStatus";
|
|
21
|
-
export * from "./useUserTier";
|
|
22
|
-
export * from "./useUserTierWithRepository";
|
|
23
|
-
export * from "./useTrialEligibility";
|
|
24
11
|
export * from "./feedback/usePaywallFeedback";
|
|
25
12
|
export * from "./feedback/useFeedbackSubmit";
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useFeatureGate Hook
|
|
3
|
-
*
|
|
3
|
+
* Unified feature gate: Auth → Subscription → Credits
|
|
4
4
|
* Uses ref pattern to avoid stale closure issues.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { useCallback, useRef, useEffect } from "react";
|
|
8
|
-
import { useAuthGate } from "./useAuthGate";
|
|
9
|
-
import { useSubscriptionGate } from "./useSubscriptionGate";
|
|
10
|
-
import { useCreditsGate } from "./useCreditsGate";
|
|
11
8
|
|
|
12
9
|
declare const __DEV__: boolean;
|
|
13
10
|
|
|
@@ -29,9 +26,7 @@ export interface UseFeatureGateResult {
|
|
|
29
26
|
readonly canAccess: boolean;
|
|
30
27
|
}
|
|
31
28
|
|
|
32
|
-
export function useFeatureGate(
|
|
33
|
-
params: UseFeatureGateParams
|
|
34
|
-
): UseFeatureGateResult {
|
|
29
|
+
export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResult {
|
|
35
30
|
const {
|
|
36
31
|
isAuthenticated,
|
|
37
32
|
onShowAuthModal,
|
|
@@ -79,22 +74,6 @@ export function useFeatureGate(
|
|
|
79
74
|
prevCreditBalanceRef.current = creditBalance;
|
|
80
75
|
}, [creditBalance]);
|
|
81
76
|
|
|
82
|
-
const authGate = useAuthGate({
|
|
83
|
-
isAuthenticated,
|
|
84
|
-
onAuthRequired: onShowAuthModal,
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
const subscriptionGate = useSubscriptionGate({
|
|
88
|
-
hasSubscription,
|
|
89
|
-
onSubscriptionRequired: () => onShowPaywall(requiredCredits),
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const creditsGate = useCreditsGate({
|
|
93
|
-
creditBalance,
|
|
94
|
-
requiredCredits,
|
|
95
|
-
onCreditsRequired: onShowPaywall,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
77
|
const requireFeature = useCallback(
|
|
99
78
|
(action: () => void | Promise<void>) => {
|
|
100
79
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -106,7 +85,7 @@ export function useFeatureGate(
|
|
|
106
85
|
});
|
|
107
86
|
}
|
|
108
87
|
|
|
109
|
-
if (!
|
|
88
|
+
if (!isAuthenticated) {
|
|
110
89
|
const postAuthAction = () => {
|
|
111
90
|
if (hasSubscriptionRef.current) {
|
|
112
91
|
action();
|
|
@@ -132,30 +111,30 @@ export function useFeatureGate(
|
|
|
132
111
|
return;
|
|
133
112
|
}
|
|
134
113
|
|
|
135
|
-
|
|
114
|
+
const currentBalance = creditBalanceRef.current;
|
|
115
|
+
if (currentBalance < requiredCredits) {
|
|
116
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
117
|
+
console.log("[useFeatureGate] No credits, showing paywall");
|
|
118
|
+
}
|
|
136
119
|
pendingActionRef.current = action;
|
|
137
120
|
isWaitingForPurchaseRef.current = true;
|
|
121
|
+
onShowPaywall(requiredCredits);
|
|
138
122
|
return;
|
|
139
123
|
}
|
|
140
124
|
|
|
141
125
|
action();
|
|
142
126
|
},
|
|
143
|
-
[
|
|
127
|
+
[isAuthenticated, hasSubscription, requiredCredits, onShowAuthModal, onShowPaywall]
|
|
144
128
|
);
|
|
145
129
|
|
|
146
130
|
const hasCredits = creditBalance >= requiredCredits;
|
|
147
131
|
|
|
148
132
|
return {
|
|
149
133
|
requireFeature,
|
|
150
|
-
isAuthenticated
|
|
151
|
-
hasSubscription
|
|
134
|
+
isAuthenticated,
|
|
135
|
+
hasSubscription,
|
|
152
136
|
hasCredits,
|
|
153
137
|
creditBalance,
|
|
154
138
|
canAccess: isAuthenticated && (hasSubscription || hasCredits),
|
|
155
139
|
};
|
|
156
140
|
}
|
|
157
|
-
|
|
158
|
-
export { useAuthGate, useSubscriptionGate, useCreditsGate };
|
|
159
|
-
export type { UseAuthGateParams, UseAuthGateResult } from "./useAuthGate";
|
|
160
|
-
export type { UseSubscriptionGateParams, UseSubscriptionGateResult } from "./useSubscriptionGate";
|
|
161
|
-
export type { UseCreditsGateParams, UseCreditsGateResult } from "./useCreditsGate";
|
|
@@ -75,3 +75,35 @@ export function getErrorMessage(error: unknown, fallback: string): string {
|
|
|
75
75
|
}
|
|
76
76
|
return fallback;
|
|
77
77
|
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Trial Eligibility Types
|
|
81
|
+
* For RevenueCat introductory offer (free trial) support
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
/** Trial info for a subscription product */
|
|
85
|
+
export interface TrialInfo {
|
|
86
|
+
/** Whether user is eligible for trial */
|
|
87
|
+
eligible: boolean;
|
|
88
|
+
/** Trial duration in days */
|
|
89
|
+
durationDays: number;
|
|
90
|
+
/** Product identifier */
|
|
91
|
+
productId: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Configuration for trial display */
|
|
95
|
+
export interface TrialDisplayConfig {
|
|
96
|
+
/** Product IDs that have trial offers */
|
|
97
|
+
trialProductIds: string[];
|
|
98
|
+
/** Default trial duration in days */
|
|
99
|
+
defaultTrialDays: number;
|
|
100
|
+
/** Whether to show trial badge */
|
|
101
|
+
showTrialBadge: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Default trial configuration */
|
|
105
|
+
export const DEFAULT_TRIAL_CONFIG: TrialDisplayConfig = {
|
|
106
|
+
trialProductIds: [],
|
|
107
|
+
defaultTrialDays: 7,
|
|
108
|
+
showTrialBadge: true,
|
|
109
|
+
};
|
package/src/revenuecat/index.ts
CHANGED
|
@@ -10,3 +10,4 @@ export * from "./presentation/hooks/useRevenueCat";
|
|
|
10
10
|
export * from "./presentation/hooks/useCustomerInfo";
|
|
11
11
|
export * from "./presentation/hooks/usePaywallFlow";
|
|
12
12
|
export * from "./presentation/hooks/useSubscriptionQueries";
|
|
13
|
+
export * from "./presentation/hooks/useRevenueCatTrialEligibility";
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useRevenueCatTrialEligibility Hook
|
|
3
|
+
* Checks if user is eligible for introductory offers via RevenueCat
|
|
4
|
+
* Uses Apple's native mechanism for trial eligibility
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
8
|
+
import Purchases, {
|
|
9
|
+
type IntroEligibility,
|
|
10
|
+
INTRO_ELIGIBILITY_STATUS,
|
|
11
|
+
} from "react-native-purchases";
|
|
12
|
+
import { getRevenueCatService } from "../../infrastructure/services/RevenueCatService";
|
|
13
|
+
|
|
14
|
+
declare const __DEV__: boolean;
|
|
15
|
+
|
|
16
|
+
/** Trial eligibility info for a single product */
|
|
17
|
+
export interface ProductTrialEligibility {
|
|
18
|
+
/** Product identifier */
|
|
19
|
+
productId: string;
|
|
20
|
+
/** Whether eligible for introductory offer (free trial) */
|
|
21
|
+
eligible: boolean;
|
|
22
|
+
/** Trial duration in days (if available from product) */
|
|
23
|
+
trialDurationDays?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Map of product ID to eligibility */
|
|
27
|
+
export type TrialEligibilityMap = Record<string, ProductTrialEligibility>;
|
|
28
|
+
|
|
29
|
+
export interface UseRevenueCatTrialEligibilityResult {
|
|
30
|
+
/** Map of product IDs to their trial eligibility */
|
|
31
|
+
eligibilityMap: TrialEligibilityMap;
|
|
32
|
+
/** Whether eligibility check is in progress */
|
|
33
|
+
isLoading: boolean;
|
|
34
|
+
/** Whether any product has an eligible trial */
|
|
35
|
+
hasEligibleTrial: boolean;
|
|
36
|
+
/** Check eligibility for specific product IDs */
|
|
37
|
+
checkEligibility: (productIds: string[]) => Promise<void>;
|
|
38
|
+
/** Get eligibility for a specific product */
|
|
39
|
+
getProductEligibility: (productId: string) => ProductTrialEligibility | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Cache duration in milliseconds (5 minutes) */
|
|
43
|
+
const CACHE_DURATION_MS = 5 * 60 * 1000;
|
|
44
|
+
|
|
45
|
+
/** Cached eligibility result */
|
|
46
|
+
interface CachedEligibility {
|
|
47
|
+
data: TrialEligibilityMap;
|
|
48
|
+
timestamp: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let eligibilityCache: CachedEligibility | null = null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Hook to check trial eligibility via RevenueCat
|
|
55
|
+
* Uses Apple's introductory offer eligibility system
|
|
56
|
+
*/
|
|
57
|
+
export function useRevenueCatTrialEligibility(): UseRevenueCatTrialEligibilityResult {
|
|
58
|
+
const [eligibilityMap, setEligibilityMap] = useState<TrialEligibilityMap>({});
|
|
59
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
60
|
+
const isMountedRef = useRef(true);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
isMountedRef.current = true;
|
|
64
|
+
return () => {
|
|
65
|
+
isMountedRef.current = false;
|
|
66
|
+
};
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
const checkEligibility = useCallback(async (productIds: string[]) => {
|
|
70
|
+
if (productIds.length === 0) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check cache validity
|
|
75
|
+
if (
|
|
76
|
+
eligibilityCache &&
|
|
77
|
+
Date.now() - eligibilityCache.timestamp < CACHE_DURATION_MS
|
|
78
|
+
) {
|
|
79
|
+
const allCached = productIds.every(
|
|
80
|
+
(id) => eligibilityCache?.data[id] !== undefined
|
|
81
|
+
);
|
|
82
|
+
if (allCached && isMountedRef.current) {
|
|
83
|
+
setEligibilityMap(eligibilityCache.data);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const service = getRevenueCatService();
|
|
89
|
+
if (!service || !service.isInitialized()) {
|
|
90
|
+
if (__DEV__) {
|
|
91
|
+
console.log("[TrialEligibility] RevenueCat not initialized");
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setIsLoading(true);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const eligibilities: Record<string, IntroEligibility> =
|
|
100
|
+
await Purchases.checkTrialOrIntroDiscountEligibility(productIds);
|
|
101
|
+
|
|
102
|
+
const newMap: TrialEligibilityMap = {};
|
|
103
|
+
|
|
104
|
+
for (const productId of productIds) {
|
|
105
|
+
const eligibility = eligibilities[productId];
|
|
106
|
+
const isEligible =
|
|
107
|
+
eligibility?.status === INTRO_ELIGIBILITY_STATUS.INTRO_ELIGIBILITY_STATUS_ELIGIBLE;
|
|
108
|
+
|
|
109
|
+
newMap[productId] = {
|
|
110
|
+
productId,
|
|
111
|
+
eligible: isEligible,
|
|
112
|
+
trialDurationDays: 7, // Default to 7 days as configured in App Store Connect
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (__DEV__) {
|
|
116
|
+
console.log(
|
|
117
|
+
`[TrialEligibility] ${productId}: ${isEligible ? "ELIGIBLE" : "NOT_ELIGIBLE"}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Update cache
|
|
123
|
+
eligibilityCache = {
|
|
124
|
+
data: { ...eligibilityCache?.data, ...newMap },
|
|
125
|
+
timestamp: Date.now(),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
if (isMountedRef.current) {
|
|
129
|
+
setEligibilityMap((prev) => ({ ...prev, ...newMap }));
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (__DEV__) {
|
|
133
|
+
console.log("[TrialEligibility] Error checking eligibility:", error);
|
|
134
|
+
}
|
|
135
|
+
// On error, default to eligible (better UX)
|
|
136
|
+
const fallbackMap: TrialEligibilityMap = {};
|
|
137
|
+
for (const productId of productIds) {
|
|
138
|
+
fallbackMap[productId] = {
|
|
139
|
+
productId,
|
|
140
|
+
eligible: true,
|
|
141
|
+
trialDurationDays: 7,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (isMountedRef.current) {
|
|
145
|
+
setEligibilityMap((prev) => ({ ...prev, ...fallbackMap }));
|
|
146
|
+
}
|
|
147
|
+
} finally {
|
|
148
|
+
if (isMountedRef.current) {
|
|
149
|
+
setIsLoading(false);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}, []);
|
|
153
|
+
|
|
154
|
+
const getProductEligibility = useCallback(
|
|
155
|
+
(productId: string): ProductTrialEligibility | null => {
|
|
156
|
+
return eligibilityMap[productId] ?? null;
|
|
157
|
+
},
|
|
158
|
+
[eligibilityMap]
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const hasEligibleTrial = Object.values(eligibilityMap).some(
|
|
162
|
+
(e) => e.eligible
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
eligibilityMap,
|
|
167
|
+
isLoading,
|
|
168
|
+
hasEligibleTrial,
|
|
169
|
+
checkEligibility,
|
|
170
|
+
getProductEligibility,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Clear eligibility cache (useful for testing)
|
|
176
|
+
*/
|
|
177
|
+
export function clearTrialEligibilityCache(): void {
|
|
178
|
+
eligibilityCache = null;
|
|
179
|
+
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
# useAuthAwarePurchase Hook
|
|
2
|
-
|
|
3
|
-
Security-focused purchase hook that requires authentication before any transaction.
|
|
4
|
-
|
|
5
|
-
## Location
|
|
6
|
-
|
|
7
|
-
**Import Path**: `@umituz/react-native-subscription`
|
|
8
|
-
|
|
9
|
-
**File**: `src/presentation/hooks/useAuthAwarePurchase.ts`
|
|
10
|
-
|
|
11
|
-
**Type**: Hook
|
|
12
|
-
|
|
13
|
-
## Strategy
|
|
14
|
-
|
|
15
|
-
### Auth-Gated Purchase Flow
|
|
16
|
-
|
|
17
|
-
1. **Auth Provider Validation**: Verify auth provider is configured at app startup
|
|
18
|
-
2. **Authentication Check**: Block purchases for unauthenticated users
|
|
19
|
-
3. **Auth Flow Trigger**: Show auth modal when guest attempts purchase
|
|
20
|
-
4. **Purchase Blocking**: Prevent all transactions without valid authentication
|
|
21
|
-
5. **Post-Auth Purchase**: Allow purchase after user completes authentication
|
|
22
|
-
6. **Security Enforcement**: Server-side validation required for final verification
|
|
23
|
-
|
|
24
|
-
### Integration Points
|
|
25
|
-
|
|
26
|
-
- **Auth Provider Configuration**: Must be configured once at app initialization
|
|
27
|
-
- **Auth Context**: User authentication state
|
|
28
|
-
- **Paywall Domain**: For subscription upgrade flow
|
|
29
|
-
- **Auth UI**: For sign-in/sign-up flows
|
|
30
|
-
- **RevenueCat**: For purchase transactions
|
|
31
|
-
|
|
32
|
-
## Restrictions
|
|
33
|
-
|
|
34
|
-
### REQUIRED
|
|
35
|
-
|
|
36
|
-
- **Auth Provider Configuration**: MUST call `configureAuthProvider()` once at app startup
|
|
37
|
-
- **isAuthenticated Function**: MUST provide function to check auth status
|
|
38
|
-
- **showAuthModal Function**: MUST provide function to show auth UI
|
|
39
|
-
- **Error Handling**: MUST handle purchase failures appropriately
|
|
40
|
-
|
|
41
|
-
### PROHIBITED
|
|
42
|
-
|
|
43
|
-
- **NEVER** use without configuring auth provider first
|
|
44
|
-
- **NEVER** bypass auth checks for convenience
|
|
45
|
-
- **NEVER** allow anonymous/guest purchases
|
|
46
|
-
- **DO NOT** call handlePurchase/handleRestore without auth provider setup
|
|
47
|
-
|
|
48
|
-
### CRITICAL SAFETY
|
|
49
|
-
|
|
50
|
-
- **ALWAYS** configure auth provider at app initialization
|
|
51
|
-
- **NEVER** allow purchases for anonymous users
|
|
52
|
-
- **MUST** implement proper auth flow with pending purchase preservation
|
|
53
|
-
- **ALWAYS** verify auth status in production
|
|
54
|
-
|
|
55
|
-
## AI Agent Guidelines
|
|
56
|
-
|
|
57
|
-
### When Implementing Auth-Gated Purchases
|
|
58
|
-
|
|
59
|
-
1. **Always** configure auth provider at app startup
|
|
60
|
-
2. **Always** implement isAuthenticated function
|
|
61
|
-
3. **Always** implement showAuthModal function
|
|
62
|
-
4. **Never** bypass auth checks
|
|
63
|
-
5. **Always** test purchase flow with authenticated and unauthenticated users
|
|
64
|
-
|
|
65
|
-
### Integration Checklist
|
|
66
|
-
|
|
67
|
-
- [ ] Import from correct path: `@umituz/react-native-subscription`
|
|
68
|
-
- [ ] Call `configureAuthProvider()` once at app startup
|
|
69
|
-
- [ ] Provide `isAuthenticated()` function
|
|
70
|
-
- [ ] Provide `showAuthModal()` function
|
|
71
|
-
- [ ] Test purchase flow with authenticated user
|
|
72
|
-
- [ ] Test purchase flow with unauthenticated user
|
|
73
|
-
- [ ] Verify auth modal appears for guests
|
|
74
|
-
- [ ] Verify purchase proceeds after authentication
|
|
75
|
-
- [ ] Check development logs for auth verification
|
|
76
|
-
- [ ] Verify purchases are blocked without auth provider
|
|
77
|
-
|
|
78
|
-
### Common Patterns
|
|
79
|
-
|
|
80
|
-
1. **App-Level Config**: Configure once in root App component
|
|
81
|
-
2. **Pending Purchase**: Store package for post-auth completion
|
|
82
|
-
3. **Auth Integration**: Use with Firebase, Auth0, or custom auth
|
|
83
|
-
4. **Error Handling**: Handle auth failures and purchase failures
|
|
84
|
-
5. **Development Testing**: Use dev logs to verify auth checks
|
|
85
|
-
|
|
86
|
-
## Related Documentation
|
|
87
|
-
|
|
88
|
-
- **usePremium**: For purchase and restore operations
|
|
89
|
-
- **usePaywallOperations**: For complete paywall purchase handling
|
|
90
|
-
- **useAuthGate**: For authentication gating
|
|
91
|
-
- **useAuthSubscriptionSync**: For syncing auth with subscription
|
|
92
|
-
- **Security Best Practices**: `src/docs/SECURITY.md`
|