@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,39 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { PaywallComponent } from '../types';
|
|
4
|
+
import { triggerHaptic, HapticType } from '../utils/hapticFeedback';
|
|
5
|
+
|
|
6
|
+
interface HapticFeedbackComponentProps {
|
|
7
|
+
component: PaywallComponent;
|
|
8
|
+
isPreviewMode?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const HAPTIC_STYLES: ReadonlySet<string> = new Set([
|
|
12
|
+
'light', 'medium', 'heavy', 'success', 'warning', 'error',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* HapticFeedbackComponent — invisible behavior component.
|
|
17
|
+
*
|
|
18
|
+
* Fires a single device haptic on mount, reusing the shared `triggerHaptic`
|
|
19
|
+
* util (the same one the haptic tap-action uses). Skipped in the builder
|
|
20
|
+
* preview. The style comes from `properties.hapticStyle` (the six canonical
|
|
21
|
+
* HapticType values), falling back to `medium`. Renders nothing visible.
|
|
22
|
+
*/
|
|
23
|
+
export const HapticFeedbackComponent: React.FC<HapticFeedbackComponentProps> = ({
|
|
24
|
+
component,
|
|
25
|
+
isPreviewMode = false,
|
|
26
|
+
}) => {
|
|
27
|
+
const hasFiredRef = useRef(false);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (isPreviewMode || hasFiredRef.current) return;
|
|
31
|
+
hasFiredRef.current = true;
|
|
32
|
+
const props = (component.properties as Record<string, unknown>) || {};
|
|
33
|
+
const raw = typeof props.hapticStyle === 'string' ? props.hapticStyle : 'medium';
|
|
34
|
+
const style = (HAPTIC_STYLES.has(raw) ? raw : 'medium') as HapticType;
|
|
35
|
+
triggerHaptic(style);
|
|
36
|
+
}, [component.id, isPreviewMode]);
|
|
37
|
+
|
|
38
|
+
return <View style={{ width: 0, height: 0 }} />;
|
|
39
|
+
};
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Horizontal Products Component for React Native
|
|
3
|
+
* Pure Child Model: renders children via renderChild with horizontal FlatList layout,
|
|
4
|
+
* overlay extraction, child sanitization, and selection state propagation.
|
|
5
|
+
* Matches Android ProductsComponent.kt and iOS ProductsRenderer.swift architecture.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
9
|
+
import {
|
|
10
|
+
View,
|
|
11
|
+
TouchableOpacity,
|
|
12
|
+
FlatList,
|
|
13
|
+
StyleSheet,
|
|
14
|
+
Text,
|
|
15
|
+
AccessibilityInfo,
|
|
16
|
+
} from 'react-native';
|
|
17
|
+
import { PaywallComponent, PaywallConfiguration } from '../types';
|
|
18
|
+
import { PaywallStateManager } from '../PaywallStateManager';
|
|
19
|
+
|
|
20
|
+
export interface HorizontalProductsComponentProps {
|
|
21
|
+
component: PaywallComponent;
|
|
22
|
+
configuration: PaywallConfiguration;
|
|
23
|
+
renderChild: (child: PaywallComponent) => React.ReactNode;
|
|
24
|
+
stateManager?: any;
|
|
25
|
+
onAction?: (componentId: string, action: string) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ============= OVERLAY EXTRACTION =============
|
|
29
|
+
|
|
30
|
+
interface OverlayExtraction {
|
|
31
|
+
badge: PaywallComponent | null;
|
|
32
|
+
checkmark: PaywallComponent | null;
|
|
33
|
+
radioIndicator: PaywallComponent | null;
|
|
34
|
+
remainingChildren: PaywallComponent[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getRole(child: PaywallComponent): string | undefined {
|
|
38
|
+
return (child as any).role || child.properties?.role;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function extractOverlays(children: PaywallComponent[] | undefined): OverlayExtraction {
|
|
42
|
+
if (!children?.length) {
|
|
43
|
+
return { badge: null, checkmark: null, radioIndicator: null, remainingChildren: [] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const badge = children.find(c => getRole(c) === 'badge') || null;
|
|
47
|
+
const checkmark = children.find(c => getRole(c) === 'checkmark') || null;
|
|
48
|
+
const radioIndicator = children.find(c => getRole(c) === 'radio-indicator') || null;
|
|
49
|
+
|
|
50
|
+
const remaining = children.filter(c => c !== badge && c !== checkmark && c !== radioIndicator);
|
|
51
|
+
|
|
52
|
+
return { badge, checkmark, radioIndicator, remainingChildren: remaining };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============= CHILD SANITIZATION =============
|
|
56
|
+
|
|
57
|
+
function sanitizeProductChild(child: PaywallComponent): PaywallComponent {
|
|
58
|
+
const sanitized = { ...child };
|
|
59
|
+
|
|
60
|
+
if (sanitized.appearance) {
|
|
61
|
+
const { fill, backgroundColor, cornerRadius, borderColor, borderWidth, shadow, ...rest } = sanitized.appearance as any;
|
|
62
|
+
sanitized.appearance = rest;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
(sanitized as any).selectedAppearance = undefined;
|
|
66
|
+
|
|
67
|
+
if (sanitized.layout) {
|
|
68
|
+
const { padding, ...restLayout } = sanitized.layout as any;
|
|
69
|
+
sanitized.layout = restLayout;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return sanitized;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============= SELECTION STATE PROPAGATION =============
|
|
76
|
+
|
|
77
|
+
function applySelectionState(component: PaywallComponent, isSelected: boolean, isHybridMode: boolean): PaywallComponent {
|
|
78
|
+
if (!isHybridMode) return component;
|
|
79
|
+
|
|
80
|
+
const role = getRole(component);
|
|
81
|
+
|
|
82
|
+
if (role === 'radio-indicator' && component.type === 'stack' && component.children) {
|
|
83
|
+
return {
|
|
84
|
+
...component,
|
|
85
|
+
children: component.children.map(iconChild => {
|
|
86
|
+
const iconRole = getRole(iconChild);
|
|
87
|
+
if (iconRole === 'selected-icon') return { ...iconChild, visible: isSelected };
|
|
88
|
+
if (iconRole === 'unselected-icon') return { ...iconChild, visible: !isSelected };
|
|
89
|
+
return applySelectionState(iconChild, isSelected, isHybridMode);
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (role === 'checkmark') {
|
|
95
|
+
return { ...component, visible: isSelected };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (component.children?.length) {
|
|
99
|
+
return {
|
|
100
|
+
...component,
|
|
101
|
+
children: component.children.map(c => applySelectionState(c, isSelected, isHybridMode)),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return component;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============= HYBRID MODE DETECTION =============
|
|
109
|
+
|
|
110
|
+
function hasHybridChildren(children: PaywallComponent[]): boolean {
|
|
111
|
+
return children.some(child => {
|
|
112
|
+
if (getRole(child)) return true;
|
|
113
|
+
return hasChildWithRole(child);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function hasChildWithRole(component: PaywallComponent): boolean {
|
|
118
|
+
if (!component.children) return false;
|
|
119
|
+
for (const child of component.children) {
|
|
120
|
+
if (getRole(child)) return true;
|
|
121
|
+
if (hasChildWithRole(child)) return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ============= HELPERS =============
|
|
127
|
+
|
|
128
|
+
function resolveNumber(val: any, fallback: number): number {
|
|
129
|
+
if (typeof val === 'number') return val;
|
|
130
|
+
if (val && typeof val === 'object' && 'value' in val) return val.value;
|
|
131
|
+
return fallback;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function resolveColor(appearance: any, key: string, fallback: string): string {
|
|
135
|
+
if (!appearance) return fallback;
|
|
136
|
+
const val = appearance[key];
|
|
137
|
+
if (!val) return fallback;
|
|
138
|
+
if (typeof val === 'string') return val;
|
|
139
|
+
if (val.resolvedColor) return val.resolvedColor;
|
|
140
|
+
if (val.color) return typeof val.color === 'string' ? val.color : fallback;
|
|
141
|
+
if (val.hex) return val.hex;
|
|
142
|
+
return fallback;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function resolveFillColor(appearance: any, fallback: string): string {
|
|
146
|
+
if (!appearance) return fallback;
|
|
147
|
+
if (appearance.fill) return resolveColor(appearance, 'fill', fallback);
|
|
148
|
+
if (appearance.backgroundColor) return resolveColor(appearance, 'backgroundColor', fallback);
|
|
149
|
+
return fallback;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ============= MAIN COMPONENT =============
|
|
153
|
+
|
|
154
|
+
export const HorizontalProductsComponent: React.FC<HorizontalProductsComponentProps> = ({
|
|
155
|
+
component,
|
|
156
|
+
configuration,
|
|
157
|
+
renderChild,
|
|
158
|
+
stateManager,
|
|
159
|
+
onAction,
|
|
160
|
+
}) => {
|
|
161
|
+
const properties = component.properties || {};
|
|
162
|
+
const allChildren = component.children || [];
|
|
163
|
+
const flatListRef = useRef<FlatList>(null);
|
|
164
|
+
|
|
165
|
+
// Package binding
|
|
166
|
+
const bindToPackages = properties.bindToPackages !== false;
|
|
167
|
+
const packageIdentifiers: string[] = properties.packageIdentifiers || [];
|
|
168
|
+
|
|
169
|
+
const boundPackages = bindToPackages
|
|
170
|
+
? (packageIdentifiers.length > 0
|
|
171
|
+
? packageIdentifiers
|
|
172
|
+
.map(id => (configuration.packages || []).find(p => (p as any).identifier === id || (p as any).productId === id))
|
|
173
|
+
.filter(Boolean)
|
|
174
|
+
: configuration.packages || [])
|
|
175
|
+
: [];
|
|
176
|
+
|
|
177
|
+
const children = (bindToPackages && boundPackages.length > 0)
|
|
178
|
+
? allChildren.slice(0, boundPackages.length)
|
|
179
|
+
: allChildren.filter(c => c.visible !== false);
|
|
180
|
+
|
|
181
|
+
const gap = resolveNumber(properties.gap, 12);
|
|
182
|
+
const defaultSelectedIndex = resolveNumber(properties.defaultSelectedIndex, 0);
|
|
183
|
+
const cardWidth = resolveNumber(properties.cardWidth, 160);
|
|
184
|
+
const cardHeight = resolveNumber(properties.cardHeight, 200);
|
|
185
|
+
const showBadge = !!properties.showBadge;
|
|
186
|
+
const badgeProductIndex = resolveNumber(properties.badgeProductIndex, 0);
|
|
187
|
+
const enableSnapScrolling = properties.enableSnapScrolling !== false;
|
|
188
|
+
|
|
189
|
+
const isHybridMode = hasHybridChildren(children);
|
|
190
|
+
|
|
191
|
+
// State
|
|
192
|
+
const stateKey = `element:${component.id}.selectedIndex`;
|
|
193
|
+
const [selectedIndex, setSelectedIndex] = useState(defaultSelectedIndex);
|
|
194
|
+
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
if (!stateManager) return;
|
|
197
|
+
const existing = stateManager.getState?.(stateKey);
|
|
198
|
+
if (existing !== undefined && existing !== null) {
|
|
199
|
+
setSelectedIndex(Number(existing));
|
|
200
|
+
} else {
|
|
201
|
+
stateManager.setState?.(stateKey, defaultSelectedIndex);
|
|
202
|
+
}
|
|
203
|
+
// Return the unsubscribe. This was dropped on the floor while nothing supplied a
|
|
204
|
+
// stateManager and every call no-opped; now that one is provided, an uncleaned
|
|
205
|
+
// subscription leaks a listener holding a stale setSelectedIndex on each remount.
|
|
206
|
+
return stateManager.subscribe?.(stateKey, (value: any) => {
|
|
207
|
+
setSelectedIndex(Number(value) || 0);
|
|
208
|
+
});
|
|
209
|
+
}, [component.id, stateManager]);
|
|
210
|
+
|
|
211
|
+
// Publish the live selection PAYWALL-WIDE so Dynamic Values can react to it
|
|
212
|
+
// (`products.selectedIndex == 1` templates) and so `selectedProduct.*` resolves for
|
|
213
|
+
// the user's actual choice. The per-element key above is scoped to this component.
|
|
214
|
+
// With multiple products components, last write wins — same contract as Android
|
|
215
|
+
// ProductsComponent and iOS ProductsRenderer.updateSelectionState.
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
stateManager?.setState?.(PaywallStateManager.SELECTED_INDEX_KEY, selectedIndex);
|
|
218
|
+
}, [selectedIndex, stateManager]);
|
|
219
|
+
|
|
220
|
+
const handleSelect = useCallback((index: number) => {
|
|
221
|
+
setSelectedIndex(index);
|
|
222
|
+
stateManager?.setState?.(stateKey, index);
|
|
223
|
+
onAction?.(component.id, `select_package:${index},${children[index]?.id}`);
|
|
224
|
+
|
|
225
|
+
AccessibilityInfo.announceForAccessibility(`Selected product ${index + 1}`);
|
|
226
|
+
}, [stateKey, stateManager, onAction, component.id, children]);
|
|
227
|
+
|
|
228
|
+
const renderProductCard = (child: PaywallComponent, index: number) => {
|
|
229
|
+
const isSelected = index === selectedIndex;
|
|
230
|
+
const childAppearance = child.appearance as any;
|
|
231
|
+
const childSelectedAppearance = (child as any).selectedAppearance as any;
|
|
232
|
+
const childLayout = child.layout as any;
|
|
233
|
+
|
|
234
|
+
const cardBg = resolveFillColor(childAppearance, 'transparent');
|
|
235
|
+
const cardCornerRadius = resolveNumber(childAppearance?.cornerRadius, 12);
|
|
236
|
+
const cardBorderColor = resolveColor(childAppearance, 'borderColor', 'rgba(128,128,128,0.2)');
|
|
237
|
+
const cardBorderWidth = resolveNumber(childAppearance?.borderWidth, 0);
|
|
238
|
+
|
|
239
|
+
const selectedBg = resolveFillColor(childSelectedAppearance, 'rgba(59,130,246,0.1)');
|
|
240
|
+
const selectedBorderColor = resolveColor(childSelectedAppearance, 'borderColor', '#3B82F6');
|
|
241
|
+
const selectedBorderWidth = resolveNumber(childSelectedAppearance?.borderWidth, 2);
|
|
242
|
+
|
|
243
|
+
const bgColor = isSelected ? selectedBg : cardBg;
|
|
244
|
+
const borderColorVal = isSelected ? selectedBorderColor : cardBorderColor;
|
|
245
|
+
const borderWidthVal = isSelected ? selectedBorderWidth : cardBorderWidth;
|
|
246
|
+
|
|
247
|
+
// Padding from child.layout.padding
|
|
248
|
+
const padding = childLayout?.padding;
|
|
249
|
+
const paddingTop = resolveNumber(padding?.top, 14);
|
|
250
|
+
const paddingRight = resolveNumber(padding?.right, 16);
|
|
251
|
+
const paddingBottom = resolveNumber(padding?.bottom, 14);
|
|
252
|
+
const paddingLeft = resolveNumber(padding?.left, 16);
|
|
253
|
+
|
|
254
|
+
const extraction = extractOverlays(child.children);
|
|
255
|
+
const childWithRemaining = { ...child, children: extraction.remainingChildren };
|
|
256
|
+
const sanitized = sanitizeProductChild(childWithRemaining);
|
|
257
|
+
const transformed = applySelectionState(sanitized, isSelected, isHybridMode);
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<TouchableOpacity
|
|
261
|
+
activeOpacity={0.8}
|
|
262
|
+
onPress={() => handleSelect(index)}
|
|
263
|
+
accessibilityRole="radio"
|
|
264
|
+
accessibilityState={{ selected: isSelected }}
|
|
265
|
+
accessibilityLabel={`Product ${index + 1}, ${isSelected ? 'selected' : 'not selected'}`}
|
|
266
|
+
style={{ marginRight: index < children.length - 1 ? gap : 0 }}
|
|
267
|
+
>
|
|
268
|
+
<View style={{ width: cardWidth, height: cardHeight, position: 'relative' }}>
|
|
269
|
+
{/* Layer 1: Background */}
|
|
270
|
+
<View
|
|
271
|
+
style={{
|
|
272
|
+
...StyleSheet.absoluteFillObject,
|
|
273
|
+
backgroundColor: bgColor,
|
|
274
|
+
borderRadius: cardCornerRadius,
|
|
275
|
+
borderColor: borderColorVal,
|
|
276
|
+
borderWidth: borderWidthVal,
|
|
277
|
+
}}
|
|
278
|
+
/>
|
|
279
|
+
|
|
280
|
+
{/* Layer 2: Content */}
|
|
281
|
+
<View
|
|
282
|
+
style={{
|
|
283
|
+
flex: 1,
|
|
284
|
+
alignItems: 'center',
|
|
285
|
+
justifyContent: 'center',
|
|
286
|
+
paddingTop,
|
|
287
|
+
paddingRight,
|
|
288
|
+
paddingBottom,
|
|
289
|
+
paddingLeft,
|
|
290
|
+
}}
|
|
291
|
+
>
|
|
292
|
+
{renderChild(transformed)}
|
|
293
|
+
</View>
|
|
294
|
+
|
|
295
|
+
{/* Layer 3: Badge overlay */}
|
|
296
|
+
{extraction.badge && showBadge && index === badgeProductIndex && (
|
|
297
|
+
<View style={{ position: 'absolute', top: -10, right: 12, zIndex: 10 }}>
|
|
298
|
+
{renderChild(extraction.badge)}
|
|
299
|
+
</View>
|
|
300
|
+
)}
|
|
301
|
+
</View>
|
|
302
|
+
</TouchableOpacity>
|
|
303
|
+
);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<View accessibilityRole="radiogroup" accessibilityLabel="Select a product">
|
|
308
|
+
<FlatList
|
|
309
|
+
ref={flatListRef}
|
|
310
|
+
data={children}
|
|
311
|
+
renderItem={({ item, index }) => renderProductCard(item, index)}
|
|
312
|
+
keyExtractor={(item, index) => item.id || `product-${index}`}
|
|
313
|
+
horizontal
|
|
314
|
+
showsHorizontalScrollIndicator={false}
|
|
315
|
+
snapToInterval={enableSnapScrolling ? cardWidth + gap : undefined}
|
|
316
|
+
snapToAlignment="start"
|
|
317
|
+
decelerationRate={enableSnapScrolling ? 'fast' : 'normal'}
|
|
318
|
+
contentContainerStyle={{ paddingTop: 12 }}
|
|
319
|
+
getItemLayout={(_, index) => ({
|
|
320
|
+
length: cardWidth + gap,
|
|
321
|
+
offset: (cardWidth + gap) * index,
|
|
322
|
+
index,
|
|
323
|
+
})}
|
|
324
|
+
/>
|
|
325
|
+
</View>
|
|
326
|
+
);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
export default HorizontalProductsComponent;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi Choice Grid Component for React Native
|
|
3
|
+
* Checkbox style grid selection (multiple selection)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useState, useEffect } from 'react';
|
|
7
|
+
import {
|
|
8
|
+
View,
|
|
9
|
+
Text,
|
|
10
|
+
TouchableOpacity,
|
|
11
|
+
StyleSheet,
|
|
12
|
+
ViewStyle,
|
|
13
|
+
TextStyle,
|
|
14
|
+
DimensionValue,
|
|
15
|
+
} from 'react-native';
|
|
16
|
+
import Svg, { Path } from 'react-native-svg';
|
|
17
|
+
import { PaywallComponent } from '../types';
|
|
18
|
+
|
|
19
|
+
interface MultiChoiceGridProps {
|
|
20
|
+
component: PaywallComponent;
|
|
21
|
+
stateManager?: any;
|
|
22
|
+
isPreviewMode?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SIZE_MAP = { sm: 16, md: 20, lg: 24 };
|
|
26
|
+
|
|
27
|
+
// Check icon paths
|
|
28
|
+
const CHECK_PATHS = {
|
|
29
|
+
check: 'M20 6L9 17L4 12',
|
|
30
|
+
cross: 'M18 6L6 18M6 6L18 18',
|
|
31
|
+
circle: 'M12 12m-8 0a8 8 0 1 0 16 0a8 8 0 1 0 -16 0',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const MultiChoiceGridComponent: React.FC<MultiChoiceGridProps> = ({
|
|
35
|
+
component,
|
|
36
|
+
stateManager,
|
|
37
|
+
isPreviewMode = false,
|
|
38
|
+
}) => {
|
|
39
|
+
const props = component.properties || {};
|
|
40
|
+
|
|
41
|
+
// Parse options
|
|
42
|
+
const optionsStr = (props as any).options || 'Option 1,Option 2,Option 3,Option 4';
|
|
43
|
+
const options = optionsStr.split(',').map((o: string) => o.trim()).filter(Boolean);
|
|
44
|
+
const descriptionsStr = (props as any).optionDescriptions || '';
|
|
45
|
+
const descriptions = descriptionsStr.split(',').map((d: string) => d.trim());
|
|
46
|
+
|
|
47
|
+
// Grid settings
|
|
48
|
+
const columns = (props as any).columns || 2;
|
|
49
|
+
const gap = parseFloat((props as any).gap?.value || (props as any).gap) || 12;
|
|
50
|
+
const minItemHeight = parseFloat((props as any).minItemHeight?.value || (props as any).minItemHeight) || 80;
|
|
51
|
+
|
|
52
|
+
// Selection settings
|
|
53
|
+
const defaultIndicesStr = (props as any).defaultSelectedIndices || '';
|
|
54
|
+
const defaultIndices = defaultIndicesStr
|
|
55
|
+
.split(',')
|
|
56
|
+
.map((s: string) => parseInt(s.trim(), 10))
|
|
57
|
+
.filter((n: number) => !isNaN(n));
|
|
58
|
+
const minSelections = (props as any).minSelections || 0;
|
|
59
|
+
const maxSelections = (props as any).maxSelections || 0;
|
|
60
|
+
const disabled = (props as any).disabled ?? false;
|
|
61
|
+
|
|
62
|
+
// State
|
|
63
|
+
const [selectedIndices, setSelectedIndices] = useState<number[]>(defaultIndices);
|
|
64
|
+
|
|
65
|
+
// State manager integration
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!stateManager) return;
|
|
68
|
+
|
|
69
|
+
const stateId = `element:${component.id}.selectedIndices`;
|
|
70
|
+
stateManager.registerState?.(stateId, selectedIndices);
|
|
71
|
+
|
|
72
|
+
return stateManager.subscribeToState?.(stateId, (value: number[]) => {
|
|
73
|
+
if (JSON.stringify(value) !== JSON.stringify(selectedIndices)) {
|
|
74
|
+
setSelectedIndices(value);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}, [component.id, selectedIndices, stateManager]);
|
|
78
|
+
|
|
79
|
+
const handleSelect = (index: number) => {
|
|
80
|
+
if (disabled) return;
|
|
81
|
+
|
|
82
|
+
let newIndices: number[];
|
|
83
|
+
|
|
84
|
+
if (selectedIndices.includes(index)) {
|
|
85
|
+
if (minSelections > 0 && selectedIndices.length <= minSelections) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
newIndices = selectedIndices.filter(i => i !== index);
|
|
89
|
+
} else {
|
|
90
|
+
if (maxSelections > 0 && selectedIndices.length >= maxSelections) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
newIndices = [...selectedIndices, index].sort((a, b) => a - b);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setSelectedIndices(newIndices);
|
|
97
|
+
|
|
98
|
+
stateManager?.updateState?.(`element:${component.id}.selectedIndices`, newIndices);
|
|
99
|
+
stateManager?.updateState?.(`element:${component.id}.selectedValues`, newIndices.map(i => options[i]));
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Styling
|
|
103
|
+
const showIndicator = (props as any).showIndicator ?? true;
|
|
104
|
+
const indicatorPosition = (props as any).indicatorPosition || 'top-right';
|
|
105
|
+
const checkIcon = (props as any).checkIcon || 'check';
|
|
106
|
+
const indicatorSize = SIZE_MAP[(props as any).indicatorSize as keyof typeof SIZE_MAP] || SIZE_MAP.md;
|
|
107
|
+
const indicatorColor = (props as any).indicatorColor || '#ffffff';
|
|
108
|
+
const indicatorBackground = (props as any).indicatorBackground || '#007AFF';
|
|
109
|
+
const indicatorBorderColor = (props as any).indicatorBorderColor || '#d1d5db';
|
|
110
|
+
|
|
111
|
+
const cardBorderRadius = parseFloat((props as any).cardBorderRadius?.value || (props as any).cardBorderRadius) || 12;
|
|
112
|
+
const cardBorderColor = (props as any).cardBorderColor || '#e5e7eb';
|
|
113
|
+
const selectedBorderColor = (props as any).selectedBorderColor || '#007AFF';
|
|
114
|
+
|
|
115
|
+
const labelFontSize = parseFloat((props as any).labelFontSize?.value || (props as any).labelFontSize) || 16;
|
|
116
|
+
const labelFontWeight = String((props as any).labelFontWeight || '500') as TextStyle['fontWeight'];
|
|
117
|
+
const labelColor = (props as any).labelColor || '#1f2937';
|
|
118
|
+
const selectedLabelColor = (props as any).selectedLabelColor || '#007AFF';
|
|
119
|
+
const descriptionFontSize = parseFloat((props as any).descriptionFontSize?.value || (props as any).descriptionFontSize) || 12;
|
|
120
|
+
const descriptionColor = (props as any).descriptionColor || '#6b7280';
|
|
121
|
+
|
|
122
|
+
// Background color helper
|
|
123
|
+
const getBackgroundColor = (fill: any): string => {
|
|
124
|
+
if (!fill) return '#ffffff';
|
|
125
|
+
if (typeof fill === 'string') return fill;
|
|
126
|
+
if (fill.type === 'color') return fill.color || '#ffffff';
|
|
127
|
+
if (fill.resolvedColor) return fill.resolvedColor;
|
|
128
|
+
return '#ffffff';
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const cardBackground = getBackgroundColor((props as any).cardBackground);
|
|
132
|
+
const selectedCardBackground = getBackgroundColor((props as any).selectedCardBackground);
|
|
133
|
+
|
|
134
|
+
// Indicator position style
|
|
135
|
+
const getIndicatorPositionStyle = (): ViewStyle => {
|
|
136
|
+
const base: ViewStyle = { position: 'absolute' };
|
|
137
|
+
switch (indicatorPosition) {
|
|
138
|
+
case 'top-left': return { ...base, top: 8, left: 8 };
|
|
139
|
+
case 'top-right': return { ...base, top: 8, right: 8 };
|
|
140
|
+
case 'bottom-left': return { ...base, bottom: 8, left: 8 };
|
|
141
|
+
case 'bottom-right': return { ...base, bottom: 8, right: 8 };
|
|
142
|
+
default: return { ...base, top: 8, right: 8 };
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Calculate item width based on columns
|
|
147
|
+
const itemWidth: DimensionValue = `${(100 - (columns - 1) * 2) / columns}%`;
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<View style={[styles.container, { opacity: disabled ? 0.6 : 1 }]}>
|
|
151
|
+
<View style={[styles.grid, { gap }]}>
|
|
152
|
+
{options.map((option: string, index: number) => {
|
|
153
|
+
const isSelected = selectedIndices.includes(index);
|
|
154
|
+
const description = descriptions[index] || '';
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<TouchableOpacity
|
|
158
|
+
key={index}
|
|
159
|
+
onPress={() => handleSelect(index)}
|
|
160
|
+
disabled={disabled}
|
|
161
|
+
activeOpacity={0.7}
|
|
162
|
+
style={[
|
|
163
|
+
styles.card,
|
|
164
|
+
{
|
|
165
|
+
width: itemWidth,
|
|
166
|
+
minHeight: minItemHeight,
|
|
167
|
+
backgroundColor: isSelected ? selectedCardBackground : cardBackground,
|
|
168
|
+
borderColor: isSelected ? selectedBorderColor : cardBorderColor,
|
|
169
|
+
borderRadius: cardBorderRadius,
|
|
170
|
+
},
|
|
171
|
+
]}
|
|
172
|
+
>
|
|
173
|
+
{/* Checkbox Indicator */}
|
|
174
|
+
{showIndicator && (
|
|
175
|
+
<View
|
|
176
|
+
style={[
|
|
177
|
+
getIndicatorPositionStyle(),
|
|
178
|
+
{
|
|
179
|
+
width: indicatorSize,
|
|
180
|
+
height: indicatorSize,
|
|
181
|
+
borderRadius: 4,
|
|
182
|
+
borderWidth: 2,
|
|
183
|
+
borderColor: isSelected ? indicatorBackground : indicatorBorderColor,
|
|
184
|
+
backgroundColor: isSelected ? indicatorBackground : 'transparent',
|
|
185
|
+
alignItems: 'center',
|
|
186
|
+
justifyContent: 'center',
|
|
187
|
+
},
|
|
188
|
+
]}
|
|
189
|
+
>
|
|
190
|
+
{isSelected && (
|
|
191
|
+
<Svg
|
|
192
|
+
width={indicatorSize * 0.7}
|
|
193
|
+
height={indicatorSize * 0.7}
|
|
194
|
+
viewBox="0 0 24 24"
|
|
195
|
+
fill="none"
|
|
196
|
+
>
|
|
197
|
+
<Path
|
|
198
|
+
d={CHECK_PATHS[checkIcon as keyof typeof CHECK_PATHS] || CHECK_PATHS.check}
|
|
199
|
+
stroke={indicatorColor}
|
|
200
|
+
strokeWidth={3}
|
|
201
|
+
strokeLinecap="round"
|
|
202
|
+
strokeLinejoin="round"
|
|
203
|
+
/>
|
|
204
|
+
</Svg>
|
|
205
|
+
)}
|
|
206
|
+
</View>
|
|
207
|
+
)}
|
|
208
|
+
|
|
209
|
+
{/* Label */}
|
|
210
|
+
<Text
|
|
211
|
+
style={[
|
|
212
|
+
styles.label,
|
|
213
|
+
{
|
|
214
|
+
fontSize: labelFontSize,
|
|
215
|
+
fontWeight: labelFontWeight,
|
|
216
|
+
color: isSelected ? selectedLabelColor : labelColor,
|
|
217
|
+
},
|
|
218
|
+
]}
|
|
219
|
+
>
|
|
220
|
+
{option}
|
|
221
|
+
</Text>
|
|
222
|
+
|
|
223
|
+
{/* Description */}
|
|
224
|
+
{description ? (
|
|
225
|
+
<Text
|
|
226
|
+
style={[
|
|
227
|
+
styles.description,
|
|
228
|
+
{
|
|
229
|
+
fontSize: descriptionFontSize,
|
|
230
|
+
color: descriptionColor,
|
|
231
|
+
},
|
|
232
|
+
]}
|
|
233
|
+
>
|
|
234
|
+
{description}
|
|
235
|
+
</Text>
|
|
236
|
+
) : null}
|
|
237
|
+
</TouchableOpacity>
|
|
238
|
+
);
|
|
239
|
+
})}
|
|
240
|
+
</View>
|
|
241
|
+
</View>
|
|
242
|
+
);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const styles = StyleSheet.create({
|
|
246
|
+
container: {
|
|
247
|
+
width: '100%',
|
|
248
|
+
},
|
|
249
|
+
grid: {
|
|
250
|
+
flexDirection: 'row',
|
|
251
|
+
flexWrap: 'wrap',
|
|
252
|
+
},
|
|
253
|
+
card: {
|
|
254
|
+
position: 'relative',
|
|
255
|
+
borderWidth: 2,
|
|
256
|
+
padding: 16,
|
|
257
|
+
justifyContent: 'center',
|
|
258
|
+
},
|
|
259
|
+
label: {
|
|
260
|
+
marginTop: 4,
|
|
261
|
+
},
|
|
262
|
+
description: {
|
|
263
|
+
marginTop: 4,
|
|
264
|
+
},
|
|
265
|
+
});
|