@umituz/react-native-subscription 2.27.41 → 2.27.43
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/PaywallContainer.tsx +5 -18
- package/src/domains/paywall/components/PaywallContainer.types.ts +2 -12
- package/src/domains/paywall/components/PaywallModal.tsx +1 -5
- package/src/domains/paywall/components/PlanCard.tsx +1 -10
- package/src/domains/paywall/components/index.ts +0 -2
- package/src/domains/paywall/entities/types.ts +0 -22
- package/src/domains/paywall/hooks/index.ts +0 -1
- package/src/utils/creditMapper.ts +1 -29
- package/src/utils/index.ts +0 -1
- package/src/domains/paywall/components/CreditCard.tsx +0 -120
- package/src/domains/paywall/components/PaywallTabBar.tsx +0 -77
- package/src/domains/paywall/hooks/usePaywall.ts +0 -54
- package/src/utils/packageFilter.ts +0 -62
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.43",
|
|
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",
|
|
@@ -8,8 +8,6 @@ import React, { useMemo, useEffect } from "react";
|
|
|
8
8
|
import { usePaywallVisibility } from "../../../presentation/hooks/usePaywallVisibility";
|
|
9
9
|
import { useSubscriptionPackages } from "../../../revenuecat/presentation/hooks/useSubscriptionPackages";
|
|
10
10
|
import { useRevenueCatTrialEligibility } from "../../../revenuecat/presentation/hooks/useRevenueCatTrialEligibility";
|
|
11
|
-
import { filterPackagesByMode } from "../../../utils/packageFilter";
|
|
12
|
-
import { createCreditAmountsFromPackages } from "../../../utils/creditMapper";
|
|
13
11
|
import { PaywallModal, type TrialEligibilityInfo } from "./PaywallModal";
|
|
14
12
|
import { usePaywallActions } from "../hooks/usePaywallActions";
|
|
15
13
|
import type { PaywallContainerProps } from "./PaywallContainer.types";
|
|
@@ -17,14 +15,10 @@ import type { PaywallContainerProps } from "./PaywallContainer.types";
|
|
|
17
15
|
export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
|
|
18
16
|
const {
|
|
19
17
|
translations,
|
|
20
|
-
mode = "subscription",
|
|
21
18
|
legalUrls,
|
|
22
19
|
features,
|
|
23
20
|
heroImage,
|
|
24
21
|
bestValueIdentifier,
|
|
25
|
-
creditsLabel,
|
|
26
|
-
creditAmounts,
|
|
27
|
-
packageFilterConfig,
|
|
28
22
|
source,
|
|
29
23
|
onPurchaseSuccess,
|
|
30
24
|
onPurchaseError,
|
|
@@ -40,7 +34,7 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
|
|
|
40
34
|
|
|
41
35
|
const purchaseSource = source ?? currentSource ?? "settings";
|
|
42
36
|
|
|
43
|
-
const { data:
|
|
37
|
+
const { data: packages = [], isLoading } = useSubscriptionPackages();
|
|
44
38
|
const { eligibilityMap, checkEligibility } = useRevenueCatTrialEligibility();
|
|
45
39
|
const { handlePurchase, handleRestore } = usePaywallActions({
|
|
46
40
|
source: purchaseSource,
|
|
@@ -53,10 +47,10 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
|
|
|
53
47
|
// Check trial eligibility only if trialConfig is enabled
|
|
54
48
|
useEffect(() => {
|
|
55
49
|
if (!trialConfig?.enabled) return;
|
|
56
|
-
if (
|
|
50
|
+
if (packages.length === 0) return;
|
|
57
51
|
|
|
58
52
|
// Get all actual product IDs from packages
|
|
59
|
-
const allProductIds =
|
|
53
|
+
const allProductIds = packages.map((pkg) => pkg.product.identifier);
|
|
60
54
|
|
|
61
55
|
// If eligibleProductIds are provided, filter to matching packages (partial match)
|
|
62
56
|
// e.g., "yearly" matches "futureus.yearly"
|
|
@@ -74,7 +68,7 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
|
|
|
74
68
|
if (productIdsToCheck.length > 0) {
|
|
75
69
|
checkEligibility(productIdsToCheck);
|
|
76
70
|
}
|
|
77
|
-
}, [
|
|
71
|
+
}, [packages, checkEligibility, trialConfig?.enabled, trialConfig?.eligibleProductIds]);
|
|
78
72
|
|
|
79
73
|
// Convert eligibility map to format expected by PaywallModal
|
|
80
74
|
// Only process if trial is enabled
|
|
@@ -91,11 +85,6 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
|
|
|
91
85
|
return result;
|
|
92
86
|
}, [eligibilityMap, trialConfig?.enabled, trialConfig?.durationDays]);
|
|
93
87
|
|
|
94
|
-
const { filteredPackages, computedCreditAmounts } = useMemo(() => ({
|
|
95
|
-
filteredPackages: filterPackagesByMode(allPackages, mode, packageFilterConfig),
|
|
96
|
-
computedCreditAmounts: mode !== "subscription" && !creditAmounts ? createCreditAmountsFromPackages(allPackages) : creditAmounts
|
|
97
|
-
}), [allPackages, mode, packageFilterConfig, creditAmounts]);
|
|
98
|
-
|
|
99
88
|
if (!isVisible) return null;
|
|
100
89
|
|
|
101
90
|
return (
|
|
@@ -103,14 +92,12 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
|
|
|
103
92
|
visible={isVisible}
|
|
104
93
|
onClose={handleClose}
|
|
105
94
|
translations={translations}
|
|
106
|
-
packages={
|
|
95
|
+
packages={packages}
|
|
107
96
|
isLoading={isLoading}
|
|
108
97
|
legalUrls={legalUrls}
|
|
109
98
|
features={features ? [...features] : undefined}
|
|
110
99
|
heroImage={heroImage}
|
|
111
100
|
bestValueIdentifier={bestValueIdentifier}
|
|
112
|
-
creditsLabel={creditsLabel}
|
|
113
|
-
creditAmounts={computedCreditAmounts}
|
|
114
101
|
onPurchase={handlePurchase}
|
|
115
102
|
onRestore={handleRestore}
|
|
116
103
|
trialEligibility={trialEligibility}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PaywallContainer Types
|
|
3
|
-
* Props for
|
|
3
|
+
* Props for subscription paywall
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ImageSourcePropType } from "react-native";
|
|
7
|
-
import type {
|
|
8
|
-
import type { PackageFilterConfig } from "../../../utils/packageFilter";
|
|
7
|
+
import type { PaywallTranslations, PaywallLegalUrls, SubscriptionFeature } from "../entities";
|
|
9
8
|
import type { PurchaseSource } from "../../../domain/entities/Credits";
|
|
10
9
|
|
|
11
10
|
/**
|
|
@@ -26,8 +25,6 @@ export interface TrialConfig {
|
|
|
26
25
|
export interface PaywallContainerProps {
|
|
27
26
|
/** Paywall translations - no defaults, must be provided */
|
|
28
27
|
readonly translations: PaywallTranslations;
|
|
29
|
-
/** Paywall mode - subscription, credits, or hybrid */
|
|
30
|
-
readonly mode?: PaywallMode;
|
|
31
28
|
/** Legal URLs for privacy and terms */
|
|
32
29
|
readonly legalUrls?: PaywallLegalUrls;
|
|
33
30
|
/** Feature list to display */
|
|
@@ -36,12 +33,6 @@ export interface PaywallContainerProps {
|
|
|
36
33
|
readonly heroImage?: ImageSourcePropType;
|
|
37
34
|
/** Best value package identifier for badge */
|
|
38
35
|
readonly bestValueIdentifier?: string;
|
|
39
|
-
/** Credits label text */
|
|
40
|
-
readonly creditsLabel?: string;
|
|
41
|
-
/** Credit amounts per package identifier */
|
|
42
|
-
readonly creditAmounts?: Record<string, number>;
|
|
43
|
-
/** Custom filter config for package categorization */
|
|
44
|
-
readonly packageFilterConfig?: PackageFilterConfig;
|
|
45
36
|
/** Source of the paywall - affects pending purchase handling */
|
|
46
37
|
readonly source?: PurchaseSource;
|
|
47
38
|
/** Callback when purchase succeeds */
|
|
@@ -57,4 +48,3 @@ export interface PaywallContainerProps {
|
|
|
57
48
|
/** Trial display configuration (Apple-compliant) */
|
|
58
49
|
readonly trialConfig?: TrialConfig;
|
|
59
50
|
}
|
|
60
|
-
|
|
@@ -33,8 +33,6 @@ export interface PaywallModalProps {
|
|
|
33
33
|
isLoading?: boolean;
|
|
34
34
|
legalUrls?: PaywallLegalUrls;
|
|
35
35
|
bestValueIdentifier?: string;
|
|
36
|
-
creditAmounts?: Record<string, number>;
|
|
37
|
-
creditsLabel?: string;
|
|
38
36
|
heroImage?: ImageSourcePropType;
|
|
39
37
|
onPurchase?: (pkg: PurchasesPackage) => Promise<void | boolean>;
|
|
40
38
|
onRestore?: () => Promise<void | boolean>;
|
|
@@ -45,7 +43,7 @@ export interface PaywallModalProps {
|
|
|
45
43
|
}
|
|
46
44
|
|
|
47
45
|
export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
48
|
-
const { visible, onClose, translations, packages = [], features = [], isLoading = false, legalUrls = {}, bestValueIdentifier,
|
|
46
|
+
const { visible, onClose, translations, packages = [], features = [], isLoading = false, legalUrls = {}, bestValueIdentifier, heroImage, onPurchase, onRestore, trialEligibility = {}, trialSubtitleText } = props;
|
|
49
47
|
const tokens = useAppDesignTokens();
|
|
50
48
|
const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);
|
|
51
49
|
const [isLocalProcessing, setIsLocalProcessing] = useState(false);
|
|
@@ -152,8 +150,6 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
|
152
150
|
isSelected={selectedPlanId === productId}
|
|
153
151
|
onSelect={() => setSelectedPlanId(productId)}
|
|
154
152
|
badge={productId === bestValueIdentifier ? translations.bestValueBadgeText : undefined}
|
|
155
|
-
creditAmount={creditAmounts?.[productId]}
|
|
156
|
-
creditsLabel={creditsLabel}
|
|
157
153
|
hasFreeTrial={hasFreeTrial}
|
|
158
154
|
trialSubtitleText={hasFreeTrial ? trialSubtitleText : undefined}
|
|
159
155
|
/>
|
|
@@ -21,8 +21,6 @@ interface PlanCardProps {
|
|
|
21
21
|
onSelect: () => void;
|
|
22
22
|
/** Badge text (e.g., "Best Value") - NOT for trial */
|
|
23
23
|
badge?: string;
|
|
24
|
-
creditAmount?: number;
|
|
25
|
-
creditsLabel?: string;
|
|
26
24
|
/** Whether this plan has a free trial (Apple-compliant display) */
|
|
27
25
|
hasFreeTrial?: boolean;
|
|
28
26
|
/** Trial subtitle text (e.g., "7 days free, then billed") - shown as small gray text */
|
|
@@ -30,7 +28,7 @@ interface PlanCardProps {
|
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
export const PlanCard: React.FC<PlanCardProps> = React.memo(
|
|
33
|
-
({ pkg, isSelected, onSelect, badge,
|
|
31
|
+
({ pkg, isSelected, onSelect, badge, hasFreeTrial, trialSubtitleText }) => {
|
|
34
32
|
const tokens = useAppDesignTokens();
|
|
35
33
|
const title = pkg.product.title;
|
|
36
34
|
const price = formatPriceWithPeriod(pkg.product.price, pkg.product.currencyCode, pkg.identifier);
|
|
@@ -75,13 +73,6 @@ export const PlanCard: React.FC<PlanCardProps> = React.memo(
|
|
|
75
73
|
{title}
|
|
76
74
|
</AtomicText>
|
|
77
75
|
|
|
78
|
-
{/* Credits info */}
|
|
79
|
-
{creditAmount && creditsLabel && (
|
|
80
|
-
<AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
|
|
81
|
-
{creditAmount} {creditsLabel}
|
|
82
|
-
</AtomicText>
|
|
83
|
-
)}
|
|
84
|
-
|
|
85
76
|
{/* Trial info - Apple-compliant: small, gray, subordinate */}
|
|
86
77
|
{hasFreeTrial && trialSubtitleText && (
|
|
87
78
|
<AtomicText
|
|
@@ -9,9 +9,7 @@ export { PaywallModal } from "./PaywallModal";
|
|
|
9
9
|
export type { PaywallModalProps } from "./PaywallModal";
|
|
10
10
|
|
|
11
11
|
export { PaywallHeader } from "./PaywallHeader";
|
|
12
|
-
export { PaywallTabBar } from "./PaywallTabBar";
|
|
13
12
|
export { PaywallFooter } from "./PaywallFooter";
|
|
14
13
|
export { FeatureList } from "./FeatureList";
|
|
15
14
|
export { FeatureItem } from "./FeatureItem";
|
|
16
15
|
export { PlanCard } from "./PlanCard";
|
|
17
|
-
export { CreditCard } from "./CreditCard";
|
|
@@ -3,25 +3,6 @@
|
|
|
3
3
|
* All paywall-related type definitions
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export type PaywallMode = "subscription" | "credits" | "hybrid";
|
|
7
|
-
|
|
8
|
-
export type PaywallTabType = "credits" | "subscription";
|
|
9
|
-
|
|
10
|
-
export interface PaywallTab {
|
|
11
|
-
id: PaywallTabType;
|
|
12
|
-
label: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface CreditsPackage {
|
|
16
|
-
id: string;
|
|
17
|
-
credits: number;
|
|
18
|
-
price: number;
|
|
19
|
-
currency: string;
|
|
20
|
-
bonus?: number;
|
|
21
|
-
badge?: string;
|
|
22
|
-
description?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
6
|
export interface SubscriptionFeature {
|
|
26
7
|
icon: string;
|
|
27
8
|
text: string;
|
|
@@ -30,10 +11,7 @@ export interface SubscriptionFeature {
|
|
|
30
11
|
export interface PaywallTranslations {
|
|
31
12
|
title: string;
|
|
32
13
|
subtitle?: string;
|
|
33
|
-
creditsTabLabel?: string;
|
|
34
|
-
subscriptionTabLabel?: string;
|
|
35
14
|
purchaseButtonText: string;
|
|
36
|
-
subscribeButtonText?: string;
|
|
37
15
|
restoreButtonText: string;
|
|
38
16
|
loadingText: string;
|
|
39
17
|
emptyText: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SubscriptionPackageType } from "./packageTypeDetector";
|
|
2
2
|
import type { PackageAllocationMap } from "../domain/entities/Credits";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -11,31 +11,3 @@ export function getCreditAllocation(
|
|
|
11
11
|
if (packageType === "unknown" || !allocations) return null;
|
|
12
12
|
return allocations[packageType]?.credits ?? null;
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Create credit amounts mapping for PaywallModal from RevenueCat packages
|
|
17
|
-
* Maps product.identifier to credit amount using dynamic allocations
|
|
18
|
-
*/
|
|
19
|
-
export function createCreditAmountsFromPackages(
|
|
20
|
-
packages: Array<{ product: { identifier: string } }>,
|
|
21
|
-
allocations?: PackageAllocationMap
|
|
22
|
-
): Record<string, number> {
|
|
23
|
-
const result: Record<string, number> = {};
|
|
24
|
-
|
|
25
|
-
if (!allocations) return result;
|
|
26
|
-
|
|
27
|
-
for (const pkg of packages) {
|
|
28
|
-
const identifier = pkg?.product?.identifier;
|
|
29
|
-
|
|
30
|
-
if (!identifier) continue;
|
|
31
|
-
|
|
32
|
-
const packageType = detectPackageType(identifier);
|
|
33
|
-
const credits = getCreditAllocation(packageType, allocations);
|
|
34
|
-
|
|
35
|
-
if (credits !== null) {
|
|
36
|
-
result[identifier] = credits;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return result;
|
|
41
|
-
}
|
package/src/utils/index.ts
CHANGED
|
@@ -2,7 +2,6 @@ export * from "./aiCreditHelpers";
|
|
|
2
2
|
export * from "./authUtils";
|
|
3
3
|
export * from "./creditChecker";
|
|
4
4
|
export * from "./creditMapper";
|
|
5
|
-
export * from "./packageFilter";
|
|
6
5
|
export * from "./packagePeriodUtils";
|
|
7
6
|
export * from "./packageTypeDetector";
|
|
8
7
|
export * from "./premiumStatusUtils";
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Credit Card
|
|
3
|
-
* Credit package selection card
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React from "react";
|
|
7
|
-
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
-
import { AtomicText, AtomicIcon, AtomicBadge, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
9
|
-
import type { CreditsPackage } from '../entities';
|
|
10
|
-
|
|
11
|
-
import { formatPrice } from '../../../utils/priceUtils';
|
|
12
|
-
|
|
13
|
-
interface CreditCardProps {
|
|
14
|
-
pkg: CreditsPackage;
|
|
15
|
-
isSelected: boolean;
|
|
16
|
-
onSelect: () => void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const CreditCard: React.FC<CreditCardProps> = React.memo(({ pkg, isSelected, onSelect }) => {
|
|
20
|
-
const tokens = useAppDesignTokens();
|
|
21
|
-
const totalCredits = pkg.credits + (pkg.bonus ?? 0);
|
|
22
|
-
const price = formatPrice(pkg.price, pkg.currency);
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<TouchableOpacity onPress={onSelect} activeOpacity={0.7} style={styles.touchable}>
|
|
26
|
-
<View
|
|
27
|
-
style={[
|
|
28
|
-
styles.container,
|
|
29
|
-
{
|
|
30
|
-
backgroundColor: tokens.colors.surface,
|
|
31
|
-
borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
|
|
32
|
-
borderWidth: isSelected ? 2 : 1,
|
|
33
|
-
},
|
|
34
|
-
]}
|
|
35
|
-
>
|
|
36
|
-
{pkg.badge && (
|
|
37
|
-
<View style={styles.badgeContainer}>
|
|
38
|
-
<AtomicBadge text={pkg.badge} variant="warning" size="sm" />
|
|
39
|
-
</View>
|
|
40
|
-
)}
|
|
41
|
-
|
|
42
|
-
<View style={styles.content}>
|
|
43
|
-
<View style={styles.leftSection}>
|
|
44
|
-
<AtomicIcon name="flash" size="md" color={isSelected ? "primary" : "secondary"} />
|
|
45
|
-
<AtomicText
|
|
46
|
-
type="headlineSmall"
|
|
47
|
-
style={[styles.credits, { color: isSelected ? tokens.colors.primary : tokens.colors.textPrimary }]}
|
|
48
|
-
>
|
|
49
|
-
{totalCredits.toLocaleString()}
|
|
50
|
-
</AtomicText>
|
|
51
|
-
</View>
|
|
52
|
-
|
|
53
|
-
<View style={styles.rightSection}>
|
|
54
|
-
<AtomicText
|
|
55
|
-
type="titleLarge"
|
|
56
|
-
style={[styles.price, { color: isSelected ? tokens.colors.primary : tokens.colors.textPrimary }]}
|
|
57
|
-
>
|
|
58
|
-
{price}
|
|
59
|
-
</AtomicText>
|
|
60
|
-
{isSelected && <AtomicIcon name="checkmark-circle" size="md" color="primary" />}
|
|
61
|
-
</View>
|
|
62
|
-
</View>
|
|
63
|
-
|
|
64
|
-
{(pkg.bonus ?? 0) > 0 && (
|
|
65
|
-
<View style={styles.bonusRow}>
|
|
66
|
-
<AtomicIcon name="gift-outline" size="sm" color="success" />
|
|
67
|
-
<AtomicText type="bodySmall" style={{ color: tokens.colors.success, marginLeft: 4 }}>
|
|
68
|
-
+{pkg.bonus}
|
|
69
|
-
</AtomicText>
|
|
70
|
-
</View>
|
|
71
|
-
)}
|
|
72
|
-
</View>
|
|
73
|
-
</TouchableOpacity>
|
|
74
|
-
);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
CreditCard.displayName = "CreditCard";
|
|
78
|
-
|
|
79
|
-
const styles = StyleSheet.create({
|
|
80
|
-
touchable: {
|
|
81
|
-
marginBottom: 10,
|
|
82
|
-
marginHorizontal: 24,
|
|
83
|
-
},
|
|
84
|
-
container: {
|
|
85
|
-
borderRadius: 16,
|
|
86
|
-
padding: 16,
|
|
87
|
-
position: "relative",
|
|
88
|
-
},
|
|
89
|
-
badgeContainer: {
|
|
90
|
-
position: "absolute",
|
|
91
|
-
top: -10,
|
|
92
|
-
right: 16,
|
|
93
|
-
},
|
|
94
|
-
content: {
|
|
95
|
-
flexDirection: "row",
|
|
96
|
-
justifyContent: "space-between",
|
|
97
|
-
alignItems: "center",
|
|
98
|
-
},
|
|
99
|
-
leftSection: {
|
|
100
|
-
flexDirection: "row",
|
|
101
|
-
alignItems: "center",
|
|
102
|
-
},
|
|
103
|
-
credits: {
|
|
104
|
-
fontWeight: "700",
|
|
105
|
-
marginLeft: 8,
|
|
106
|
-
},
|
|
107
|
-
rightSection: {
|
|
108
|
-
flexDirection: "row",
|
|
109
|
-
alignItems: "center",
|
|
110
|
-
},
|
|
111
|
-
price: {
|
|
112
|
-
fontWeight: "700",
|
|
113
|
-
marginRight: 8,
|
|
114
|
-
},
|
|
115
|
-
bonusRow: {
|
|
116
|
-
flexDirection: "row",
|
|
117
|
-
alignItems: "center",
|
|
118
|
-
marginTop: 8,
|
|
119
|
-
},
|
|
120
|
-
});
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Paywall Tab Bar
|
|
3
|
-
* Segmented control for hybrid mode
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React from "react";
|
|
7
|
-
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
8
|
-
import { AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
9
|
-
import type { PaywallTabType } from '../entities';
|
|
10
|
-
|
|
11
|
-
interface PaywallTabBarProps {
|
|
12
|
-
activeTab: PaywallTabType;
|
|
13
|
-
onTabChange: (tab: PaywallTabType) => void;
|
|
14
|
-
creditsLabel: string;
|
|
15
|
-
subscriptionLabel: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const PaywallTabBar: React.FC<PaywallTabBarProps> = React.memo(
|
|
19
|
-
({ activeTab, onTabChange, creditsLabel, subscriptionLabel }) => {
|
|
20
|
-
const tokens = useAppDesignTokens();
|
|
21
|
-
|
|
22
|
-
const renderTab = (tab: PaywallTabType, label: string) => {
|
|
23
|
-
const isActive = activeTab === tab;
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<TouchableOpacity
|
|
27
|
-
key={tab}
|
|
28
|
-
style={[
|
|
29
|
-
styles.tab,
|
|
30
|
-
isActive ? { backgroundColor: tokens.colors.surface } : undefined,
|
|
31
|
-
]}
|
|
32
|
-
onPress={() => onTabChange(tab)}
|
|
33
|
-
activeOpacity={0.7}
|
|
34
|
-
>
|
|
35
|
-
<AtomicText
|
|
36
|
-
type="labelLarge"
|
|
37
|
-
style={[
|
|
38
|
-
styles.tabText,
|
|
39
|
-
{ color: isActive ? tokens.colors.primary : tokens.colors.textSecondary },
|
|
40
|
-
]}
|
|
41
|
-
>
|
|
42
|
-
{label}
|
|
43
|
-
</AtomicText>
|
|
44
|
-
</TouchableOpacity>
|
|
45
|
-
);
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
<View style={[styles.container, { backgroundColor: tokens.colors.surfaceSecondary }]}>
|
|
50
|
-
{renderTab("credits", creditsLabel)}
|
|
51
|
-
{renderTab("subscription", subscriptionLabel)}
|
|
52
|
-
</View>
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
PaywallTabBar.displayName = "PaywallTabBar";
|
|
58
|
-
|
|
59
|
-
const styles = StyleSheet.create({
|
|
60
|
-
container: {
|
|
61
|
-
flexDirection: "row",
|
|
62
|
-
borderRadius: 12,
|
|
63
|
-
padding: 4,
|
|
64
|
-
marginHorizontal: 24,
|
|
65
|
-
marginBottom: 16,
|
|
66
|
-
height: 44,
|
|
67
|
-
},
|
|
68
|
-
tab: {
|
|
69
|
-
flex: 1,
|
|
70
|
-
alignItems: "center",
|
|
71
|
-
justifyContent: "center",
|
|
72
|
-
borderRadius: 8,
|
|
73
|
-
},
|
|
74
|
-
tabText: {
|
|
75
|
-
fontWeight: "600",
|
|
76
|
-
},
|
|
77
|
-
});
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { useState, useCallback } from "react";
|
|
2
|
-
import type { PurchasesPackage } from "react-native-purchases";
|
|
3
|
-
import type { PaywallTabType } from "../entities";
|
|
4
|
-
|
|
5
|
-
interface UsePaywallProps {
|
|
6
|
-
initialTab?: PaywallTabType;
|
|
7
|
-
onCreditsPurchase: (packageId: string) => Promise<void>;
|
|
8
|
-
onSubscriptionPurchase: (pkg: PurchasesPackage) => Promise<void>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const usePaywall = ({
|
|
12
|
-
initialTab = "credits",
|
|
13
|
-
onCreditsPurchase,
|
|
14
|
-
onSubscriptionPurchase,
|
|
15
|
-
}: UsePaywallProps) => {
|
|
16
|
-
const [activeTab, setActiveTab] = useState<PaywallTabType>(initialTab);
|
|
17
|
-
const [selectedCreditsPackageId, setSelectedCreditsPackageId] = useState<string | null>(null);
|
|
18
|
-
const [selectedSubscriptionPkg, setSelectedSubscriptionPkg] = useState<PurchasesPackage | null>(null);
|
|
19
|
-
|
|
20
|
-
const handleTabChange = useCallback((tab: PaywallTabType) => {
|
|
21
|
-
setActiveTab(tab);
|
|
22
|
-
}, []);
|
|
23
|
-
|
|
24
|
-
const handleCreditsPackageSelect = useCallback((packageId: string) => {
|
|
25
|
-
setSelectedCreditsPackageId(packageId);
|
|
26
|
-
}, []);
|
|
27
|
-
|
|
28
|
-
const handleSubscriptionPackageSelect = useCallback((pkg: PurchasesPackage) => {
|
|
29
|
-
setSelectedSubscriptionPkg(pkg);
|
|
30
|
-
}, []);
|
|
31
|
-
|
|
32
|
-
const handleCreditsPurchase = useCallback(async () => {
|
|
33
|
-
if (selectedCreditsPackageId) {
|
|
34
|
-
await onCreditsPurchase(selectedCreditsPackageId);
|
|
35
|
-
}
|
|
36
|
-
}, [selectedCreditsPackageId, onCreditsPurchase]);
|
|
37
|
-
|
|
38
|
-
const handleSubscriptionPurchase = useCallback(async () => {
|
|
39
|
-
if (selectedSubscriptionPkg) {
|
|
40
|
-
await onSubscriptionPurchase(selectedSubscriptionPkg);
|
|
41
|
-
}
|
|
42
|
-
}, [selectedSubscriptionPkg, onSubscriptionPurchase]);
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
activeTab,
|
|
46
|
-
selectedCreditsPackageId,
|
|
47
|
-
selectedSubscriptionPkg,
|
|
48
|
-
handleTabChange,
|
|
49
|
-
handleCreditsPackageSelect,
|
|
50
|
-
handleSubscriptionPackageSelect,
|
|
51
|
-
handleCreditsPurchase,
|
|
52
|
-
handleSubscriptionPurchase,
|
|
53
|
-
};
|
|
54
|
-
};
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Package Filter Utility
|
|
3
|
-
* Filters RevenueCat packages by type (credits vs subscription)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { PurchasesPackage } from "react-native-purchases";
|
|
7
|
-
|
|
8
|
-
export type PackageCategory = "credits" | "subscription";
|
|
9
|
-
|
|
10
|
-
export interface PackageFilterConfig {
|
|
11
|
-
creditIdentifierPattern?: RegExp;
|
|
12
|
-
subscriptionIdentifierPattern?: RegExp;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const DEFAULT_CONFIG: PackageFilterConfig = {
|
|
16
|
-
creditIdentifierPattern: /credit/i,
|
|
17
|
-
subscriptionIdentifierPattern: /(monthly|yearly|annual|weekly|premium|subscription)/i,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export function getPackageCategory(
|
|
21
|
-
pkg: PurchasesPackage,
|
|
22
|
-
config: PackageFilterConfig = DEFAULT_CONFIG
|
|
23
|
-
): PackageCategory {
|
|
24
|
-
const identifier = pkg.identifier.toLowerCase();
|
|
25
|
-
const productIdentifier = pkg.product.identifier.toLowerCase();
|
|
26
|
-
|
|
27
|
-
const isCreditPackage =
|
|
28
|
-
config.creditIdentifierPattern?.test(identifier) ||
|
|
29
|
-
config.creditIdentifierPattern?.test(productIdentifier);
|
|
30
|
-
|
|
31
|
-
return isCreditPackage ? "credits" : "subscription";
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function filterPackagesByMode(
|
|
35
|
-
packages: PurchasesPackage[],
|
|
36
|
-
mode: "credits" | "subscription" | "hybrid",
|
|
37
|
-
config: PackageFilterConfig = DEFAULT_CONFIG
|
|
38
|
-
): PurchasesPackage[] {
|
|
39
|
-
if (mode === "hybrid") {
|
|
40
|
-
return packages;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return packages.filter((pkg) => getPackageCategory(pkg, config) === mode);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function separatePackages(
|
|
47
|
-
packages: PurchasesPackage[],
|
|
48
|
-
config: PackageFilterConfig = DEFAULT_CONFIG
|
|
49
|
-
): { creditPackages: PurchasesPackage[]; subscriptionPackages: PurchasesPackage[] } {
|
|
50
|
-
const creditPackages: PurchasesPackage[] = [];
|
|
51
|
-
const subscriptionPackages: PurchasesPackage[] = [];
|
|
52
|
-
|
|
53
|
-
for (const pkg of packages) {
|
|
54
|
-
if (getPackageCategory(pkg, config) === "credits") {
|
|
55
|
-
creditPackages.push(pkg);
|
|
56
|
-
} else {
|
|
57
|
-
subscriptionPackages.push(pkg);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return { creditPackages, subscriptionPackages };
|
|
62
|
-
}
|