@developer_tribe/react-builder 1.2.27 → 1.2.29
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/dist/assets/samples/getSamples.d.ts +0 -3
- package/dist/build-components/BIcon/BIconProps.generated.d.ts +1 -2
- package/dist/build-components/CountDown/CountDownProps.generated.d.ts +2 -1
- package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +1 -2
- package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +1 -2
- package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +1 -2
- package/dist/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.d.ts +1 -2
- package/dist/build-components/PaywallOptions/usePaywallOptionParamsFactory.d.ts +1 -1
- package/dist/build-components/PriceTag/PriceTag.d.ts +5 -0
- package/dist/build-components/PriceTag/PriceTagProps.generated.d.ts +63 -0
- package/dist/build-components/Pricing/Pricing.d.ts +5 -0
- package/dist/build-components/Pricing/PricingProps.generated.d.ts +59 -0
- package/dist/build-components/Promo/Promo.d.ts +5 -0
- package/dist/build-components/Promo/PromoProps.generated.d.ts +59 -0
- package/dist/build-components/Text/TextProps.generated.d.ts +1 -2
- package/dist/build-components/index.d.ts +4 -1
- package/dist/build-components/patterns.generated.d.ts +1405 -202
- package/dist/components/BuilderProvider.d.ts +5 -3
- package/dist/components/ParamsProvider.d.ts +16 -8
- package/dist/hooks/useSyncHtmlThemeClass.d.ts +1 -1
- package/dist/index.cjs.js +4 -4
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +16 -3
- package/dist/index.esm.js +4 -4
- package/dist/index.esm.js.map +1 -1
- package/dist/index.web.cjs.js +4 -4
- package/dist/index.web.cjs.js.map +1 -1
- package/dist/index.web.esm.js +4 -4
- package/dist/index.web.esm.js.map +1 -1
- package/dist/logger.d.ts +18 -0
- package/dist/modals/InspectModal.d.ts +5 -0
- package/dist/modals/index.d.ts +1 -1
- package/dist/pages/ProjectPage.d.ts +3 -3
- package/dist/paywall/hooks/useCalculateLocalizedPrice.d.ts +4 -2
- package/dist/paywall/hooks/useDiscountRate.d.ts +3 -2
- package/dist/paywall/types/paywall-types.d.ts +7 -32
- package/dist/product-base/buildPaywallLocalizationParams.d.ts +16 -0
- package/dist/product-base/calculations.d.ts +29 -0
- package/dist/product-base/extractAndroidParams.d.ts +24 -0
- package/dist/product-base/extractIOSParams.d.ts +24 -0
- package/dist/product-base/index.d.ts +51 -0
- package/dist/product-base/periodLocalizationKeys.d.ts +44 -0
- package/dist/product-base/types.d.ts +155 -0
- package/dist/product-base/usePaywallLocalizationParams.d.ts +29 -0
- package/dist/store.d.ts +7 -1
- package/dist/styles.css +1 -1
- package/dist/types/PreviewConfig.d.ts +10 -16
- package/dist/utils/extractTextStyle/extractTextStyle.d.ts +2 -2
- package/dist/utils/extractTextStyle/extractTextStyleNative.d.ts +2 -2
- package/dist/utils/replaceLocalizationParams.d.ts +1 -1
- package/package.json +2 -2
- package/scripts/migrate-samples-to-current.ts +3 -3
- package/scripts/prebuild/utils/validateAllComponentsOrThrow.js +28 -12
- package/src/DeviceMockFrame.tsx +15 -10
- package/src/assets/meta.json +1 -1
- package/src/assets/samples/carousel-sample.json +6 -5
- package/src/assets/samples/getSamples.ts +16 -49
- package/src/assets/samples/paywall-1.json +64 -22
- package/src/assets/samples/paywall-2.json +0 -15
- package/src/assets/samples/paywall-app-delete-offer.json +0 -15
- package/src/assets/samples/paywall-app-open-offer.json +0 -15
- package/src/assets/samples/paywall-back-offer.json +0 -15
- package/src/assets/samples/paywall-notification-offer.json +0 -15
- package/src/assets/samples/simple-1.json +1 -16
- package/src/assets/samples/simple-2.json +0 -15
- package/src/assets/samples/unmigrated-builder-1.1.1.json +0 -3
- package/src/assets/samples/unmigrated-builder1.json +0 -3
- package/src/assets/samples/unvalidated-builder1.json +0 -3
- package/src/assets/samples/unvalidated-crash1.json +0 -3
- package/src/assets/samples/unvalidated-crashcomponent1.json +0 -3
- package/src/assets/samples/vpn-onboard-1.json +1 -34
- package/src/assets/samples/vpn-onboard-2.json +1 -34
- package/src/assets/samples/vpn-onboard-3.json +1 -42
- package/src/assets/samples/vpn-onboard-4.json +0 -73
- package/src/assets/samples/vpn-onboard-5.json +0 -73
- package/src/assets/samples/vpn-onboard-6.json +0 -73
- package/src/assets/samples/vpn-onboard-7.json +529 -0
- package/src/attribute-analyser/style/native/useExtractImageStyle.ts +1 -4
- package/src/attribute-analyser/style/native/useExtractTextStyle.ts +3 -12
- package/src/attribute-analyser/style/native/useExtractViewStyle.ts +1 -4
- package/src/attribute-analyser/style/web/useExtractImageStyle.ts +1 -4
- package/src/attribute-analyser/style/web/useExtractTextStyle.ts +3 -12
- package/src/attribute-analyser/style/web/useExtractViewStyle.ts +1 -4
- package/src/attributes-editor/useAttributesEditorModel.ts +5 -52
- package/src/build-components/BIcon/BIconProps.generated.ts +1 -2
- package/src/build-components/CarouselDots/CarouselDots.tsx +6 -13
- package/src/build-components/CountDown/CountDownProps.generated.ts +2 -1
- package/src/build-components/NavigationBarColor/NavigationBarColor.tsx +2 -2
- package/src/build-components/OnboardButton/OnboardButton.tsx +1 -2
- package/src/build-components/OnboardDot/OnboardDot.tsx +6 -18
- package/src/build-components/OnboardFooter/OnboardFooter.tsx +5 -3
- package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +1 -2
- package/src/build-components/OnboardFooter/pattern.json +1 -1
- package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +1 -2
- package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +1 -2
- package/src/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.ts +1 -2
- package/src/build-components/PaywallOptions/PaywallOptions.tsx +3 -3
- package/src/build-components/PaywallOptions/usePaywallOptionParamsFactory.ts +26 -13
- package/src/build-components/PaywallProvider/PaywallProvider.tsx +51 -12
- package/src/build-components/PriceTag/PriceTag.tsx +25 -0
- package/src/build-components/PriceTag/PriceTagProps.generated.ts +83 -0
- package/src/build-components/PriceTag/pattern.json +53 -0
- package/src/build-components/Pricing/Pricing.tsx +13 -0
- package/src/build-components/Pricing/PricingProps.generated.ts +76 -0
- package/src/build-components/Pricing/pattern.json +25 -0
- package/src/build-components/Promo/Promo.tsx +13 -0
- package/src/build-components/Promo/PromoProps.generated.ts +76 -0
- package/src/build-components/Promo/pattern.json +25 -0
- package/src/build-components/RadioButton/RadioButton.tsx +3 -5
- package/src/build-components/RenderNode.generated.tsx +15 -0
- package/src/build-components/StatusBarColor/StatusBarColor.tsx +2 -2
- package/src/build-components/Text/Text.tsx +12 -5
- package/src/build-components/Text/TextProps.generated.ts +1 -2
- package/src/build-components/Text/pattern.json +3 -2
- package/src/build-components/index.ts +15 -0
- package/src/build-components/patterns.generated.ts +1454 -181
- package/src/components/BottomBar.tsx +42 -39
- package/src/components/BuilderProvider.tsx +41 -14
- package/src/components/LocalizationParamsProvider.tsx +1 -1
- package/src/components/ParamsProvider.tsx +36 -11
- package/src/hooks/useLocalize.ts +7 -4
- package/src/hooks/useParams.ts +1 -1
- package/src/hooks/useSyncHtmlThemeClass.ts +2 -2
- package/src/index.ts +54 -8
- package/src/logger.ts +39 -0
- package/src/modals/InspectModal.tsx +331 -0
- package/src/modals/ProductPresetsModal.tsx +7 -14
- package/src/modals/index.ts +1 -1
- package/src/pages/DebugJsonPage.tsx +9 -22
- package/src/pages/ProjectDebug.tsx +1 -1
- package/src/pages/ProjectPage.tsx +29 -11
- package/src/pages/tabs/SideTool.tsx +28 -104
- package/src/paywall/hooks/useCalculateLocalizedPrice.ts +8 -3
- package/src/paywall/hooks/useDiscountRate.ts +11 -3
- package/src/paywall/types/paywall-types.ts +7 -38
- package/src/product-base/buildPaywallLocalizationParams.ts +100 -0
- package/src/product-base/calculations.ts +93 -0
- package/src/product-base/extractAndroidParams.ts +207 -0
- package/src/product-base/extractIOSParams.ts +199 -0
- package/src/product-base/index.ts +64 -0
- package/src/product-base/mockProducts.json +489 -0
- package/src/product-base/periodLocalizationKeys.ts +114 -0
- package/src/product-base/types.ts +183 -0
- package/src/product-base/usePaywallLocalizationParams.ts +61 -0
- package/src/store.ts +18 -1
- package/src/styles/index.scss +1 -0
- package/src/styles/modals/_inspect-modal.scss +155 -0
- package/src/types/PreviewConfig.ts +157 -16
- package/src/utils/extractTextStyle/extractTextStyle.ts +14 -6
- package/src/utils/extractTextStyle/extractTextStyleNative.ts +8 -6
- package/src/utils/logRenderStore.ts +6 -10
- package/src/utils/parseColor.ts +0 -1
- package/src/utils/replaceLocalizationParams.ts +8 -4
- package/dist/modals/ScreenColorsModal.d.ts +0 -8
- package/src/assets/products.json +0 -98
- package/src/modals/ScreenColorsModal.tsx +0 -121
|
@@ -8,80 +8,39 @@ import { Checkbox } from '../../components/Checkbox';
|
|
|
8
8
|
import { LocalicationModal } from '../../modals/LocalicationModal';
|
|
9
9
|
import { DebugJsonPage } from '../DebugJsonPage';
|
|
10
10
|
|
|
11
|
-
const screenStyleDefaults = {
|
|
12
|
-
light: { backgroundColor: '#FDFDFD', color: '#161827' },
|
|
13
|
-
dark: { backgroundColor: '#12131A', color: '#E9EBF9' },
|
|
14
|
-
} as const;
|
|
15
|
-
|
|
16
|
-
type ScreenMode = keyof typeof screenStyleDefaults;
|
|
17
|
-
type ScreenColorKey = keyof (typeof screenStyleDefaults)['light'];
|
|
18
|
-
|
|
19
11
|
type SideToolProps = {
|
|
20
12
|
data: Node;
|
|
21
13
|
setData: React.Dispatch<React.SetStateAction<Node>>;
|
|
22
14
|
};
|
|
23
15
|
|
|
24
|
-
const colorFields = [
|
|
25
|
-
{
|
|
26
|
-
id: 'light-bg',
|
|
27
|
-
label: 'Light Background Color',
|
|
28
|
-
mode: 'light' as ScreenMode,
|
|
29
|
-
key: 'backgroundColor' as ScreenColorKey,
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
id: 'light-color',
|
|
33
|
-
label: 'Light Color',
|
|
34
|
-
mode: 'light' as ScreenMode,
|
|
35
|
-
key: 'color' as ScreenColorKey,
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
id: 'dark-bg',
|
|
39
|
-
label: 'Dark Background Color',
|
|
40
|
-
mode: 'dark' as ScreenMode,
|
|
41
|
-
key: 'backgroundColor' as ScreenColorKey,
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: 'dark-color',
|
|
45
|
-
label: 'Dark Color',
|
|
46
|
-
mode: 'dark' as ScreenMode,
|
|
47
|
-
key: 'color' as ScreenColorKey,
|
|
48
|
-
},
|
|
49
|
-
];
|
|
50
|
-
|
|
51
16
|
export function SideTool({ data, setData }: SideToolProps) {
|
|
52
17
|
useLogRender('SideTool');
|
|
53
18
|
const [isDebugModalOpen, setIsDebugModalOpen] = useState(false);
|
|
54
19
|
const [isLocalicationModalOpen, setIsLocalicationModalOpen] = useState(false);
|
|
55
20
|
const [isCompactPanelVisible, setIsCompactPanelVisible] = useState(false);
|
|
56
|
-
const {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
...appConfig.screenStyle?.[mode],
|
|
80
|
-
[key]: value,
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
};
|
|
21
|
+
const {
|
|
22
|
+
appConfig,
|
|
23
|
+
setAppConfig,
|
|
24
|
+
theme,
|
|
25
|
+
setTheme,
|
|
26
|
+
defaultLanguage,
|
|
27
|
+
setDefaultLanguage,
|
|
28
|
+
previewMode,
|
|
29
|
+
setPreviewMode,
|
|
30
|
+
isRtl,
|
|
31
|
+
setIsRtl,
|
|
32
|
+
} = useRenderStore((s) => ({
|
|
33
|
+
appConfig: s.appConfig,
|
|
34
|
+
setAppConfig: s.setAppConfig,
|
|
35
|
+
theme: s.theme,
|
|
36
|
+
setTheme: s.setTheme,
|
|
37
|
+
defaultLanguage: s.defaultLanguage,
|
|
38
|
+
setDefaultLanguage: s.setDefaultLanguage,
|
|
39
|
+
previewMode: s.previewMode,
|
|
40
|
+
setPreviewMode: s.setPreviewMode,
|
|
41
|
+
isRtl: s.isRtl,
|
|
42
|
+
setIsRtl: s.setIsRtl,
|
|
43
|
+
}));
|
|
85
44
|
|
|
86
45
|
const handleLocalicationChange = (data: Localication) => {
|
|
87
46
|
setAppConfig({ ...appConfig, localication: data });
|
|
@@ -101,10 +60,8 @@ export function SideTool({ data, setData }: SideToolProps) {
|
|
|
101
60
|
{isCompactPanelVisible && (
|
|
102
61
|
<div className="side-tool">
|
|
103
62
|
<select
|
|
104
|
-
value={
|
|
105
|
-
onChange={(e) =>
|
|
106
|
-
setAppConfig({ ...appConfig, defaultLanguage: e.target.value })
|
|
107
|
-
}
|
|
63
|
+
value={defaultLanguage}
|
|
64
|
+
onChange={(e) => setDefaultLanguage(e.target.value)}
|
|
108
65
|
>
|
|
109
66
|
{Object.keys(appConfig.localication ?? {}).map((language) => (
|
|
110
67
|
<option key={language} value={language}>
|
|
@@ -115,19 +72,11 @@ export function SideTool({ data, setData }: SideToolProps) {
|
|
|
115
72
|
|
|
116
73
|
<Checkbox
|
|
117
74
|
label="Dark Mode"
|
|
118
|
-
checked={
|
|
119
|
-
onChange={(checked) =>
|
|
120
|
-
setAppConfig({ ...appConfig, theme: checked ? 'dark' : 'light' })
|
|
121
|
-
}
|
|
75
|
+
checked={theme === 'dark'}
|
|
76
|
+
onChange={(checked) => setTheme(checked ? 'dark' : 'light')}
|
|
122
77
|
/>
|
|
123
78
|
|
|
124
|
-
<Checkbox
|
|
125
|
-
label="Is RTL"
|
|
126
|
-
checked={appConfig.isRtl ?? false}
|
|
127
|
-
onChange={(checked) =>
|
|
128
|
-
setAppConfig({ ...appConfig, isRtl: checked })
|
|
129
|
-
}
|
|
130
|
-
/>
|
|
79
|
+
<Checkbox label="Is RTL" checked={isRtl} onChange={setIsRtl} />
|
|
131
80
|
|
|
132
81
|
<Checkbox
|
|
133
82
|
label="Preview mode"
|
|
@@ -135,31 +84,6 @@ export function SideTool({ data, setData }: SideToolProps) {
|
|
|
135
84
|
onChange={setPreviewMode}
|
|
136
85
|
/>
|
|
137
86
|
|
|
138
|
-
<div>
|
|
139
|
-
<div
|
|
140
|
-
style={{
|
|
141
|
-
display: 'grid',
|
|
142
|
-
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
|
|
143
|
-
gap: 12,
|
|
144
|
-
}}
|
|
145
|
-
>
|
|
146
|
-
{colorFields.map(({ id, label, mode, key }) => (
|
|
147
|
-
<React.Fragment key={id}>
|
|
148
|
-
<div>{label}</div>
|
|
149
|
-
<input
|
|
150
|
-
id={id}
|
|
151
|
-
type="color"
|
|
152
|
-
className="input input--color"
|
|
153
|
-
value={getScreenColorValue(mode, key)}
|
|
154
|
-
onChange={(e) =>
|
|
155
|
-
handleScreenStyleChange(mode, key, e.target.value)
|
|
156
|
-
}
|
|
157
|
-
/>
|
|
158
|
-
</React.Fragment>
|
|
159
|
-
))}
|
|
160
|
-
</div>
|
|
161
|
-
</div>
|
|
162
|
-
|
|
163
87
|
<div
|
|
164
88
|
style={{
|
|
165
89
|
marginTop: 'auto',
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { extractPrice } from '../../product-base';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
4
|
+
* Extracts a numeric price string from a localized/formatted price.
|
|
5
|
+
* @param formattedPrice e.g. "$9.99", "€4,99"
|
|
6
|
+
* @returns Numeric string e.g. "9.99", "4.99"
|
|
3
7
|
*/
|
|
4
|
-
export function useCalculateLocalizedPrice(): string {
|
|
5
|
-
return '';
|
|
8
|
+
export function useCalculateLocalizedPrice(formattedPrice?: string): string {
|
|
9
|
+
if (!formattedPrice) return '';
|
|
10
|
+
return extractPrice(formattedPrice);
|
|
6
11
|
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import { calculateDiscount } from '../../product-base';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
4
|
+
* Calculates the discount percentage between a regular and promo price.
|
|
5
|
+
* @returns Discount as integer (e.g. 50 for 50%), or 0 if not calculable.
|
|
3
6
|
*/
|
|
4
|
-
export function useDiscountRate(
|
|
5
|
-
|
|
7
|
+
export function useDiscountRate(
|
|
8
|
+
regularPrice?: string,
|
|
9
|
+
promoPrice?: string,
|
|
10
|
+
): number {
|
|
11
|
+
if (!regularPrice || !promoPrice) return 0;
|
|
12
|
+
const discount = calculateDiscount(regularPrice, promoPrice);
|
|
13
|
+
return discount ? parseInt(discount, 10) : 0;
|
|
6
14
|
}
|
|
@@ -1,51 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Paywall-related types.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Product is now re-exported from product-base (the unified type with all
|
|
5
|
+
* platform-specific fields). This keeps every consumer compatible while
|
|
6
|
+
* gaining access to subscriptionOffers, pricingPhases, introductoryPrice, etc.
|
|
5
7
|
*
|
|
6
|
-
*
|
|
7
|
-
* calls like `getProducts()` / `getSubscriptions()`.
|
|
8
|
-
* See: `https://www.npmjs.com/package/react-native-iap`
|
|
8
|
+
* See: product-base/types.ts for the full definition.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
export type { PaywallBenefits, PaywallBenefitValue } from './benefits';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Minimal "product" representation (compatible with `react-native-iap` product-ish objects).
|
|
15
|
-
*
|
|
16
|
-
* Note: `react-native-iap` has slightly different fields per platform/product type.
|
|
17
|
-
* This is the common subset most paywalls need.
|
|
18
|
-
*/
|
|
19
|
-
export interface Product {
|
|
20
|
-
/** iOS: `productId`, Android: `productId` */
|
|
21
|
-
productId: string;
|
|
22
|
-
|
|
23
|
-
/** Display name / title from the store (when available). */
|
|
24
|
-
title?: string;
|
|
25
|
-
|
|
26
|
-
/** Description from the store (when available). */
|
|
27
|
-
description?: string;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Localized formatted price (e.g. "$4.99", "€9,99").
|
|
31
|
-
* In `react-native-iap` this is typically `localizedPrice`.
|
|
32
|
-
*/
|
|
33
|
-
localizedPrice?: string;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Raw price string from the store (often numeric string).
|
|
37
|
-
* In `react-native-iap` this is typically `price`.
|
|
38
|
-
*/
|
|
39
|
-
price?: string;
|
|
40
|
-
|
|
41
|
-
/** Currency code (e.g. "USD", "EUR"). */
|
|
42
|
-
currency?: string;
|
|
43
|
-
}
|
|
12
|
+
export type { Product } from '../../product-base/types';
|
|
44
13
|
|
|
45
14
|
/**
|
|
46
15
|
* A simple paywall model you can store/serialize.
|
|
47
16
|
*/
|
|
48
17
|
export interface PaywallModel {
|
|
49
|
-
product: Product[];
|
|
18
|
+
product: import('../../product-base/types').Product[];
|
|
50
19
|
benefits: import('./benefits').PaywallBenefits;
|
|
51
20
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure function that extracts all paywall localization params from a product.
|
|
3
|
+
*
|
|
4
|
+
* Platform-agnostic: caller provides `platform` to select iOS vs Android extraction.
|
|
5
|
+
* Optionally accepts a `localize` function for period text translation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
extractAndroidParams,
|
|
10
|
+
type AndroidParams,
|
|
11
|
+
} from './extractAndroidParams';
|
|
12
|
+
import { extractIOSParams, type IOSParams } from './extractIOSParams';
|
|
13
|
+
import {
|
|
14
|
+
getPeriodLocalizationKey,
|
|
15
|
+
PAYWALL_TEXT_KEYS,
|
|
16
|
+
} from './periodLocalizationKeys';
|
|
17
|
+
import type { Product, ProductParams } from './types';
|
|
18
|
+
|
|
19
|
+
type PeriodUnit = 'day' | 'week' | 'month' | 'year';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extracts paywall localization params from a product.
|
|
23
|
+
*
|
|
24
|
+
* @param product - The selected product (iOS or Android shape)
|
|
25
|
+
* @param platform - 'ios' | 'android' — determines which extractor runs
|
|
26
|
+
* @param offerId - Optional offer id (for promo/discount matching)
|
|
27
|
+
* @param localize - Optional function to translate period keys (falls back to identity)
|
|
28
|
+
*/
|
|
29
|
+
export function buildPaywallLocalizationParams(
|
|
30
|
+
product: Product,
|
|
31
|
+
platform: 'ios' | 'android',
|
|
32
|
+
offerId?: string,
|
|
33
|
+
localize?: (key: string) => string,
|
|
34
|
+
): ProductParams {
|
|
35
|
+
const resolve = localize ?? ((k: string) => k);
|
|
36
|
+
|
|
37
|
+
const extractedParams: AndroidParams | IOSParams =
|
|
38
|
+
platform === 'android'
|
|
39
|
+
? extractAndroidParams(product, offerId)
|
|
40
|
+
: extractIOSParams(product, offerId);
|
|
41
|
+
|
|
42
|
+
const period = (extractedParams.period || 'month') as PeriodUnit;
|
|
43
|
+
const hasPromo = !!extractedParams.promoPrice;
|
|
44
|
+
const hasTrial = extractedParams.hasTrial === 'true';
|
|
45
|
+
|
|
46
|
+
const periodKey = getPeriodLocalizationKey(period, false);
|
|
47
|
+
const promoPeriodKey = hasPromo ? getPeriodLocalizationKey(period, true) : '';
|
|
48
|
+
|
|
49
|
+
const localizedPromoPrice = hasPromo
|
|
50
|
+
? `${extractedParams.promoPrice} ${extractedParams.currency}`.trim()
|
|
51
|
+
: '';
|
|
52
|
+
|
|
53
|
+
const localizedPeriod = resolve(periodKey);
|
|
54
|
+
const localizedPromoPeriod = hasPromo ? resolve(promoPeriodKey) : '';
|
|
55
|
+
|
|
56
|
+
// Fallback-aware: use promo values when available, otherwise regular
|
|
57
|
+
const localizedCalculatedPrice =
|
|
58
|
+
localizedPromoPrice || extractedParams.localizedPrice;
|
|
59
|
+
const localizedCalculatedPeriod = localizedPromoPeriod || localizedPeriod;
|
|
60
|
+
|
|
61
|
+
// Pick the right pricing/promo template based on product state
|
|
62
|
+
let pricingTextKey: string;
|
|
63
|
+
let promoTextKey: string;
|
|
64
|
+
if (hasPromo) {
|
|
65
|
+
pricingTextKey = PAYWALL_TEXT_KEYS.pricingDefault;
|
|
66
|
+
promoTextKey = PAYWALL_TEXT_KEYS.promoDefault;
|
|
67
|
+
} else if (hasTrial) {
|
|
68
|
+
pricingTextKey = PAYWALL_TEXT_KEYS.pricingFreeTrial;
|
|
69
|
+
promoTextKey = PAYWALL_TEXT_KEYS.promoFreeTrial;
|
|
70
|
+
} else {
|
|
71
|
+
pricingTextKey = PAYWALL_TEXT_KEYS.pricingRegular;
|
|
72
|
+
promoTextKey = PAYWALL_TEXT_KEYS.promoRegular;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
price: extractedParams.price,
|
|
77
|
+
promoPrice: extractedParams.promoPrice,
|
|
78
|
+
currency: extractedParams.currency,
|
|
79
|
+
localizedPrice: extractedParams.localizedPrice,
|
|
80
|
+
period: extractedParams.period,
|
|
81
|
+
promoPeriod: extractedParams.promoPeriod,
|
|
82
|
+
promoPeriodUnit: extractedParams.promoPeriodUnit,
|
|
83
|
+
hasTrial: extractedParams.hasTrial,
|
|
84
|
+
trialPeriod: extractedParams.trialPeriod,
|
|
85
|
+
trialPeriodUnit: extractedParams.trialPeriodUnit,
|
|
86
|
+
discountPercentage: extractedParams.discountPercentage,
|
|
87
|
+
localizedPeriod,
|
|
88
|
+
localizedPromoPeriod,
|
|
89
|
+
localizedPromoPrice,
|
|
90
|
+
localizedCalculatedPrice,
|
|
91
|
+
localizedCalculatedPeriod,
|
|
92
|
+
baseLocalizedPricingText: resolve(pricingTextKey),
|
|
93
|
+
baseLocalizedPromoText: resolve(promoTextKey),
|
|
94
|
+
productTitle: String(product.title ?? ''),
|
|
95
|
+
productDescription: String(product.description ?? ''),
|
|
96
|
+
productCurreny: extractedParams.currency,
|
|
97
|
+
productId: String(product.productId ?? ''),
|
|
98
|
+
productSelected: 'true',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product calculation utilities
|
|
3
|
+
* Price, discount, period calculations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Fiyat string'inden sadece sayıları extract eder
|
|
8
|
+
* @example "$9.99" → "9.99"
|
|
9
|
+
* @example "€4,99" → "4.99"
|
|
10
|
+
*/
|
|
11
|
+
export function extractPrice(formattedPrice: string): string {
|
|
12
|
+
if (!formattedPrice) {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
return formattedPrice.replace(/[^0-9.]/g, '');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* İndirim yüzdesini hesaplar
|
|
20
|
+
* @returns Discount percentage as string (e.g. "50")
|
|
21
|
+
*/
|
|
22
|
+
export function calculateDiscount(
|
|
23
|
+
regularPrice: string,
|
|
24
|
+
promoPrice: string,
|
|
25
|
+
): string {
|
|
26
|
+
if (!promoPrice || !regularPrice) {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const regular = parseFloat(regularPrice);
|
|
31
|
+
const promo = parseFloat(promoPrice);
|
|
32
|
+
|
|
33
|
+
if (isNaN(regular) || isNaN(promo) || regular <= 0) {
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const discount = Math.round(((regular - promo) / regular) * 100);
|
|
38
|
+
return String(Math.max(0, discount));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Aylık eşdeğer fiyat hesaplar
|
|
43
|
+
* @param price - Fiyat (number)
|
|
44
|
+
* @param unit - Period unit (day/week/month/year)
|
|
45
|
+
* @returns Monthly price as string (e.g. "3.33")
|
|
46
|
+
*/
|
|
47
|
+
export function calculatePricePerMonth(price: number, unit: string): string {
|
|
48
|
+
if (isNaN(price)) {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (unit === 'month') {
|
|
53
|
+
return price.toFixed(2);
|
|
54
|
+
}
|
|
55
|
+
if (unit === 'year') {
|
|
56
|
+
return (price / 12).toFixed(2);
|
|
57
|
+
}
|
|
58
|
+
if (unit === 'week') {
|
|
59
|
+
return ((price * 52) / 12).toFixed(2);
|
|
60
|
+
}
|
|
61
|
+
if (unit === 'day') {
|
|
62
|
+
return ((price * 365) / 12).toFixed(2);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return price.toFixed(2);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Yıllık eşdeğer fiyat hesaplar
|
|
70
|
+
* @param price - Fiyat (number)
|
|
71
|
+
* @param unit - Period unit (day/week/month/year)
|
|
72
|
+
* @returns Yearly price as string (e.g. "119.88")
|
|
73
|
+
*/
|
|
74
|
+
export function calculatePricePerYear(price: number, unit: string): string {
|
|
75
|
+
if (isNaN(price)) {
|
|
76
|
+
return '';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (unit === 'year') {
|
|
80
|
+
return price.toFixed(2);
|
|
81
|
+
}
|
|
82
|
+
if (unit === 'month') {
|
|
83
|
+
return (price * 12).toFixed(2);
|
|
84
|
+
}
|
|
85
|
+
if (unit === 'week') {
|
|
86
|
+
return (price * 52).toFixed(2);
|
|
87
|
+
}
|
|
88
|
+
if (unit === 'day') {
|
|
89
|
+
return (price * 365).toFixed(2);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return price.toFixed(2);
|
|
93
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { parseBillingPeriod } from './periodLocalizationKeys';
|
|
2
|
+
import { iapLogger } from '../logger';
|
|
3
|
+
import {
|
|
4
|
+
extractPrice,
|
|
5
|
+
calculateDiscount,
|
|
6
|
+
calculatePricePerMonth,
|
|
7
|
+
calculatePricePerYear,
|
|
8
|
+
} from './calculations';
|
|
9
|
+
import type { Product, SubscriptionOffer, PricingPhase } from './types';
|
|
10
|
+
|
|
11
|
+
export interface AndroidParams {
|
|
12
|
+
price: string;
|
|
13
|
+
promoPrice: string;
|
|
14
|
+
currency: string;
|
|
15
|
+
localizedPrice: string;
|
|
16
|
+
period: string;
|
|
17
|
+
periodValue: string;
|
|
18
|
+
periodType: string;
|
|
19
|
+
promoPeriod: string;
|
|
20
|
+
promoCycles: string;
|
|
21
|
+
promoPeriodUnit: string;
|
|
22
|
+
hasTrial: string;
|
|
23
|
+
trialPeriod: string;
|
|
24
|
+
trialPeriodUnit: string;
|
|
25
|
+
discountPercentage: string;
|
|
26
|
+
pricePerMonth: string;
|
|
27
|
+
pricePerYear: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function findOffer(
|
|
31
|
+
subscriptionOffers: SubscriptionOffer[],
|
|
32
|
+
offerId: string,
|
|
33
|
+
): SubscriptionOffer | undefined {
|
|
34
|
+
return subscriptionOffers.find(
|
|
35
|
+
(offer) => offer.id === offerId || offer.basePlanIdAndroid === offerId,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Android product'tan params'ları extract eder
|
|
41
|
+
* pricingPhases'den trial, promo, regular bilgilerini parse eder
|
|
42
|
+
*/
|
|
43
|
+
export function extractAndroidParams(
|
|
44
|
+
product: Product,
|
|
45
|
+
offerId?: string,
|
|
46
|
+
): AndroidParams {
|
|
47
|
+
try {
|
|
48
|
+
const subscriptionOffers = product.subscriptionOffers ?? [];
|
|
49
|
+
|
|
50
|
+
let selectedOffer: SubscriptionOffer | undefined = subscriptionOffers[0];
|
|
51
|
+
|
|
52
|
+
if (offerId && subscriptionOffers.length > 0) {
|
|
53
|
+
const found = findOffer(subscriptionOffers, offerId);
|
|
54
|
+
if (found) {
|
|
55
|
+
selectedOffer = found;
|
|
56
|
+
} else {
|
|
57
|
+
iapLogger.error(
|
|
58
|
+
['extractAndroidParams'],
|
|
59
|
+
'Requested offer not found, using default',
|
|
60
|
+
{
|
|
61
|
+
productId: product.id || product.productId,
|
|
62
|
+
requestedOfferId: offerId,
|
|
63
|
+
availableOffers: subscriptionOffers.map((o) => o.id),
|
|
64
|
+
},
|
|
65
|
+
{ remote: true },
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!selectedOffer) {
|
|
71
|
+
iapLogger.warn(['extractAndroidParams'], 'No offers found in product', {
|
|
72
|
+
productId: product.id || product.productId,
|
|
73
|
+
});
|
|
74
|
+
return getEmptyParams();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const pricingPhases =
|
|
78
|
+
selectedOffer.pricingPhasesAndroid?.pricingPhaseList ?? [];
|
|
79
|
+
|
|
80
|
+
if (pricingPhases.length === 0) {
|
|
81
|
+
iapLogger.warn(['extractAndroidParams'], 'No pricing phases found', {
|
|
82
|
+
productId: product.id || product.productId,
|
|
83
|
+
offerId: selectedOffer.id,
|
|
84
|
+
});
|
|
85
|
+
return getEmptyParams();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Trial phase: priceAmountMicros === "0"
|
|
89
|
+
const trialPhase = pricingPhases.find(
|
|
90
|
+
(p: PricingPhase) => p.priceAmountMicros === '0',
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Promo phase: recurrenceMode === 2 (finite) ve ücretli
|
|
94
|
+
const promoPhase = pricingPhases.find(
|
|
95
|
+
(p: PricingPhase) =>
|
|
96
|
+
p.recurrenceMode === 2 && p.priceAmountMicros !== '0',
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Regular phase: recurrenceMode === 1 (infinite) veya son phase
|
|
100
|
+
const regularPhase =
|
|
101
|
+
pricingPhases.find((p: PricingPhase) => p.recurrenceMode === 1) ??
|
|
102
|
+
pricingPhases[pricingPhases.length - 1];
|
|
103
|
+
|
|
104
|
+
if (!regularPhase) {
|
|
105
|
+
iapLogger.error(
|
|
106
|
+
['extractAndroidParams'],
|
|
107
|
+
'No regular phase found',
|
|
108
|
+
{
|
|
109
|
+
productId: product.id || product.productId,
|
|
110
|
+
pricingPhasesCount: pricingPhases.length,
|
|
111
|
+
},
|
|
112
|
+
{ remote: true },
|
|
113
|
+
);
|
|
114
|
+
return getEmptyParams();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const regularPeriod = parseBillingPeriod(regularPhase.billingPeriod);
|
|
118
|
+
const periodType = `${regularPeriod.value} ${regularPeriod.unit}${regularPeriod.value > 1 ? 's' : ''}`;
|
|
119
|
+
|
|
120
|
+
const price = extractPrice(regularPhase.formattedPrice);
|
|
121
|
+
const currency = regularPhase.priceCurrencyCode || '';
|
|
122
|
+
const localizedPrice = regularPhase.formattedPrice || '';
|
|
123
|
+
|
|
124
|
+
let promoPrice = '';
|
|
125
|
+
let promoPeriod = '';
|
|
126
|
+
let promoCycles = '';
|
|
127
|
+
let promoPeriodUnit = '';
|
|
128
|
+
|
|
129
|
+
if (promoPhase) {
|
|
130
|
+
promoPrice = extractPrice(promoPhase.formattedPrice);
|
|
131
|
+
const cycles = promoPhase.billingCycleCount || 0;
|
|
132
|
+
const period = parseBillingPeriod(promoPhase.billingPeriod);
|
|
133
|
+
const totalValue = cycles * period.value;
|
|
134
|
+
promoPeriod = `${totalValue} ${period.unit}${totalValue > 1 ? 's' : ''}`;
|
|
135
|
+
promoCycles = String(cycles);
|
|
136
|
+
promoPeriodUnit = period.unit;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let hasTrial = 'false';
|
|
140
|
+
let trialPeriod = '';
|
|
141
|
+
let trialPeriodUnit = '';
|
|
142
|
+
|
|
143
|
+
if (trialPhase) {
|
|
144
|
+
hasTrial = 'true';
|
|
145
|
+
const period = parseBillingPeriod(trialPhase.billingPeriod);
|
|
146
|
+
trialPeriod = String(period.value);
|
|
147
|
+
trialPeriodUnit = period.unit;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const discountPercentage = calculateDiscount(price, promoPrice);
|
|
151
|
+
const priceNum = parseFloat(price);
|
|
152
|
+
const pricePerMonth = calculatePricePerMonth(priceNum, regularPeriod.unit);
|
|
153
|
+
const pricePerYear = calculatePricePerYear(priceNum, regularPeriod.unit);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
price,
|
|
157
|
+
promoPrice,
|
|
158
|
+
currency,
|
|
159
|
+
localizedPrice,
|
|
160
|
+
period: regularPeriod.unit,
|
|
161
|
+
periodValue: String(regularPeriod.value),
|
|
162
|
+
periodType,
|
|
163
|
+
promoPeriod,
|
|
164
|
+
promoCycles,
|
|
165
|
+
promoPeriodUnit,
|
|
166
|
+
hasTrial,
|
|
167
|
+
trialPeriod,
|
|
168
|
+
trialPeriodUnit,
|
|
169
|
+
discountPercentage,
|
|
170
|
+
pricePerMonth,
|
|
171
|
+
pricePerYear,
|
|
172
|
+
};
|
|
173
|
+
} catch (error) {
|
|
174
|
+
iapLogger.error(
|
|
175
|
+
['extractAndroidParams'],
|
|
176
|
+
'Failed to extract Android params',
|
|
177
|
+
{
|
|
178
|
+
productId: product?.id || product?.productId,
|
|
179
|
+
error: error instanceof Error ? error.message : String(error),
|
|
180
|
+
},
|
|
181
|
+
{ remote: true },
|
|
182
|
+
);
|
|
183
|
+
return getEmptyParams();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Boş params döner (fallback) */
|
|
188
|
+
function getEmptyParams(): AndroidParams {
|
|
189
|
+
return {
|
|
190
|
+
price: '',
|
|
191
|
+
promoPrice: '',
|
|
192
|
+
currency: '',
|
|
193
|
+
localizedPrice: '',
|
|
194
|
+
period: 'month',
|
|
195
|
+
periodValue: '1',
|
|
196
|
+
periodType: '1 month',
|
|
197
|
+
promoPeriod: '',
|
|
198
|
+
promoCycles: '',
|
|
199
|
+
promoPeriodUnit: '',
|
|
200
|
+
hasTrial: 'false',
|
|
201
|
+
trialPeriod: '',
|
|
202
|
+
trialPeriodUnit: '',
|
|
203
|
+
discountPercentage: '',
|
|
204
|
+
pricePerMonth: '',
|
|
205
|
+
pricePerYear: '',
|
|
206
|
+
};
|
|
207
|
+
}
|