@developer_tribe/react-builder 1.2.44 → 1.2.46
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/components/DeviceButton.d.ts +2 -1
- package/dist/index.cjs.js +28 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +28 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.web.cjs.js +3 -3
- package/dist/index.web.cjs.js.map +1 -1
- package/dist/index.web.esm.js +3 -3
- package/dist/index.web.esm.js.map +1 -1
- package/dist/modals/CreateDeviceModal.d.ts +8 -0
- package/dist/product-base/periodLocalizationKeys.d.ts +16 -0
- package/dist/store/customDeviceStore.d.ts +21 -0
- package/dist/store.d.ts +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/.DS_Store +0 -0
- package/src/assets/.DS_Store +0 -0
- package/src/assets/meta.json +1 -1
- package/src/build-components/FormCheckbox/FormCheckbox.tsx +2 -0
- package/src/build-components/Text/Text.tsx +2 -3
- package/src/components/DeviceButton.tsx +34 -1
- package/src/components/EditorHeader.tsx +22 -4
- package/src/modals/CreateDeviceModal.tsx +264 -0
- package/src/modals/DeviceSelectorModal.tsx +44 -7
- package/src/product-base/calculations.ts +30 -1
- package/src/product-base/extractAndroidParams.ts +2 -3
- package/src/product-base/extractIOSParams.ts +14 -8
- package/src/product-base/mockProducts.json +102 -0
- package/src/product-base/periodLocalizationKeys.ts +46 -0
- package/src/store/customDeviceStore.ts +38 -0
- package/src/styles/components/_editor-shell.scss +12 -2
- package/src/styles/index.scss +1 -0
- package/src/styles/modals/_create-device.scss +113 -0
- package/src/utils/__special_exceptions.ts +8 -0
- package/src/utils/analyseNodeByPatterns.ts +8 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useForm } from 'react-hook-form';
|
|
3
|
+
import Modal from './Modal';
|
|
4
|
+
import { Device } from '../types/Device';
|
|
5
|
+
import { useCustomDeviceStore } from '../store/customDeviceStore';
|
|
6
|
+
|
|
7
|
+
type CreateDeviceModalProps = {
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
onSuccess: (device: Device) => void;
|
|
10
|
+
deviceToEdit?: Device;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type FormData = {
|
|
14
|
+
name: string;
|
|
15
|
+
platform: 'ios' | 'android';
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
type: 'phone' | 'tablet';
|
|
19
|
+
radius: number;
|
|
20
|
+
insetTop: number;
|
|
21
|
+
insetRight: number;
|
|
22
|
+
insetBottom: number;
|
|
23
|
+
insetLeft: number;
|
|
24
|
+
navigationBarType: Device['navigationBarType'];
|
|
25
|
+
importance: number;
|
|
26
|
+
multiplier: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function CreateDeviceModal({
|
|
30
|
+
onClose,
|
|
31
|
+
onSuccess,
|
|
32
|
+
deviceToEdit,
|
|
33
|
+
}: CreateDeviceModalProps) {
|
|
34
|
+
const { addCustomDevice, updateCustomDevice } = useCustomDeviceStore();
|
|
35
|
+
const {
|
|
36
|
+
register,
|
|
37
|
+
handleSubmit,
|
|
38
|
+
watch,
|
|
39
|
+
formState: { errors },
|
|
40
|
+
} = useForm<FormData>({
|
|
41
|
+
defaultValues: deviceToEdit
|
|
42
|
+
? {
|
|
43
|
+
name: deviceToEdit.name,
|
|
44
|
+
platform: deviceToEdit.platform as 'ios' | 'android',
|
|
45
|
+
type: deviceToEdit.type,
|
|
46
|
+
width: deviceToEdit.width,
|
|
47
|
+
height: deviceToEdit.height,
|
|
48
|
+
insetTop: deviceToEdit.insets?.[0] ?? 0,
|
|
49
|
+
insetRight: deviceToEdit.insets?.[1] ?? 0,
|
|
50
|
+
insetBottom: deviceToEdit.insets?.[2] ?? 0,
|
|
51
|
+
insetLeft: deviceToEdit.insets?.[3] ?? 0,
|
|
52
|
+
navigationBarType: deviceToEdit.navigationBarType,
|
|
53
|
+
radius: deviceToEdit.radius,
|
|
54
|
+
importance: deviceToEdit.importance,
|
|
55
|
+
multiplier: deviceToEdit.multiplier,
|
|
56
|
+
}
|
|
57
|
+
: {
|
|
58
|
+
platform: 'ios',
|
|
59
|
+
type: 'phone',
|
|
60
|
+
navigationBarType: 'homeIndicator',
|
|
61
|
+
width: 375,
|
|
62
|
+
height: 812,
|
|
63
|
+
insetTop: 44,
|
|
64
|
+
insetBottom: 34,
|
|
65
|
+
insetLeft: 0,
|
|
66
|
+
insetRight: 0,
|
|
67
|
+
radius: 40,
|
|
68
|
+
importance: 10,
|
|
69
|
+
multiplier: 1,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const platform = watch('platform');
|
|
74
|
+
|
|
75
|
+
const onSubmit = (data: FormData) => {
|
|
76
|
+
const newDevice: Device = {
|
|
77
|
+
name: data.name,
|
|
78
|
+
platform: data.platform,
|
|
79
|
+
width: Number(data.width),
|
|
80
|
+
height: Number(data.height),
|
|
81
|
+
type: data.type,
|
|
82
|
+
radius: Number(data.radius),
|
|
83
|
+
insets: [
|
|
84
|
+
Number(data.insetTop),
|
|
85
|
+
Number(data.insetRight),
|
|
86
|
+
Number(data.insetBottom),
|
|
87
|
+
Number(data.insetLeft),
|
|
88
|
+
],
|
|
89
|
+
navigationBarType: data.navigationBarType,
|
|
90
|
+
aspect: (() => {
|
|
91
|
+
const r = Number(data.height) / Number(data.width);
|
|
92
|
+
if (r >= 2.05) return 'tall';
|
|
93
|
+
if (r <= 1.75) return 'wide';
|
|
94
|
+
return 'regular';
|
|
95
|
+
})(),
|
|
96
|
+
importance: Number(data.importance),
|
|
97
|
+
multiplier: Number(data.multiplier),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (deviceToEdit) {
|
|
101
|
+
updateCustomDevice(deviceToEdit.name, newDevice);
|
|
102
|
+
} else {
|
|
103
|
+
addCustomDevice(newDevice);
|
|
104
|
+
}
|
|
105
|
+
onSuccess(newDevice);
|
|
106
|
+
onClose();
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<Modal
|
|
111
|
+
onClose={onClose}
|
|
112
|
+
ariaLabelledBy="create-device-title"
|
|
113
|
+
contentClassName="create-device-modal"
|
|
114
|
+
>
|
|
115
|
+
<div className="modal__header">
|
|
116
|
+
<h3 id="create-device-title" className="modal__title">
|
|
117
|
+
{deviceToEdit ? 'Edit Custom Device' : 'Create Custom Device'}
|
|
118
|
+
</h3>
|
|
119
|
+
<button type="button" className="editor-button" onClick={onClose}>
|
|
120
|
+
Cancel
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
<form
|
|
124
|
+
onSubmit={handleSubmit(onSubmit)}
|
|
125
|
+
className="modal__body create-device-form"
|
|
126
|
+
>
|
|
127
|
+
<div className="create-device-form__group">
|
|
128
|
+
<label className="create-device-form__label">Device Name</label>
|
|
129
|
+
<input
|
|
130
|
+
{...register('name', { required: 'Name is required' })}
|
|
131
|
+
className="editor-input"
|
|
132
|
+
placeholder="e.g. My Custom iPhone"
|
|
133
|
+
/>
|
|
134
|
+
{errors.name && (
|
|
135
|
+
<span className="create-device-form__error">
|
|
136
|
+
{errors.name.message}
|
|
137
|
+
</span>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div className="create-device-form__row">
|
|
142
|
+
<div className="create-device-form__group">
|
|
143
|
+
<label className="create-device-form__label">Platform</label>
|
|
144
|
+
<select {...register('platform')} className="editor-input">
|
|
145
|
+
<option value="ios">iOS</option>
|
|
146
|
+
<option value="android">Android</option>
|
|
147
|
+
</select>
|
|
148
|
+
</div>
|
|
149
|
+
<div className="create-device-form__group">
|
|
150
|
+
<label className="create-device-form__label">Type</label>
|
|
151
|
+
<select {...register('type')} className="editor-input">
|
|
152
|
+
<option value="phone">Phone</option>
|
|
153
|
+
<option value="tablet">Tablet</option>
|
|
154
|
+
</select>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div className="create-device-form__row">
|
|
159
|
+
<div className="create-device-form__group">
|
|
160
|
+
<label className="create-device-form__label">Width (px)</label>
|
|
161
|
+
<input
|
|
162
|
+
type="number"
|
|
163
|
+
{...register('width', { required: true, min: 1 })}
|
|
164
|
+
className="editor-input"
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
<div className="create-device-form__group">
|
|
168
|
+
<label className="create-device-form__label">Height (px)</label>
|
|
169
|
+
<input
|
|
170
|
+
type="number"
|
|
171
|
+
{...register('height', { required: true, min: 1 })}
|
|
172
|
+
className="editor-input"
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div className="create-device-form__row">
|
|
178
|
+
<div className="create-device-form__group">
|
|
179
|
+
<label className="create-device-form__label">Safe Area Top</label>
|
|
180
|
+
<input
|
|
181
|
+
type="number"
|
|
182
|
+
{...register('insetTop', { min: 0 })}
|
|
183
|
+
className="editor-input"
|
|
184
|
+
/>
|
|
185
|
+
</div>
|
|
186
|
+
<div className="create-device-form__group">
|
|
187
|
+
<label className="create-device-form__label">
|
|
188
|
+
Safe Area Bottom
|
|
189
|
+
</label>
|
|
190
|
+
<input
|
|
191
|
+
type="number"
|
|
192
|
+
{...register('insetBottom', { min: 0 })}
|
|
193
|
+
className="editor-input"
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div className="create-device-form__row">
|
|
199
|
+
<div className="create-device-form__group">
|
|
200
|
+
<label className="create-device-form__label">Navigation Bar</label>
|
|
201
|
+
<select {...register('navigationBarType')} className="editor-input">
|
|
202
|
+
<option value="none">None</option>
|
|
203
|
+
{platform === 'ios' ? (
|
|
204
|
+
<>
|
|
205
|
+
<option value="homeIndicator">Home Indicator</option>
|
|
206
|
+
<option value="tabBar">Tab Bar</option>
|
|
207
|
+
</>
|
|
208
|
+
) : (
|
|
209
|
+
<>
|
|
210
|
+
<option value="threeButtons">Three Buttons</option>
|
|
211
|
+
<option value="gesture">Gesture</option>
|
|
212
|
+
</>
|
|
213
|
+
)}
|
|
214
|
+
</select>
|
|
215
|
+
</div>
|
|
216
|
+
<div className="create-device-form__group">
|
|
217
|
+
<label className="create-device-form__label">Corner Radius</label>
|
|
218
|
+
<input
|
|
219
|
+
type="number"
|
|
220
|
+
{...register('radius', { min: 0 })}
|
|
221
|
+
className="editor-input"
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div className="create-device-form__row">
|
|
227
|
+
<div className="create-device-form__group">
|
|
228
|
+
<label className="create-device-form__label">
|
|
229
|
+
Scale Multiplier
|
|
230
|
+
</label>
|
|
231
|
+
<input
|
|
232
|
+
type="number"
|
|
233
|
+
step="0.01"
|
|
234
|
+
{...register('multiplier', { min: 0.1, max: 2 })}
|
|
235
|
+
className="editor-input"
|
|
236
|
+
/>
|
|
237
|
+
</div>
|
|
238
|
+
<div className="create-device-form__group">
|
|
239
|
+
<label className="create-device-form__label">
|
|
240
|
+
Importance (1-100)
|
|
241
|
+
</label>
|
|
242
|
+
<input
|
|
243
|
+
type="number"
|
|
244
|
+
{...register('importance', { min: 1, max: 100 })}
|
|
245
|
+
className="editor-input"
|
|
246
|
+
/>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<div className="create-device-modal__footer">
|
|
251
|
+
<button
|
|
252
|
+
type="submit"
|
|
253
|
+
className="editor-button editor-button--primary"
|
|
254
|
+
style={{ width: '100%' }}
|
|
255
|
+
>
|
|
256
|
+
{deviceToEdit ? 'Update Device' : 'Save Device'}
|
|
257
|
+
</button>
|
|
258
|
+
</div>
|
|
259
|
+
</form>
|
|
260
|
+
</Modal>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export default CreateDeviceModal;
|
|
@@ -3,6 +3,8 @@ import { Device } from '../types/Device';
|
|
|
3
3
|
import Modal from './Modal';
|
|
4
4
|
import { DeviceButton } from '../components/DeviceButton';
|
|
5
5
|
import { useRenderStore } from '../store';
|
|
6
|
+
import { useCustomDeviceStore } from '../store/customDeviceStore';
|
|
7
|
+
import { CreateDeviceModal } from './CreateDeviceModal';
|
|
6
8
|
|
|
7
9
|
type DeviceSelectorModalProps = {
|
|
8
10
|
devices: Device[];
|
|
@@ -18,10 +20,17 @@ export function DeviceSelectorModal({
|
|
|
18
20
|
onClose,
|
|
19
21
|
}: DeviceSelectorModalProps) {
|
|
20
22
|
const [searchTerm, setSearchTerm] = useState('');
|
|
23
|
+
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
|
24
|
+
const [editingDevice, setEditingDevice] = useState<Device | null>(null);
|
|
21
25
|
const { favoriteDevices, toggleFavoriteDevice } = useRenderStore((state) => ({
|
|
22
26
|
favoriteDevices: state.favoriteDevices || [],
|
|
23
27
|
toggleFavoriteDevice: state.toggleFavoriteDevice,
|
|
24
28
|
}));
|
|
29
|
+
const { customDevices, removeCustomDevice } = useCustomDeviceStore();
|
|
30
|
+
|
|
31
|
+
const allDevices = useMemo(() => {
|
|
32
|
+
return [...customDevices, ...devices];
|
|
33
|
+
}, [customDevices, devices]);
|
|
25
34
|
|
|
26
35
|
const handleDeviceSelect = (device: Device) => {
|
|
27
36
|
onSelect(device);
|
|
@@ -29,10 +38,10 @@ export function DeviceSelectorModal({
|
|
|
29
38
|
};
|
|
30
39
|
|
|
31
40
|
const filteredDevices = useMemo(() => {
|
|
32
|
-
return
|
|
41
|
+
return allDevices.filter((device) =>
|
|
33
42
|
device.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
34
43
|
);
|
|
35
|
-
}, [
|
|
44
|
+
}, [allDevices, searchTerm]);
|
|
36
45
|
|
|
37
46
|
const { favorites, others } = useMemo(() => {
|
|
38
47
|
const favs: Device[] = [];
|
|
@@ -66,18 +75,21 @@ export function DeviceSelectorModal({
|
|
|
66
75
|
Close
|
|
67
76
|
</button>
|
|
68
77
|
</div>
|
|
69
|
-
<div
|
|
70
|
-
className="device-selector-modal__search"
|
|
71
|
-
style={{ padding: '0 16px', marginBottom: '8px' }}
|
|
72
|
-
>
|
|
78
|
+
<div className="device-selector-modal__search-container">
|
|
73
79
|
<input
|
|
74
80
|
type="text"
|
|
75
81
|
placeholder="Search devices..."
|
|
76
82
|
value={searchTerm}
|
|
77
83
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
78
84
|
className="editor-input"
|
|
79
|
-
style={{ width: '100%', padding: '8px', boxSizing: 'border-box' }}
|
|
80
85
|
/>
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
className="editor-button editor-button--primary"
|
|
89
|
+
onClick={() => setIsCreateModalOpen(true)}
|
|
90
|
+
>
|
|
91
|
+
Add New
|
|
92
|
+
</button>
|
|
81
93
|
</div>
|
|
82
94
|
<div
|
|
83
95
|
className="device-selector-modal__body"
|
|
@@ -103,6 +115,7 @@ export function DeviceSelectorModal({
|
|
|
103
115
|
onSelect={handleDeviceSelect}
|
|
104
116
|
isFavorite={true}
|
|
105
117
|
onToggleFavorite={(d) => toggleFavoriteDevice(d.name)}
|
|
118
|
+
onEdit={(d) => setEditingDevice(d)}
|
|
106
119
|
/>
|
|
107
120
|
))}
|
|
108
121
|
</div>
|
|
@@ -129,11 +142,35 @@ export function DeviceSelectorModal({
|
|
|
129
142
|
onSelect={handleDeviceSelect}
|
|
130
143
|
isFavorite={false}
|
|
131
144
|
onToggleFavorite={(d) => toggleFavoriteDevice(d.name)}
|
|
145
|
+
onEdit={
|
|
146
|
+
customDevices.some((cd) => cd.name === device.name)
|
|
147
|
+
? (d) => setEditingDevice(d)
|
|
148
|
+
: undefined
|
|
149
|
+
}
|
|
132
150
|
/>
|
|
133
151
|
))}
|
|
134
152
|
</div>
|
|
135
153
|
</section>
|
|
136
154
|
</div>
|
|
155
|
+
{isCreateModalOpen && (
|
|
156
|
+
<CreateDeviceModal
|
|
157
|
+
onClose={() => setIsCreateModalOpen(false)}
|
|
158
|
+
onSuccess={(newDevice) => {
|
|
159
|
+
onSelect(newDevice);
|
|
160
|
+
onClose();
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
)}
|
|
164
|
+
{editingDevice && (
|
|
165
|
+
<CreateDeviceModal
|
|
166
|
+
deviceToEdit={editingDevice}
|
|
167
|
+
onClose={() => setEditingDevice(null)}
|
|
168
|
+
onSuccess={(updatedDevice) => {
|
|
169
|
+
onSelect(updatedDevice);
|
|
170
|
+
onClose();
|
|
171
|
+
}}
|
|
172
|
+
/>
|
|
173
|
+
)}
|
|
137
174
|
</Modal>
|
|
138
175
|
);
|
|
139
176
|
}
|
|
@@ -12,7 +12,36 @@ export function extractPrice(formattedPrice: string): string {
|
|
|
12
12
|
if (!formattedPrice) {
|
|
13
13
|
return '';
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
// Remove currency symbols and other non-numeric chars except dot/comma
|
|
16
|
+
const cleaned = formattedPrice.replace(/[^0-9.,]/g, '');
|
|
17
|
+
|
|
18
|
+
// If there's both a dot and a comma, comma is likely decimal (e.g. 1.999,99)
|
|
19
|
+
// If there's multiple dots, dots are likely thousands and comma is decimal (e.g. 1.999,99)
|
|
20
|
+
// If there's only a comma, it's decimal (e.g. 149,99)
|
|
21
|
+
|
|
22
|
+
let numeric: string;
|
|
23
|
+
if (cleaned.includes(',') && cleaned.includes('.')) {
|
|
24
|
+
// Mixed: 1.234,56 -> 1234.56
|
|
25
|
+
numeric = cleaned.replace(/\./g, '').replace(',', '.');
|
|
26
|
+
} else if (cleaned.includes(',')) {
|
|
27
|
+
// Only comma: 149,99 -> 149.99
|
|
28
|
+
numeric = cleaned.replace(',', '.');
|
|
29
|
+
} else {
|
|
30
|
+
// Only dots or none: 1,999.99 (impossible with logic above but for safety) or 9.99
|
|
31
|
+
// If multiple dots, they are thousand separators
|
|
32
|
+
const dots = (cleaned.match(/\./g) || []).length;
|
|
33
|
+
if (dots > 1) {
|
|
34
|
+
numeric = cleaned.replace(/\./g, '');
|
|
35
|
+
} else {
|
|
36
|
+
numeric = cleaned;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const parsed = parseFloat(numeric);
|
|
41
|
+
if (isNaN(parsed)) return '';
|
|
42
|
+
|
|
43
|
+
// Round to 2 decimal places to avoid precision issues like 1999.9900000000002
|
|
44
|
+
return String(Math.round(parsed * 100) / 100);
|
|
16
45
|
}
|
|
17
46
|
|
|
18
47
|
/**
|
|
@@ -195,9 +195,8 @@ export function extractAndroidParams(
|
|
|
195
195
|
|
|
196
196
|
/** Boş params ama mock product price'ları ile birleştirilmiş (fallback) */
|
|
197
197
|
function getFallbackParams(product: Product): AndroidParams {
|
|
198
|
-
const price =
|
|
199
|
-
|
|
200
|
-
'',
|
|
198
|
+
const price = extractPrice(
|
|
199
|
+
String(product.price || product.localizedPrice || ''),
|
|
201
200
|
);
|
|
202
201
|
return {
|
|
203
202
|
...getEmptyParams(),
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
convertIOSPeriodUnit,
|
|
3
|
+
normalizeIOSPeriod,
|
|
4
|
+
} from './periodLocalizationKeys';
|
|
2
5
|
import { iapLogger } from '../logger';
|
|
3
6
|
import {
|
|
7
|
+
extractPrice,
|
|
4
8
|
calculateDiscount,
|
|
5
9
|
calculatePricePerMonth,
|
|
6
10
|
calculatePricePerYear,
|
|
@@ -53,17 +57,19 @@ export function extractIOSParams(
|
|
|
53
57
|
offerId?: string,
|
|
54
58
|
): IOSParams {
|
|
55
59
|
try {
|
|
56
|
-
const price =
|
|
57
|
-
|
|
58
|
-
'',
|
|
60
|
+
const price = extractPrice(
|
|
61
|
+
String(product.price || product.localizedPrice || ''),
|
|
59
62
|
);
|
|
60
63
|
const currency = product.currency || product.currencyCode || '';
|
|
61
64
|
const localizedPrice = product.localizedPrice || '';
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
// iOS bazen WEEK yerine DAY+7 döndürür — normalize et
|
|
67
|
+
const normalizedPeriod = normalizeIOSPeriod(
|
|
68
|
+
product.subscriptionPeriodUnitIOS,
|
|
69
|
+
product.subscriptionPeriodNumberIOS,
|
|
65
70
|
);
|
|
66
|
-
const
|
|
71
|
+
const periodUnit = normalizedPeriod.unit;
|
|
72
|
+
const periodValue = String(normalizedPeriod.value);
|
|
67
73
|
const periodType = `${periodValue} ${periodUnit}${parseInt(periodValue, 10) > 1 ? 's' : ''}`;
|
|
68
74
|
|
|
69
75
|
const introPrice =
|
|
@@ -114,7 +120,7 @@ export function extractIOSParams(
|
|
|
114
120
|
);
|
|
115
121
|
|
|
116
122
|
if (discount) {
|
|
117
|
-
promoPrice = String(discount.price || '')
|
|
123
|
+
promoPrice = extractPrice(String(discount.price || ''));
|
|
118
124
|
const cycles = discount.numberOfPeriods || 1;
|
|
119
125
|
const unit = convertIOSPeriodUnit(
|
|
120
126
|
discount.subscriptionPeriod || 'MONTH',
|
|
@@ -485,5 +485,107 @@
|
|
|
485
485
|
"subscriptionPeriodUnitIOS": "YEAR",
|
|
486
486
|
"subscriptionPeriodNumberIOS": 1
|
|
487
487
|
}
|
|
488
|
+
],
|
|
489
|
+
"vpn-pro": [
|
|
490
|
+
{
|
|
491
|
+
"productId": "com.vpn111.pro.weekly",
|
|
492
|
+
"title": "Pro Subscription (Weekly)",
|
|
493
|
+
"description": "Pro Subscription (Weekly)",
|
|
494
|
+
"localizedPrice": "₺179,99",
|
|
495
|
+
"price": "179.99",
|
|
496
|
+
"currency": "TRY",
|
|
497
|
+
"type": "subs",
|
|
498
|
+
"subscriptionOffers": [],
|
|
499
|
+
"discounts": [],
|
|
500
|
+
"subscriptionPeriodNumberIOS": 7,
|
|
501
|
+
"subscriptionPeriodUnitIOS": "DAY",
|
|
502
|
+
"isConsumable": false
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
"productId": "com.vpn111.pro.monthly",
|
|
506
|
+
"title": "Pro Subscription (Monthly)",
|
|
507
|
+
"description": "Pro Subscription (Monthly)",
|
|
508
|
+
"localizedPrice": "₺399,99",
|
|
509
|
+
"price": "399.99",
|
|
510
|
+
"currency": "TRY",
|
|
511
|
+
"type": "subs",
|
|
512
|
+
"subscriptionOffers": [],
|
|
513
|
+
"discounts": [],
|
|
514
|
+
"subscriptionPeriodNumberIOS": 1,
|
|
515
|
+
"subscriptionPeriodUnitIOS": "MONTH",
|
|
516
|
+
"isConsumable": false
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
"productId": "com.vpn111.pro.annual",
|
|
520
|
+
"title": "Pro Subscription (Annual)",
|
|
521
|
+
"description": "Pro Subscription (Annual)",
|
|
522
|
+
"localizedPrice": "₺1.999,99",
|
|
523
|
+
"price": "1999.99",
|
|
524
|
+
"currency": "TRY",
|
|
525
|
+
"type": "subs",
|
|
526
|
+
"subscriptionOffers": [
|
|
527
|
+
{
|
|
528
|
+
"numberOfPeriodsIOS": 1,
|
|
529
|
+
"paymentMode": "pay-up-front",
|
|
530
|
+
"type": "promotional",
|
|
531
|
+
"period": { "value": 1, "unit": "year" },
|
|
532
|
+
"id": "one.year.discount.annual.offer",
|
|
533
|
+
"price": 999.99,
|
|
534
|
+
"periodCount": 1,
|
|
535
|
+
"displayPrice": "₺999,99",
|
|
536
|
+
"localizedPriceIOS": "₺999,99",
|
|
537
|
+
"offerId": "one.year.discount.annual.offer",
|
|
538
|
+
"localizedPrice": "₺999,99"
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
"numberOfPeriodsIOS": 1,
|
|
542
|
+
"localizedPriceIOS": "₺149,99",
|
|
543
|
+
"displayPrice": "₺149,99",
|
|
544
|
+
"period": { "value": 1, "unit": "month" },
|
|
545
|
+
"periodCount": 1,
|
|
546
|
+
"id": "one.month.discount.annual.offer",
|
|
547
|
+
"type": "promotional",
|
|
548
|
+
"paymentMode": "pay-up-front",
|
|
549
|
+
"price": 149.99,
|
|
550
|
+
"offerId": "one.month.discount.annual.offer",
|
|
551
|
+
"localizedPrice": "₺149,99"
|
|
552
|
+
}
|
|
553
|
+
],
|
|
554
|
+
"discounts": [
|
|
555
|
+
{
|
|
556
|
+
"paymentMode": "pay-up-front",
|
|
557
|
+
"priceAmount": 999.99,
|
|
558
|
+
"localizedPrice": "₺999,99",
|
|
559
|
+
"type": "promotional",
|
|
560
|
+
"subscriptionPeriod": "YEAR",
|
|
561
|
+
"numberOfPeriods": 1,
|
|
562
|
+
"price": "999.99",
|
|
563
|
+
"identifier": "one.year.discount.annual.offer"
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
"numberOfPeriods": 1,
|
|
567
|
+
"priceAmount": 149.99,
|
|
568
|
+
"localizedPrice": "₺149,99",
|
|
569
|
+
"identifier": "one.month.discount.annual.offer",
|
|
570
|
+
"subscriptionPeriod": "MONTH",
|
|
571
|
+
"type": "promotional",
|
|
572
|
+
"paymentMode": "pay-up-front",
|
|
573
|
+
"price": "149.99"
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
"numberOfPeriods": 1,
|
|
577
|
+
"type": "promotional",
|
|
578
|
+
"subscriptionPeriod": "YEAR",
|
|
579
|
+
"priceAmount": 999.99,
|
|
580
|
+
"paymentMode": "pay-up-front",
|
|
581
|
+
"price": "999.99",
|
|
582
|
+
"localizedPrice": "₺999,99",
|
|
583
|
+
"identifier": "one.year.50discount.annual.offer"
|
|
584
|
+
}
|
|
585
|
+
],
|
|
586
|
+
"subscriptionPeriodNumberIOS": 1,
|
|
587
|
+
"subscriptionPeriodUnitIOS": "YEAR",
|
|
588
|
+
"isConsumable": false
|
|
589
|
+
}
|
|
488
590
|
]
|
|
489
591
|
}
|
|
@@ -112,3 +112,49 @@ export function convertIOSPeriodUnit(
|
|
|
112
112
|
const normalized = unitMap[iosUnit.toUpperCase()];
|
|
113
113
|
return normalized || 'month';
|
|
114
114
|
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* iOS'un döndüğü ham period değerlerini semantik olarak normalize eder.
|
|
118
|
+
*
|
|
119
|
+
* iOS bazen "WEEK" yerine "DAY" + 7, "MONTH" yerine "DAY" + 30 döndürür.
|
|
120
|
+
* Bu fonksiyon bu ham değerleri doğru semantic period'a çevirir.
|
|
121
|
+
*
|
|
122
|
+
* @example normalizeIOSPeriod('DAY', 7) → { unit: 'week', value: 1 }
|
|
123
|
+
* @example normalizeIOSPeriod('DAY', 14) → { unit: 'week', value: 2 }
|
|
124
|
+
* @example normalizeIOSPeriod('DAY', 30) → { unit: 'month', value: 1 }
|
|
125
|
+
* @example normalizeIOSPeriod('DAY', 365) → { unit: 'year', value: 1 }
|
|
126
|
+
* @example normalizeIOSPeriod('MONTH', 1) → { unit: 'month', value: 1 }
|
|
127
|
+
*/
|
|
128
|
+
export function normalizeIOSPeriod(
|
|
129
|
+
iosUnit: string | undefined,
|
|
130
|
+
iosValue: number | undefined,
|
|
131
|
+
): { unit: 'day' | 'week' | 'month' | 'year'; value: number } {
|
|
132
|
+
const rawUnit = convertIOSPeriodUnit(iosUnit);
|
|
133
|
+
const rawValue = iosValue || 1;
|
|
134
|
+
|
|
135
|
+
// DAY cinsinden gelen değerleri daha büyük birime çevir
|
|
136
|
+
if (rawUnit === 'day') {
|
|
137
|
+
if (rawValue % 365 === 0) {
|
|
138
|
+
return { unit: 'year', value: rawValue / 365 };
|
|
139
|
+
}
|
|
140
|
+
if (rawValue === 366) {
|
|
141
|
+
return { unit: 'year', value: 1 };
|
|
142
|
+
}
|
|
143
|
+
if (rawValue % 30 === 0) {
|
|
144
|
+
return { unit: 'month', value: rawValue / 30 };
|
|
145
|
+
}
|
|
146
|
+
if (rawValue === 31) {
|
|
147
|
+
return { unit: 'month', value: 1 };
|
|
148
|
+
}
|
|
149
|
+
if (rawValue % 7 === 0) {
|
|
150
|
+
return { unit: 'week', value: rawValue / 7 };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// MONTH cinsinden gelen değerleri yıla çevir
|
|
155
|
+
if (rawUnit === 'month' && rawValue % 12 === 0) {
|
|
156
|
+
return { unit: 'year', value: rawValue / 12 };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { unit: rawUnit, value: rawValue };
|
|
160
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { persist } from 'zustand/middleware';
|
|
3
|
+
import { Device } from '../types/Device';
|
|
4
|
+
|
|
5
|
+
interface CustomDeviceState {
|
|
6
|
+
customDevices: Device[];
|
|
7
|
+
addCustomDevice: (device: Device) => void;
|
|
8
|
+
removeCustomDevice: (name: string) => void;
|
|
9
|
+
updateCustomDevice: (name: string, updatedDevice: Device) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const useCustomDeviceStore = create<CustomDeviceState>()(
|
|
13
|
+
persist(
|
|
14
|
+
(set) => ({
|
|
15
|
+
customDevices: [],
|
|
16
|
+
addCustomDevice: (device) =>
|
|
17
|
+
set((state) => ({
|
|
18
|
+
customDevices: [
|
|
19
|
+
device,
|
|
20
|
+
...state.customDevices.filter((d) => d.name !== device.name),
|
|
21
|
+
],
|
|
22
|
+
})),
|
|
23
|
+
removeCustomDevice: (name) =>
|
|
24
|
+
set((state) => ({
|
|
25
|
+
customDevices: state.customDevices.filter((d) => d.name !== name),
|
|
26
|
+
})),
|
|
27
|
+
updateCustomDevice: (name, updatedDevice) =>
|
|
28
|
+
set((state) => ({
|
|
29
|
+
customDevices: state.customDevices.map((d) =>
|
|
30
|
+
d.name === name ? updatedDevice : d,
|
|
31
|
+
),
|
|
32
|
+
})),
|
|
33
|
+
}),
|
|
34
|
+
{
|
|
35
|
+
name: 'custom-devices-storage',
|
|
36
|
+
},
|
|
37
|
+
),
|
|
38
|
+
);
|