@billdog.io/react-native 1.0.0-beta.2
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/BillDogReactNative.podspec +30 -0
- package/LICENSE +21 -0
- package/README.md +330 -0
- package/android/build.gradle +99 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogABTestModule.kt +317 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogAnalyticsModule.kt +389 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogModule.kt +817 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogPaywallViewManager.kt +383 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogSurveyModule.kt +427 -0
- package/android/src/main/kotlin/io/billdog/reactnative/BillDogVirtualCurrencyModule.kt +407 -0
- package/android/src/main/kotlin/io/billdog/virtualcurrency/BillDogVirtualCurrencyPackage.kt +44 -0
- package/dist/AdvancedPaywallCache.d.ts +70 -0
- package/dist/AdvancedPaywallCache.js +344 -0
- package/dist/BillDog.d.ts +255 -0
- package/dist/BillDog.js +918 -0
- package/dist/BillDogConfiguration.d.ts +145 -0
- package/dist/BillDogConfiguration.js +229 -0
- package/dist/ComponentRenderer.d.ts +17 -0
- package/dist/ComponentRenderer.js +886 -0
- package/dist/LayoutEngine.d.ts +101 -0
- package/dist/LayoutEngine.js +394 -0
- package/dist/LocalizationResolver.d.ts +99 -0
- package/dist/LocalizationResolver.js +266 -0
- package/dist/NativePaywallView.d.ts +36 -0
- package/dist/NativePaywallView.js +59 -0
- package/dist/PaywallCache.d.ts +74 -0
- package/dist/PaywallCache.js +293 -0
- package/dist/PaywallStateManager.d.ts +62 -0
- package/dist/PaywallStateManager.js +147 -0
- package/dist/PaywallView.d.ts +42 -0
- package/dist/PaywallView.js +275 -0
- package/dist/ProductVariableProvider.d.ts +60 -0
- package/dist/ProductVariableProvider.js +239 -0
- package/dist/VariableResolver.d.ts +120 -0
- package/dist/VariableResolver.js +379 -0
- package/dist/WebViewPaywall.d.ts +75 -0
- package/dist/WebViewPaywall.js +360 -0
- package/dist/animations/presets.d.ts +49 -0
- package/dist/animations/presets.js +367 -0
- package/dist/components/AccoladeCpsComponent.d.ts +13 -0
- package/dist/components/AccoladeCpsComponent.js +248 -0
- package/dist/components/AccordionComponent.d.ts +10 -0
- package/dist/components/AccordionComponent.js +187 -0
- package/dist/components/AnimatedWrapper.d.ts +35 -0
- package/dist/components/AnimatedWrapper.js +277 -0
- package/dist/components/CarouselComponent.d.ts +23 -0
- package/dist/components/CarouselComponent.js +484 -0
- package/dist/components/ChartCurveComponent.d.ts +13 -0
- package/dist/components/ChartCurveComponent.js +333 -0
- package/dist/components/CheckboxComponent.d.ts +9 -0
- package/dist/components/CheckboxComponent.js +172 -0
- package/dist/components/ConfettiComponent.d.ts +9 -0
- package/dist/components/ConfettiComponent.js +172 -0
- package/dist/components/CountdownComponent.d.ts +16 -0
- package/dist/components/CountdownComponent.js +414 -0
- package/dist/components/DelayedXComponent.d.ts +12 -0
- package/dist/components/DelayedXComponent.js +240 -0
- package/dist/components/DrawerModalComponent.d.ts +20 -0
- package/dist/components/DrawerModalComponent.js +286 -0
- package/dist/components/GradientView.d.ts +38 -0
- package/dist/components/GradientView.js +126 -0
- package/dist/components/HapticFeedbackComponent.d.ts +16 -0
- package/dist/components/HapticFeedbackComponent.js +64 -0
- package/dist/components/HorizontalProductsComponent.d.ts +17 -0
- package/dist/components/HorizontalProductsComponent.js +284 -0
- package/dist/components/MultiChoiceGridComponent.d.ts +13 -0
- package/dist/components/MultiChoiceGridComponent.js +234 -0
- package/dist/components/NPSScaleComponent.d.ts +16 -0
- package/dist/components/NPSScaleComponent.js +260 -0
- package/dist/components/NavigationComponent.d.ts +26 -0
- package/dist/components/NavigationComponent.js +302 -0
- package/dist/components/OAuthButtonComponent.d.ts +20 -0
- package/dist/components/OAuthButtonComponent.js +237 -0
- package/dist/components/ProgressLoaderComponent.d.ts +13 -0
- package/dist/components/ProgressLoaderComponent.js +308 -0
- package/dist/components/ProgressStepperComponent.d.ts +18 -0
- package/dist/components/ProgressStepperComponent.js +228 -0
- package/dist/components/SegmentedPickerComponent.d.ts +10 -0
- package/dist/components/SegmentedPickerComponent.js +221 -0
- package/dist/components/ShareButtonsComponent.d.ts +21 -0
- package/dist/components/ShareButtonsComponent.js +265 -0
- package/dist/components/ShareCodeComponent.d.ts +16 -0
- package/dist/components/ShareCodeComponent.js +274 -0
- package/dist/components/SingleChoiceGridComponent.d.ts +13 -0
- package/dist/components/SingleChoiceGridComponent.js +210 -0
- package/dist/components/SliderComponent.d.ts +9 -0
- package/dist/components/SliderComponent.js +263 -0
- package/dist/components/StarRatingInputComponent.d.ts +20 -0
- package/dist/components/StarRatingInputComponent.js +198 -0
- package/dist/components/StickerCompositeComponent.d.ts +13 -0
- package/dist/components/StickerCompositeComponent.js +136 -0
- package/dist/components/TabsComponent.d.ts +19 -0
- package/dist/components/TabsComponent.js +431 -0
- package/dist/components/TextInputComponent.d.ts +18 -0
- package/dist/components/TextInputComponent.js +227 -0
- package/dist/components/TimelineComponent.d.ts +19 -0
- package/dist/components/TimelineComponent.js +104 -0
- package/dist/components/ToggleComponent.d.ts +14 -0
- package/dist/components/ToggleComponent.js +132 -0
- package/dist/components/VerticalProductsComponent.d.ts +17 -0
- package/dist/components/VerticalProductsComponent.js +311 -0
- package/dist/dynamicValues.d.ts +36 -0
- package/dist/dynamicValues.js +142 -0
- package/dist/filters/dateFilters.d.ts +50 -0
- package/dist/filters/dateFilters.js +390 -0
- package/dist/filters/index.d.ts +18 -0
- package/dist/filters/index.js +253 -0
- package/dist/hooks/useOfferings.d.ts +76 -0
- package/dist/hooks/useOfferings.js +99 -0
- package/dist/hooks/usePaywall.d.ts +27 -0
- package/dist/hooks/usePaywall.js +114 -0
- package/dist/hooks/usePlacement.d.ts +34 -0
- package/dist/hooks/usePlacement.js +89 -0
- package/dist/hooks/useSubscription.d.ts +40 -0
- package/dist/hooks/useSubscription.js +97 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +198 -0
- package/dist/models/BillDogError.d.ts +40 -0
- package/dist/models/BillDogError.js +11 -0
- package/dist/models/CacheFetchPolicy.d.ts +8 -0
- package/dist/models/CacheFetchPolicy.js +4 -0
- package/dist/models/CustomProduct.d.ts +32 -0
- package/dist/models/CustomProduct.js +15 -0
- package/dist/models/CustomerInfo.d.ts +17 -0
- package/dist/models/CustomerInfo.js +11 -0
- package/dist/models/Delegate.d.ts +20 -0
- package/dist/models/Delegate.js +11 -0
- package/dist/models/EntitlementInfo.d.ts +19 -0
- package/dist/models/EntitlementInfo.js +2 -0
- package/dist/models/Placement.d.ts +54 -0
- package/dist/models/Placement.js +15 -0
- package/dist/models/PurchaseParams.d.ts +39 -0
- package/dist/models/PurchaseParams.js +60 -0
- package/dist/models/PurchaseResult.d.ts +30 -0
- package/dist/models/PurchaseResult.js +15 -0
- package/dist/models/RestoreResult.d.ts +30 -0
- package/dist/models/RestoreResult.js +22 -0
- package/dist/models/RestoredItem.d.ts +26 -0
- package/dist/models/RestoredItem.js +13 -0
- package/dist/modules/ABTest.d.ts +65 -0
- package/dist/modules/ABTest.js +113 -0
- package/dist/modules/Analytics.d.ts +91 -0
- package/dist/modules/Analytics.js +182 -0
- package/dist/modules/Survey.d.ts +65 -0
- package/dist/modules/Survey.js +82 -0
- package/dist/modules/VirtualCurrency.d.ts +63 -0
- package/dist/modules/VirtualCurrency.js +147 -0
- package/dist/murmur.d.ts +23 -0
- package/dist/murmur.js +104 -0
- package/dist/notifications/LiveActivitiesManager.d.ts +47 -0
- package/dist/notifications/LiveActivitiesManager.js +106 -0
- package/dist/notifications/NotificationScheduler.d.ts +55 -0
- package/dist/notifications/NotificationScheduler.js +199 -0
- package/dist/notifications/PurchaseNotificationHandler.d.ts +24 -0
- package/dist/notifications/PurchaseNotificationHandler.js +55 -0
- package/dist/notifications/useDeviceToken.d.ts +36 -0
- package/dist/notifications/useDeviceToken.js +97 -0
- package/dist/notifications/useLiveActivity.d.ts +39 -0
- package/dist/notifications/useLiveActivity.js +104 -0
- package/dist/review/ReviewAnalytics.d.ts +34 -0
- package/dist/review/ReviewAnalytics.js +73 -0
- package/dist/review/ReviewManager.d.ts +83 -0
- package/dist/review/ReviewManager.js +339 -0
- package/dist/review/ReviewTimingManager.d.ts +82 -0
- package/dist/review/ReviewTimingManager.js +224 -0
- package/dist/survey-core/types.d.ts +215 -0
- package/dist/survey-core/types.js +8 -0
- package/dist/types.d.ts +438 -0
- package/dist/types.js +158 -0
- package/dist/utils/ApiClient.d.ts +65 -0
- package/dist/utils/ApiClient.js +132 -0
- package/dist/utils/CSSConverter.d.ts +49 -0
- package/dist/utils/CSSConverter.js +470 -0
- package/dist/utils/animations.d.ts +41 -0
- package/dist/utils/animations.js +194 -0
- package/dist/utils/calendarIntegration.d.ts +36 -0
- package/dist/utils/calendarIntegration.js +89 -0
- package/dist/utils/configurationResolver.d.ts +23 -0
- package/dist/utils/configurationResolver.js +78 -0
- package/dist/utils/deviceDetection.d.ts +38 -0
- package/dist/utils/deviceDetection.js +59 -0
- package/dist/utils/hapticFeedback.d.ts +17 -0
- package/dist/utils/hapticFeedback.js +53 -0
- package/dist/utils/iconMapper.d.ts +26 -0
- package/dist/utils/iconMapper.js +113 -0
- package/dist/utils/notificationPermissions.d.ts +13 -0
- package/dist/utils/notificationPermissions.js +51 -0
- package/dist/utils/priceFormatting.d.ts +19 -0
- package/dist/utils/priceFormatting.js +99 -0
- package/dist/utils/shareSheet.d.ts +17 -0
- package/dist/utils/shareSheet.js +42 -0
- package/dist/utils/subscriptionUpgrade.d.ts +84 -0
- package/dist/utils/subscriptionUpgrade.js +100 -0
- package/dist/valueUtils.d.ts +48 -0
- package/dist/valueUtils.js +129 -0
- package/dist/xFlowTags.d.ts +18 -0
- package/dist/xFlowTags.js +385 -0
- package/ios/BillDogABTest.m +50 -0
- package/ios/BillDogABTest.swift +188 -0
- package/ios/BillDogAnalytics.m +105 -0
- package/ios/BillDogAnalytics.swift +324 -0
- package/ios/BillDogBridge.m +222 -0
- package/ios/BillDogBridge.swift +715 -0
- package/ios/BillDogLiveActivities.m +41 -0
- package/ios/BillDogLiveActivities.swift +104 -0
- package/ios/BillDogPaywallViewManager.m +39 -0
- package/ios/BillDogPaywallViewManager.swift +361 -0
- package/ios/BillDogSurvey.m +34 -0
- package/ios/BillDogSurvey.swift +257 -0
- package/ios/BillDogVirtualCurrencyBridge.m +63 -0
- package/ios/BillDogVirtualCurrencyBridge.swift +511 -0
- package/junit.xml +25 -0
- package/package.json +61 -0
- package/react-native.config.js +13 -0
- package/src/AdvancedPaywallCache.ts +433 -0
- package/src/BillDog.ts +983 -0
- package/src/BillDogConfiguration.ts +318 -0
- package/src/ComponentRenderer.tsx +1325 -0
- package/src/LayoutEngine.ts +459 -0
- package/src/LocalizationResolver.ts +311 -0
- package/src/NativePaywallView.tsx +132 -0
- package/src/PaywallCache.ts +358 -0
- package/src/PaywallStateManager.ts +154 -0
- package/src/PaywallView.tsx +328 -0
- package/src/ProductVariableProvider.ts +282 -0
- package/src/VariableResolver.ts +516 -0
- package/src/WebViewPaywall.tsx +454 -0
- package/src/animations/presets.ts +421 -0
- package/src/components/AccoladeCpsComponent.tsx +259 -0
- package/src/components/AccordionComponent.tsx +213 -0
- package/src/components/AnimatedWrapper.tsx +316 -0
- package/src/components/CarouselComponent.tsx +615 -0
- package/src/components/ChartCurveComponent.tsx +452 -0
- package/src/components/CheckboxComponent.tsx +185 -0
- package/src/components/ConfettiComponent.tsx +178 -0
- package/src/components/CountdownComponent.tsx +454 -0
- package/src/components/DelayedXComponent.tsx +266 -0
- package/src/components/DrawerModalComponent.tsx +324 -0
- package/src/components/GradientView.tsx +129 -0
- package/src/components/HapticFeedbackComponent.tsx +39 -0
- package/src/components/HorizontalProductsComponent.tsx +329 -0
- package/src/components/MultiChoiceGridComponent.tsx +265 -0
- package/src/components/NPSScaleComponent.tsx +294 -0
- package/src/components/NavigationComponent.tsx +329 -0
- package/src/components/OAuthButtonComponent.tsx +266 -0
- package/src/components/ProgressLoaderComponent.tsx +366 -0
- package/src/components/ProgressStepperComponent.tsx +250 -0
- package/src/components/SegmentedPickerComponent.tsx +245 -0
- package/src/components/ShareButtonsComponent.tsx +299 -0
- package/src/components/ShareCodeComponent.tsx +303 -0
- package/src/components/SingleChoiceGridComponent.tsx +229 -0
- package/src/components/SliderComponent.tsx +280 -0
- package/src/components/StarRatingInputComponent.tsx +223 -0
- package/src/components/StickerCompositeComponent.tsx +168 -0
- package/src/components/TabsComponent.tsx +496 -0
- package/src/components/TextInputComponent.tsx +258 -0
- package/src/components/TimelineComponent.tsx +129 -0
- package/src/components/ToggleComponent.tsx +142 -0
- package/src/components/VerticalProductsComponent.tsx +357 -0
- package/src/dynamicValues.ts +180 -0
- package/src/filters/dateFilters.ts +411 -0
- package/src/filters/index.ts +281 -0
- package/src/hooks/useOfferings.ts +192 -0
- package/src/hooks/usePaywall.ts +154 -0
- package/src/hooks/usePlacement.ts +143 -0
- package/src/hooks/useSubscription.ts +154 -0
- package/src/index.ts +245 -0
- package/src/models/BillDogError.ts +24 -0
- package/src/models/CacheFetchPolicy.ts +13 -0
- package/src/models/CustomProduct.ts +33 -0
- package/src/models/CustomerInfo.ts +26 -0
- package/src/models/Delegate.ts +22 -0
- package/src/models/EntitlementInfo.ts +27 -0
- package/src/models/Placement.ts +50 -0
- package/src/models/PurchaseParams.ts +81 -0
- package/src/models/PurchaseResult.ts +32 -0
- package/src/models/RestoreResult.ts +27 -0
- package/src/models/RestoredItem.ts +25 -0
- package/src/modules/ABTest.ts +131 -0
- package/src/modules/Analytics.ts +227 -0
- package/src/modules/Survey.ts +126 -0
- package/src/modules/VirtualCurrency.ts +223 -0
- package/src/murmur.ts +102 -0
- package/src/notifications/LiveActivitiesManager.ts +140 -0
- package/src/notifications/NotificationScheduler.ts +292 -0
- package/src/notifications/PurchaseNotificationHandler.ts +67 -0
- package/src/notifications/useDeviceToken.ts +116 -0
- package/src/notifications/useLiveActivity.ts +125 -0
- package/src/review/ReviewAnalytics.ts +89 -0
- package/src/review/ReviewManager.ts +355 -0
- package/src/review/ReviewTimingManager.ts +265 -0
- package/src/survey-core/types.ts +244 -0
- package/src/types/ambient.d.ts +9 -0
- package/src/types.ts +680 -0
- package/src/utils/ApiClient.ts +163 -0
- package/src/utils/CSSConverter.ts +552 -0
- package/src/utils/animations.ts +232 -0
- package/src/utils/calendarIntegration.ts +113 -0
- package/src/utils/configurationResolver.ts +106 -0
- package/src/utils/deviceDetection.ts +83 -0
- package/src/utils/hapticFeedback.ts +55 -0
- package/src/utils/iconMapper.ts +102 -0
- package/src/utils/notificationPermissions.ts +48 -0
- package/src/utils/priceFormatting.ts +119 -0
- package/src/utils/shareSheet.ts +55 -0
- package/src/utils/subscriptionUpgrade.ts +183 -0
- package/src/valueUtils.ts +108 -0
- package/src/xFlowTags.ts +463 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import PushNotification from 'react-native-push-notification';
|
|
3
|
+
import { Platform } from 'react-native';
|
|
4
|
+
import { ApiClient } from '../utils/ApiClient';
|
|
5
|
+
|
|
6
|
+
interface UseDeviceTokenOptions {
|
|
7
|
+
userId: string;
|
|
8
|
+
projectId: string;
|
|
9
|
+
/** @deprecated Use BillDogConfiguration.configure({ apiKey }) instead */
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
onTokenRegistered?: (token: string) => void;
|
|
12
|
+
onError?: (error: Error) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Hook to register device token for push notifications.
|
|
17
|
+
* Automatically registers the device token when the component mounts.
|
|
18
|
+
*
|
|
19
|
+
* Credentials are resolved from `BillDogConfiguration` by default.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* import { useDeviceToken } from '@billdog.io/react-native';
|
|
24
|
+
*
|
|
25
|
+
* function App() {
|
|
26
|
+
* useDeviceToken({
|
|
27
|
+
* userId: 'user-123',
|
|
28
|
+
* projectId: 'project-456',
|
|
29
|
+
* onTokenRegistered: (token) => console.log('Token registered:', token),
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* return <YourApp />;
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function useDeviceToken({
|
|
37
|
+
userId,
|
|
38
|
+
projectId,
|
|
39
|
+
apiKey: apiKeyOverride,
|
|
40
|
+
onTokenRegistered,
|
|
41
|
+
onError,
|
|
42
|
+
}: UseDeviceTokenOptions) {
|
|
43
|
+
const registeredRef = useRef(false);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (registeredRef.current) return;
|
|
47
|
+
|
|
48
|
+
const registerToken = async () => {
|
|
49
|
+
try {
|
|
50
|
+
PushNotification.configure({
|
|
51
|
+
onRegister: async (tokenData: any) => {
|
|
52
|
+
try {
|
|
53
|
+
const token = tokenData.token;
|
|
54
|
+
const platform = Platform.OS;
|
|
55
|
+
|
|
56
|
+
await ApiClient.post('register-device-token', {
|
|
57
|
+
userId,
|
|
58
|
+
projectId,
|
|
59
|
+
token,
|
|
60
|
+
platform,
|
|
61
|
+
}, {
|
|
62
|
+
apiKey: apiKeyOverride,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
registeredRef.current = true;
|
|
66
|
+
onTokenRegistered?.(token);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('Failed to register device token:', error);
|
|
69
|
+
onError?.(error as Error);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
onNotification: (notification: any) => {
|
|
73
|
+
console.log('Notification received:', notification);
|
|
74
|
+
},
|
|
75
|
+
permissions: {
|
|
76
|
+
alert: true,
|
|
77
|
+
badge: true,
|
|
78
|
+
sound: true,
|
|
79
|
+
},
|
|
80
|
+
popInitialNotification: true,
|
|
81
|
+
requestPermissions: true,
|
|
82
|
+
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('Failed to register device token:', error);
|
|
85
|
+
onError?.(error as Error);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
registerToken();
|
|
90
|
+
|
|
91
|
+
return () => {
|
|
92
|
+
PushNotification.unregister();
|
|
93
|
+
};
|
|
94
|
+
}, [userId, projectId, apiKeyOverride, onTokenRegistered, onError]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Manually register a device token.
|
|
99
|
+
* Resolves the endpoint from BillDogConfiguration (api.billdog.io/v1).
|
|
100
|
+
*/
|
|
101
|
+
export async function registerDeviceToken(
|
|
102
|
+
token: string,
|
|
103
|
+
userId: string,
|
|
104
|
+
projectId: string,
|
|
105
|
+
apiKey?: string,
|
|
106
|
+
platform?: 'ios' | 'android'
|
|
107
|
+
): Promise<void> {
|
|
108
|
+
await ApiClient.post('register-device-token', {
|
|
109
|
+
userId,
|
|
110
|
+
projectId,
|
|
111
|
+
token,
|
|
112
|
+
platform: platform || 'ios',
|
|
113
|
+
}, {
|
|
114
|
+
apiKey,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { liveActivities, LiveActivityConfig, LiveActivityUpdate } from './LiveActivitiesManager';
|
|
3
|
+
|
|
4
|
+
export interface UseLiveActivityOptions {
|
|
5
|
+
autoStart?: boolean;
|
|
6
|
+
config?: LiveActivityConfig;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface UseLiveActivityReturn {
|
|
10
|
+
activityId: string | null;
|
|
11
|
+
isActive: boolean;
|
|
12
|
+
isSupported: boolean;
|
|
13
|
+
areEnabled: boolean;
|
|
14
|
+
startActivity: (config: LiveActivityConfig) => Promise<string>;
|
|
15
|
+
updateActivity: (update: LiveActivityUpdate) => Promise<void>;
|
|
16
|
+
endActivity: (finalState?: Record<string, any>) => Promise<void>;
|
|
17
|
+
error: Error | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* React hook for managing a Live Activity
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* const { activityId, isActive, startActivity, updateActivity, endActivity } = useLiveActivity();
|
|
26
|
+
*
|
|
27
|
+
* // Start activity
|
|
28
|
+
* await startActivity({
|
|
29
|
+
* activityType: 'order_tracking',
|
|
30
|
+
* attributes: { orderNumber: '12345' },
|
|
31
|
+
* contentState: { status: 'preparing' }
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* // Update activity
|
|
35
|
+
* await updateActivity({
|
|
36
|
+
* contentState: { status: 'shipped' }
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* // End activity
|
|
40
|
+
* await endActivity({ status: 'delivered' });
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function useLiveActivity(options: UseLiveActivityOptions = {}): UseLiveActivityReturn {
|
|
44
|
+
const [activityId, setActivityId] = useState<string | null>(null);
|
|
45
|
+
const [isSupported, setIsSupported] = useState(false);
|
|
46
|
+
const [areEnabled, setAreEnabled] = useState(false);
|
|
47
|
+
const [error, setError] = useState<Error | null>(null);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const checkSupport = async () => {
|
|
51
|
+
const supported = liveActivities.isSupported();
|
|
52
|
+
setIsSupported(supported);
|
|
53
|
+
|
|
54
|
+
if (supported) {
|
|
55
|
+
const enabled = await liveActivities.areActivitiesEnabled();
|
|
56
|
+
setAreEnabled(enabled);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
checkSupport();
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (options.autoStart && options.config && isSupported && areEnabled && !activityId) {
|
|
65
|
+
startActivity(options.config).catch((err) => {
|
|
66
|
+
console.error('[useLiveActivity] Auto-start failed:', err);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}, [options.autoStart, options.config, isSupported, areEnabled, activityId]);
|
|
70
|
+
|
|
71
|
+
const startActivity = useCallback(async (config: LiveActivityConfig): Promise<string> => {
|
|
72
|
+
try {
|
|
73
|
+
setError(null);
|
|
74
|
+
const id = await liveActivities.startActivity(config);
|
|
75
|
+
setActivityId(id);
|
|
76
|
+
return id;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
const error = err instanceof Error ? err : new Error('Failed to start activity');
|
|
79
|
+
setError(error);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const updateActivity = useCallback(async (update: LiveActivityUpdate): Promise<void> => {
|
|
85
|
+
if (!activityId) {
|
|
86
|
+
throw new Error('No active activity to update');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
setError(null);
|
|
91
|
+
await liveActivities.updateActivity(activityId, update);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const error = err instanceof Error ? err : new Error('Failed to update activity');
|
|
94
|
+
setError(error);
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}, [activityId]);
|
|
98
|
+
|
|
99
|
+
const endActivity = useCallback(async (finalState?: Record<string, any>): Promise<void> => {
|
|
100
|
+
if (!activityId) {
|
|
101
|
+
throw new Error('No active activity to end');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
setError(null);
|
|
106
|
+
await liveActivities.endActivity(activityId, finalState);
|
|
107
|
+
setActivityId(null);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
const error = err instanceof Error ? err : new Error('Failed to end activity');
|
|
110
|
+
setError(error);
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}, [activityId]);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
activityId,
|
|
117
|
+
isActive: !!activityId,
|
|
118
|
+
isSupported,
|
|
119
|
+
areEnabled,
|
|
120
|
+
startActivity,
|
|
121
|
+
updateActivity,
|
|
122
|
+
endActivity,
|
|
123
|
+
error,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import { BillDogConfiguration } from '../BillDogConfiguration';
|
|
3
|
+
import { ApiClient } from '../utils/ApiClient';
|
|
4
|
+
|
|
5
|
+
export interface ReviewAnalyticsEvent {
|
|
6
|
+
eventType: 'requested' | 'shown' | 'dismissed' | 'completed' | 'error';
|
|
7
|
+
platform: 'ios' | 'android' | 'other';
|
|
8
|
+
customerId?: string;
|
|
9
|
+
sdkVersion?: string;
|
|
10
|
+
appVersion?: string;
|
|
11
|
+
osVersion?: string;
|
|
12
|
+
deviceModel?: string;
|
|
13
|
+
success?: boolean;
|
|
14
|
+
errorMessage?: string;
|
|
15
|
+
timeToShowMs?: number;
|
|
16
|
+
userContext?: Record<string, any>;
|
|
17
|
+
metadata?: Record<string, any>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class ReviewAnalytics {
|
|
21
|
+
/**
|
|
22
|
+
* @deprecated No longer needed. Credentials are read from BillDogConfiguration.
|
|
23
|
+
* This method is a no-op retained for backward compatibility.
|
|
24
|
+
*/
|
|
25
|
+
static initialize(_apiKey: string, _projectUrl: string) {
|
|
26
|
+
console.warn(
|
|
27
|
+
'[ReviewAnalytics] initialize() is deprecated. ' +
|
|
28
|
+
'Credentials are now read from BillDogConfiguration.configure(). This call is a no-op.'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Track a review event via the canonical `ingest-events` pipeline.
|
|
34
|
+
*
|
|
35
|
+
* Event naming follows `review_<eventType>` to preserve historical
|
|
36
|
+
* `bdanalytics_events.event_name` values written by the deprecated
|
|
37
|
+
* `track?type=review` router. Analytics surfaces (MessagingAnalytics,
|
|
38
|
+
* useReport, analytics-query) require no schema change.
|
|
39
|
+
*
|
|
40
|
+
* Per `mem://architecture/analytics-system-standard`: `ingest-events` is
|
|
41
|
+
* the sole analytics ingestion endpoint. Do not introduce alternative paths.
|
|
42
|
+
*/
|
|
43
|
+
static async trackEvent(event: ReviewAnalyticsEvent): Promise<void> {
|
|
44
|
+
if (BillDogConfiguration.config.analyticsEnabled === false) {
|
|
45
|
+
console.log('[ReviewAnalytics] Analytics disabled, skipping event:', event.eventType);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!BillDogConfiguration.isConfigured()) {
|
|
50
|
+
console.warn('[ReviewAnalytics] BillDogConfiguration not configured. Call BillDogConfiguration.configure() first.');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const platform = event.platform || this.getPlatform();
|
|
55
|
+
const properties: Record<string, unknown> = {
|
|
56
|
+
platform,
|
|
57
|
+
sdk_version: event.sdkVersion,
|
|
58
|
+
app_version: event.appVersion,
|
|
59
|
+
os_version: event.osVersion || Platform.Version.toString(),
|
|
60
|
+
device_model: event.deviceModel,
|
|
61
|
+
success: event.success,
|
|
62
|
+
error_message: event.errorMessage,
|
|
63
|
+
time_to_show_ms: event.timeToShowMs,
|
|
64
|
+
user_context: event.userContext,
|
|
65
|
+
...(event.metadata || {}),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
await ApiClient.post('ingest-events', {
|
|
70
|
+
events: [
|
|
71
|
+
{
|
|
72
|
+
event_name: `review_${event.eventType}`,
|
|
73
|
+
timestamp: new Date().toISOString(),
|
|
74
|
+
distinct_id: event.customerId,
|
|
75
|
+
properties,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('[ReviewAnalytics] Error tracking event:', error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private static getPlatform(): 'ios' | 'android' | 'other' {
|
|
85
|
+
if (Platform.OS === 'ios') return 'ios';
|
|
86
|
+
if (Platform.OS === 'android') return 'android';
|
|
87
|
+
return 'other';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { Platform, Linking } from 'react-native';
|
|
2
|
+
import { ReviewAnalytics } from './ReviewAnalytics';
|
|
3
|
+
import { ReviewTimingManager, ReviewTimingConfig } from './ReviewTimingManager';
|
|
4
|
+
import { BillDogConfiguration } from '../BillDogConfiguration';
|
|
5
|
+
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
7
|
+
const RN = (() => {
|
|
8
|
+
try {
|
|
9
|
+
return require('react-native');
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
})();
|
|
14
|
+
|
|
15
|
+
const native: any = RN?.NativeModules?.BillDog ?? null;
|
|
16
|
+
|
|
17
|
+
let timingManager: ReviewTimingManager | null = null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Optional native in-app-review module loader.
|
|
21
|
+
*
|
|
22
|
+
* `react-native` core does NOT export `StoreReview`/`InAppReview` — the previous
|
|
23
|
+
* implementation referenced non-existent exports, so the native prompt never ran
|
|
24
|
+
* and every request silently fell through to a (placeholder) store URL. The real
|
|
25
|
+
* native prompt requires `react-native-in-app-review` (cross-platform: iOS
|
|
26
|
+
* `SKStoreReviewController` + Android Play In-App Review), declared as an OPTIONAL
|
|
27
|
+
* peer dependency. Memoized; resolves `null` when the host app hasn't installed
|
|
28
|
+
* it (we then fall back to the configured store page).
|
|
29
|
+
*/
|
|
30
|
+
let inAppReviewModule: unknown | undefined = undefined;
|
|
31
|
+
async function loadInAppReview(): Promise<any | null> {
|
|
32
|
+
if (inAppReviewModule !== undefined) return inAppReviewModule as any;
|
|
33
|
+
try {
|
|
34
|
+
// STATIC require: React Native's Metro bundler rejects dynamic
|
|
35
|
+
// `require(variable)` ("Invalid call … require(s)"), so the optional native
|
|
36
|
+
// module must be referenced by a literal specifier. The try/catch keeps it
|
|
37
|
+
// soft at runtime; the @ts-ignore avoids TS2307 at SDK build time when the
|
|
38
|
+
// optional peer is absent. Consumers who want native in-app review install
|
|
39
|
+
// `react-native-in-app-review` (declared in peerDependenciesMeta as optional).
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
41
|
+
// @ts-ignore optional peer dependency, may be absent at build time
|
|
42
|
+
const mod: any = require('react-native-in-app-review');
|
|
43
|
+
inAppReviewModule = (mod?.default ?? mod) ?? null;
|
|
44
|
+
} catch {
|
|
45
|
+
inAppReviewModule = null;
|
|
46
|
+
}
|
|
47
|
+
return inAppReviewModule as any;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** True only when a store-page fallback is actually possible (IDs configured). */
|
|
51
|
+
function hasStoreFallback(): boolean {
|
|
52
|
+
const cfg = BillDogConfiguration.config as { appStoreId?: string; androidPackageName?: string };
|
|
53
|
+
return Platform.OS === 'ios' ? !!cfg.appStoreId : Platform.OS === 'android' ? !!cfg.androidPackageName : false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Initialize review timing manager with configuration
|
|
58
|
+
* Call this on app launch to enable smart timing
|
|
59
|
+
*
|
|
60
|
+
* @param config - Timing configuration options
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* import { initializeReviewTiming } from '@billdog.io/react-native';
|
|
65
|
+
*
|
|
66
|
+
* await initializeReviewTiming({
|
|
67
|
+
* minDaysSinceInstall: 7,
|
|
68
|
+
* minSessionCount: 5,
|
|
69
|
+
* minSignificantEvents: 3,
|
|
70
|
+
* enableAutomaticTiming: true,
|
|
71
|
+
* });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export async function initializeReviewTiming(config?: ReviewTimingConfig): Promise<void> {
|
|
75
|
+
if (native && typeof native.initializeReviewTiming === 'function') {
|
|
76
|
+
try {
|
|
77
|
+
await native.initializeReviewTiming(config);
|
|
78
|
+
return;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('Error invoking native initializeReviewTiming:', error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
timingManager = new ReviewTimingManager(config);
|
|
84
|
+
await timingManager.initialize();
|
|
85
|
+
await timingManager.recordSession();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Record a new app session (call on app launch)
|
|
90
|
+
*/
|
|
91
|
+
export async function recordReviewSession(): Promise<void> {
|
|
92
|
+
if (native && typeof native.recordReviewSession === 'function') {
|
|
93
|
+
try {
|
|
94
|
+
await native.recordReviewSession();
|
|
95
|
+
return;
|
|
96
|
+
} catch (e) {}
|
|
97
|
+
}
|
|
98
|
+
if (timingManager) {
|
|
99
|
+
await timingManager.recordSession();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Record a significant event (e.g., purchase, completed action)
|
|
105
|
+
* These events help determine optimal timing for review requests
|
|
106
|
+
*/
|
|
107
|
+
export async function recordSignificantEvent(): Promise<void> {
|
|
108
|
+
if (native && typeof native.recordSignificantEvent === 'function') {
|
|
109
|
+
try {
|
|
110
|
+
await native.recordSignificantEvent();
|
|
111
|
+
return;
|
|
112
|
+
} catch (e) {}
|
|
113
|
+
}
|
|
114
|
+
if (timingManager) {
|
|
115
|
+
await timingManager.recordSignificantEvent();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if it's a good time to request a review based on timing logic
|
|
121
|
+
*
|
|
122
|
+
* @returns Object with shouldRequest flag, reason, and timing data
|
|
123
|
+
*/
|
|
124
|
+
export async function shouldRequestReview(): Promise<{
|
|
125
|
+
shouldRequest: boolean;
|
|
126
|
+
reason?: string;
|
|
127
|
+
timingData?: any;
|
|
128
|
+
}> {
|
|
129
|
+
if (native && typeof native.shouldRequestReview === 'function') {
|
|
130
|
+
try {
|
|
131
|
+
const should = await native.shouldRequestReview();
|
|
132
|
+
return { shouldRequest: should, reason: should ? 'All timing conditions met' : 'Timing conditions not met' };
|
|
133
|
+
} catch (e) {}
|
|
134
|
+
}
|
|
135
|
+
if (!timingManager) {
|
|
136
|
+
return { shouldRequest: true, reason: 'Timing manager not initialized' };
|
|
137
|
+
}
|
|
138
|
+
return await timingManager.shouldRequestReview();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Request in-app review from the user
|
|
143
|
+
* Optionally checks timing logic before showing
|
|
144
|
+
*
|
|
145
|
+
* @param options - Options for review request
|
|
146
|
+
* @param options.checkTiming - Whether to check timing logic before showing (default: true)
|
|
147
|
+
* @returns Promise with success status and whether review was shown
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* import { requestReview } from '@billdog.io/react-native';
|
|
152
|
+
*
|
|
153
|
+
* const { success, shown } = await requestReview();
|
|
154
|
+
* if (shown) {
|
|
155
|
+
* console.log('Review prompt shown to user');
|
|
156
|
+
* }
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
export async function requestReview(options: { checkTiming?: boolean } = {}): Promise<{
|
|
160
|
+
success: boolean;
|
|
161
|
+
shown: boolean;
|
|
162
|
+
reason?: string;
|
|
163
|
+
}> {
|
|
164
|
+
const { checkTiming = true } = options;
|
|
165
|
+
const startTime = Date.now();
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
if (native && typeof native.requestReview === 'function') {
|
|
169
|
+
if (checkTiming) {
|
|
170
|
+
const timingCheck = await shouldRequestReview();
|
|
171
|
+
if (!timingCheck.shouldRequest) {
|
|
172
|
+
console.log('Review request skipped:', timingCheck.reason);
|
|
173
|
+
return {
|
|
174
|
+
success: false,
|
|
175
|
+
shown: false,
|
|
176
|
+
reason: timingCheck.reason
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const result = await native.requestReview();
|
|
182
|
+
await ReviewAnalytics.trackEvent({
|
|
183
|
+
eventType: 'shown',
|
|
184
|
+
platform: Platform.OS === 'ios' ? 'ios' : Platform.OS === 'android' ? 'android' : 'other',
|
|
185
|
+
success: !!result?.success,
|
|
186
|
+
timeToShowMs: Date.now() - startTime,
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
success: !!result?.success,
|
|
190
|
+
shown: !!result?.shown,
|
|
191
|
+
reason: result?.reason
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check timing logic if enabled
|
|
196
|
+
if (checkTiming && timingManager) {
|
|
197
|
+
const timingCheck = await timingManager.shouldRequestReview();
|
|
198
|
+
if (!timingCheck.shouldRequest) {
|
|
199
|
+
console.log('Review request skipped:', timingCheck.reason);
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
shown: false,
|
|
203
|
+
reason: timingCheck.reason
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Record that we're requesting a review
|
|
209
|
+
if (timingManager) {
|
|
210
|
+
await timingManager.recordReviewRequested();
|
|
211
|
+
}
|
|
212
|
+
// Track request event
|
|
213
|
+
await ReviewAnalytics.trackEvent({
|
|
214
|
+
eventType: 'requested',
|
|
215
|
+
platform: Platform.OS === 'ios' ? 'ios' : Platform.OS === 'android' ? 'android' : 'other',
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Native in-app review — iOS SKStoreReviewController / Android Play In-App
|
|
219
|
+
// Review — via the optional `react-native-in-app-review` module (one call
|
|
220
|
+
// handles both platforms). Audit fix: the old code imported StoreReview/
|
|
221
|
+
// InAppReview from `react-native` (no such exports) so this branch never ran.
|
|
222
|
+
const InAppReview = await loadInAppReview();
|
|
223
|
+
if (
|
|
224
|
+
InAppReview &&
|
|
225
|
+
typeof InAppReview.isAvailable === 'function' &&
|
|
226
|
+
InAppReview.isAvailable() &&
|
|
227
|
+
typeof InAppReview.RequestInAppReview === 'function'
|
|
228
|
+
) {
|
|
229
|
+
// Resolves true when the flow launched/finished. Note: neither platform
|
|
230
|
+
// reports whether the dialog displayed or whether the user submitted a
|
|
231
|
+
// review, so `shown` is best-effort. We do NOT auto-record "completed"
|
|
232
|
+
// (that permanently blocked future prompts and made maxRequestsPerUser
|
|
233
|
+
// unreachable); recordReviewRequested() above is the correct cap.
|
|
234
|
+
const flowFinished = await InAppReview.RequestInAppReview();
|
|
235
|
+
await ReviewAnalytics.trackEvent({
|
|
236
|
+
eventType: 'shown',
|
|
237
|
+
platform: Platform.OS === 'ios' ? 'ios' : Platform.OS === 'android' ? 'android' : 'other',
|
|
238
|
+
success: !!flowFinished,
|
|
239
|
+
timeToShowMs: Date.now() - startTime,
|
|
240
|
+
});
|
|
241
|
+
return { success: !!flowFinished, shown: !!flowFinished };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Fallback: open store page (only when store IDs are configured)
|
|
245
|
+
const result = await openStoreForReview();
|
|
246
|
+
|
|
247
|
+
if (result.success) {
|
|
248
|
+
await ReviewAnalytics.trackEvent({
|
|
249
|
+
eventType: 'shown',
|
|
250
|
+
platform: Platform.OS === 'ios' ? 'ios' : Platform.OS === 'android' ? 'android' : 'other',
|
|
251
|
+
success: true,
|
|
252
|
+
timeToShowMs: Date.now() - startTime,
|
|
253
|
+
});
|
|
254
|
+
// Audit fix — removed the auto recordReviewCompleted() call (see above): it
|
|
255
|
+
// permanently blocked future prompts and made maxRequestsPerUser unreachable.
|
|
256
|
+
// recordReviewRequested() already counted this attempt toward the cap.
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return result;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.error('Error requesting review:', error);
|
|
262
|
+
|
|
263
|
+
// Track error event
|
|
264
|
+
await ReviewAnalytics.trackEvent({
|
|
265
|
+
eventType: 'error',
|
|
266
|
+
platform: Platform.OS === 'ios' ? 'ios' : Platform.OS === 'android' ? 'android' : 'other',
|
|
267
|
+
success: false,
|
|
268
|
+
errorMessage: error instanceof Error ? error.message : 'Unknown error',
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return { success: false, shown: false };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Check if review request is available
|
|
277
|
+
* Respects platform rate limiting
|
|
278
|
+
*
|
|
279
|
+
* @returns Promise with availability status
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* ```typescript
|
|
283
|
+
* import { canRequestReview } from '@billdog.io/react-native';
|
|
284
|
+
*
|
|
285
|
+
* const { available } = await canRequestReview();
|
|
286
|
+
* if (available) {
|
|
287
|
+
* await requestReview();
|
|
288
|
+
* }
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
export async function canRequestReview(): Promise<{ available: boolean }> {
|
|
292
|
+
if (native && typeof native.canRequestReview === 'function') {
|
|
293
|
+
try {
|
|
294
|
+
const available = await native.canRequestReview();
|
|
295
|
+
return { available: !!available };
|
|
296
|
+
} catch (e) {}
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const InAppReview = await loadInAppReview();
|
|
300
|
+
if (InAppReview && typeof InAppReview.isAvailable === 'function') {
|
|
301
|
+
return { available: !!InAppReview.isAvailable() };
|
|
302
|
+
}
|
|
303
|
+
// No native module installed — only the store-page fallback is possible,
|
|
304
|
+
// and only if store IDs are configured. (Previously this returned `true`
|
|
305
|
+
// unconditionally even though the fallback URL was a placeholder.)
|
|
306
|
+
return { available: hasStoreFallback() };
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error('Error checking review availability:', error);
|
|
309
|
+
return { available: false };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Open the app store for manual review
|
|
315
|
+
* Used as fallback when native review is unavailable
|
|
316
|
+
*/
|
|
317
|
+
async function openStoreForReview(): Promise<{ success: boolean; shown: boolean }> {
|
|
318
|
+
try {
|
|
319
|
+
// Audit fix — source real store IDs from BillDogConfiguration instead of the
|
|
320
|
+
// old `YOUR_APP_ID` / `YOUR_PACKAGE_NAME` placeholders (which produced a
|
|
321
|
+
// broken deep-link). If neither a native module nor a configured ID exists,
|
|
322
|
+
// report unavailable rather than opening a dead URL.
|
|
323
|
+
const cfg = BillDogConfiguration.config as { appStoreId?: string; androidPackageName?: string };
|
|
324
|
+
let storeUrl: string | null = null;
|
|
325
|
+
|
|
326
|
+
if (Platform.OS === 'ios') {
|
|
327
|
+
if (cfg.appStoreId) {
|
|
328
|
+
storeUrl = `itms-apps://itunes.apple.com/app/id${cfg.appStoreId}?action=write-review`;
|
|
329
|
+
}
|
|
330
|
+
} else if (Platform.OS === 'android') {
|
|
331
|
+
if (cfg.androidPackageName) {
|
|
332
|
+
storeUrl = `market://details?id=${cfg.androidPackageName}`;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!storeUrl) {
|
|
337
|
+
console.warn(
|
|
338
|
+
'[BillDog] Cannot request a review: install `react-native-in-app-review` for the native prompt, ' +
|
|
339
|
+
'or set `appStoreId` (iOS) / `androidPackageName` (Android) in BillDogConfiguration.configure() for the store-page fallback.'
|
|
340
|
+
);
|
|
341
|
+
return { success: false, shown: false };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const canOpen = await Linking.canOpenURL(storeUrl);
|
|
345
|
+
if (canOpen) {
|
|
346
|
+
await Linking.openURL(storeUrl);
|
|
347
|
+
return { success: true, shown: true };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return { success: false, shown: false };
|
|
351
|
+
} catch (error) {
|
|
352
|
+
console.error('Error opening store:', error);
|
|
353
|
+
return { success: false, shown: false };
|
|
354
|
+
}
|
|
355
|
+
}
|