@umituz/react-native-subscription 2.37.46 → 2.37.47
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/application/credit-strategies/CreditAllocationOrchestrator.ts +0 -2
- package/src/domains/credits/application/creditDocumentHelpers.ts +0 -5
- package/src/domains/credits/core/Credits.ts +0 -5
- package/src/domains/credits/core/CreditsMapper.ts +0 -5
- package/src/domains/credits/core/UserCreditsDocument.ts +0 -5
- package/src/domains/paywall/components/PaywallContainer.tsx +1 -15
- package/src/domains/paywall/components/PaywallContainer.types.ts +0 -8
- package/src/domains/paywall/components/PaywallModal.tsx +2 -3
- package/src/domains/paywall/components/PaywallModal.types.ts +0 -7
- package/src/domains/paywall/components/PlanCard.tsx +1 -4
- package/src/domains/paywall/components/PlanCard.types.ts +0 -2
- package/src/domains/revenuecat/infrastructure/services/userSwitchHandler.ts +1 -1
- package/src/domains/subscription/core/SubscriptionConstants.ts +0 -3
- package/src/domains/subscription/core/SubscriptionStatus.ts +1 -5
- package/src/domains/subscription/core/SubscriptionStatusHandlers.ts +0 -10
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +1 -1
- package/src/domains/subscription/presentation/components/details/PremiumDetailsCard.tsx +1 -7
- package/src/domains/subscription/presentation/components/details/PremiumStatusBadge.tsx +1 -8
- package/src/index.ts +1 -9
- package/src/domains/credits/application/credit-strategies/TrialCreditStrategy.ts +0 -14
- package/src/domains/paywall/hooks/useTrialEligibilityCheck.ts +0 -65
- package/src/domains/subscription/infrastructure/hooks/useRevenueCatTrialEligibility.ts +0 -81
- package/src/domains/subscription/infrastructure/utils/trialEligibilityUtils.ts +0 -59
- package/src/domains/trial/application/TrialEligibilityService.ts +0 -25
- package/src/domains/trial/application/TrialService.ts +0 -80
- package/src/domains/trial/core/TrialTypes.ts +0 -23
- package/src/domains/trial/infrastructure/DeviceTrialRepository.ts +0 -49
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.37.
|
|
3
|
+
"version": "2.37.47",
|
|
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,10 +1,8 @@
|
|
|
1
1
|
import type { ICreditStrategy, CreditAllocationParams } from "./ICreditStrategy";
|
|
2
|
-
import { TrialCreditStrategy } from "./TrialCreditStrategy";
|
|
3
2
|
import { StandardPurchaseCreditStrategy } from "./StandardPurchaseCreditStrategy";
|
|
4
3
|
|
|
5
4
|
class CreditAllocationOrchestrator {
|
|
6
5
|
private strategies: ICreditStrategy[] = [
|
|
7
|
-
new TrialCreditStrategy(),
|
|
8
6
|
new StandardPurchaseCreditStrategy(),
|
|
9
7
|
];
|
|
10
8
|
|
|
@@ -34,11 +34,6 @@ export function getCreditDocumentOrDefault(
|
|
|
34
34
|
ownershipType: null,
|
|
35
35
|
appVersion: null,
|
|
36
36
|
periodType: null,
|
|
37
|
-
isTrialing: false,
|
|
38
|
-
trialStartDate: null,
|
|
39
|
-
trialEndDate: null,
|
|
40
|
-
trialCredits: 0,
|
|
41
|
-
convertedFromTrial: false,
|
|
42
37
|
purchaseSource: null,
|
|
43
38
|
purchaseType: null,
|
|
44
39
|
};
|
|
@@ -21,11 +21,6 @@ export interface UserCredits {
|
|
|
21
21
|
packageType: PackageType | null;
|
|
22
22
|
originalTransactionId: string | null;
|
|
23
23
|
periodType: string | null;
|
|
24
|
-
isTrialing: boolean | null;
|
|
25
|
-
trialStartDate: Date | null;
|
|
26
|
-
trialEndDate: Date | null;
|
|
27
|
-
trialCredits: number | null;
|
|
28
|
-
convertedFromTrial: boolean | null;
|
|
29
24
|
credits: number;
|
|
30
25
|
creditLimit: number;
|
|
31
26
|
purchaseSource: PurchaseSource | null;
|
|
@@ -43,11 +43,6 @@ export function mapCreditsDocumentToEntity(doc: UserCreditsDocumentRead): UserCr
|
|
|
43
43
|
packageType: doc.packageType,
|
|
44
44
|
originalTransactionId: doc.originalTransactionId,
|
|
45
45
|
periodType,
|
|
46
|
-
isTrialing: doc.isTrialing,
|
|
47
|
-
trialStartDate: toSafeDate(doc.trialStartDate),
|
|
48
|
-
trialEndDate: toSafeDate(doc.trialEndDate),
|
|
49
|
-
trialCredits: doc.trialCredits,
|
|
50
|
-
convertedFromTrial: doc.convertedFromTrial,
|
|
51
46
|
credits: doc.credits,
|
|
52
47
|
creditLimit: doc.creditLimit,
|
|
53
48
|
purchaseSource: doc.purchaseSource,
|
|
@@ -46,11 +46,6 @@ export interface UserCreditsDocumentRead {
|
|
|
46
46
|
store: Store | null;
|
|
47
47
|
ownershipType: OwnershipType | null;
|
|
48
48
|
periodType: string | null;
|
|
49
|
-
isTrialing: boolean | null;
|
|
50
|
-
trialStartDate: FirestoreTimestamp | null;
|
|
51
|
-
trialEndDate: FirestoreTimestamp | null;
|
|
52
|
-
trialCredits: number | null;
|
|
53
|
-
convertedFromTrial: boolean | null;
|
|
54
49
|
credits: number;
|
|
55
50
|
creditLimit: number;
|
|
56
51
|
purchaseSource: PurchaseSource | null;
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import React, { useMemo } from "react";
|
|
2
2
|
import { usePaywallVisibility } from "../../subscription/presentation/usePaywallVisibility";
|
|
3
3
|
import { useSubscriptionPackages } from "../../subscription/infrastructure/hooks/useSubscriptionPackages";
|
|
4
|
-
import { useRevenueCatTrialEligibility } from "../../subscription/infrastructure/hooks/useRevenueCatTrialEligibility";
|
|
5
4
|
import { createCreditAmountsFromPackages } from "../../../utils/creditMapper";
|
|
6
5
|
import { PaywallModal } from "./PaywallModal";
|
|
7
6
|
import { useAuthAwarePurchase } from "../../subscription/presentation/useAuthAwarePurchase";
|
|
8
|
-
import { useTrialEligibilityCheck } from "../hooks/useTrialEligibilityCheck";
|
|
9
7
|
import type { PaywallContainerProps } from "./PaywallContainer.types";
|
|
10
8
|
|
|
11
9
|
export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
|
|
@@ -24,7 +22,6 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
|
|
|
24
22
|
onAuthRequired,
|
|
25
23
|
visible,
|
|
26
24
|
onClose,
|
|
27
|
-
trialConfig,
|
|
28
25
|
} = props;
|
|
29
26
|
|
|
30
27
|
const { showPaywall, closePaywall, currentSource } = usePaywallVisibility();
|
|
@@ -33,21 +30,12 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
|
|
|
33
30
|
|
|
34
31
|
const purchaseSource = source ?? currentSource ?? "settings";
|
|
35
32
|
|
|
36
|
-
const { data: packages = []
|
|
37
|
-
const { eligibilityMap, checkEligibility } = useRevenueCatTrialEligibility();
|
|
33
|
+
const { data: packages = [] } = useSubscriptionPackages();
|
|
38
34
|
|
|
39
35
|
const { handlePurchase: performPurchase, handleRestore: performRestore } = useAuthAwarePurchase({
|
|
40
36
|
source: purchaseSource
|
|
41
37
|
});
|
|
42
38
|
|
|
43
|
-
const trialEligibility = useTrialEligibilityCheck({
|
|
44
|
-
packages,
|
|
45
|
-
isLoading,
|
|
46
|
-
eligibilityMap,
|
|
47
|
-
checkEligibility,
|
|
48
|
-
trialConfig,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
39
|
const creditAmounts = useMemo(() => {
|
|
52
40
|
if (providedCreditAmounts) return providedCreditAmounts;
|
|
53
41
|
if (!packageAllocations || packages.length === 0) return undefined;
|
|
@@ -77,8 +65,6 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
|
|
|
77
65
|
creditsLabel={creditsLabel}
|
|
78
66
|
onPurchase={performPurchase}
|
|
79
67
|
onRestore={performRestore}
|
|
80
|
-
trialEligibility={trialEligibility}
|
|
81
|
-
trialSubtitleText={trialConfig?.enabled ? trialConfig.trialText : undefined}
|
|
82
68
|
onPurchaseSuccess={onPurchaseSuccess}
|
|
83
69
|
onPurchaseError={onPurchaseError}
|
|
84
70
|
onAuthRequired={onAuthRequired}
|
|
@@ -3,13 +3,6 @@ import type { PaywallTranslations, PaywallLegalUrls, SubscriptionFeature } from
|
|
|
3
3
|
import type { PurchaseSource } from "../../subscription/core/SubscriptionConstants";
|
|
4
4
|
import type { PackageAllocationMap } from "../../credits/core/Credits";
|
|
5
5
|
|
|
6
|
-
export interface TrialConfig {
|
|
7
|
-
readonly enabled: boolean;
|
|
8
|
-
readonly eligibleProductIds?: readonly string[];
|
|
9
|
-
readonly durationDays?: number;
|
|
10
|
-
readonly trialText?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
6
|
export interface PaywallContainerProps {
|
|
14
7
|
readonly translations: PaywallTranslations;
|
|
15
8
|
readonly legalUrls?: PaywallLegalUrls;
|
|
@@ -25,5 +18,4 @@ export interface PaywallContainerProps {
|
|
|
25
18
|
readonly onAuthRequired?: () => void;
|
|
26
19
|
readonly visible?: boolean;
|
|
27
20
|
readonly onClose?: () => void;
|
|
28
|
-
readonly trialConfig?: TrialConfig;
|
|
29
21
|
}
|
|
@@ -11,7 +11,7 @@ import { usePaywallActions } from "../hooks/usePaywallActions";
|
|
|
11
11
|
import { PaywallModalProps } from "./PaywallModal.types";
|
|
12
12
|
|
|
13
13
|
export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
14
|
-
const { visible, onClose, translations, packages = [], features = [], legalUrls = {}, bestValueIdentifier, creditAmounts, creditsLabel, heroImage, onPurchase, onRestore,
|
|
14
|
+
const { visible, onClose, translations, packages = [], features = [], legalUrls = {}, bestValueIdentifier, creditAmounts, creditsLabel, heroImage, onPurchase, onRestore, onPurchaseSuccess, onPurchaseError, onAuthRequired, source } = props;
|
|
15
15
|
const tokens = useAppDesignTokens();
|
|
16
16
|
const insets = useSafeAreaInsets();
|
|
17
17
|
const { selectedPlanId, setSelectedPlanId, isProcessing, handlePurchase, handleRestore, resetState } = usePaywallActions({
|
|
@@ -55,9 +55,8 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
|
55
55
|
<View style={styles.plans}>
|
|
56
56
|
{packages.map((pkg) => {
|
|
57
57
|
const pid = pkg.product.identifier;
|
|
58
|
-
const hasTrial = trialEligibility[pid]?.eligible ?? false;
|
|
59
58
|
return (
|
|
60
|
-
<PlanCard key={pid} pkg={pkg} isSelected={selectedPlanId === pid} onSelect={() => setSelectedPlanId(pid)} badge={pid === bestValueIdentifier ? translations.bestValueBadgeText : undefined} creditAmount={creditAmounts?.[pid]} creditsLabel={creditsLabel}
|
|
59
|
+
<PlanCard key={pid} pkg={pkg} isSelected={selectedPlanId === pid} onSelect={() => setSelectedPlanId(pid)} badge={pid === bestValueIdentifier ? translations.bestValueBadgeText : undefined} creditAmount={creditAmounts?.[pid]} creditsLabel={creditsLabel} />
|
|
61
60
|
);
|
|
62
61
|
})}
|
|
63
62
|
</View>
|
|
@@ -3,11 +3,6 @@ import type { PurchasesPackage } from "react-native-purchases";
|
|
|
3
3
|
import type { SubscriptionFeature, PaywallTranslations, PaywallLegalUrls } from "../entities/types";
|
|
4
4
|
import type { PurchaseSource } from "../../subscription/core/SubscriptionConstants";
|
|
5
5
|
|
|
6
|
-
export interface TrialEligibilityInfo {
|
|
7
|
-
eligible: boolean;
|
|
8
|
-
durationDays?: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
6
|
export interface PaywallModalProps {
|
|
12
7
|
visible: boolean;
|
|
13
8
|
onClose: () => void;
|
|
@@ -21,8 +16,6 @@ export interface PaywallModalProps {
|
|
|
21
16
|
heroImage?: ImageSourcePropType;
|
|
22
17
|
onPurchase?: (pkg: PurchasesPackage) => Promise<void | boolean>;
|
|
23
18
|
onRestore?: () => Promise<void | boolean>;
|
|
24
|
-
trialEligibility?: Record<string, TrialEligibilityInfo>;
|
|
25
|
-
trialSubtitleText?: string;
|
|
26
19
|
onPurchaseSuccess?: () => void;
|
|
27
20
|
onPurchaseError?: (error: Error | string) => void;
|
|
28
21
|
onAuthRequired?: () => void;
|
|
@@ -5,7 +5,7 @@ import { formatPriceWithPeriod } from '../../../utils/priceUtils';
|
|
|
5
5
|
import { PlanCardProps } from "./PlanCard.types";
|
|
6
6
|
|
|
7
7
|
export const PlanCard: React.FC<PlanCardProps> = React.memo(
|
|
8
|
-
({ pkg, isSelected, onSelect, badge, creditAmount, creditsLabel
|
|
8
|
+
({ pkg, isSelected, onSelect, badge, creditAmount, creditsLabel }) => {
|
|
9
9
|
const tokens = useAppDesignTokens();
|
|
10
10
|
const title = pkg.product.title;
|
|
11
11
|
const price = formatPriceWithPeriod(pkg.product.price, pkg.product.currencyCode, pkg.identifier);
|
|
@@ -31,9 +31,6 @@ export const PlanCard: React.FC<PlanCardProps> = React.memo(
|
|
|
31
31
|
{creditAmount != null && creditAmount > 0 && creditsLabel && (
|
|
32
32
|
<AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>{creditAmount} {creditsLabel}</AtomicText>
|
|
33
33
|
)}
|
|
34
|
-
{hasFreeTrial && trialSubtitleText && (
|
|
35
|
-
<AtomicText type="bodySmall" style={{ color: tokens.colors.textTertiary ?? tokens.colors.textSecondary, fontSize: 11, marginTop: 2 }}>{trialSubtitleText}</AtomicText>
|
|
36
|
-
)}
|
|
37
34
|
</View>
|
|
38
35
|
</View>
|
|
39
36
|
<AtomicText type="titleMedium" style={{ color: isSelected ? tokens.colors.primary : tokens.colors.textPrimary, fontWeight: "700", fontSize: 18 }}>{price}</AtomicText>
|
|
@@ -190,7 +190,7 @@ export async function handleInitialConfiguration(
|
|
|
190
190
|
premiumEntitlement.productIdentifier,
|
|
191
191
|
premiumEntitlement.expirationDate ?? undefined,
|
|
192
192
|
premiumEntitlement.willRenew,
|
|
193
|
-
premiumEntitlement.periodType as "NORMAL" | "INTRO" |
|
|
193
|
+
premiumEntitlement.periodType as "NORMAL" | "INTRO" | undefined
|
|
194
194
|
);
|
|
195
195
|
} else {
|
|
196
196
|
await deps.config.onPremiumStatusChanged(
|
|
@@ -8,8 +8,6 @@ export type UserTierType = (typeof USER_TIER)[keyof typeof USER_TIER];
|
|
|
8
8
|
|
|
9
9
|
export const SUBSCRIPTION_STATUS = {
|
|
10
10
|
ACTIVE: 'active',
|
|
11
|
-
TRIAL: 'trial',
|
|
12
|
-
TRIAL_CANCELED: 'trial_canceled',
|
|
13
11
|
EXPIRED: 'expired',
|
|
14
12
|
CANCELED: 'canceled',
|
|
15
13
|
NONE: 'none',
|
|
@@ -20,7 +18,6 @@ export type SubscriptionStatusType = (typeof SUBSCRIPTION_STATUS)[keyof typeof S
|
|
|
20
18
|
export const PERIOD_TYPE = {
|
|
21
19
|
NORMAL: 'NORMAL',
|
|
22
20
|
INTRO: 'INTRO',
|
|
23
|
-
TRIAL: 'TRIAL',
|
|
24
21
|
} as const;
|
|
25
22
|
|
|
26
23
|
export type PeriodType = (typeof PERIOD_TYPE)[keyof typeof PERIOD_TYPE];
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
} from "./SubscriptionConstants";
|
|
6
6
|
import {
|
|
7
7
|
InactiveStatusHandler,
|
|
8
|
-
TrialStatusHandler,
|
|
9
8
|
ActiveStatusHandler
|
|
10
9
|
} from "./SubscriptionStatusHandlers";
|
|
11
10
|
|
|
@@ -20,7 +19,6 @@ export interface SubscriptionStatus {
|
|
|
20
19
|
syncedAt?: string | null;
|
|
21
20
|
status?: SubscriptionStatusType;
|
|
22
21
|
periodType?: string;
|
|
23
|
-
isTrialing?: boolean;
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
export const createDefaultSubscriptionStatus = (): SubscriptionStatus => ({
|
|
@@ -47,9 +45,7 @@ export interface StatusResolverInput {
|
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
const inactiveHandler = new InactiveStatusHandler();
|
|
50
|
-
inactiveHandler
|
|
51
|
-
.setNext(new TrialStatusHandler())
|
|
52
|
-
.setNext(new ActiveStatusHandler());
|
|
48
|
+
inactiveHandler.setNext(new ActiveStatusHandler());
|
|
53
49
|
|
|
54
50
|
export const resolveSubscriptionStatus = (input: StatusResolverInput): SubscriptionStatusType => {
|
|
55
51
|
return inactiveHandler.handle(input);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SUBSCRIPTION_STATUS,
|
|
3
|
-
PERIOD_TYPE,
|
|
4
3
|
type SubscriptionStatusType
|
|
5
4
|
} from "./SubscriptionConstants";
|
|
6
5
|
import type { StatusResolverInput } from "./SubscriptionStatus";
|
|
@@ -31,15 +30,6 @@ export class InactiveStatusHandler extends BaseStatusHandler {
|
|
|
31
30
|
}
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
export class TrialStatusHandler extends BaseStatusHandler {
|
|
35
|
-
handle(input: StatusResolverInput): SubscriptionStatusType {
|
|
36
|
-
if (input.periodType === PERIOD_TYPE.TRIAL) {
|
|
37
|
-
return input.willRenew === false ? SUBSCRIPTION_STATUS.TRIAL_CANCELED : SUBSCRIPTION_STATUS.TRIAL;
|
|
38
|
-
}
|
|
39
|
-
return this.nextOrFallback(input, SUBSCRIPTION_STATUS.ACTIVE);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
33
|
export class ActiveStatusHandler extends BaseStatusHandler {
|
|
44
34
|
handle(input: StatusResolverInput): SubscriptionStatusType {
|
|
45
35
|
if (input.willRenew === false) {
|
|
@@ -34,7 +34,7 @@ export async function syncPremiumStatus(
|
|
|
34
34
|
premiumEntitlement.productIdentifier,
|
|
35
35
|
premiumEntitlement.expirationDate ?? undefined,
|
|
36
36
|
premiumEntitlement.willRenew,
|
|
37
|
-
premiumEntitlement.periodType as "NORMAL" | "INTRO" |
|
|
37
|
+
premiumEntitlement.periodType as "NORMAL" | "INTRO" | undefined
|
|
38
38
|
);
|
|
39
39
|
} else {
|
|
40
40
|
await config.onPremiumStatusChanged(userId, false, undefined, undefined, undefined, undefined);
|
|
@@ -26,15 +26,9 @@ export const PremiumDetailsCard: React.FC<PremiumDetailsCardProps> = ({
|
|
|
26
26
|
const tokens = useAppDesignTokens();
|
|
27
27
|
const showCredits = isPremium && credits && credits.length > 0;
|
|
28
28
|
|
|
29
|
-
// Map trial and trial_canceled statuses for display
|
|
30
|
-
const displayStatusType: "active" | "expired" | "none" | "canceled" =
|
|
31
|
-
statusType === "trial" ? "active" :
|
|
32
|
-
statusType === "trial_canceled" ? "canceled" :
|
|
33
|
-
statusType;
|
|
34
|
-
|
|
35
29
|
return (
|
|
36
30
|
<View style={[styles.card, { backgroundColor: tokens.colors.surface }]}>
|
|
37
|
-
{(isPremium || showCredits) && <PremiumDetailsCardHeader statusType={
|
|
31
|
+
{(isPremium || showCredits) && <PremiumDetailsCardHeader statusType={statusType} translations={translations} />}
|
|
38
32
|
|
|
39
33
|
|
|
40
34
|
{isPremium && (
|
|
@@ -19,8 +19,6 @@ export interface PremiumStatusBadgeProps {
|
|
|
19
19
|
expiredLabel: string;
|
|
20
20
|
noneLabel: string;
|
|
21
21
|
canceledLabel: string;
|
|
22
|
-
/** Label for trial_canceled status (defaults to canceledLabel if not provided) */
|
|
23
|
-
trialCanceledLabel?: string;
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
export const PremiumStatusBadge: React.FC<PremiumStatusBadgeProps> = ({
|
|
@@ -29,24 +27,19 @@ export const PremiumStatusBadge: React.FC<PremiumStatusBadgeProps> = ({
|
|
|
29
27
|
expiredLabel,
|
|
30
28
|
noneLabel,
|
|
31
29
|
canceledLabel,
|
|
32
|
-
trialCanceledLabel,
|
|
33
30
|
}) => {
|
|
34
31
|
const tokens = useAppDesignTokens();
|
|
35
32
|
|
|
36
33
|
const labels: Record<SubscriptionStatusType, string> = useMemo(() => ({
|
|
37
34
|
[SUBSCRIPTION_STATUS.ACTIVE]: activeLabel,
|
|
38
|
-
[SUBSCRIPTION_STATUS.TRIAL]: activeLabel,
|
|
39
|
-
[SUBSCRIPTION_STATUS.TRIAL_CANCELED]: trialCanceledLabel ?? canceledLabel,
|
|
40
35
|
[SUBSCRIPTION_STATUS.EXPIRED]: expiredLabel,
|
|
41
36
|
[SUBSCRIPTION_STATUS.NONE]: noneLabel,
|
|
42
37
|
[SUBSCRIPTION_STATUS.CANCELED]: canceledLabel,
|
|
43
|
-
}), [activeLabel,
|
|
38
|
+
}), [activeLabel, canceledLabel, expiredLabel, noneLabel]);
|
|
44
39
|
|
|
45
40
|
const backgroundColor = useMemo(() => {
|
|
46
41
|
const colors: Record<SubscriptionStatusType, string> = {
|
|
47
42
|
[SUBSCRIPTION_STATUS.ACTIVE]: tokens.colors.success,
|
|
48
|
-
[SUBSCRIPTION_STATUS.TRIAL]: tokens.colors.primary, // Blue/purple for trial
|
|
49
|
-
[SUBSCRIPTION_STATUS.TRIAL_CANCELED]: tokens.colors.warning, // Orange for trial canceled
|
|
50
43
|
[SUBSCRIPTION_STATUS.EXPIRED]: tokens.colors.error,
|
|
51
44
|
[SUBSCRIPTION_STATUS.NONE]: tokens.colors.textTertiary,
|
|
52
45
|
[SUBSCRIPTION_STATUS.CANCELED]: tokens.colors.warning,
|
package/src/index.ts
CHANGED
|
@@ -29,14 +29,6 @@ export type { Result, Success, Failure } from "./shared/utils/Result";
|
|
|
29
29
|
|
|
30
30
|
// Infrastructure Layer (Services & Repositories)
|
|
31
31
|
export { initializeSubscription, type SubscriptionInitConfig, type CreditPackageConfig } from "./domains/subscription/application/initializer";
|
|
32
|
-
export {
|
|
33
|
-
getDeviceId,
|
|
34
|
-
checkTrialEligibility,
|
|
35
|
-
recordTrialStart,
|
|
36
|
-
recordTrialEnd,
|
|
37
|
-
recordTrialConversion,
|
|
38
|
-
type TrialEligibilityResult
|
|
39
|
-
} from "./domains/trial/application/TrialService";
|
|
40
32
|
|
|
41
33
|
export { CreditsRepository } from "./domains/credits/infrastructure/CreditsRepository";
|
|
42
34
|
export {
|
|
@@ -99,7 +91,7 @@ export { creditsQueryKeys } from "./domains/credits/presentation/creditsQueryKey
|
|
|
99
91
|
|
|
100
92
|
// Paywall Types
|
|
101
93
|
export type { PaywallTranslations, PaywallLegalUrls } from "./domains/paywall/entities/types";
|
|
102
|
-
export type {
|
|
94
|
+
export type { PaywallContainerProps } from "./domains/paywall/components/PaywallContainer.types";
|
|
103
95
|
|
|
104
96
|
// Purchase Loading Overlay
|
|
105
97
|
export { PurchaseLoadingOverlay } from "./domains/subscription/presentation/components/overlay/PurchaseLoadingOverlay";
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { ICreditStrategy, type CreditAllocationParams } from "./ICreditStrategy";
|
|
2
|
-
import { SUBSCRIPTION_STATUS } from "../../../subscription/core/SubscriptionConstants";
|
|
3
|
-
import { TRIAL_CONFIG } from "../../../trial/core/TrialTypes";
|
|
4
|
-
|
|
5
|
-
export class TrialCreditStrategy implements ICreditStrategy {
|
|
6
|
-
canHandle(params: CreditAllocationParams): boolean {
|
|
7
|
-
return params.status === SUBSCRIPTION_STATUS.TRIAL ||
|
|
8
|
-
params.status === SUBSCRIPTION_STATUS.TRIAL_CANCELED;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
execute(_params: CreditAllocationParams): number {
|
|
12
|
-
return TRIAL_CONFIG.CREDITS;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useMemo } from "react";
|
|
2
|
-
import type { PurchasesPackage } from "react-native-purchases";
|
|
3
|
-
import type { TrialEligibilityInfo } from "../components/PaywallModal.types";
|
|
4
|
-
import type { PaywallContainerProps } from "../components/PaywallContainer.types";
|
|
5
|
-
|
|
6
|
-
interface UseTrialEligibilityCheckParams {
|
|
7
|
-
packages: PurchasesPackage[];
|
|
8
|
-
isLoading: boolean;
|
|
9
|
-
eligibilityMap: Record<string, { eligible: boolean; trialDurationDays?: number }>;
|
|
10
|
-
checkEligibility: (productIds: string[]) => void;
|
|
11
|
-
trialConfig: PaywallContainerProps["trialConfig"];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const useTrialEligibilityCheck = ({
|
|
15
|
-
packages,
|
|
16
|
-
isLoading,
|
|
17
|
-
eligibilityMap,
|
|
18
|
-
checkEligibility,
|
|
19
|
-
trialConfig,
|
|
20
|
-
}: UseTrialEligibilityCheckParams) => {
|
|
21
|
-
const checkedPackagesRef = useRef<string[]>([]);
|
|
22
|
-
|
|
23
|
-
useEffect(() => {
|
|
24
|
-
if (!trialConfig?.enabled || packages.length === 0 || isLoading) return;
|
|
25
|
-
|
|
26
|
-
const currentPackageIds = packages.map((pkg) => pkg.product.identifier);
|
|
27
|
-
const sortedIds = [...currentPackageIds].sort().join(",");
|
|
28
|
-
|
|
29
|
-
if (checkedPackagesRef.current.join(",") === sortedIds) return;
|
|
30
|
-
|
|
31
|
-
checkedPackagesRef.current = currentPackageIds;
|
|
32
|
-
|
|
33
|
-
const allProductIds = packages.map((pkg) => pkg.product.identifier);
|
|
34
|
-
|
|
35
|
-
let productIdsToCheck: string[];
|
|
36
|
-
if (trialConfig.eligibleProductIds?.length) {
|
|
37
|
-
productIdsToCheck = allProductIds.filter((actualId) =>
|
|
38
|
-
trialConfig.eligibleProductIds?.some((configId) =>
|
|
39
|
-
actualId.toLowerCase().includes(configId.toLowerCase())
|
|
40
|
-
)
|
|
41
|
-
);
|
|
42
|
-
} else {
|
|
43
|
-
productIdsToCheck = allProductIds;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (productIdsToCheck.length > 0) {
|
|
47
|
-
checkEligibility(productIdsToCheck);
|
|
48
|
-
}
|
|
49
|
-
}, [packages, isLoading, checkEligibility, trialConfig?.enabled, trialConfig?.eligibleProductIds]);
|
|
50
|
-
|
|
51
|
-
const trialEligibility = useMemo((): Record<string, TrialEligibilityInfo> => {
|
|
52
|
-
if (!trialConfig?.enabled) return {};
|
|
53
|
-
|
|
54
|
-
const result: Record<string, TrialEligibilityInfo> = {};
|
|
55
|
-
for (const [productId, info] of Object.entries(eligibilityMap)) {
|
|
56
|
-
result[productId] = {
|
|
57
|
-
eligible: info.eligible,
|
|
58
|
-
durationDays: trialConfig.durationDays ?? info.trialDurationDays ?? 7,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
return result;
|
|
62
|
-
}, [eligibilityMap, trialConfig?.enabled, trialConfig?.durationDays]);
|
|
63
|
-
|
|
64
|
-
return trialEligibility;
|
|
65
|
-
};
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
-
import { getRevenueCatService } from "../services/RevenueCatService";
|
|
3
|
-
import {
|
|
4
|
-
checkTrialEligibility,
|
|
5
|
-
createFallbackEligibilityMap,
|
|
6
|
-
hasAnyEligibleTrial,
|
|
7
|
-
type ProductTrialEligibility,
|
|
8
|
-
type TrialEligibilityMap,
|
|
9
|
-
} from "../utils/trialEligibilityUtils";
|
|
10
|
-
|
|
11
|
-
interface UseRevenueCatTrialEligibilityResult {
|
|
12
|
-
eligibilityMap: TrialEligibilityMap;
|
|
13
|
-
isLoading: boolean;
|
|
14
|
-
hasEligibleTrial: boolean;
|
|
15
|
-
checkEligibility: (productIds: string[]) => Promise<void>;
|
|
16
|
-
getProductEligibility: (productId: string) => ProductTrialEligibility | null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function useRevenueCatTrialEligibility(): UseRevenueCatTrialEligibilityResult {
|
|
20
|
-
const [eligibilityMap, setEligibilityMap] = useState<TrialEligibilityMap>({});
|
|
21
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
22
|
-
const isMountedRef = useRef(true);
|
|
23
|
-
const currentRequestRef = useRef<number | null>(null);
|
|
24
|
-
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
isMountedRef.current = true;
|
|
27
|
-
return () => {
|
|
28
|
-
isMountedRef.current = false;
|
|
29
|
-
};
|
|
30
|
-
}, []);
|
|
31
|
-
|
|
32
|
-
const checkEligibility = useCallback(async (productIds: string[]) => {
|
|
33
|
-
if (productIds.length === 0) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const service = getRevenueCatService();
|
|
38
|
-
if (!service || !service.isInitialized()) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const requestId = Date.now();
|
|
43
|
-
currentRequestRef.current = requestId;
|
|
44
|
-
setIsLoading(true);
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
const newMap = await checkTrialEligibility(productIds);
|
|
48
|
-
|
|
49
|
-
if (isMountedRef.current && currentRequestRef.current === requestId) {
|
|
50
|
-
setEligibilityMap((prev) => ({ ...prev, ...newMap }));
|
|
51
|
-
}
|
|
52
|
-
} catch {
|
|
53
|
-
const fallbackMap = createFallbackEligibilityMap(productIds);
|
|
54
|
-
|
|
55
|
-
if (isMountedRef.current && currentRequestRef.current === requestId) {
|
|
56
|
-
setEligibilityMap((prev) => ({ ...prev, ...fallbackMap }));
|
|
57
|
-
}
|
|
58
|
-
} finally {
|
|
59
|
-
if (isMountedRef.current && currentRequestRef.current === requestId) {
|
|
60
|
-
setIsLoading(false);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}, []);
|
|
64
|
-
|
|
65
|
-
const getProductEligibility = useCallback(
|
|
66
|
-
(productId: string): ProductTrialEligibility | null => {
|
|
67
|
-
return eligibilityMap[productId] ?? null;
|
|
68
|
-
},
|
|
69
|
-
[eligibilityMap]
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
const hasEligibleTrial = hasAnyEligibleTrial(eligibilityMap);
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
eligibilityMap,
|
|
76
|
-
isLoading,
|
|
77
|
-
hasEligibleTrial,
|
|
78
|
-
checkEligibility,
|
|
79
|
-
getProductEligibility,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import Purchases, {
|
|
2
|
-
type IntroEligibility,
|
|
3
|
-
INTRO_ELIGIBILITY_STATUS,
|
|
4
|
-
} from "react-native-purchases";
|
|
5
|
-
|
|
6
|
-
export interface ProductTrialEligibility {
|
|
7
|
-
productId: string;
|
|
8
|
-
eligible: boolean;
|
|
9
|
-
trialDurationDays?: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export type TrialEligibilityMap = Record<string, ProductTrialEligibility>;
|
|
13
|
-
|
|
14
|
-
const DEFAULT_TRIAL_DURATION_DAYS = 7;
|
|
15
|
-
|
|
16
|
-
export async function checkTrialEligibility(
|
|
17
|
-
productIds: string[]
|
|
18
|
-
): Promise<TrialEligibilityMap> {
|
|
19
|
-
const eligibilities: Record<string, IntroEligibility> =
|
|
20
|
-
await Purchases.checkTrialOrIntroductoryPriceEligibility(productIds);
|
|
21
|
-
|
|
22
|
-
const result: TrialEligibilityMap = {};
|
|
23
|
-
|
|
24
|
-
for (const productId of productIds) {
|
|
25
|
-
const eligibility = eligibilities[productId];
|
|
26
|
-
const isEligible =
|
|
27
|
-
eligibility?.status === INTRO_ELIGIBILITY_STATUS.INTRO_ELIGIBILITY_STATUS_ELIGIBLE;
|
|
28
|
-
|
|
29
|
-
result[productId] = {
|
|
30
|
-
productId,
|
|
31
|
-
eligible: isEligible,
|
|
32
|
-
trialDurationDays: DEFAULT_TRIAL_DURATION_DAYS,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return result;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function createFallbackEligibilityMap(
|
|
40
|
-
productIds: string[]
|
|
41
|
-
): TrialEligibilityMap {
|
|
42
|
-
const result: TrialEligibilityMap = {};
|
|
43
|
-
|
|
44
|
-
for (const productId of productIds) {
|
|
45
|
-
result[productId] = {
|
|
46
|
-
productId,
|
|
47
|
-
eligible: false,
|
|
48
|
-
trialDurationDays: DEFAULT_TRIAL_DURATION_DAYS,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return result;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function hasAnyEligibleTrial(
|
|
56
|
-
eligibilityMap: TrialEligibilityMap
|
|
57
|
-
): boolean {
|
|
58
|
-
return Object.values(eligibilityMap).some((e) => e.eligible);
|
|
59
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { TrialEligibilityResult, DeviceTrialRecord } from "../core/TrialTypes";
|
|
2
|
-
|
|
3
|
-
export class TrialEligibilityService {
|
|
4
|
-
static check(
|
|
5
|
-
userId: string | undefined,
|
|
6
|
-
deviceId: string,
|
|
7
|
-
record: DeviceTrialRecord | null
|
|
8
|
-
): TrialEligibilityResult {
|
|
9
|
-
if (!record) {
|
|
10
|
-
return { eligible: true, deviceId };
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const { hasUsedTrial, trialInProgress, userIds = [] } = record;
|
|
14
|
-
|
|
15
|
-
if (userId && userId.length > 0 && userIds.includes(userId)) {
|
|
16
|
-
return { eligible: false, reason: "user_already_used", deviceId };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (hasUsedTrial || trialInProgress) {
|
|
20
|
-
return { eligible: false, reason: "already_used", deviceId };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return { eligible: true, deviceId };
|
|
24
|
-
}
|
|
25
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { arrayUnion, type FieldValue } from "firebase/firestore";
|
|
2
|
-
import { serverTimestamp } from "@umituz/react-native-firebase";
|
|
3
|
-
import { PersistentDeviceIdService } from "@umituz/react-native-design-system";
|
|
4
|
-
import { DeviceTrialRepository } from "../infrastructure/DeviceTrialRepository";
|
|
5
|
-
import { TrialEligibilityService } from "./TrialEligibilityService";
|
|
6
|
-
import type { TrialEligibilityResult } from "../core/TrialTypes";
|
|
7
|
-
export type { TrialEligibilityResult };
|
|
8
|
-
|
|
9
|
-
interface TrialRecordWrite {
|
|
10
|
-
deviceId?: string;
|
|
11
|
-
hasUsedTrial?: boolean;
|
|
12
|
-
trialInProgress?: boolean;
|
|
13
|
-
trialStartedAt?: FieldValue;
|
|
14
|
-
trialEndedAt?: FieldValue;
|
|
15
|
-
trialConvertedAt?: FieldValue;
|
|
16
|
-
lastUserId?: string;
|
|
17
|
-
userIds?: FieldValue;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const repository = new DeviceTrialRepository();
|
|
21
|
-
|
|
22
|
-
export const getDeviceId = () => PersistentDeviceIdService.getDeviceId();
|
|
23
|
-
|
|
24
|
-
async function ensureDeviceId(deviceId?: string): Promise<string> {
|
|
25
|
-
return (deviceId && deviceId.length > 0) ? deviceId : await getDeviceId();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function checkTrialEligibility(userId?: string, deviceId?: string): Promise<TrialEligibilityResult> {
|
|
29
|
-
try {
|
|
30
|
-
const id = await ensureDeviceId(deviceId);
|
|
31
|
-
const record = await repository.getRecord(id);
|
|
32
|
-
return TrialEligibilityService.check(userId, id, record);
|
|
33
|
-
} catch {
|
|
34
|
-
return { eligible: false, reason: "error" };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function recordTrialStart(userId: string, deviceId?: string): Promise<boolean> {
|
|
39
|
-
try {
|
|
40
|
-
const id = await ensureDeviceId(deviceId);
|
|
41
|
-
const record: TrialRecordWrite = {
|
|
42
|
-
deviceId: id,
|
|
43
|
-
trialInProgress: true,
|
|
44
|
-
trialStartedAt: serverTimestamp(),
|
|
45
|
-
lastUserId: userId,
|
|
46
|
-
userIds: arrayUnion(userId),
|
|
47
|
-
};
|
|
48
|
-
return await repository.saveRecord(id, record as any);
|
|
49
|
-
} catch {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function recordTrialEnd(deviceId?: string): Promise<boolean> {
|
|
55
|
-
try {
|
|
56
|
-
const id = await ensureDeviceId(deviceId);
|
|
57
|
-
const record: TrialRecordWrite = {
|
|
58
|
-
hasUsedTrial: true,
|
|
59
|
-
trialInProgress: false,
|
|
60
|
-
trialEndedAt: serverTimestamp(),
|
|
61
|
-
};
|
|
62
|
-
return await repository.saveRecord(id, record as any);
|
|
63
|
-
} catch {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export async function recordTrialConversion(deviceId?: string): Promise<boolean> {
|
|
69
|
-
try {
|
|
70
|
-
const id = await ensureDeviceId(deviceId);
|
|
71
|
-
const record: TrialRecordWrite = {
|
|
72
|
-
hasUsedTrial: true,
|
|
73
|
-
trialInProgress: false,
|
|
74
|
-
trialConvertedAt: serverTimestamp(),
|
|
75
|
-
};
|
|
76
|
-
return await repository.saveRecord(id, record as any);
|
|
77
|
-
} catch {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export const TRIAL_CONFIG = {
|
|
2
|
-
DURATION_DAYS: 3,
|
|
3
|
-
CREDITS: 0,
|
|
4
|
-
} as const;
|
|
5
|
-
|
|
6
|
-
export interface DeviceTrialRecord {
|
|
7
|
-
deviceId: string;
|
|
8
|
-
hasUsedTrial: boolean;
|
|
9
|
-
trialInProgress?: boolean;
|
|
10
|
-
trialStartedAt?: Date;
|
|
11
|
-
trialEndedAt?: Date;
|
|
12
|
-
trialConvertedAt?: Date;
|
|
13
|
-
lastUserId?: string;
|
|
14
|
-
userIds: string[];
|
|
15
|
-
createdAt: Date;
|
|
16
|
-
updatedAt: Date;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface TrialEligibilityResult {
|
|
20
|
-
eligible: boolean;
|
|
21
|
-
reason?: "already_used" | "device_not_found" | "error" | "user_already_used";
|
|
22
|
-
deviceId?: string;
|
|
23
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { doc } from "firebase/firestore";
|
|
2
|
-
import { getFirestore, runTransaction, serverTimestamp, type Firestore, type Transaction } from "@umituz/react-native-firebase";
|
|
3
|
-
import type { DeviceTrialRecord } from "../core/TrialTypes";
|
|
4
|
-
|
|
5
|
-
const DEVICE_TRIALS_COLLECTION = "device_trials";
|
|
6
|
-
|
|
7
|
-
export class DeviceTrialRepository {
|
|
8
|
-
private get db(): Firestore | null {
|
|
9
|
-
return getFirestore();
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async getRecord(deviceId: string): Promise<DeviceTrialRecord | null> {
|
|
13
|
-
if (!this.db) return null;
|
|
14
|
-
const ref = doc(this.db, DEVICE_TRIALS_COLLECTION, deviceId);
|
|
15
|
-
const snap = await runTransaction(async (tx: Transaction) => {
|
|
16
|
-
return tx.get(ref);
|
|
17
|
-
});
|
|
18
|
-
return snap.exists() ? snap.data() as DeviceTrialRecord : null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async saveRecord(deviceId: string, data: Partial<DeviceTrialRecord>): Promise<boolean> {
|
|
22
|
-
if (!this.db) return false;
|
|
23
|
-
const ref = doc(this.db, DEVICE_TRIALS_COLLECTION, deviceId);
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
// Atomic check-then-act: ensure createdAt is set only once
|
|
27
|
-
await runTransaction(async (tx: Transaction) => {
|
|
28
|
-
const snap = await tx.get(ref);
|
|
29
|
-
const existingData = snap.data();
|
|
30
|
-
|
|
31
|
-
const updateData: Record<string, unknown> = {
|
|
32
|
-
...data,
|
|
33
|
-
updatedAt: serverTimestamp(),
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
if (!existingData?.createdAt) {
|
|
37
|
-
updateData.createdAt = serverTimestamp();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
tx.set(ref, updateData, { merge: true });
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
return true;
|
|
44
|
-
} catch (error) {
|
|
45
|
-
console.error('[DeviceTrialRepository] Failed to save record:', error instanceof Error ? error.message : String(error));
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|