@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
package/src/murmur.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MurmurHash3 (x86_32) — the ONE canonical deterministic bucketing hash.
|
|
3
|
+
*
|
|
4
|
+
* React Native port of the backend canonical `murmurhash3`
|
|
5
|
+
* (`supabase/functions/_shared/edge-serving/pure.ts`) and the dashboard copy
|
|
6
|
+
* (`src/lib/murmur.ts`). Used to derive the render-time `user.seed` Dynamic
|
|
7
|
+
* Values variable so a user buckets IDENTICALLY on every surface.
|
|
8
|
+
*
|
|
9
|
+
* RN-SPECIFIC: uses a manual UTF-8 encoder instead of `TextEncoder`, because
|
|
10
|
+
* Hermes historically ships without `TextEncoder`. The bytes it produces are
|
|
11
|
+
* identical to `TextEncoder().encode(...)` for the fixture inputs (incl.
|
|
12
|
+
* surrogate-pair emoji), so it stays pinned to the same corpus.
|
|
13
|
+
*
|
|
14
|
+
* CONTRACT (do not change without regenerating the fixture on ALL ports):
|
|
15
|
+
* user.seed = murmurhash3(appUserId) % 1000 → range 0..999
|
|
16
|
+
*
|
|
17
|
+
* Conformance-pinned to `native-sdks/_test-fixtures/conformance/core_user_seed.json`.
|
|
18
|
+
* Never validated against another SDK port.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** UTF-8 encode without depending on TextEncoder (Hermes-safe). */
|
|
22
|
+
function utf8Bytes(str: string): number[] {
|
|
23
|
+
const bytes: number[] = [];
|
|
24
|
+
for (let i = 0; i < str.length; i++) {
|
|
25
|
+
let code = str.charCodeAt(i);
|
|
26
|
+
if (code < 0x80) {
|
|
27
|
+
bytes.push(code);
|
|
28
|
+
} else if (code < 0x800) {
|
|
29
|
+
bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
|
|
30
|
+
} else if (code >= 0xd800 && code <= 0xdbff) {
|
|
31
|
+
// High surrogate — must be followed by a low surrogate.
|
|
32
|
+
const lo = str.charCodeAt(i + 1);
|
|
33
|
+
if (lo >= 0xdc00 && lo <= 0xdfff) {
|
|
34
|
+
i++;
|
|
35
|
+
code = 0x10000 + ((code - 0xd800) << 10) + (lo - 0xdc00);
|
|
36
|
+
bytes.push(
|
|
37
|
+
0xf0 | (code >> 18),
|
|
38
|
+
0x80 | ((code >> 12) & 0x3f),
|
|
39
|
+
0x80 | ((code >> 6) & 0x3f),
|
|
40
|
+
0x80 | (code & 0x3f),
|
|
41
|
+
);
|
|
42
|
+
} else {
|
|
43
|
+
// Lone high surrogate → U+FFFD replacement char, matching TextEncoder.
|
|
44
|
+
bytes.push(0xef, 0xbf, 0xbd);
|
|
45
|
+
}
|
|
46
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
47
|
+
// Lone low surrogate → U+FFFD, matching TextEncoder.
|
|
48
|
+
bytes.push(0xef, 0xbf, 0xbd);
|
|
49
|
+
} else {
|
|
50
|
+
bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return bytes;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function murmurhash3(key: string, seed = 0): number {
|
|
57
|
+
const keyBytes = utf8Bytes(key);
|
|
58
|
+
let h1 = seed;
|
|
59
|
+
const c1 = 0xcc9e2d51;
|
|
60
|
+
const c2 = 0x1b873593;
|
|
61
|
+
const blocks = Math.floor(keyBytes.length / 4);
|
|
62
|
+
for (let i = 0; i < blocks; i++) {
|
|
63
|
+
let k1 = keyBytes[i * 4] | (keyBytes[i * 4 + 1] << 8) | (keyBytes[i * 4 + 2] << 16) | (keyBytes[i * 4 + 3] << 24);
|
|
64
|
+
k1 = Math.imul(k1, c1);
|
|
65
|
+
k1 = (k1 << 15) | (k1 >>> 17);
|
|
66
|
+
k1 = Math.imul(k1, c2);
|
|
67
|
+
h1 ^= k1;
|
|
68
|
+
h1 = (h1 << 13) | (h1 >>> 19);
|
|
69
|
+
h1 = Math.imul(h1, 5) + 0xe6546b64;
|
|
70
|
+
}
|
|
71
|
+
let k1 = 0;
|
|
72
|
+
const offset = blocks * 4;
|
|
73
|
+
switch (keyBytes.length % 4) {
|
|
74
|
+
case 3:
|
|
75
|
+
k1 ^= keyBytes[offset + 2] << 16;
|
|
76
|
+
/* falls through */
|
|
77
|
+
case 2:
|
|
78
|
+
k1 ^= keyBytes[offset + 1] << 8;
|
|
79
|
+
/* falls through */
|
|
80
|
+
case 1:
|
|
81
|
+
k1 ^= keyBytes[offset];
|
|
82
|
+
k1 = Math.imul(k1, c1);
|
|
83
|
+
k1 = (k1 << 15) | (k1 >>> 17);
|
|
84
|
+
k1 = Math.imul(k1, c2);
|
|
85
|
+
h1 ^= k1;
|
|
86
|
+
}
|
|
87
|
+
h1 ^= keyBytes.length;
|
|
88
|
+
h1 ^= h1 >>> 16;
|
|
89
|
+
h1 = Math.imul(h1, 0x85ebca6b);
|
|
90
|
+
h1 ^= h1 >>> 13;
|
|
91
|
+
h1 = Math.imul(h1, 0xc2b2ae35);
|
|
92
|
+
h1 ^= h1 >>> 16;
|
|
93
|
+
return h1 >>> 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const USER_SEED_MODULUS = 1000;
|
|
97
|
+
|
|
98
|
+
/** Canonical `user.seed` derivation: 0..999 bucket from a stable user id. */
|
|
99
|
+
export function userSeedBucket(appUserId: string | null | undefined): number {
|
|
100
|
+
if (!appUserId) return 0;
|
|
101
|
+
return murmurhash3(appUserId) % USER_SEED_MODULUS;
|
|
102
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { NativeModules, Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
const { BillDogLiveActivities } = NativeModules;
|
|
4
|
+
|
|
5
|
+
export interface LiveActivityConfig {
|
|
6
|
+
activityType: string;
|
|
7
|
+
attributes: Record<string, any>;
|
|
8
|
+
contentState: Record<string, any>;
|
|
9
|
+
staleDate?: Date;
|
|
10
|
+
dismissalDate?: Date;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface LiveActivityUpdate {
|
|
14
|
+
contentState: Record<string, any>;
|
|
15
|
+
staleDate?: Date;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Manager for iOS Live Activities
|
|
20
|
+
* Only available on iOS 16.1+
|
|
21
|
+
*/
|
|
22
|
+
export class LiveActivitiesManager {
|
|
23
|
+
private static instance: LiveActivitiesManager;
|
|
24
|
+
|
|
25
|
+
private constructor() {}
|
|
26
|
+
|
|
27
|
+
static getInstance(): LiveActivitiesManager {
|
|
28
|
+
if (!LiveActivitiesManager.instance) {
|
|
29
|
+
LiveActivitiesManager.instance = new LiveActivitiesManager();
|
|
30
|
+
}
|
|
31
|
+
return LiveActivitiesManager.instance;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if Live Activities are supported
|
|
36
|
+
*/
|
|
37
|
+
isSupported(): boolean {
|
|
38
|
+
return Platform.OS === 'ios' && Platform.Version >= '16.2' && !!BillDogLiveActivities;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if Live Activities are enabled by the user
|
|
43
|
+
*/
|
|
44
|
+
async areActivitiesEnabled(): Promise<boolean> {
|
|
45
|
+
if (!this.isSupported()) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
return await BillDogLiveActivities.areActivitiesEnabled();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('[LiveActivitiesManager] Error checking enabled status:', error);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Start a new Live Activity
|
|
59
|
+
* Returns the activity ID
|
|
60
|
+
*/
|
|
61
|
+
async startActivity(config: LiveActivityConfig): Promise<string> {
|
|
62
|
+
if (!this.isSupported()) {
|
|
63
|
+
throw new Error('Live Activities are not supported on this platform');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const activityId = await BillDogLiveActivities.startActivity(
|
|
68
|
+
config.activityType,
|
|
69
|
+
config.attributes,
|
|
70
|
+
config.contentState,
|
|
71
|
+
config.staleDate?.toISOString(),
|
|
72
|
+
config.dismissalDate?.toISOString()
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return activityId;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('[LiveActivitiesManager] Error starting activity:', error);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Update an existing Live Activity
|
|
84
|
+
*/
|
|
85
|
+
async updateActivity(activityId: string, update: LiveActivityUpdate): Promise<void> {
|
|
86
|
+
if (!this.isSupported()) {
|
|
87
|
+
throw new Error('Live Activities are not supported on this platform');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await BillDogLiveActivities.updateActivity(
|
|
92
|
+
activityId,
|
|
93
|
+
update.contentState,
|
|
94
|
+
update.staleDate?.toISOString()
|
|
95
|
+
);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('[LiveActivitiesManager] Error updating activity:', error);
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* End a Live Activity
|
|
104
|
+
*/
|
|
105
|
+
async endActivity(
|
|
106
|
+
activityId: string,
|
|
107
|
+
finalState?: Record<string, any>,
|
|
108
|
+
dismissalPolicy: 'default' | 'immediate' | 'after' = 'default'
|
|
109
|
+
): Promise<void> {
|
|
110
|
+
if (!this.isSupported()) {
|
|
111
|
+
throw new Error('Live Activities are not supported on this platform');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await BillDogLiveActivities.endActivity(activityId, finalState, dismissalPolicy);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('[LiveActivitiesManager] Error ending activity:', error);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get all active Live Activities
|
|
124
|
+
* Returns array of activity IDs
|
|
125
|
+
*/
|
|
126
|
+
async getActiveActivities(): Promise<string[]> {
|
|
127
|
+
if (!this.isSupported()) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
return await BillDogLiveActivities.getActiveActivities();
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error('[LiveActivitiesManager] Error getting active activities:', error);
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const liveActivities = LiveActivitiesManager.getInstance();
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { Platform, PermissionsAndroid } from 'react-native';
|
|
2
|
+
import PushNotification from 'react-native-push-notification';
|
|
3
|
+
import PushNotificationIOS from '@react-native-community/push-notification-ios';
|
|
4
|
+
import { ApiClient } from '../utils/ApiClient';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handles scheduling and managing trial reminder notifications for React Native.
|
|
8
|
+
*
|
|
9
|
+
* All API calls route through `ApiClient` → `BillDogConfiguration.apiBaseUrl`
|
|
10
|
+
* (the single branded host, `https://api.billdog.io/v1`).
|
|
11
|
+
*/
|
|
12
|
+
export class NotificationScheduler {
|
|
13
|
+
private static instance: NotificationScheduler;
|
|
14
|
+
|
|
15
|
+
private constructor() {
|
|
16
|
+
this.configure();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public static getInstance(): NotificationScheduler {
|
|
20
|
+
if (!NotificationScheduler.instance) {
|
|
21
|
+
NotificationScheduler.instance = new NotificationScheduler();
|
|
22
|
+
}
|
|
23
|
+
return NotificationScheduler.instance;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private configure() {
|
|
27
|
+
PushNotification.configure({
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- callback shape comes from the optional, untyped react-native-push-notification peer dep
|
|
29
|
+
onNotification: (notification: any) => {
|
|
30
|
+
console.log('Notification received:', notification);
|
|
31
|
+
if (Platform.OS === 'ios') {
|
|
32
|
+
notification.finish(PushNotificationIOS.FetchResult.NoData);
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
permissions: {
|
|
36
|
+
alert: true,
|
|
37
|
+
badge: true,
|
|
38
|
+
sound: true,
|
|
39
|
+
},
|
|
40
|
+
popInitialNotification: true,
|
|
41
|
+
requestPermissions: false,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (Platform.OS === 'android') {
|
|
45
|
+
PushNotification.createChannel(
|
|
46
|
+
{
|
|
47
|
+
channelId: 'trial-reminders',
|
|
48
|
+
channelName: 'Trial Reminders',
|
|
49
|
+
channelDescription: 'Notifications about your trial subscription',
|
|
50
|
+
importance: 4,
|
|
51
|
+
vibrate: true,
|
|
52
|
+
},
|
|
53
|
+
(created: boolean) => console.log(`Channel created: ${created}`)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public async requestPermissions(): Promise<boolean> {
|
|
59
|
+
if (Platform.OS === 'ios') {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
PushNotification.requestPermissions((permissions: any) => {
|
|
62
|
+
resolve(permissions.alert === true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
} else if (Platform.OS === 'android') {
|
|
66
|
+
if (Platform.Version >= 33) {
|
|
67
|
+
const granted = await PermissionsAndroid.request(
|
|
68
|
+
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS
|
|
69
|
+
);
|
|
70
|
+
return granted === PermissionsAndroid.RESULTS.GRANTED;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public async checkPermissions(): Promise<boolean> {
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
PushNotification.checkPermissions((permissions: any) => {
|
|
80
|
+
resolve(permissions.alert === true || permissions.alert === 1);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Schedule a trial reminder notification.
|
|
87
|
+
*
|
|
88
|
+
* @param apiKey - @deprecated Resolved from BillDogConfiguration if omitted
|
|
89
|
+
*/
|
|
90
|
+
public async scheduleTrialNotification(
|
|
91
|
+
subscription: Subscription,
|
|
92
|
+
config: NotificationConfig,
|
|
93
|
+
apiKey?: string
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
if (!subscription.isTrial) {
|
|
96
|
+
console.warn('⚠️ Not a trial subscription, skipping notification');
|
|
97
|
+
throw new Error('Not a trial subscription');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!subscription.expiresAt || new Date(subscription.expiresAt) < new Date()) {
|
|
101
|
+
console.warn('⚠️ Trial already expired, skipping notification');
|
|
102
|
+
throw new Error('Trial already expired');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!subscription.purchasedAt) {
|
|
106
|
+
console.warn('⚠️ Missing trial start date, skipping notification');
|
|
107
|
+
throw new Error('Missing trial start date');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const purchaseDate = new Date(subscription.purchasedAt);
|
|
111
|
+
const notificationDate = new Date(purchaseDate);
|
|
112
|
+
notificationDate.setDate(notificationDate.getDate() + config.dayOfTrial);
|
|
113
|
+
|
|
114
|
+
if (notificationDate < new Date()) {
|
|
115
|
+
console.warn(
|
|
116
|
+
`⚠️ Notification date is in the past (Day ${config.dayOfTrial} was ${notificationDate}), skipping`
|
|
117
|
+
);
|
|
118
|
+
throw new Error('Notification date in the past');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (notificationDate > new Date(subscription.expiresAt)) {
|
|
122
|
+
console.warn('⚠️ Notification date is after trial expiration, skipping');
|
|
123
|
+
throw new Error('Notification date after expiration');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (subscription.notificationScheduledAt) {
|
|
127
|
+
console.warn(
|
|
128
|
+
`⚠️ Notification already scheduled at ${subscription.notificationScheduledAt}`
|
|
129
|
+
);
|
|
130
|
+
throw new Error('Notification already scheduled');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const hasPermission = await this.checkPermissions();
|
|
134
|
+
if (!hasPermission) {
|
|
135
|
+
if (config.requestPermissions) {
|
|
136
|
+
const granted = await this.requestPermissions();
|
|
137
|
+
if (!granted) {
|
|
138
|
+
throw new Error('Notification permission denied');
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
throw new Error('Notification permission not granted');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
PushNotification.localNotificationSchedule({
|
|
146
|
+
id: `trial_reminder_${subscription.id}`,
|
|
147
|
+
channelId: 'trial-reminders',
|
|
148
|
+
title: config.title,
|
|
149
|
+
message: config.body,
|
|
150
|
+
date: notificationDate,
|
|
151
|
+
allowWhileIdle: true,
|
|
152
|
+
userInfo: {
|
|
153
|
+
subscription_id: subscription.id,
|
|
154
|
+
notification_type: 'trial_reminder',
|
|
155
|
+
day_of_trial: config.dayOfTrial,
|
|
156
|
+
},
|
|
157
|
+
...(config.subtitle && Platform.OS === 'ios' ? { subtitle: config.subtitle } : {}),
|
|
158
|
+
...(config.sound ? { soundName: config.sound } : {}),
|
|
159
|
+
...(config.badge ? { number: config.badge } : {}),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Update subscription via ApiClient
|
|
163
|
+
await this.updateSubscriptionNotificationStatus(
|
|
164
|
+
subscription.id,
|
|
165
|
+
{ scheduledAt: new Date() },
|
|
166
|
+
apiKey
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
console.log(
|
|
170
|
+
`✅ Scheduled trial reminder notification for Day ${config.dayOfTrial} at ${notificationDate}`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public async cancelNotification(
|
|
175
|
+
subscriptionId: string,
|
|
176
|
+
apiKey?: string
|
|
177
|
+
): Promise<void> {
|
|
178
|
+
PushNotification.cancelLocalNotification(`trial_reminder_${subscriptionId}`);
|
|
179
|
+
|
|
180
|
+
await this.updateSubscriptionNotificationStatus(
|
|
181
|
+
subscriptionId,
|
|
182
|
+
{ cancelledAt: new Date() },
|
|
183
|
+
apiKey
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
console.log('✅ Cancelled trial reminder notification');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
public async getScheduledNotifications(): Promise<any[]> {
|
|
190
|
+
return new Promise((resolve) => {
|
|
191
|
+
PushNotification.getScheduledLocalNotifications((notifications: any[]) => {
|
|
192
|
+
resolve(notifications);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Update subscription notification status via ApiClient.
|
|
199
|
+
*/
|
|
200
|
+
private async updateSubscriptionNotificationStatus(
|
|
201
|
+
subscriptionId: string,
|
|
202
|
+
updates: {
|
|
203
|
+
scheduledAt?: Date;
|
|
204
|
+
sentAt?: Date;
|
|
205
|
+
cancelledAt?: Date;
|
|
206
|
+
},
|
|
207
|
+
apiKey?: string
|
|
208
|
+
): Promise<void> {
|
|
209
|
+
const body: Record<string, string> = {};
|
|
210
|
+
|
|
211
|
+
if (updates.scheduledAt) {
|
|
212
|
+
body.notification_scheduled_at = updates.scheduledAt.toISOString();
|
|
213
|
+
}
|
|
214
|
+
if (updates.sentAt) {
|
|
215
|
+
body.notification_sent_at = updates.sentAt.toISOString();
|
|
216
|
+
}
|
|
217
|
+
if (updates.cancelledAt) {
|
|
218
|
+
body.notification_cancelled_at = updates.cancelledAt.toISOString();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (Object.keys(body).length === 0) return;
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
await ApiClient.post('update-subscription-notification', {
|
|
225
|
+
subscription_id: subscriptionId,
|
|
226
|
+
...body,
|
|
227
|
+
}, {
|
|
228
|
+
apiKey,
|
|
229
|
+
});
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.warn('⚠️ Failed to update subscription status:', error);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Type definitions
|
|
238
|
+
*/
|
|
239
|
+
export interface NotificationConfig {
|
|
240
|
+
enabled: boolean;
|
|
241
|
+
dayOfTrial: number;
|
|
242
|
+
title: string;
|
|
243
|
+
subtitle?: string;
|
|
244
|
+
body: string;
|
|
245
|
+
requestPermissions: boolean;
|
|
246
|
+
removeNotificationEnabled?: boolean;
|
|
247
|
+
sound?: string;
|
|
248
|
+
badge?: number;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface Subscription {
|
|
252
|
+
id: string;
|
|
253
|
+
isTrial: boolean;
|
|
254
|
+
purchasedAt?: string;
|
|
255
|
+
expiresAt?: string;
|
|
256
|
+
notificationScheduledAt?: string;
|
|
257
|
+
notificationSentAt?: string;
|
|
258
|
+
notificationCancelledAt?: string;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Convenience exports
|
|
262
|
+
export const requestNotificationPermissions = () =>
|
|
263
|
+
NotificationScheduler.getInstance().requestPermissions();
|
|
264
|
+
|
|
265
|
+
export const checkNotificationPermissions = () =>
|
|
266
|
+
NotificationScheduler.getInstance().checkPermissions();
|
|
267
|
+
|
|
268
|
+
export const scheduleTrialNotification = (
|
|
269
|
+
subscription: Subscription,
|
|
270
|
+
config: NotificationConfig,
|
|
271
|
+
apiKey?: string
|
|
272
|
+
) =>
|
|
273
|
+
NotificationScheduler.getInstance().scheduleTrialNotification(
|
|
274
|
+
subscription,
|
|
275
|
+
config,
|
|
276
|
+
apiKey
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
export const cancelTrialNotification = (
|
|
280
|
+
subscriptionId: string,
|
|
281
|
+
apiKey?: string
|
|
282
|
+
) =>
|
|
283
|
+
NotificationScheduler.getInstance().cancelNotification(
|
|
284
|
+
subscriptionId,
|
|
285
|
+
apiKey
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
export const getScheduledNotifications = () =>
|
|
289
|
+
NotificationScheduler.getInstance().getScheduledNotifications();
|
|
290
|
+
|
|
291
|
+
// Re-export device token registration
|
|
292
|
+
export { useDeviceToken, registerDeviceToken } from './useDeviceToken';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import PushNotification from 'react-native-push-notification';
|
|
2
|
+
import { ApiClient } from '../utils/ApiClient';
|
|
3
|
+
|
|
4
|
+
export interface PurchaseNotificationConfig {
|
|
5
|
+
/** @deprecated Use BillDogConfiguration.configure({ apiKey }) instead */
|
|
6
|
+
apiKey?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface PurchaseDetails {
|
|
10
|
+
subscriptionId: string;
|
|
11
|
+
userId: string;
|
|
12
|
+
projectId: string;
|
|
13
|
+
productId: string;
|
|
14
|
+
platform: 'ios' | 'android';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Handles notification cancellation when purchases are completed.
|
|
19
|
+
*
|
|
20
|
+
* Resolves API endpoint from BillDogConfiguration by default.
|
|
21
|
+
*/
|
|
22
|
+
export class PurchaseNotificationHandler {
|
|
23
|
+
/** @deprecated */
|
|
24
|
+
private apiKey?: string;
|
|
25
|
+
|
|
26
|
+
constructor(config: PurchaseNotificationConfig = {}) {
|
|
27
|
+
this.apiKey = config.apiKey;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async handlePurchaseCompleted(details: PurchaseDetails): Promise<boolean> {
|
|
31
|
+
try {
|
|
32
|
+
this.cancelLocalNotification(details.subscriptionId);
|
|
33
|
+
await this.cancelRemoteNotification(details);
|
|
34
|
+
return true;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('[PurchaseNotificationHandler] Error handling purchase:', error);
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private cancelLocalNotification(subscriptionId: string): void {
|
|
42
|
+
const notificationId = `trial-reminder-${subscriptionId}`;
|
|
43
|
+
PushNotification.cancelLocalNotification(notificationId);
|
|
44
|
+
PushNotification.cancelAllLocalNotifications();
|
|
45
|
+
console.log('[PurchaseNotificationHandler] Cancelled local notification:', notificationId);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private async cancelRemoteNotification(details: PurchaseDetails): Promise<void> {
|
|
49
|
+
try {
|
|
50
|
+
const result = await ApiClient.post('handle-purchase-notification', {
|
|
51
|
+
subscriptionId: details.subscriptionId,
|
|
52
|
+
userId: details.userId,
|
|
53
|
+
projectId: details.projectId,
|
|
54
|
+
productId: details.productId,
|
|
55
|
+
platform: details.platform,
|
|
56
|
+
purchaseDate: new Date().toISOString(),
|
|
57
|
+
}, {
|
|
58
|
+
apiKey: this.apiKey,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
console.log('[PurchaseNotificationHandler] Successfully updated backend:', result);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('[PurchaseNotificationHandler] Error updating backend:', error);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|