@umituz/react-native-subscription 2.27.140 → 2.27.142

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.27.140",
3
+ "version": "2.27.142",
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",
@@ -0,0 +1,4 @@
1
+ export const creditsQueryKeys = {
2
+ all: ["credits"] as const,
3
+ user: (userId: string) => ["credits", userId] as const,
4
+ };
@@ -2,7 +2,6 @@ import { useQuery, useQueryClient } from "@umituz/react-native-design-system";
2
2
  import { useCallback, useMemo, useEffect, useRef } from "react";
3
3
  import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
4
4
  import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
5
- import type { UserCredits } from "../core/Credits";
6
5
  import {
7
6
  getCreditsRepository,
8
7
  getCreditsConfig,
@@ -11,35 +10,18 @@ import {
11
10
  import { calculateCreditPercentage, canAfford as canAffordCheck } from "../../../shared/utils/numberUtils";
12
11
  import { createUserQueryKey } from "../../../shared/utils/queryKeyFactory";
13
12
  import { isAuthenticated } from "../../subscription/utils/authGuards";
13
+ import { creditsQueryKeys } from "./creditsQueryKeys";
14
+ import type { UseCreditsResult, CreditsLoadStatus } from "./useCredits.types";
14
15
 
15
- export const creditsQueryKeys = {
16
- all: ["credits"] as const,
17
- user: (userId: string) => ["credits", userId] as const,
18
- };
19
-
20
- export type CreditsLoadStatus = "idle" | "loading" | "ready" | "error";
21
-
22
- export interface UseCreditsResult {
23
- credits: UserCredits | null;
24
- isLoading: boolean;
25
- isCreditsLoaded: boolean;
26
- loadStatus: CreditsLoadStatus;
27
- error: Error | null;
28
- hasCredits: boolean;
29
- creditsPercent: number;
30
- refetch: () => void;
31
- canAfford: (cost: number) => boolean;
32
- }
33
-
34
- function deriveLoadStatus(
16
+ const deriveLoadStatus = (
35
17
  queryStatus: "pending" | "error" | "success",
36
18
  queryEnabled: boolean
37
- ): CreditsLoadStatus {
19
+ ): CreditsLoadStatus => {
38
20
  if (!queryEnabled) return "idle";
39
21
  if (queryStatus === "pending") return "loading";
40
22
  if (queryStatus === "error") return "error";
41
23
  return "ready";
42
- }
24
+ };
43
25
 
44
26
  export const useCredits = (): UseCreditsResult => {
45
27
  const userId = useAuthStore(selectUserId);
@@ -122,4 +104,3 @@ export const useHasCredits = (): boolean => {
122
104
  const { hasCredits } = useCredits();
123
105
  return hasCredits;
124
106
  };
125
-
@@ -0,0 +1,15 @@
1
+ import type { UserCredits } from "../core/Credits";
2
+
3
+ export type CreditsLoadStatus = "idle" | "loading" | "ready" | "error";
4
+
5
+ export interface UseCreditsResult {
6
+ credits: UserCredits | null;
7
+ isLoading: boolean;
8
+ isCreditsLoaded: boolean;
9
+ loadStatus: CreditsLoadStatus;
10
+ error: Error | null;
11
+ hasCredits: boolean;
12
+ creditsPercent: number;
13
+ refetch: () => void;
14
+ canAfford: (cost: number) => boolean;
15
+ }
@@ -1,19 +1,12 @@
1
- /**
2
- * PaywallContainer Component
3
- * Uses centralized pending purchase state - no local auth handling
4
- * Apple Guideline 3.1.2 compliant trial display
5
- */
6
-
7
- import React, { useMemo, useEffect } from "react";
1
+ import React, { useMemo } from "react";
8
2
  import { usePaywallVisibility } from "../../subscription/presentation/usePaywallVisibility";
9
3
  import { useSubscriptionPackages } from "../../subscription/infrastructure/hooks/useSubscriptionPackages";
10
4
  import { useRevenueCatTrialEligibility } from "../../subscription/infrastructure/hooks/useRevenueCatTrialEligibility";
11
5
  import { createCreditAmountsFromPackages } from "../../../utils/creditMapper";
12
6
  import { PaywallModal } from "./PaywallModal";
13
- import type { TrialEligibilityInfo } from "./PaywallModal.types";
14
-
15
7
  import { usePaywallActions } from "../hooks/usePaywallActions";
16
8
  import { useAuthAwarePurchase } from "../../subscription/presentation/useAuthAwarePurchase";
9
+ import { useTrialEligibilityCheck } from "../hooks/useTrialEligibilityCheck";
17
10
  import type { PaywallContainerProps } from "./PaywallContainer.types";
18
11
 
19
12
  export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
@@ -59,62 +52,13 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
59
52
  onClose: handleClose,
60
53
  });
61
54
 
62
- // Check trial eligibility only if trialConfig is enabled
63
- // Use ref to track if we've already checked for these packages to avoid redundant calls
64
- const checkedPackagesRef = React.useRef<string[]>([]);
65
-
66
- useEffect(() => {
67
- if (!trialConfig?.enabled) return;
68
- if (packages.length === 0) return;
69
- if (isLoading) return; // Wait for packages to fully load
70
-
71
- // Get current package identifiers
72
- const currentPackageIds = packages.map((pkg) => pkg.product.identifier);
73
- const sortedIds = [...currentPackageIds].sort().join(",");
74
-
75
- // Skip if we've already checked these exact packages
76
- if (checkedPackagesRef.current.join(",") === sortedIds) {
77
- return;
78
- }
79
-
80
- // Update ref
81
- checkedPackagesRef.current = currentPackageIds;
82
-
83
- // Get all actual product IDs from packages
84
- const allProductIds = packages.map((pkg) => pkg.product.identifier);
85
-
86
- // If eligibleProductIds are provided, filter to matching packages (partial match)
87
- // e.g., "yearly" matches "futureus.yearly"
88
- let productIdsToCheck: string[];
89
- if (trialConfig.eligibleProductIds?.length) {
90
- productIdsToCheck = allProductIds.filter((actualId) =>
91
- trialConfig.eligibleProductIds?.some((configId) =>
92
- actualId.toLowerCase().includes(configId.toLowerCase())
93
- )
94
- );
95
- } else {
96
- productIdsToCheck = allProductIds;
97
- }
98
-
99
- if (productIdsToCheck.length > 0) {
100
- checkEligibility(productIdsToCheck);
101
- }
102
- }, [packages, isLoading, checkEligibility, trialConfig?.enabled, trialConfig?.eligibleProductIds]);
103
-
104
- // Convert eligibility map to format expected by PaywallModal
105
- // Only process if trial is enabled
106
- const trialEligibility = useMemo((): Record<string, TrialEligibilityInfo> => {
107
- if (!trialConfig?.enabled) return {};
108
-
109
- const result: Record<string, TrialEligibilityInfo> = {};
110
- for (const [productId, info] of Object.entries(eligibilityMap)) {
111
- result[productId] = {
112
- eligible: info.eligible,
113
- durationDays: trialConfig.durationDays ?? info.trialDurationDays ?? 7,
114
- };
115
- }
116
- return result;
117
- }, [eligibilityMap, trialConfig?.enabled, trialConfig?.durationDays]);
55
+ const trialEligibility = useTrialEligibilityCheck({
56
+ packages,
57
+ isLoading,
58
+ eligibilityMap,
59
+ checkEligibility,
60
+ trialConfig,
61
+ });
118
62
 
119
63
  // Compute credit amounts from packageAllocations if not provided directly
120
64
  const creditAmounts = useMemo(() => {
@@ -0,0 +1,65 @@
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
+ };
@@ -0,0 +1,7 @@
1
+ export const EXPIRATION_WARNING_DAYS = 7;
2
+
3
+ export const REVENUE_CAT_IGNORED_LOG_MESSAGES = [
4
+ 'Purchase was cancelled',
5
+ 'AppTransaction',
6
+ "Couldn't find previous transactions",
7
+ ] as const;
@@ -0,0 +1,6 @@
1
+ export const FEEDBACK_TEXT_MAX_LENGTH = 200;
2
+
3
+ export const FEEDBACK_CHECKBOX_SIZE = 22;
4
+ export const FEEDBACK_CHECKBOX_RADIUS = 11;
5
+
6
+ export const FEEDBACK_TEXT_MIN_HEIGHT = 80;
@@ -1,141 +1,17 @@
1
- /**
2
- * Wallet Error
3
- *
4
- * Custom error classes for wallet and payment operations.
5
- * Follows the same pattern as SubscriptionError.
6
- */
7
-
8
- import { WALLET_ERROR_MESSAGES } from "./WalletErrorMessages";
9
-
10
- export type WalletErrorCategory =
11
- | "PAYMENT"
12
- | "VALIDATION"
13
- | "INFRASTRUCTURE"
14
- | "BUSINESS";
15
-
16
- export abstract class WalletError extends Error {
17
- abstract readonly code: string;
18
- abstract readonly userMessage: string;
19
- abstract readonly category: WalletErrorCategory;
20
-
21
- constructor(
22
- message: string,
23
- public readonly cause?: Error
24
- ) {
25
- super(message);
26
- this.name = this.constructor.name;
27
- }
28
-
29
- toJSON() {
30
- return {
31
- name: this.name,
32
- code: this.code,
33
- userMessage: this.userMessage,
34
- category: this.category,
35
- message: this.message,
36
- cause: this.cause?.message,
37
- };
38
- }
39
- }
40
-
41
- export class PaymentValidationError extends WalletError {
42
- readonly code = "PAYMENT_VALIDATION_ERROR";
43
- readonly category = "PAYMENT" as const;
44
- readonly userMessage = WALLET_ERROR_MESSAGES.PAYMENT_VALIDATION_FAILED;
45
-
46
- constructor(message: string, cause?: Error) {
47
- super(message, cause);
48
- }
49
- }
50
-
51
- export class PaymentProviderError extends WalletError {
52
- readonly code = "PAYMENT_PROVIDER_ERROR";
53
- readonly category = "PAYMENT" as const;
54
- readonly userMessage = WALLET_ERROR_MESSAGES.PAYMENT_PROVIDER_ERROR;
55
-
56
- constructor(message: string, cause?: Error) {
57
- super(message, cause);
58
- }
59
- }
60
-
61
- export class DuplicatePaymentError extends WalletError {
62
- readonly code = "DUPLICATE_PAYMENT";
63
- readonly category = "PAYMENT" as const;
64
- readonly userMessage = WALLET_ERROR_MESSAGES.DUPLICATE_PAYMENT;
65
- }
66
-
67
- export class UserValidationError extends WalletError {
68
- readonly code = "USER_VALIDATION_ERROR";
69
- readonly category = "VALIDATION" as const;
70
- readonly userMessage = WALLET_ERROR_MESSAGES.USER_VALIDATION_FAILED;
71
- }
72
-
73
- export class PackageValidationError extends WalletError {
74
- readonly code = "PACKAGE_VALIDATION_ERROR";
75
- readonly category = "VALIDATION" as const;
76
- readonly userMessage = WALLET_ERROR_MESSAGES.PACKAGE_VALIDATION_FAILED;
77
- }
78
-
79
- export class ReceiptValidationError extends WalletError {
80
- readonly code = "RECEIPT_VALIDATION_ERROR";
81
- readonly category = "VALIDATION" as const;
82
- readonly userMessage = WALLET_ERROR_MESSAGES.RECEIPT_VALIDATION_FAILED;
83
- }
84
-
85
- export class TransactionError extends WalletError {
86
- readonly code = "TRANSACTION_ERROR";
87
- readonly category = "INFRASTRUCTURE" as const;
88
- readonly userMessage = WALLET_ERROR_MESSAGES.TRANSACTION_FAILED;
89
-
90
- constructor(message: string, cause?: Error) {
91
- super(message, cause);
92
- }
93
- }
94
-
95
- export class NetworkError extends WalletError {
96
- readonly code = "NETWORK_ERROR";
97
- readonly category = "INFRASTRUCTURE" as const;
98
- readonly userMessage = WALLET_ERROR_MESSAGES.NETWORK_ERROR;
99
-
100
- constructor(message: string, cause?: Error) {
101
- super(message, cause);
102
- }
103
- }
104
-
105
- export class CreditLimitError extends WalletError {
106
- readonly code = "CREDIT_LIMIT_ERROR";
107
- readonly category = "BUSINESS" as const;
108
- readonly userMessage = WALLET_ERROR_MESSAGES.CREDIT_LIMIT_EXCEEDED;
109
- }
110
-
111
- export class RefundError extends WalletError {
112
- readonly code = "REFUND_ERROR";
113
- readonly category = "BUSINESS" as const;
114
- readonly userMessage = WALLET_ERROR_MESSAGES.REFUND_FAILED;
115
-
116
- constructor(message: string, cause?: Error) {
117
- super(message, cause);
118
- }
119
- }
120
-
121
- export function handleWalletError(error: unknown): WalletError {
122
- if (error instanceof WalletError) {
123
- return error;
124
- }
125
-
126
- if (error instanceof Error) {
127
- const message = error.message.toLowerCase();
128
-
129
- if (message.includes("network") || message.includes("timeout")) {
130
- return new NetworkError(error.message, error);
131
- }
132
-
133
- if (message.includes("permission") || message.includes("unauthorized")) {
134
- return new UserValidationError("Authentication failed");
135
- }
136
-
137
- return new TransactionError(error.message, error);
138
- }
139
-
140
- return new TransactionError("Unexpected error occurred");
141
- }
1
+ export type { WalletErrorCategory } from "./WalletError.types";
2
+ export { WalletError } from "./WalletError.types";
3
+
4
+ export {
5
+ PaymentValidationError,
6
+ PaymentProviderError,
7
+ DuplicatePaymentError,
8
+ UserValidationError,
9
+ PackageValidationError,
10
+ ReceiptValidationError,
11
+ TransactionError,
12
+ NetworkError,
13
+ CreditLimitError,
14
+ RefundError,
15
+ } from "./WalletErrorClasses";
16
+
17
+ export { handleWalletError } from "./WalletErrorFactory";
@@ -0,0 +1,30 @@
1
+ export type WalletErrorCategory =
2
+ | "PAYMENT"
3
+ | "VALIDATION"
4
+ | "INFRASTRUCTURE"
5
+ | "BUSINESS";
6
+
7
+ export abstract class WalletError extends Error {
8
+ abstract readonly code: string;
9
+ abstract readonly userMessage: string;
10
+ abstract readonly category: WalletErrorCategory;
11
+
12
+ constructor(
13
+ message: string,
14
+ public readonly cause?: Error
15
+ ) {
16
+ super(message);
17
+ this.name = this.constructor.name;
18
+ }
19
+
20
+ toJSON() {
21
+ return {
22
+ name: this.name,
23
+ code: this.code,
24
+ userMessage: this.userMessage,
25
+ category: this.category,
26
+ message: this.message,
27
+ cause: this.cause?.message,
28
+ };
29
+ }
30
+ }
@@ -0,0 +1,82 @@
1
+ import { WalletError } from "./WalletError.types";
2
+ import { WALLET_ERROR_MESSAGES } from "./WalletErrorMessages";
3
+
4
+ export class PaymentValidationError extends WalletError {
5
+ readonly code = "PAYMENT_VALIDATION_ERROR";
6
+ readonly category = "PAYMENT" as const;
7
+ readonly userMessage = WALLET_ERROR_MESSAGES.PAYMENT_VALIDATION_FAILED;
8
+
9
+ constructor(message: string, cause?: Error) {
10
+ super(message, cause);
11
+ }
12
+ }
13
+
14
+ export class PaymentProviderError extends WalletError {
15
+ readonly code = "PAYMENT_PROVIDER_ERROR";
16
+ readonly category = "PAYMENT" as const;
17
+ readonly userMessage = WALLET_ERROR_MESSAGES.PAYMENT_PROVIDER_ERROR;
18
+
19
+ constructor(message: string, cause?: Error) {
20
+ super(message, cause);
21
+ }
22
+ }
23
+
24
+ export class DuplicatePaymentError extends WalletError {
25
+ readonly code = "DUPLICATE_PAYMENT";
26
+ readonly category = "PAYMENT" as const;
27
+ readonly userMessage = WALLET_ERROR_MESSAGES.DUPLICATE_PAYMENT;
28
+ }
29
+
30
+ export class UserValidationError extends WalletError {
31
+ readonly code = "USER_VALIDATION_ERROR";
32
+ readonly category = "VALIDATION" as const;
33
+ readonly userMessage = WALLET_ERROR_MESSAGES.USER_VALIDATION_FAILED;
34
+ }
35
+
36
+ export class PackageValidationError extends WalletError {
37
+ readonly code = "PACKAGE_VALIDATION_ERROR";
38
+ readonly category = "VALIDATION" as const;
39
+ readonly userMessage = WALLET_ERROR_MESSAGES.PACKAGE_VALIDATION_FAILED;
40
+ }
41
+
42
+ export class ReceiptValidationError extends WalletError {
43
+ readonly code = "RECEIPT_VALIDATION_ERROR";
44
+ readonly category = "VALIDATION" as const;
45
+ readonly userMessage = WALLET_ERROR_MESSAGES.RECEIPT_VALIDATION_FAILED;
46
+ }
47
+
48
+ export class TransactionError extends WalletError {
49
+ readonly code = "TRANSACTION_ERROR";
50
+ readonly category = "INFRASTRUCTURE" as const;
51
+ readonly userMessage = WALLET_ERROR_MESSAGES.TRANSACTION_FAILED;
52
+
53
+ constructor(message: string, cause?: Error) {
54
+ super(message, cause);
55
+ }
56
+ }
57
+
58
+ export class NetworkError extends WalletError {
59
+ readonly code = "NETWORK_ERROR";
60
+ readonly category = "INFRASTRUCTURE" as const;
61
+ readonly userMessage = WALLET_ERROR_MESSAGES.NETWORK_ERROR;
62
+
63
+ constructor(message: string, cause?: Error) {
64
+ super(message, cause);
65
+ }
66
+ }
67
+
68
+ export class CreditLimitError extends WalletError {
69
+ readonly code = "CREDIT_LIMIT_ERROR";
70
+ readonly category = "BUSINESS" as const;
71
+ readonly userMessage = WALLET_ERROR_MESSAGES.CREDIT_LIMIT_EXCEEDED;
72
+ }
73
+
74
+ export class RefundError extends WalletError {
75
+ readonly code = "REFUND_ERROR";
76
+ readonly category = "BUSINESS" as const;
77
+ readonly userMessage = WALLET_ERROR_MESSAGES.REFUND_FAILED;
78
+
79
+ constructor(message: string, cause?: Error) {
80
+ super(message, cause);
81
+ }
82
+ }
@@ -0,0 +1,24 @@
1
+ import { WalletError } from "./WalletError.types";
2
+ import { NetworkError, UserValidationError, TransactionError } from "./WalletErrorClasses";
3
+
4
+ export const handleWalletError = (error: unknown): WalletError => {
5
+ if (error instanceof WalletError) {
6
+ return error;
7
+ }
8
+
9
+ if (error instanceof Error) {
10
+ const message = error.message.toLowerCase();
11
+
12
+ if (message.includes("network") || message.includes("timeout")) {
13
+ return new NetworkError(error.message, error);
14
+ }
15
+
16
+ if (message.includes("permission") || message.includes("unauthorized")) {
17
+ return new UserValidationError("Authentication failed");
18
+ }
19
+
20
+ return new TransactionError(error.message, error);
21
+ }
22
+
23
+ return new TransactionError("Unexpected error occurred");
24
+ };
@@ -0,0 +1 @@
1
+ export const TRANSACTION_LIST_MAX_HEIGHT = 400;