@umituz/react-native-subscription 2.21.0 → 2.22.1

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.21.0",
3
+ "version": "2.22.1",
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",
package/src/index.ts CHANGED
@@ -14,6 +14,13 @@ export type { ISubscriptionRepository } from "./application/ports/ISubscriptionR
14
14
 
15
15
  // Infrastructure Layer
16
16
  export { SubscriptionService, initializeSubscriptionService } from "./infrastructure/services/SubscriptionService";
17
+ export {
18
+ submitFeedback,
19
+ submitPaywallFeedback,
20
+ submitSettingsFeedback,
21
+ type FeedbackData,
22
+ type FeedbackSubmitResult,
23
+ } from "./infrastructure/services/FeedbackService";
17
24
  export { initializeSubscription, type SubscriptionInitConfig, type CreditPackageConfig } from "./infrastructure/services/SubscriptionInitializer";
18
25
  export { CreditsRepository, createCreditsRepository } from "./infrastructure/repositories/CreditsRepository";
19
26
  export { configureCreditsRepository, getCreditsRepository, getCreditsConfig, resetCreditsRepository, isCreditsRepositoryConfigured } from "./infrastructure/repositories/CreditsRepositoryProvider";
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Feedback Service
3
+ * Handles feedback submission to Firestore
4
+ */
5
+
6
+ import { getFirestore } from "@umituz/react-native-firebase";
7
+ import { collection, addDoc } from "firebase/firestore";
8
+
9
+ export interface FeedbackData {
10
+ userId: string | null;
11
+ userEmail: string | null;
12
+ type: string;
13
+ title: string;
14
+ description: string;
15
+ rating?: number;
16
+ status?: string;
17
+ }
18
+
19
+ export interface FeedbackSubmitResult {
20
+ success: boolean;
21
+ error?: Error;
22
+ }
23
+
24
+ const FEEDBACK_COLLECTION = "feedback";
25
+
26
+ /**
27
+ * Submit feedback to Firestore
28
+ */
29
+ export async function submitFeedback(
30
+ data: FeedbackData
31
+ ): Promise<FeedbackSubmitResult> {
32
+ const db = getFirestore();
33
+
34
+ if (!db) {
35
+ if (__DEV__) {
36
+ console.warn("[FeedbackService] Firestore not available");
37
+ }
38
+ return { success: false, error: new Error("Firestore not available") };
39
+ }
40
+
41
+ try {
42
+ if (__DEV__) {
43
+ console.log("[FeedbackService] Submitting feedback:", {
44
+ type: data.type,
45
+ userId: data.userId?.slice(0, 8),
46
+ });
47
+ }
48
+
49
+ const now = new Date().toISOString();
50
+
51
+ await addDoc(collection(db, FEEDBACK_COLLECTION), {
52
+ userId: data.userId,
53
+ userEmail: data.userEmail,
54
+ type: data.type,
55
+ title: data.title,
56
+ description: data.description,
57
+ rating: data.rating ?? null,
58
+ status: data.status ?? "pending",
59
+ createdAt: now,
60
+ updatedAt: now,
61
+ });
62
+
63
+ if (__DEV__) {
64
+ console.log("[FeedbackService] Feedback submitted successfully");
65
+ }
66
+
67
+ return { success: true };
68
+ } catch (error) {
69
+ if (__DEV__) {
70
+ console.error("[FeedbackService] Submit error:", error);
71
+ }
72
+ return {
73
+ success: false,
74
+ error: error instanceof Error ? error : new Error("Unknown error"),
75
+ };
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Submit paywall decline feedback
81
+ */
82
+ export async function submitPaywallFeedback(
83
+ userId: string | null,
84
+ userEmail: string | null,
85
+ reason: string
86
+ ): Promise<FeedbackSubmitResult> {
87
+ return submitFeedback({
88
+ userId,
89
+ userEmail,
90
+ type: "paywall_declined",
91
+ title: "Paywall Declined",
92
+ description: reason,
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Submit general settings feedback
98
+ */
99
+ export async function submitSettingsFeedback(
100
+ userId: string | null,
101
+ userEmail: string | null,
102
+ data: {
103
+ type?: string;
104
+ title?: string;
105
+ description: string;
106
+ rating?: number;
107
+ }
108
+ ): Promise<FeedbackSubmitResult> {
109
+ return submitFeedback({
110
+ userId,
111
+ userEmail,
112
+ type: data.type ?? "general",
113
+ title: data.title ?? "Settings Feedback",
114
+ description: data.description,
115
+ rating: data.rating,
116
+ });
117
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Feedback Submit Hooks
3
+ * React hooks for submitting feedback to Firestore
4
+ */
5
+
6
+ import { useCallback } from "react";
7
+ import { useAuth } from "@umituz/react-native-auth";
8
+ import {
9
+ submitPaywallFeedback,
10
+ submitSettingsFeedback,
11
+ } from "../../../infrastructure/services/FeedbackService";
12
+
13
+ export interface UsePaywallFeedbackSubmitOptions {
14
+ onSuccess?: () => void;
15
+ onError?: (error: Error) => void;
16
+ onComplete?: () => void;
17
+ }
18
+
19
+ /**
20
+ * Hook for submitting paywall decline feedback
21
+ */
22
+ export function usePaywallFeedbackSubmit(
23
+ options: UsePaywallFeedbackSubmitOptions = {}
24
+ ) {
25
+ const { user } = useAuth();
26
+ const { onSuccess, onError, onComplete } = options;
27
+
28
+ const submit = useCallback(
29
+ async (reason: string) => {
30
+ if (__DEV__) {
31
+ console.log("[usePaywallFeedbackSubmit] Submitting:", {
32
+ userId: user?.uid?.slice(0, 8),
33
+ reason: reason.slice(0, 20),
34
+ });
35
+ }
36
+
37
+ const result = await submitPaywallFeedback(
38
+ user?.uid ?? null,
39
+ user?.email ?? null,
40
+ reason
41
+ );
42
+
43
+ if (result.success) {
44
+ onSuccess?.();
45
+ } else if (result.error) {
46
+ onError?.(result.error);
47
+ }
48
+
49
+ onComplete?.();
50
+ },
51
+ [user, onSuccess, onError, onComplete]
52
+ );
53
+
54
+ return { submit };
55
+ }
56
+
57
+ export interface SettingsFeedbackData {
58
+ type?: string;
59
+ title?: string;
60
+ description: string;
61
+ rating?: number;
62
+ }
63
+
64
+ export interface UseSettingsFeedbackSubmitOptions {
65
+ onSuccess?: () => void;
66
+ onError?: (error: Error) => void;
67
+ }
68
+
69
+ /**
70
+ * Hook for submitting general settings feedback
71
+ */
72
+ export function useSettingsFeedbackSubmit(
73
+ options: UseSettingsFeedbackSubmitOptions = {}
74
+ ) {
75
+ const { user } = useAuth();
76
+ const { onSuccess, onError } = options;
77
+
78
+ const submit = useCallback(
79
+ async (data: SettingsFeedbackData) => {
80
+ if (__DEV__) {
81
+ console.log("[useSettingsFeedbackSubmit] Submitting:", {
82
+ userId: user?.uid?.slice(0, 8),
83
+ type: data.type,
84
+ });
85
+ }
86
+
87
+ const result = await submitSettingsFeedback(
88
+ user?.uid ?? null,
89
+ user?.email ?? null,
90
+ data
91
+ );
92
+
93
+ if (result.success) {
94
+ onSuccess?.();
95
+ } else if (result.error) {
96
+ onError?.(result.error);
97
+ }
98
+
99
+ return result;
100
+ },
101
+ [user, onSuccess, onError]
102
+ );
103
+
104
+ return { submit };
105
+ }
@@ -21,3 +21,4 @@ export * from "./useSubscriptionStatus";
21
21
  export * from "./useUserTier";
22
22
  export * from "./useUserTierWithRepository";
23
23
  export * from "./feedback/usePaywallFeedback";
24
+ export * from "./feedback/useFeedbackSubmit";
@@ -1,9 +1,13 @@
1
1
  /**
2
2
  * Purchase Handler
3
3
  * Handles RevenueCat purchase operations for both subscriptions and consumables
4
+ *
5
+ * IMPORTANT: Uses race condition with CustomerInfo listener to handle
6
+ * known RevenueCat bug where purchasePackage can hang indefinitely.
7
+ * @see https://github.com/RevenueCat/react-native-purchases/issues/1082
4
8
  */
5
9
 
6
- import Purchases, { type PurchasesPackage } from "react-native-purchases";
10
+ import Purchases, { type PurchasesPackage, type CustomerInfo } from "react-native-purchases";
7
11
  import type { PurchaseResult } from "../../application/ports/IRevenueCatService";
8
12
  import {
9
13
  RevenueCatPurchaseError,
@@ -20,6 +24,50 @@ import {
20
24
  } from "../utils/PremiumStatusSyncer";
21
25
  import { getSavedPurchase, clearSavedPurchase } from "../../../presentation/hooks/useAuthAwarePurchase";
22
26
 
27
+ const PURCHASE_LISTENER_TIMEOUT_MS = 30000; // 30 seconds fallback timeout
28
+
29
+ /**
30
+ * Creates a promise that resolves when CustomerInfo listener detects premium
31
+ * This is a workaround for RevenueCat bug where purchasePackage can hang
32
+ */
33
+ function createPremiumListenerPromise(
34
+ entitlementId: string
35
+ ): { promise: Promise<CustomerInfo>; cleanup: () => void } {
36
+ let cleanup: () => void = () => {};
37
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
38
+
39
+ const promise = new Promise<CustomerInfo>((resolve, reject) => {
40
+ const listener = (info: CustomerInfo) => {
41
+ const isPremium = !!info.entitlements.active[entitlementId];
42
+ if (isPremium) {
43
+ if (__DEV__) {
44
+ console.log('[DEBUG PurchaseHandler] Listener detected premium!');
45
+ }
46
+ cleanup();
47
+ resolve(info);
48
+ }
49
+ };
50
+
51
+ Purchases.addCustomerInfoUpdateListener(listener);
52
+
53
+ // Fallback timeout - if neither purchasePackage nor listener resolves
54
+ timeoutId = setTimeout(() => {
55
+ cleanup();
56
+ reject(new Error('Purchase timed out. Please try again.'));
57
+ }, PURCHASE_LISTENER_TIMEOUT_MS);
58
+
59
+ cleanup = () => {
60
+ Purchases.removeCustomerInfoUpdateListener(listener);
61
+ if (timeoutId) {
62
+ clearTimeout(timeoutId);
63
+ timeoutId = null;
64
+ }
65
+ };
66
+ });
67
+
68
+ return { promise, cleanup };
69
+ }
70
+
23
71
  export interface PurchaseHandlerDeps {
24
72
  config: RevenueCatConfig;
25
73
  isInitialized: () => boolean;
@@ -63,9 +111,13 @@ export async function handlePurchase(
63
111
  const consumableIds = deps.config.consumableProductIdentifiers || [];
64
112
  const isConsumable = isConsumableProduct(pkg, consumableIds);
65
113
 
114
+ // Set up listener-based detection as fallback for purchasePackage bug
115
+ const entitlementIdentifier = deps.config.entitlementIdentifier;
116
+ const { promise: listenerPromise, cleanup: cleanupListener } = createPremiumListenerPromise(entitlementIdentifier);
117
+
66
118
  try {
67
119
  if (__DEV__) {
68
- console.log('[DEBUG PurchaseHandler] Calling Purchases.purchasePackage...', {
120
+ console.log('[DEBUG PurchaseHandler] Calling Purchases.purchasePackage with listener fallback...', {
69
121
  productId: pkg.product.identifier,
70
122
  packageIdentifier: pkg.identifier,
71
123
  offeringIdentifier: pkg.offeringIdentifier,
@@ -74,12 +126,28 @@ export async function handlePurchase(
74
126
  }
75
127
 
76
128
  const startTime = Date.now();
77
- const purchaseResult = await Purchases.purchasePackage(pkg);
129
+
130
+ // Race between purchasePackage and listener detection
131
+ // This handles the known RevenueCat bug where purchasePackage can hang
132
+ const purchasePromise = Purchases.purchasePackage(pkg).then(result => ({
133
+ source: 'purchasePackage' as const,
134
+ customerInfo: result.customerInfo
135
+ }));
136
+
137
+ const listenerWithSource = listenerPromise.then(info => ({
138
+ source: 'listener' as const,
139
+ customerInfo: info
140
+ }));
141
+
142
+ const result = await Promise.race([purchasePromise, listenerWithSource]);
143
+ cleanupListener();
144
+
78
145
  const duration = Date.now() - startTime;
79
- const customerInfo = purchaseResult.customerInfo;
146
+ const customerInfo = result.customerInfo;
80
147
 
81
148
  if (__DEV__) {
82
- console.log('[DEBUG PurchaseHandler] Purchases.purchasePackage returned', {
149
+ console.log('[DEBUG PurchaseHandler] Purchase resolved via:', {
150
+ source: result.source,
83
151
  duration: `${duration}ms`,
84
152
  productId: pkg.product.identifier
85
153
  });
@@ -118,7 +186,6 @@ export async function handlePurchase(
118
186
  };
119
187
  }
120
188
 
121
- const entitlementIdentifier = deps.config.entitlementIdentifier;
122
189
  const isPremium = !!customerInfo.entitlements.active[entitlementIdentifier];
123
190
 
124
191
  if (__DEV__) {
@@ -173,6 +240,9 @@ export async function handlePurchase(
173
240
  pkg.product.identifier
174
241
  );
175
242
  } catch (error) {
243
+ // Ensure listener cleanup on error
244
+ cleanupListener();
245
+
176
246
  if (__DEV__) {
177
247
  console.error('[DEBUG PurchaseHandler] Purchase error caught', {
178
248
  error,