@umituz/react-native-subscription 2.27.123 → 2.27.125

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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/src/domains/credits/application/creditOperationUtils.ts +65 -144
  3. package/src/domains/credits/application/creditOperationUtils.types.ts +19 -0
  4. package/src/domains/credits/presentation/useCredits.ts +1 -11
  5. package/src/domains/paywall/components/PaywallContainer.tsx +3 -1
  6. package/src/domains/paywall/components/PaywallModal.tsx +19 -107
  7. package/src/domains/paywall/components/PaywallModal.types.ts +26 -0
  8. package/src/domains/paywall/components/PlanCard.tsx +45 -148
  9. package/src/domains/paywall/components/PlanCard.types.ts +12 -0
  10. package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +116 -0
  11. package/src/domains/subscription/application/SubscriptionSyncService.ts +17 -99
  12. package/src/domains/subscription/application/SubscriptionSyncUtils.ts +0 -2
  13. package/src/domains/subscription/core/SubscriptionConstants.ts +1 -13
  14. package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +7 -13
  15. package/src/domains/subscription/infrastructure/managers/SubscriptionManager.types.ts +15 -0
  16. package/src/domains/subscription/infrastructure/managers/subscriptionManagerUtils.ts +2 -1
  17. package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +20 -5
  18. package/src/domains/subscription/presentation/screens/SubscriptionDetailScreen.tsx +13 -92
  19. package/src/domains/subscription/presentation/screens/SubscriptionDetailScreen.types.ts +47 -0
  20. package/src/domains/subscription/presentation/screens/components/UpgradePrompt.tsx +34 -126
  21. package/src/domains/subscription/presentation/screens/components/UpgradePrompt.types.ts +12 -0
  22. package/src/domains/subscription/presentation/usePremium.ts +3 -22
  23. package/src/domains/subscription/presentation/usePremium.types.ts +16 -0
  24. package/src/domains/subscription/presentation/useSubscriptionStatus.ts +30 -22
  25. package/src/domains/subscription/presentation/useSubscriptionStatus.types.ts +7 -0
  26. package/src/domains/wallet/index.ts +6 -2
  27. package/src/domains/wallet/infrastructure/config/walletConfig.ts +2 -1
  28. package/src/domains/wallet/presentation/hooks/useProductMetadata.ts +3 -16
  29. package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +1 -13
  30. package/src/domains/wallet/presentation/screens/WalletScreen.tsx +25 -112
  31. package/src/domains/wallet/presentation/screens/WalletScreen.types.ts +15 -0
  32. package/src/shared/utils/appValidators.ts +38 -0
  33. package/src/shared/utils/validators.ts +4 -122
  34. package/src/domains/paywall/components/README.md +0 -41
  35. package/src/domains/subscription/presentation/screens/README.md +0 -52
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.27.123",
3
+ "version": "2.27.125",
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",
@@ -1,154 +1,75 @@
1
- /**
2
- * Credit Operation Utilities
3
- * Business logic for credit calculations and data building
4
- */
5
-
1
+ import { Timestamp, serverTimestamp } from "firebase/firestore";
6
2
  import { resolveSubscriptionStatus } from "../../subscription/core/SubscriptionStatus";
7
3
  import { creditAllocationOrchestrator } from "./credit-strategies/CreditAllocationOrchestrator";
8
4
  import { isPast } from "../../../utils";
9
- import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
10
- import type { InitializeCreditsMetadata } from "../../subscription/application/SubscriptionInitializerTypes";
11
- import { serverTimestamp } from "firebase/firestore";
12
-
13
- interface CalculateCreditsParams {
14
- metadata: InitializeCreditsMetadata;
15
- existingData: UserCreditsDocumentRead;
16
- creditLimit: number;
17
- purchaseId: string;
18
- }
19
-
20
- interface BuildCreditsDataParams {
21
- existingData: UserCreditsDocumentRead;
22
- newCredits: number;
23
- creditLimit: number;
24
- purchaseId: string;
25
- metadata: InitializeCreditsMetadata;
26
- purchaseHistory: any[];
27
- platform: "ios" | "android";
5
+ import {
6
+ CalculateCreditsParams,
7
+ BuildCreditsDataParams
8
+ } from "./creditOperationUtils.types";
9
+
10
+ export function calculateNewCredits({ metadata, existingData, creditLimit, purchaseId }: CalculateCreditsParams): number {
11
+ const isPremium = metadata.isPremium;
12
+ const isExpired = metadata.expirationDate ? isPast(metadata.expirationDate) : false;
13
+ const status = resolveSubscriptionStatus({
14
+ isPremium,
15
+ willRenew: metadata.willRenew ?? false,
16
+ isExpired,
17
+ periodType: metadata.periodType ?? undefined,
18
+ });
19
+ const isStatusSync = purchaseId.startsWith("status_sync_");
20
+ return creditAllocationOrchestrator.allocate({
21
+ status,
22
+ isStatusSync,
23
+ existingData,
24
+ creditLimit,
25
+ isSubscriptionActive: isPremium && !isExpired,
26
+ productId: metadata.productId,
27
+ });
28
28
  }
29
29
 
30
- /**
31
- * Calculate new credits based on subscription status
32
- */
33
- export function calculateNewCredits(params: CalculateCreditsParams): number {
34
- const { metadata, existingData, creditLimit, purchaseId } = params;
35
-
36
- const isPremium = metadata.isPremium;
37
- const isExpired = metadata.expirationDate ? isPast(metadata.expirationDate) : false;
38
-
39
- const status = resolveSubscriptionStatus({
40
- isPremium,
41
- willRenew: metadata.willRenew ?? false,
42
- isExpired,
43
- periodType: metadata.periodType ?? undefined,
44
- });
45
-
46
- const isStatusSync = purchaseId.startsWith("status_sync_");
47
- const isSubscriptionActive = isPremium && !isExpired;
48
-
49
- return creditAllocationOrchestrator.allocate({
50
- status,
51
- isStatusSync,
52
- existingData,
53
- creditLimit,
54
- isSubscriptionActive,
55
- productId: metadata.productId,
56
- });
57
- }
58
-
59
- /**
60
- * Build credits data object for Firestore update
61
- */
62
- export function buildCreditsData(params: BuildCreditsDataParams): Record<string, any> {
63
- const {
64
- existingData,
65
- newCredits,
66
- creditLimit,
67
- purchaseId,
68
- metadata,
69
- purchaseHistory,
70
- platform,
71
- } = params;
72
-
73
- const isPremium = metadata.isPremium;
74
- const isExpired = metadata.expirationDate ? isPast(metadata.expirationDate) : false;
75
-
76
- const status = resolveSubscriptionStatus({
77
- isPremium,
78
- willRenew: metadata.willRenew ?? false,
79
- isExpired,
80
- periodType: metadata.periodType ?? undefined,
81
- });
82
-
83
- const newProcessedPurchases = addProcessedPurchase(existingData.processedPurchases, purchaseId);
84
-
85
- const creditsData: Record<string, any> = {
86
- isPremium,
87
- status,
88
- credits: newCredits,
89
- creditLimit,
90
- lastUpdatedAt: serverTimestamp(),
91
- processedPurchases: newProcessedPurchases,
92
- };
93
-
94
- if (purchaseHistory.length > 0) {
95
- creditsData.purchaseHistory = purchaseHistory;
96
- }
97
-
98
- const isNewPurchaseOrRenewal = purchaseId.startsWith("purchase_") || purchaseId.startsWith("renewal_");
99
- if (isNewPurchaseOrRenewal) {
100
- creditsData.lastPurchaseAt = serverTimestamp();
101
- }
102
-
103
- if (metadata.expirationDate) {
104
- creditsData.expirationDate = serverTimestamp();
105
- }
106
-
107
- if (metadata.willRenew !== undefined) {
108
- creditsData.willRenew = metadata.willRenew;
109
- }
110
-
111
- if (metadata.originalTransactionId) {
112
- creditsData.originalTransactionId = metadata.originalTransactionId;
113
- }
114
-
115
- creditsData.productId = metadata.productId;
116
- creditsData.platform = platform;
117
-
118
- return creditsData;
30
+ export function buildCreditsData({
31
+ existingData, newCredits, creditLimit, purchaseId, metadata, purchaseHistory, platform
32
+ }: BuildCreditsDataParams): Record<string, any> {
33
+ const isPremium = metadata.isPremium;
34
+ const isExpired = metadata.expirationDate ? isPast(metadata.expirationDate) : false;
35
+ const status = resolveSubscriptionStatus({
36
+ isPremium,
37
+ willRenew: metadata.willRenew ?? false,
38
+ isExpired,
39
+ periodType: metadata.periodType ?? undefined,
40
+ });
41
+
42
+ const creditsData: Record<string, any> = {
43
+ isPremium,
44
+ status,
45
+ credits: newCredits,
46
+ creditLimit,
47
+ lastUpdatedAt: serverTimestamp(),
48
+ processedPurchases: [...(existingData?.processedPurchases ?? []), purchaseId].slice(-50),
49
+ productId: metadata.productId,
50
+ platform,
51
+ };
52
+
53
+ if (purchaseHistory.length > 0) creditsData.purchaseHistory = purchaseHistory;
54
+ if (purchaseId.startsWith("purchase_") || purchaseId.startsWith("renewal_")) creditsData.lastPurchaseAt = serverTimestamp();
55
+ if (metadata.expirationDate) creditsData.expirationDate = Timestamp.fromDate(new Date(metadata.expirationDate));
56
+ if (metadata.willRenew !== undefined) creditsData.willRenew = metadata.willRenew;
57
+ if (metadata.originalTransactionId) creditsData.originalTransactionId = metadata.originalTransactionId;
58
+
59
+ return creditsData;
119
60
  }
120
61
 
121
- /**
122
- * Check if status sync write should be skipped (no changes)
123
- */
124
62
  export function shouldSkipStatusSyncWrite(
125
- purchaseId: string,
126
- existingData: UserCreditsDocumentRead,
127
- newCreditsData: Record<string, any>
63
+ purchaseId: string,
64
+ existingData: any,
65
+ newCreditsData: Record<string, any>
128
66
  ): boolean {
129
- const isStatusSync = purchaseId.startsWith("status_sync_");
130
-
131
- if (!isStatusSync) {
132
- return false;
133
- }
134
-
135
- const hasChanged =
136
- existingData.isPremium !== newCreditsData.isPremium ||
137
- existingData.status !== newCreditsData.status ||
138
- existingData.credits !== newCreditsData.credits ||
139
- existingData.creditLimit !== newCreditsData.creditLimit ||
140
- existingData.productId !== newCreditsData.productId;
141
-
142
- return !hasChanged;
143
- }
144
-
145
- /**
146
- * Add purchase ID to processed purchases list
147
- */
148
- function addProcessedPurchase(
149
- existing: string[],
150
- purchaseId: string,
151
- limit: number = 50
152
- ): string[] {
153
- return [...existing, purchaseId].slice(-limit);
67
+ if (!purchaseId.startsWith("status_sync_")) return false;
68
+ return (
69
+ existingData.isPremium === newCreditsData.isPremium &&
70
+ existingData.status === newCreditsData.status &&
71
+ existingData.credits === newCreditsData.credits &&
72
+ existingData.creditLimit === newCreditsData.creditLimit &&
73
+ existingData.productId === newCreditsData.productId
74
+ );
154
75
  }
@@ -0,0 +1,19 @@
1
+ import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
2
+ import type { InitializeCreditsMetadata } from "../../subscription/application/SubscriptionInitializerTypes";
3
+
4
+ export interface CalculateCreditsParams {
5
+ metadata: InitializeCreditsMetadata;
6
+ existingData: UserCreditsDocumentRead;
7
+ creditLimit: number;
8
+ purchaseId: string;
9
+ }
10
+
11
+ export interface BuildCreditsDataParams {
12
+ existingData: UserCreditsDocumentRead;
13
+ newCredits: number;
14
+ creditLimit: number;
15
+ purchaseId: string;
16
+ metadata: InitializeCreditsMetadata;
17
+ purchaseHistory: any[];
18
+ platform: "ios" | "android";
19
+ }
@@ -1,10 +1,3 @@
1
- /**
2
- * useCredits Hook
3
- *
4
- * Fetches user credits with TanStack Query best practices.
5
- * Uses status-based state management for reliable loading detection.
6
- */
7
-
8
1
  import { useQuery, useQueryClient } from "@umituz/react-native-design-system";
9
2
  import { useCallback, useMemo, useEffect } from "react";
10
3
  import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
@@ -50,7 +43,6 @@ export const useCredits = (): UseCreditsResult => {
50
43
  const userId = useAuthStore(selectUserId);
51
44
  const isConfigured = isCreditsRepositoryConfigured();
52
45
 
53
- // Only access config if configured to avoid throwing errors
54
46
  const config = isConfigured ? getCreditsConfig() : null;
55
47
  const queryEnabled = !!userId && isConfigured;
56
48
 
@@ -69,8 +61,6 @@ export const useCredits = (): UseCreditsResult => {
69
61
  return result.data || null;
70
62
  },
71
63
  enabled: queryEnabled,
72
- staleTime: 30 * 1000, // 30 seconds - data considered fresh
73
- gcTime: 5 * 60 * 1000, // 5 minutes - keep in cache after unmount
74
64
  refetchOnMount: "always",
75
65
  refetchOnWindowFocus: true,
76
66
  refetchOnReconnect: true,
@@ -78,7 +68,6 @@ export const useCredits = (): UseCreditsResult => {
78
68
 
79
69
  const queryClient = useQueryClient();
80
70
 
81
- // Observer Pattern: Listen for credit updates
82
71
  useEffect(() => {
83
72
  if (!userId) return;
84
73
 
@@ -126,3 +115,4 @@ export const useHasCredits = (): boolean => {
126
115
  const { hasCredits } = useCredits();
127
116
  return hasCredits;
128
117
  };
118
+
@@ -9,7 +9,9 @@ import { usePaywallVisibility } from "../../subscription/presentation/usePaywall
9
9
  import { useSubscriptionPackages } from "../../subscription/infrastructure/hooks/useSubscriptionPackages";
10
10
  import { useRevenueCatTrialEligibility } from "../../subscription/infrastructure/hooks/useRevenueCatTrialEligibility";
11
11
  import { createCreditAmountsFromPackages } from "../../../utils/creditMapper";
12
- import { PaywallModal, type TrialEligibilityInfo } from "./PaywallModal";
12
+ import { PaywallModal } from "./PaywallModal";
13
+ import type { TrialEligibilityInfo } from "./PaywallModal.types";
14
+
13
15
  import { usePaywallActions } from "../hooks/usePaywallActions";
14
16
  import { useAuthAwarePurchase } from "../../subscription/presentation/useAuthAwarePurchase";
15
17
  import type { PaywallContainerProps } from "./PaywallContainer.types";
@@ -1,151 +1,63 @@
1
- /**
2
- * Paywall Modal
3
- */
4
-
5
1
  import React, { useCallback, useEffect } from "react";
6
- import { View, TouchableOpacity, Linking, type ImageSourcePropType } from "react-native";
2
+ import { View, TouchableOpacity, Linking } from "react-native";
7
3
  import { BaseModal, useAppDesignTokens, AtomicText, AtomicIcon, AtomicSpinner, useSafeAreaInsets } from "@umituz/react-native-design-system";
8
4
  import { ScreenLayout } from "../../../shared/presentation";
9
5
  import { Image } from "expo-image";
10
- import type { PurchasesPackage } from "react-native-purchases";
11
6
  import { PlanCard } from "./PlanCard";
12
- import type { SubscriptionFeature, PaywallTranslations, PaywallLegalUrls } from "../entities/types";
13
7
  import { paywallModalStyles as styles } from "./PaywallModal.styles";
14
8
  import { PaywallFeatures } from "./PaywallFeatures";
15
9
  import { PaywallFooter } from "./PaywallFooter";
16
10
  import { usePaywallActions } from "../hooks/usePaywallActions";
17
-
18
- /** Trial eligibility info per product */
19
- export interface TrialEligibilityInfo {
20
- /** Whether eligible for trial */
21
- eligible: boolean;
22
- /** Trial duration in days */
23
- durationDays?: number;
24
- }
25
-
26
- export interface PaywallModalProps {
27
- visible: boolean;
28
- onClose: () => void;
29
- translations: PaywallTranslations;
30
- packages?: PurchasesPackage[];
31
- features?: SubscriptionFeature[];
32
- isLoading?: boolean;
33
- legalUrls?: PaywallLegalUrls;
34
- bestValueIdentifier?: string;
35
- /** Credit amounts per product identifier */
36
- creditAmounts?: Record<string, number>;
37
- /** Credits label text (e.g., "credits") */
38
- creditsLabel?: string;
39
- heroImage?: ImageSourcePropType;
40
- onPurchase?: (pkg: PurchasesPackage) => Promise<void | boolean>;
41
- onRestore?: () => Promise<void | boolean>;
42
- /** Trial eligibility map per product ID */
43
- trialEligibility?: Record<string, TrialEligibilityInfo>;
44
- /** Trial subtitle text for PlanCard (e.g., "7 days free, then billed") */
45
- trialSubtitleText?: string;
46
- }
11
+ import { PaywallModalProps } from "./PaywallModal.types";
47
12
 
48
13
  export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
49
14
  const { visible, onClose, translations, packages = [], features = [], isLoading = false, legalUrls = {}, bestValueIdentifier, creditAmounts, creditsLabel, heroImage, onPurchase, onRestore, trialEligibility = {}, trialSubtitleText } = props;
50
15
  const tokens = useAppDesignTokens();
51
16
  const insets = useSafeAreaInsets();
52
-
53
- const {
54
- selectedPlanId,
55
- setSelectedPlanId,
56
- isProcessing,
57
- handlePurchase,
58
- handleRestore,
59
- resetState,
60
- } = usePaywallActions({ packages, onPurchase, onRestore });
61
-
62
- // Reset selected plan when packages change
63
- useEffect(() => {
64
- setSelectedPlanId(null);
65
- }, [packages, setSelectedPlanId]);
17
+ const { selectedPlanId, setSelectedPlanId, isProcessing, handlePurchase, handleRestore, resetState } = usePaywallActions({ packages, onPurchase, onRestore });
66
18
 
67
- // Cleanup state when modal closes to prevent stale state
68
- useEffect(() => {
69
- if (!visible) {
70
- resetState();
71
- }
72
- }, [visible, resetState]);
19
+ useEffect(() => { setSelectedPlanId(null); }, [packages, setSelectedPlanId]);
20
+ useEffect(() => { if (!visible) resetState(); }, [visible, resetState]);
73
21
 
74
22
  const handleLegalUrl = useCallback(async (url: string | undefined) => {
75
23
  if (!url) return;
76
- try { if (await Linking.canOpenURL(url)) await Linking.openURL(url); } catch { /* Silent fail */ }
24
+ try {
25
+ if (await Linking.canOpenURL(url)) await Linking.openURL(url);
26
+ } catch (err) {
27
+ console.error("[PaywallModal] Legal link error:", err);
28
+ }
77
29
  }, []);
78
30
 
31
+
79
32
  return (
80
33
  <BaseModal visible={visible} onClose={onClose} contentStyle={styles.modalContent}>
81
34
  <View style={[styles.container, { backgroundColor: tokens.colors.surface }]}>
82
- <TouchableOpacity
83
- onPress={onClose}
84
- style={[
85
- styles.closeBtn,
86
- { backgroundColor: tokens.colors.surfaceSecondary, top: Math.max(insets.top, 12) }
87
- ]}
88
- hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
89
- >
35
+ <TouchableOpacity onPress={onClose} style={[styles.closeBtn, { backgroundColor: tokens.colors.surfaceSecondary, top: Math.max(insets.top, 12) }]} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}>
90
36
  <AtomicIcon name="close-outline" size="md" customColor={tokens.colors.textPrimary} />
91
37
  </TouchableOpacity>
92
-
93
38
  <ScreenLayout scrollable={true} edges={["bottom"]} backgroundColor="transparent" contentContainerStyle={styles.scroll}>
94
- {heroImage && (
95
- <View style={styles.heroContainer}>
96
- <Image source={heroImage} style={styles.heroImage} contentFit="cover" transition={0} />
97
- </View>
98
- )}
99
-
39
+ {heroImage && <View style={styles.heroContainer}><Image source={heroImage} style={styles.heroImage} contentFit="cover" transition={0} /></View>}
100
40
  <View style={styles.header}>
101
41
  <AtomicText type="headlineMedium" style={[styles.title, { color: tokens.colors.textPrimary }]}>{translations.title}</AtomicText>
102
- {/* Apple compliance: Don't promote trial in header, show regular subtitle only */}
103
- {translations.subtitle && (
104
- <AtomicText type="bodyMedium" style={[styles.subtitle, { color: tokens.colors.textSecondary }]}>
105
- {translations.subtitle}
106
- </AtomicText>
107
- )}
42
+ {translations.subtitle && <AtomicText type="bodyMedium" style={[styles.subtitle, { color: tokens.colors.textSecondary }]}>{translations.subtitle}</AtomicText>}
108
43
  </View>
109
-
110
44
  <PaywallFeatures features={features} />
111
-
112
45
  {isLoading ? (
113
46
  <View style={styles.loading}><AtomicSpinner size="lg" color="primary" text={translations.loadingText} /></View>
114
47
  ) : (
115
48
  <View style={styles.plans}>
116
49
  {packages.map((pkg) => {
117
- const productId = pkg.product.identifier;
118
- const eligibility = trialEligibility[productId];
119
- const hasFreeTrial = eligibility?.eligible ?? false;
120
-
50
+ const pid = pkg.product.identifier;
51
+ const hasTrial = trialEligibility[pid]?.eligible ?? false;
121
52
  return (
122
- <PlanCard
123
- key={productId}
124
- pkg={pkg}
125
- isSelected={selectedPlanId === productId}
126
- onSelect={() => setSelectedPlanId(productId)}
127
- badge={productId === bestValueIdentifier ? translations.bestValueBadgeText : undefined}
128
- creditAmount={creditAmounts?.[productId]}
129
- creditsLabel={creditsLabel}
130
- hasFreeTrial={hasFreeTrial}
131
- trialSubtitleText={hasFreeTrial ? trialSubtitleText : undefined}
132
- />
53
+ <PlanCard key={pid} pkg={pkg} isSelected={selectedPlanId === pid} onSelect={() => setSelectedPlanId(pid)} badge={pid === bestValueIdentifier ? translations.bestValueBadgeText : undefined} creditAmount={creditAmounts?.[pid]} creditsLabel={creditsLabel} hasFreeTrial={hasTrial} trialSubtitleText={hasTrial ? trialSubtitleText : undefined} />
133
54
  );
134
55
  })}
135
56
  </View>
136
57
  )}
137
-
138
- <TouchableOpacity
139
- onPress={handlePurchase}
140
- disabled={!selectedPlanId || isProcessing}
141
- style={[styles.cta, { backgroundColor: tokens.colors.primary }, (!selectedPlanId || isProcessing) && styles.ctaDisabled]}
142
- activeOpacity={0.8}
143
- >
144
- <AtomicText type="titleLarge" style={[styles.ctaText, { color: tokens.colors.onPrimary }]}>
145
- {isProcessing ? translations.processingText : translations.purchaseButtonText}
146
- </AtomicText>
58
+ <TouchableOpacity onPress={handlePurchase} disabled={!selectedPlanId || isProcessing} style={[styles.cta, { backgroundColor: tokens.colors.primary }, (!selectedPlanId || isProcessing) && styles.ctaDisabled]} activeOpacity={0.8}>
59
+ <AtomicText type="titleLarge" style={[styles.ctaText, { color: tokens.colors.onPrimary }]}>{isProcessing ? translations.processingText : translations.purchaseButtonText}</AtomicText>
147
60
  </TouchableOpacity>
148
-
149
61
  <PaywallFooter translations={translations} legalUrls={legalUrls} isProcessing={isProcessing} onRestore={onRestore ? handleRestore : undefined} onLegalClick={handleLegalUrl} />
150
62
  </ScreenLayout>
151
63
  </View>
@@ -0,0 +1,26 @@
1
+ import type { ImageSourcePropType } from "react-native";
2
+ import type { PurchasesPackage } from "react-native-purchases";
3
+ import type { SubscriptionFeature, PaywallTranslations, PaywallLegalUrls } from "../entities/types";
4
+
5
+ export interface TrialEligibilityInfo {
6
+ eligible: boolean;
7
+ durationDays?: number;
8
+ }
9
+
10
+ export interface PaywallModalProps {
11
+ visible: boolean;
12
+ onClose: () => void;
13
+ translations: PaywallTranslations;
14
+ packages?: PurchasesPackage[];
15
+ features?: SubscriptionFeature[];
16
+ isLoading?: boolean;
17
+ legalUrls?: PaywallLegalUrls;
18
+ bestValueIdentifier?: string;
19
+ creditAmounts?: Record<string, number>;
20
+ creditsLabel?: string;
21
+ heroImage?: ImageSourcePropType;
22
+ onPurchase?: (pkg: PurchasesPackage) => Promise<void | boolean>;
23
+ onRestore?: () => Promise<void | boolean>;
24
+ trialEligibility?: Record<string, TrialEligibilityInfo>;
25
+ trialSubtitleText?: string;
26
+ }