@developer_tribe/react-builder 1.2.27 → 1.2.28

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.
Files changed (156) hide show
  1. package/dist/assets/samples/getSamples.d.ts +0 -3
  2. package/dist/build-components/BIcon/BIconProps.generated.d.ts +1 -2
  3. package/dist/build-components/CountDown/CountDownProps.generated.d.ts +2 -1
  4. package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +1 -2
  5. package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +1 -2
  6. package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +1 -2
  7. package/dist/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.d.ts +1 -2
  8. package/dist/build-components/PaywallOptions/usePaywallOptionParamsFactory.d.ts +1 -1
  9. package/dist/build-components/PriceTag/PriceTag.d.ts +5 -0
  10. package/dist/build-components/PriceTag/PriceTagProps.generated.d.ts +63 -0
  11. package/dist/build-components/Pricing/Pricing.d.ts +5 -0
  12. package/dist/build-components/Pricing/PricingProps.generated.d.ts +59 -0
  13. package/dist/build-components/Promo/Promo.d.ts +5 -0
  14. package/dist/build-components/Promo/PromoProps.generated.d.ts +59 -0
  15. package/dist/build-components/Text/TextProps.generated.d.ts +1 -2
  16. package/dist/build-components/index.d.ts +4 -1
  17. package/dist/build-components/patterns.generated.d.ts +1405 -202
  18. package/dist/components/BuilderProvider.d.ts +5 -3
  19. package/dist/components/ParamsProvider.d.ts +16 -8
  20. package/dist/hooks/useSyncHtmlThemeClass.d.ts +1 -1
  21. package/dist/index.cjs.js +4 -4
  22. package/dist/index.cjs.js.map +1 -1
  23. package/dist/index.d.ts +14 -3
  24. package/dist/index.esm.js +4 -4
  25. package/dist/index.esm.js.map +1 -1
  26. package/dist/index.web.cjs.js +4 -4
  27. package/dist/index.web.cjs.js.map +1 -1
  28. package/dist/index.web.esm.js +4 -4
  29. package/dist/index.web.esm.js.map +1 -1
  30. package/dist/logger.d.ts +18 -0
  31. package/dist/modals/InspectModal.d.ts +5 -0
  32. package/dist/modals/index.d.ts +1 -1
  33. package/dist/pages/ProjectPage.d.ts +3 -3
  34. package/dist/paywall/hooks/useCalculateLocalizedPrice.d.ts +4 -2
  35. package/dist/paywall/hooks/useDiscountRate.d.ts +3 -2
  36. package/dist/paywall/types/paywall-types.d.ts +7 -32
  37. package/dist/product-base/buildPaywallLocalizationParams.d.ts +16 -0
  38. package/dist/product-base/calculations.d.ts +29 -0
  39. package/dist/product-base/extractAndroidParams.d.ts +24 -0
  40. package/dist/product-base/extractIOSParams.d.ts +24 -0
  41. package/dist/product-base/index.d.ts +27 -0
  42. package/dist/product-base/periodLocalizationKeys.d.ts +44 -0
  43. package/dist/product-base/types.d.ts +155 -0
  44. package/dist/product-base/usePaywallLocalizationParams.d.ts +29 -0
  45. package/dist/store.d.ts +7 -1
  46. package/dist/styles.css +1 -1
  47. package/dist/types/PreviewConfig.d.ts +10 -16
  48. package/dist/utils/extractTextStyle/extractTextStyle.d.ts +2 -2
  49. package/dist/utils/extractTextStyle/extractTextStyleNative.d.ts +2 -2
  50. package/dist/utils/replaceLocalizationParams.d.ts +1 -1
  51. package/package.json +2 -2
  52. package/scripts/migrate-samples-to-current.ts +3 -3
  53. package/scripts/prebuild/utils/validateAllComponentsOrThrow.js +28 -12
  54. package/src/DeviceMockFrame.tsx +15 -10
  55. package/src/assets/meta.json +1 -1
  56. package/src/assets/samples/carousel-sample.json +6 -5
  57. package/src/assets/samples/getSamples.ts +16 -49
  58. package/src/assets/samples/paywall-1.json +64 -22
  59. package/src/assets/samples/paywall-2.json +0 -15
  60. package/src/assets/samples/paywall-app-delete-offer.json +0 -15
  61. package/src/assets/samples/paywall-app-open-offer.json +0 -15
  62. package/src/assets/samples/paywall-back-offer.json +0 -15
  63. package/src/assets/samples/paywall-notification-offer.json +0 -15
  64. package/src/assets/samples/simple-1.json +1 -16
  65. package/src/assets/samples/simple-2.json +0 -15
  66. package/src/assets/samples/unmigrated-builder-1.1.1.json +0 -3
  67. package/src/assets/samples/unmigrated-builder1.json +0 -3
  68. package/src/assets/samples/unvalidated-builder1.json +0 -3
  69. package/src/assets/samples/unvalidated-crash1.json +0 -3
  70. package/src/assets/samples/unvalidated-crashcomponent1.json +0 -3
  71. package/src/assets/samples/vpn-onboard-1.json +1 -34
  72. package/src/assets/samples/vpn-onboard-2.json +1 -34
  73. package/src/assets/samples/vpn-onboard-3.json +1 -42
  74. package/src/assets/samples/vpn-onboard-4.json +0 -73
  75. package/src/assets/samples/vpn-onboard-5.json +0 -73
  76. package/src/assets/samples/vpn-onboard-6.json +0 -73
  77. package/src/assets/samples/vpn-onboard-7.json +529 -0
  78. package/src/attribute-analyser/style/native/useExtractImageStyle.ts +1 -4
  79. package/src/attribute-analyser/style/native/useExtractTextStyle.ts +3 -12
  80. package/src/attribute-analyser/style/native/useExtractViewStyle.ts +1 -4
  81. package/src/attribute-analyser/style/web/useExtractImageStyle.ts +1 -4
  82. package/src/attribute-analyser/style/web/useExtractTextStyle.ts +3 -12
  83. package/src/attribute-analyser/style/web/useExtractViewStyle.ts +1 -4
  84. package/src/attributes-editor/useAttributesEditorModel.ts +5 -52
  85. package/src/build-components/BIcon/BIconProps.generated.ts +1 -2
  86. package/src/build-components/CarouselDots/CarouselDots.tsx +6 -13
  87. package/src/build-components/CountDown/CountDownProps.generated.ts +2 -1
  88. package/src/build-components/NavigationBarColor/NavigationBarColor.tsx +2 -2
  89. package/src/build-components/OnboardButton/OnboardButton.tsx +1 -2
  90. package/src/build-components/OnboardDot/OnboardDot.tsx +6 -18
  91. package/src/build-components/OnboardFooter/OnboardFooter.tsx +5 -3
  92. package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +1 -2
  93. package/src/build-components/OnboardFooter/pattern.json +1 -1
  94. package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +1 -2
  95. package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +1 -2
  96. package/src/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.ts +1 -2
  97. package/src/build-components/PaywallOptions/PaywallOptions.tsx +3 -3
  98. package/src/build-components/PaywallOptions/usePaywallOptionParamsFactory.ts +26 -13
  99. package/src/build-components/PaywallProvider/PaywallProvider.tsx +51 -12
  100. package/src/build-components/PriceTag/PriceTag.tsx +25 -0
  101. package/src/build-components/PriceTag/PriceTagProps.generated.ts +83 -0
  102. package/src/build-components/PriceTag/pattern.json +53 -0
  103. package/src/build-components/Pricing/Pricing.tsx +13 -0
  104. package/src/build-components/Pricing/PricingProps.generated.ts +76 -0
  105. package/src/build-components/Pricing/pattern.json +25 -0
  106. package/src/build-components/Promo/Promo.tsx +13 -0
  107. package/src/build-components/Promo/PromoProps.generated.ts +76 -0
  108. package/src/build-components/Promo/pattern.json +25 -0
  109. package/src/build-components/RadioButton/RadioButton.tsx +3 -5
  110. package/src/build-components/RenderNode.generated.tsx +15 -0
  111. package/src/build-components/StatusBarColor/StatusBarColor.tsx +2 -2
  112. package/src/build-components/Text/Text.tsx +12 -5
  113. package/src/build-components/Text/TextProps.generated.ts +1 -2
  114. package/src/build-components/Text/pattern.json +3 -2
  115. package/src/build-components/index.ts +15 -0
  116. package/src/build-components/patterns.generated.ts +1454 -181
  117. package/src/components/BottomBar.tsx +42 -39
  118. package/src/components/BuilderProvider.tsx +41 -14
  119. package/src/components/LocalizationParamsProvider.tsx +1 -1
  120. package/src/components/ParamsProvider.tsx +36 -11
  121. package/src/hooks/useLocalize.ts +7 -4
  122. package/src/hooks/useParams.ts +1 -1
  123. package/src/hooks/useSyncHtmlThemeClass.ts +2 -2
  124. package/src/index.ts +52 -8
  125. package/src/logger.ts +39 -0
  126. package/src/modals/InspectModal.tsx +331 -0
  127. package/src/modals/ProductPresetsModal.tsx +6 -13
  128. package/src/modals/index.ts +1 -1
  129. package/src/pages/DebugJsonPage.tsx +9 -22
  130. package/src/pages/ProjectDebug.tsx +1 -1
  131. package/src/pages/ProjectPage.tsx +29 -11
  132. package/src/pages/tabs/SideTool.tsx +28 -104
  133. package/src/paywall/hooks/useCalculateLocalizedPrice.ts +8 -3
  134. package/src/paywall/hooks/useDiscountRate.ts +11 -3
  135. package/src/paywall/types/paywall-types.ts +7 -38
  136. package/src/product-base/buildPaywallLocalizationParams.ts +100 -0
  137. package/src/product-base/calculations.ts +93 -0
  138. package/src/product-base/extractAndroidParams.ts +207 -0
  139. package/src/product-base/extractIOSParams.ts +199 -0
  140. package/src/product-base/index.ts +28 -0
  141. package/src/product-base/mockProducts.json +489 -0
  142. package/src/product-base/periodLocalizationKeys.ts +114 -0
  143. package/src/product-base/types.ts +183 -0
  144. package/src/product-base/usePaywallLocalizationParams.ts +61 -0
  145. package/src/store.ts +18 -1
  146. package/src/styles/index.scss +1 -0
  147. package/src/styles/modals/_inspect-modal.scss +155 -0
  148. package/src/types/PreviewConfig.ts +157 -16
  149. package/src/utils/extractTextStyle/extractTextStyle.ts +14 -6
  150. package/src/utils/extractTextStyle/extractTextStyleNative.ts +8 -6
  151. package/src/utils/logRenderStore.ts +6 -10
  152. package/src/utils/parseColor.ts +0 -1
  153. package/src/utils/replaceLocalizationParams.ts +8 -4
  154. package/dist/modals/ScreenColorsModal.d.ts +0 -8
  155. package/src/assets/products.json +0 -98
  156. 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 { appConfig, setAppConfig, previewMode, setPreviewMode } =
57
- useRenderStore((s) => ({
58
- appConfig: s.appConfig,
59
- setAppConfig: s.setAppConfig,
60
- previewMode: s.previewMode,
61
- setPreviewMode: s.setPreviewMode,
62
- }));
63
-
64
- const getScreenColorValue = (mode: ScreenMode, key: ScreenColorKey) =>
65
- appConfig.screenStyle?.[mode]?.[key] ?? screenStyleDefaults[mode][key];
66
-
67
- const handleScreenStyleChange = (
68
- mode: ScreenMode,
69
- key: ScreenColorKey,
70
- value: string,
71
- ) => {
72
- setAppConfig({
73
- ...appConfig,
74
- screenStyle: {
75
- ...screenStyleDefaults,
76
- ...appConfig.screenStyle,
77
- [mode]: {
78
- ...screenStyleDefaults[mode],
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={appConfig.defaultLanguage ?? 'en'}
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={appConfig.theme === 'dark'}
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
- * Placeholder hook will be implemented later.
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
- * Placeholder hook will be implemented later.
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(): number {
5
- return 0;
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
- * Simple paywall-related types.
2
+ * Paywall-related types.
3
3
  *
4
- * We keep this intentionally small and runtime-agnostic.
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
- * Product fields are based on the common shape returned by `react-native-iap`
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
+ }