@umituz/react-native-subscription 2.27.96 → 2.27.97
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/credits/core/Credits.ts +3 -11
- package/src/domains/paywall/components/PaywallContainer.tsx +17 -1
- package/src/domains/paywall/components/PaywallContainer.types.ts +2 -1
- package/src/domains/paywall/hooks/usePaywallActions.ts +1 -1
- package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +32 -12
- package/src/domains/subscription/infrastructure/hooks/subscriptionQueryKeys.ts +4 -4
- package/src/domains/subscription/infrastructure/hooks/useSubscriptionPackages.ts +1 -7
- package/src/domains/subscription/infrastructure/hooks/useSubscriptionQueries.ts +0 -2
- package/src/domains/subscription/infrastructure/utils/InitializationCache.ts +20 -7
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +1 -1
- package/src/domains/subscription/presentation/screens/SubscriptionDetailScreen.tsx +55 -16
- package/src/domains/subscription/presentation/screens/components/CreditsList.tsx +14 -1
- package/src/domains/subscription/presentation/screens/components/DevTestSection.tsx +10 -2
- package/src/domains/subscription/presentation/screens/components/SubscriptionActions.tsx +6 -1
- package/src/domains/subscription/presentation/screens/components/SubscriptionHeader.tsx +20 -1
- package/src/domains/subscription/presentation/screens/components/UpgradePrompt.tsx +13 -1
- package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +1 -1
- package/src/domains/subscription/presentation/useFeatureGate.ts +11 -7
- package/src/domains/subscription/presentation/usePaywallVisibility.ts +1 -1
- package/src/domains/subscription/presentation/useSubscriptionStatus.ts +1 -5
- package/src/init/index.ts +0 -3
- package/src/presentation/hooks/index.ts +0 -4
- package/src/shared/infrastructure/SubscriptionEventBus.ts +27 -0
- package/src/utils/packageTypeDetector.ts +0 -4
- package/src/domains/subscription/presentation/types/README.md +0 -22
- package/src/domains/subscription/presentation/types/SubscriptionDetailTypes.ts +0 -153
- package/src/domains/subscription/presentation/types/SubscriptionSettingsTypes.ts +0 -74
- package/src/domains/subscription/presentation/useAuthSubscriptionSync.ts +0 -63
- package/src/domains/subscription/presentation/usePremiumGate.ts +0 -84
- package/src/domains/subscription/presentation/useSavedPurchaseAutoExecution.ts +0 -148
- package/src/domains/subscription/presentation/useSubscriptionSettingsConfig.ts +0 -115
- package/src/domains/subscription/presentation/useSubscriptionSettingsConfig.utils.ts +0 -57
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.27.
|
|
3
|
+
"version": "2.27.97",
|
|
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",
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { SubscriptionPackageType } from "../../../utils/packageTypeDetector";
|
|
9
|
+
// Types imported from SubscriptionConstants are used directly in UserCredits interface
|
|
9
10
|
import type {
|
|
10
11
|
SubscriptionStatusType,
|
|
11
12
|
PeriodType,
|
|
@@ -15,15 +16,6 @@ import type {
|
|
|
15
16
|
PurchaseType
|
|
16
17
|
} from "../../subscription/core/SubscriptionConstants";
|
|
17
18
|
|
|
18
|
-
export type {
|
|
19
|
-
SubscriptionStatusType,
|
|
20
|
-
PeriodType,
|
|
21
|
-
PackageType,
|
|
22
|
-
Platform,
|
|
23
|
-
PurchaseSource,
|
|
24
|
-
PurchaseType
|
|
25
|
-
};
|
|
26
|
-
|
|
27
19
|
export type CreditType = "text" | "image";
|
|
28
20
|
|
|
29
21
|
/** Single Source of Truth for user subscription + credits data */
|
|
@@ -67,10 +59,10 @@ export interface CreditAllocation {
|
|
|
67
59
|
credits: number;
|
|
68
60
|
}
|
|
69
61
|
|
|
70
|
-
export type PackageAllocationMap = Record<
|
|
62
|
+
export type PackageAllocationMap = Partial<Record<
|
|
71
63
|
Exclude<SubscriptionPackageType, "unknown">,
|
|
72
64
|
CreditAllocation
|
|
73
|
-
|
|
65
|
+
>>;
|
|
74
66
|
|
|
75
67
|
export interface CreditsConfig {
|
|
76
68
|
collectionName: string;
|
|
@@ -49,9 +49,25 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
|
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
// Check trial eligibility only if trialConfig is enabled
|
|
52
|
+
// Use ref to track if we've already checked for these packages to avoid redundant calls
|
|
53
|
+
const checkedPackagesRef = React.useRef<string[]>([]);
|
|
54
|
+
|
|
52
55
|
useEffect(() => {
|
|
53
56
|
if (!trialConfig?.enabled) return;
|
|
54
57
|
if (packages.length === 0) return;
|
|
58
|
+
if (isLoading) return; // Wait for packages to fully load
|
|
59
|
+
|
|
60
|
+
// Get current package identifiers
|
|
61
|
+
const currentPackageIds = packages.map((pkg) => pkg.product.identifier);
|
|
62
|
+
const sortedIds = [...currentPackageIds].sort().join(",");
|
|
63
|
+
|
|
64
|
+
// Skip if we've already checked these exact packages
|
|
65
|
+
if (checkedPackagesRef.current.join(",") === sortedIds) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Update ref
|
|
70
|
+
checkedPackagesRef.current = currentPackageIds;
|
|
55
71
|
|
|
56
72
|
// Get all actual product IDs from packages
|
|
57
73
|
const allProductIds = packages.map((pkg) => pkg.product.identifier);
|
|
@@ -72,7 +88,7 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
|
|
|
72
88
|
if (productIdsToCheck.length > 0) {
|
|
73
89
|
checkEligibility(productIdsToCheck);
|
|
74
90
|
}
|
|
75
|
-
}, [packages, checkEligibility, trialConfig?.enabled, trialConfig?.eligibleProductIds]);
|
|
91
|
+
}, [packages, isLoading, checkEligibility, trialConfig?.enabled, trialConfig?.eligibleProductIds]);
|
|
76
92
|
|
|
77
93
|
// Convert eligibility map to format expected by PaywallModal
|
|
78
94
|
// Only process if trial is enabled
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ImageSourcePropType } from "react-native";
|
|
7
7
|
import type { PaywallTranslations, PaywallLegalUrls, SubscriptionFeature } from "../entities/types";
|
|
8
|
-
import type { PurchaseSource
|
|
8
|
+
import type { PurchaseSource } from "../../subscription/core/SubscriptionConstants";
|
|
9
|
+
import type { PackageAllocationMap } from "../../credits/core/Credits";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Trial display configuration
|
|
@@ -2,7 +2,7 @@ import { useCallback } from "react";
|
|
|
2
2
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
3
3
|
import { useRestorePurchase } from "../../subscription/infrastructure/hooks/useRestorePurchase";
|
|
4
4
|
import { useAuthAwarePurchase } from "../../subscription/presentation/useAuthAwarePurchase";
|
|
5
|
-
import type { PurchaseSource } from "../../
|
|
5
|
+
import type { PurchaseSource } from "../../subscription/core/SubscriptionConstants";
|
|
6
6
|
|
|
7
7
|
interface UsePaywallActionsProps {
|
|
8
8
|
source?: PurchaseSource;
|
|
@@ -25,21 +25,41 @@ export class PackageHandler {
|
|
|
25
25
|
|
|
26
26
|
async fetchPackages(): Promise<PurchasesPackage[]> {
|
|
27
27
|
if (!this.service.isInitialized()) {
|
|
28
|
-
throw new Error("Service not initialized");
|
|
28
|
+
throw new Error("Service not initialized. Please initialize before fetching packages.");
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
try {
|
|
32
|
+
const offering = await this.service.fetchOfferings();
|
|
33
|
+
|
|
34
|
+
if (!offering) {
|
|
35
|
+
if (__DEV__) {
|
|
36
|
+
console.warn("[PackageHandler] No offerings available from RevenueCat");
|
|
37
|
+
}
|
|
38
|
+
// Return empty array instead of throwing - allows graceful degradation
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const packages = offering.availablePackages;
|
|
43
|
+
if (!packages || packages.length === 0) {
|
|
44
|
+
if (__DEV__) {
|
|
45
|
+
console.warn("[PackageHandler] No packages available in offering");
|
|
46
|
+
}
|
|
47
|
+
// Return empty array instead of throwing - allows graceful degradation
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return packages;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (__DEV__) {
|
|
54
|
+
console.error("[PackageHandler] Failed to fetch packages:", error);
|
|
55
|
+
}
|
|
56
|
+
// Re-throw with more context
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Failed to fetch subscription packages. ${
|
|
59
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
60
|
+
}`
|
|
61
|
+
);
|
|
35
62
|
}
|
|
36
|
-
|
|
37
|
-
const packages = offering.availablePackages;
|
|
38
|
-
if (!packages) {
|
|
39
|
-
throw new Error("No packages available in offering");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return packages;
|
|
43
63
|
}
|
|
44
64
|
|
|
45
65
|
async purchase(pkg: PurchasesPackage, userId: string): Promise<boolean> {
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* TanStack Query keys and constants for subscription state
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/** Query cache time constants */
|
|
7
|
+
|
|
8
|
+
|
|
6
9
|
/**
|
|
7
10
|
* Query keys for TanStack Query
|
|
8
11
|
*/
|
|
@@ -12,7 +15,4 @@ export const SUBSCRIPTION_QUERY_KEYS = {
|
|
|
12
15
|
["subscription", "initialized", userId] as const,
|
|
13
16
|
} as const;
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
// This ensures users always see real-time subscription status
|
|
17
|
-
export const STALE_TIME = 0; // Always stale - refetch immediately
|
|
18
|
-
export const GC_TIME = 0; // Don't cache - garbage collect immediately
|
|
18
|
+
|
|
@@ -12,8 +12,6 @@ import {
|
|
|
12
12
|
import { SubscriptionManager } from '../../infrastructure/managers/SubscriptionManager';
|
|
13
13
|
import {
|
|
14
14
|
SUBSCRIPTION_QUERY_KEYS,
|
|
15
|
-
STALE_TIME,
|
|
16
|
-
GC_TIME,
|
|
17
15
|
} from "./subscriptionQueryKeys";
|
|
18
16
|
|
|
19
17
|
/**
|
|
@@ -41,11 +39,7 @@ export const useSubscriptionPackages = () => {
|
|
|
41
39
|
|
|
42
40
|
return SubscriptionManager.getPackages();
|
|
43
41
|
},
|
|
44
|
-
staleTime: STALE_TIME,
|
|
45
|
-
gcTime: GC_TIME,
|
|
46
42
|
enabled: isConfigured,
|
|
47
|
-
|
|
48
|
-
refetchOnWindowFocus: true, // Refetch when app becomes active
|
|
49
|
-
refetchOnReconnect: true, // Refetch when network reconnects
|
|
43
|
+
|
|
50
44
|
});
|
|
51
45
|
};
|
|
@@ -11,6 +11,8 @@ export class InitializationCache {
|
|
|
11
11
|
private initializationInProgress = false;
|
|
12
12
|
// Track which userId the promise is for (separate from currentUserId which is set after completion)
|
|
13
13
|
private promiseUserId: string | null = null;
|
|
14
|
+
// Track promise completion state to avoid returning failed promises
|
|
15
|
+
private promiseCompleted = true;
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Atomically check if reinitialization is needed AND reserve the slot
|
|
@@ -22,17 +24,24 @@ export class InitializationCache {
|
|
|
22
24
|
return { shouldInit: false, existingPromise: this.initPromise };
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
// If already initialized for this user and promise
|
|
26
|
-
if
|
|
27
|
+
// If already initialized for this user and promise completed successfully, return it
|
|
28
|
+
// Only return cached promise if it completed AND it's for the same user
|
|
29
|
+
if (this.initPromise && this.currentUserId === userId && !this.initializationInProgress && this.promiseCompleted) {
|
|
27
30
|
return { shouldInit: false, existingPromise: this.initPromise };
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
// Different user
|
|
31
|
-
// Atomically set the flag
|
|
32
|
-
this.initializationInProgress
|
|
33
|
-
|
|
33
|
+
// Different user, no initialization, or failed promise - need to reinitialize
|
|
34
|
+
// Atomically set the flag and clear previous state if needed
|
|
35
|
+
if (!this.initializationInProgress) {
|
|
36
|
+
this.initializationInProgress = true;
|
|
37
|
+
this.promiseUserId = userId;
|
|
38
|
+
this.promiseCompleted = false;
|
|
39
|
+
return { shouldInit: true, existingPromise: null };
|
|
40
|
+
}
|
|
34
41
|
|
|
35
|
-
|
|
42
|
+
// If we reach here, initialization is in progress for a different user
|
|
43
|
+
// Wait for current initialization to complete
|
|
44
|
+
return { shouldInit: false, existingPromise: this.initPromise };
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
setPromise(promise: Promise<boolean>, userId: string): void {
|
|
@@ -45,6 +54,7 @@ export class InitializationCache {
|
|
|
45
54
|
if (result && this.promiseUserId === userId) {
|
|
46
55
|
this.currentUserId = userId;
|
|
47
56
|
}
|
|
57
|
+
this.promiseCompleted = true;
|
|
48
58
|
return result;
|
|
49
59
|
})
|
|
50
60
|
.catch(() => {
|
|
@@ -52,7 +62,9 @@ export class InitializationCache {
|
|
|
52
62
|
if (this.promiseUserId === userId) {
|
|
53
63
|
this.initPromise = null;
|
|
54
64
|
this.promiseUserId = null;
|
|
65
|
+
this.currentUserId = null; // Clear user on failure
|
|
55
66
|
}
|
|
67
|
+
this.promiseCompleted = true;
|
|
56
68
|
})
|
|
57
69
|
.finally(() => {
|
|
58
70
|
// Always release the mutex
|
|
@@ -71,5 +83,6 @@ export class InitializationCache {
|
|
|
71
83
|
this.currentUserId = null;
|
|
72
84
|
this.initializationInProgress = false;
|
|
73
85
|
this.promiseUserId = null;
|
|
86
|
+
this.promiseCompleted = true;
|
|
74
87
|
}
|
|
75
88
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { CustomerInfo } from "react-native-purchases";
|
|
7
7
|
import type { RevenueCatConfig } from "../../core/RevenueCatConfig";
|
|
8
|
-
import type { PurchaseSource } from "../../../
|
|
8
|
+
import type { PurchaseSource } from "../../../subscription/core/SubscriptionConstants";
|
|
9
9
|
import { getPremiumEntitlement } from "../../core/RevenueCatTypes";
|
|
10
10
|
|
|
11
11
|
export async function syncPremiumStatus(
|
|
@@ -11,22 +11,61 @@ import {
|
|
|
11
11
|
ScreenLayout,
|
|
12
12
|
} from "@umituz/react-native-design-system";
|
|
13
13
|
import { SubscriptionHeader } from "./components/SubscriptionHeader";
|
|
14
|
-
import { CreditsList } from "./components/CreditsList";
|
|
15
|
-
import { UpgradePrompt } from "./components/UpgradePrompt";
|
|
16
|
-
import { DevTestSection } from "./components/DevTestSection";
|
|
17
|
-
import type { SubscriptionDetailScreenProps } from "../types/SubscriptionDetailTypes";
|
|
14
|
+
import { CreditsList, type CreditItem } from "./components/CreditsList";
|
|
15
|
+
import { UpgradePrompt, type Benefit } from "./components/UpgradePrompt";
|
|
16
|
+
import { DevTestSection, type DevTestActions } from "./components/DevTestSection";
|
|
18
17
|
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
export interface SubscriptionDisplayFlags {
|
|
19
|
+
showHeader: boolean;
|
|
20
|
+
showCredits: boolean;
|
|
21
|
+
showUpgradePrompt: boolean;
|
|
22
|
+
showExpirationDate: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SubscriptionDetailTranslations {
|
|
26
|
+
title: string;
|
|
27
|
+
statusActive: string;
|
|
28
|
+
statusExpired: string;
|
|
29
|
+
statusFree: string;
|
|
30
|
+
statusCanceled: string;
|
|
31
|
+
statusLabel: string;
|
|
32
|
+
lifetimeLabel: string;
|
|
33
|
+
expiresLabel: string;
|
|
34
|
+
purchasedLabel: string;
|
|
35
|
+
usageTitle?: string;
|
|
36
|
+
creditsTitle: string;
|
|
37
|
+
creditsResetInfo?: string;
|
|
38
|
+
remainingLabel?: string;
|
|
39
|
+
upgradeButton: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DevToolsConfig {
|
|
43
|
+
actions: DevTestActions;
|
|
44
|
+
title?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface UpgradePromptConfig {
|
|
48
|
+
title: string;
|
|
49
|
+
subtitle?: string;
|
|
50
|
+
benefits?: readonly Benefit[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SubscriptionDetailConfig {
|
|
54
|
+
display: SubscriptionDisplayFlags;
|
|
55
|
+
statusType: "active" | "expired" | "none" | "canceled";
|
|
56
|
+
isLifetime: boolean;
|
|
57
|
+
expirationDate?: string;
|
|
58
|
+
purchaseDate?: string;
|
|
59
|
+
daysRemaining?: number | null;
|
|
60
|
+
credits?: readonly CreditItem[];
|
|
61
|
+
translations: SubscriptionDetailTranslations;
|
|
62
|
+
upgradePrompt?: UpgradePromptConfig & { onUpgrade?: () => void };
|
|
63
|
+
devTools?: DevToolsConfig;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SubscriptionDetailScreenProps {
|
|
67
|
+
config: SubscriptionDetailConfig;
|
|
68
|
+
}
|
|
30
69
|
|
|
31
70
|
export const SubscriptionDetailScreen: React.FC<
|
|
32
71
|
SubscriptionDetailScreenProps
|
|
@@ -98,7 +137,7 @@ export const SubscriptionDetailScreen: React.FC<
|
|
|
98
137
|
subtitle={config.upgradePrompt.subtitle}
|
|
99
138
|
benefits={config.upgradePrompt.benefits}
|
|
100
139
|
upgradeButtonLabel={config.translations.upgradeButton}
|
|
101
|
-
onUpgrade={config.onUpgrade}
|
|
140
|
+
onUpgrade={config.upgradePrompt.onUpgrade ?? (() => {})}
|
|
102
141
|
/>
|
|
103
142
|
)}
|
|
104
143
|
</View>
|
|
@@ -7,7 +7,20 @@ import React, { useMemo } from "react";
|
|
|
7
7
|
import { View, StyleSheet } from "react-native";
|
|
8
8
|
import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
|
|
9
9
|
import { CreditRow } from "../../components/details/CreditRow";
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
export interface CreditItem {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
current: number;
|
|
15
|
+
total: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CreditsListProps {
|
|
19
|
+
credits: readonly CreditItem[];
|
|
20
|
+
title?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
remainingLabel?: string;
|
|
23
|
+
}
|
|
11
24
|
|
|
12
25
|
export const CreditsList: React.FC<CreditsListProps> = ({
|
|
13
26
|
credits,
|
|
@@ -7,9 +7,17 @@
|
|
|
7
7
|
import React, { useMemo } from "react";
|
|
8
8
|
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
9
9
|
import { AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
10
|
-
import type { DevTestSectionProps } from "../../types/SubscriptionDetailTypes";
|
|
11
10
|
|
|
12
|
-
export
|
|
11
|
+
export interface DevTestActions {
|
|
12
|
+
onTestRenewal: () => void;
|
|
13
|
+
onCheckCredits: () => void;
|
|
14
|
+
onTestDuplicate: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DevTestSectionProps {
|
|
18
|
+
actions: DevTestActions;
|
|
19
|
+
title?: string;
|
|
20
|
+
}
|
|
13
21
|
|
|
14
22
|
/** Dev test button translations */
|
|
15
23
|
export interface DevTestTranslations {
|
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
import React, { useMemo } from "react";
|
|
7
7
|
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
8
8
|
import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
export interface SubscriptionActionsProps {
|
|
11
|
+
isPremium: boolean;
|
|
12
|
+
upgradeButtonLabel?: string;
|
|
13
|
+
onUpgrade?: () => void;
|
|
14
|
+
}
|
|
10
15
|
|
|
11
16
|
export const SubscriptionActions: React.FC<SubscriptionActionsProps> = ({
|
|
12
17
|
isPremium,
|
|
@@ -8,7 +8,26 @@ import { View, StyleSheet } from "react-native";
|
|
|
8
8
|
import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
|
|
9
9
|
import { PremiumStatusBadge } from "../../components/details/PremiumStatusBadge";
|
|
10
10
|
import { DetailRow } from "../../components/details/DetailRow";
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
export interface SubscriptionHeaderProps {
|
|
13
|
+
statusType: "active" | "expired" | "none" | "canceled";
|
|
14
|
+
showExpirationDate: boolean;
|
|
15
|
+
isLifetime: boolean;
|
|
16
|
+
expirationDate?: string;
|
|
17
|
+
purchaseDate?: string;
|
|
18
|
+
daysRemaining?: number | null;
|
|
19
|
+
translations: {
|
|
20
|
+
title: string;
|
|
21
|
+
statusActive: string;
|
|
22
|
+
statusExpired: string;
|
|
23
|
+
statusFree: string;
|
|
24
|
+
statusCanceled: string;
|
|
25
|
+
statusLabel: string;
|
|
26
|
+
lifetimeLabel: string;
|
|
27
|
+
expiresLabel: string;
|
|
28
|
+
purchasedLabel: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
12
31
|
|
|
13
32
|
export const SubscriptionHeader: React.FC<SubscriptionHeaderProps> = ({
|
|
14
33
|
statusType,
|
|
@@ -10,7 +10,19 @@ import {
|
|
|
10
10
|
AtomicText,
|
|
11
11
|
AtomicIcon,
|
|
12
12
|
} from "@umituz/react-native-design-system";
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
export interface Benefit {
|
|
15
|
+
icon?: string;
|
|
16
|
+
text: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UpgradePromptProps {
|
|
20
|
+
title: string;
|
|
21
|
+
subtitle?: string;
|
|
22
|
+
benefits?: readonly Benefit[];
|
|
23
|
+
upgradeButtonLabel: string;
|
|
24
|
+
onUpgrade: () => void;
|
|
25
|
+
}
|
|
14
26
|
|
|
15
27
|
export const UpgradePrompt: React.FC<UpgradePromptProps> = ({
|
|
16
28
|
title,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { useCallback } from "react";
|
|
7
7
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
8
8
|
import { usePremium } from "./usePremium";
|
|
9
|
-
import type { PurchaseSource } from "
|
|
9
|
+
import type { PurchaseSource } from "../core/SubscriptionConstants";
|
|
10
10
|
|
|
11
11
|
export interface PurchaseAuthProvider {
|
|
12
12
|
isAuthenticated: () => boolean;
|
|
@@ -135,9 +135,9 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
135
135
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
136
136
|
console.log("[useFeatureGate] requireFeature", {
|
|
137
137
|
isAuthenticated,
|
|
138
|
-
hasSubscription,
|
|
138
|
+
hasSubscription: hasSubscriptionRef.current,
|
|
139
139
|
creditBalance: creditBalanceRef.current,
|
|
140
|
-
requiredCredits,
|
|
140
|
+
requiredCredits: requiredCreditsRef.current,
|
|
141
141
|
isCreditsLoaded,
|
|
142
142
|
});
|
|
143
143
|
}
|
|
@@ -155,25 +155,29 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
155
155
|
return;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
|
|
158
|
+
// Use ref values to avoid stale closure
|
|
159
|
+
const currentHasSubscription = hasSubscriptionRef.current;
|
|
160
|
+
const currentBalance = creditBalanceRef.current;
|
|
161
|
+
const currentRequiredCredits = requiredCreditsRef.current;
|
|
162
|
+
|
|
163
|
+
if (currentHasSubscription) {
|
|
159
164
|
action();
|
|
160
165
|
return;
|
|
161
166
|
}
|
|
162
167
|
|
|
163
|
-
|
|
164
|
-
if (currentBalance < requiredCredits) {
|
|
168
|
+
if (currentBalance < currentRequiredCredits) {
|
|
165
169
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
166
170
|
console.log("[useFeatureGate] No credits, showing paywall");
|
|
167
171
|
}
|
|
168
172
|
pendingActionRef.current = action;
|
|
169
173
|
isWaitingForPurchaseRef.current = true;
|
|
170
|
-
|
|
174
|
+
onShowPaywallRef.current(currentRequiredCredits);
|
|
171
175
|
return;
|
|
172
176
|
}
|
|
173
177
|
|
|
174
178
|
action();
|
|
175
179
|
},
|
|
176
|
-
[isAuthenticated,
|
|
180
|
+
[isAuthenticated, onShowAuthModal, isCreditsLoaded]
|
|
177
181
|
);
|
|
178
182
|
|
|
179
183
|
const hasCredits = creditBalance >= requiredCredits;
|
|
@@ -48,11 +48,7 @@ export const useSubscriptionStatus = (): SubscriptionStatusResult => {
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
enabled: !!userId && SubscriptionManager.isInitializedForUser(userId),
|
|
51
|
-
|
|
52
|
-
gcTime: 5 * 60 * 1000, // 5 minutes
|
|
53
|
-
refetchOnMount: "always",
|
|
54
|
-
refetchOnWindowFocus: true,
|
|
55
|
-
refetchOnReconnect: true,
|
|
51
|
+
|
|
56
52
|
});
|
|
57
53
|
|
|
58
54
|
return {
|
package/src/init/index.ts
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
export * from "../../domains/subscription/presentation/useAuthAwarePurchase";
|
|
2
|
-
export * from "../../domains/subscription/presentation/useAuthSubscriptionSync";
|
|
3
|
-
export * from "../../domains/subscription/presentation/useSavedPurchaseAutoExecution";
|
|
4
2
|
export * from "../../domains/credits/presentation/useCredits";
|
|
5
3
|
export * from "../../domains/credits/presentation/useDeductCredit";
|
|
6
4
|
export * from "../../domains/subscription/presentation/useFeatureGate";
|
|
7
5
|
export * from "../../domains/subscription/presentation/usePaywallVisibility";
|
|
8
6
|
export * from "../../domains/subscription/presentation/usePremium";
|
|
9
|
-
export * from "../../domains/subscription/presentation/usePremiumGate";
|
|
10
|
-
export * from "../../domains/subscription/presentation/useSubscriptionSettingsConfig";
|
|
11
7
|
export * from "../../domains/subscription/presentation/useSubscriptionStatus";
|
|
12
8
|
export * from "./feedback/usePaywallFeedback";
|
|
13
9
|
export * from "./feedback/useFeedbackSubmit";
|
|
@@ -28,6 +28,11 @@ export class SubscriptionEventBus {
|
|
|
28
28
|
const listeners = this.listeners[event];
|
|
29
29
|
if (listeners) {
|
|
30
30
|
this.listeners[event] = listeners.filter(l => l !== callback);
|
|
31
|
+
|
|
32
|
+
// Clean up empty event arrays to prevent memory leak
|
|
33
|
+
if (this.listeners[event].length === 0) {
|
|
34
|
+
delete this.listeners[event];
|
|
35
|
+
}
|
|
31
36
|
}
|
|
32
37
|
};
|
|
33
38
|
}
|
|
@@ -42,6 +47,28 @@ export class SubscriptionEventBus {
|
|
|
42
47
|
}
|
|
43
48
|
});
|
|
44
49
|
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Clear all listeners for a specific event or all events
|
|
53
|
+
* Useful for cleanup during testing or app state reset
|
|
54
|
+
*/
|
|
55
|
+
clear(event?: string): void {
|
|
56
|
+
if (event) {
|
|
57
|
+
delete this.listeners[event];
|
|
58
|
+
} else {
|
|
59
|
+
this.listeners = {};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get listener count for debugging
|
|
65
|
+
*/
|
|
66
|
+
getListenerCount(event?: string): number {
|
|
67
|
+
if (event) {
|
|
68
|
+
return this.listeners[event]?.length ?? 0;
|
|
69
|
+
}
|
|
70
|
+
return Object.values(this.listeners).reduce((total, arr) => total + arr.length, 0);
|
|
71
|
+
}
|
|
45
72
|
}
|
|
46
73
|
|
|
47
74
|
export const subscriptionEventBus = SubscriptionEventBus.getInstance();
|
|
@@ -7,10 +7,6 @@ import { PACKAGE_TYPE, type PackageType } from "../domains/subscription/core/Sub
|
|
|
7
7
|
|
|
8
8
|
export type SubscriptionPackageType = PackageType;
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
* Check if identifier is a credit package (consumable purchase)
|
|
12
|
-
* Credit packages use a different system and don't need type detection
|
|
13
|
-
*/
|
|
14
10
|
/**
|
|
15
11
|
* Check if identifier is a credit package (consumable purchase)
|
|
16
12
|
* Credit packages use a different system and don't need type detection
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# Presentation Types
|
|
2
|
-
|
|
3
|
-
TypeScript type definitions and interfaces for the presentation layer.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
This directory contains all type definitions used by presentation components and hooks.
|
|
8
|
-
|
|
9
|
-
## Contents
|
|
10
|
-
|
|
11
|
-
### Subscription Types
|
|
12
|
-
|
|
13
|
-
- **SubscriptionSettingsTypes.ts** - Configuration types for subscription settings UI
|
|
14
|
-
- **PaywallTypes.ts** - Paywall component types
|
|
15
|
-
- **SubscriptionTypes.ts** - General subscription UI types
|
|
16
|
-
|
|
17
|
-
## Key Exports
|
|
18
|
-
|
|
19
|
-
## Related
|
|
20
|
-
|
|
21
|
-
- [Hooks](../hooks/README.md)
|
|
22
|
-
- [Components](../components/README.md)
|