@developer_tribe/react-builder 1.2.44-test.2 → 1.2.45

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 (92) hide show
  1. package/dist/attributes-editor/Field.d.ts +1 -3
  2. package/dist/attributes-editor/attributesEditorModelTypes.d.ts +0 -3
  3. package/dist/attributes-editor/useAttributesEditorModel.d.ts +1 -1
  4. package/dist/build-components/FormSubmitButton/FormSubmitButtonProps.generated.d.ts +3 -8
  5. package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +3 -8
  6. package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +0 -2
  7. package/dist/build-components/PaywallProvider/PaywallProviderProps.generated.d.ts +0 -2
  8. package/dist/build-components/SystemButton/SystemButtonProps.generated.d.ts +3 -8
  9. package/dist/build-components/SystemButton/usePlacementButtonEvents.d.ts +2 -15
  10. package/dist/build-components/index.d.ts +1 -3
  11. package/dist/build-components/patterns.generated.d.ts +1334 -2129
  12. package/dist/components/DeviceButton.d.ts +2 -1
  13. package/dist/index.cjs.js +28 -1
  14. package/dist/index.cjs.js.map +1 -1
  15. package/dist/index.esm.js +28 -1
  16. package/dist/index.esm.js.map +1 -1
  17. package/dist/index.web.cjs.js +4 -4
  18. package/dist/index.web.cjs.js.map +1 -1
  19. package/dist/index.web.esm.js +3 -3
  20. package/dist/index.web.esm.js.map +1 -1
  21. package/dist/modals/CreateDeviceModal.d.ts +8 -0
  22. package/dist/product-base/periodLocalizationKeys.d.ts +16 -0
  23. package/dist/store/customDeviceStore.d.ts +21 -0
  24. package/dist/store.d.ts +1 -1
  25. package/dist/styles.css +1 -1
  26. package/dist/utils/nodeTree.d.ts +0 -18
  27. package/package.json +1 -1
  28. package/scripts/prebuild/utils/validateAllComponentsOrThrow.js +4 -68
  29. package/src/.DS_Store +0 -0
  30. package/src/assets/.DS_Store +0 -0
  31. package/src/assets/prompt-scheme-onboard.generated.ts +1 -1
  32. package/src/assets/prompt-scheme-paywall.generated.ts +1 -1
  33. package/src/assets/samples/getSamples.ts +0 -2
  34. package/src/assets/samples/terms-and-privacy-no-form.json +1 -1
  35. package/src/assets/samples/terms-and-privacy.json +1 -1
  36. package/src/attributes-editor/AttributesEditorView.tsx +0 -3
  37. package/src/attributes-editor/Field.tsx +2 -91
  38. package/src/attributes-editor/attributesEditorModelTypes.ts +0 -3
  39. package/src/attributes-editor/useAttributesEditorModel.ts +0 -8
  40. package/src/build-components/FormCheckbox/FormCheckbox.tsx +3 -3
  41. package/src/build-components/FormSubmitButton/FormSubmitButton.tsx +0 -6
  42. package/src/build-components/FormSubmitButton/FormSubmitButtonProps.generated.ts +3 -26
  43. package/src/build-components/OnboardButton/OnboardButton.tsx +1 -8
  44. package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +3 -26
  45. package/src/build-components/OnboardButton/pattern.json +3 -5
  46. package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +0 -12
  47. package/src/build-components/OnboardProvider/pattern.json +1 -9
  48. package/src/build-components/PaywallProvider/PaywallProviderProps.generated.ts +0 -12
  49. package/src/build-components/PaywallProvider/pattern.json +1 -9
  50. package/src/build-components/RenderNode.generated.tsx +0 -10
  51. package/src/build-components/SystemButton/SystemButton.tsx +0 -6
  52. package/src/build-components/SystemButton/SystemButtonProps.generated.ts +3 -26
  53. package/src/build-components/SystemButton/pattern.json +3 -5
  54. package/src/build-components/SystemButton/usePlacementButtonEvents.ts +27 -51
  55. package/src/build-components/Text/Text.tsx +2 -3
  56. package/src/build-components/index.ts +0 -10
  57. package/src/build-components/patterns.generated.ts +1379 -2270
  58. package/src/components/AttributesEditorPanel.tsx +0 -1
  59. package/src/components/DeviceButton.tsx +34 -1
  60. package/src/components/EditorHeader.tsx +22 -4
  61. package/src/modals/CreateDeviceModal.tsx +264 -0
  62. package/src/modals/DeviceSelectorModal.tsx +44 -7
  63. package/src/product-base/extractIOSParams.ts +10 -4
  64. package/src/product-base/periodLocalizationKeys.ts +46 -0
  65. package/src/store/customDeviceStore.ts +38 -0
  66. package/src/styles/components/_editor-shell.scss +12 -2
  67. package/src/styles/index.scss +1 -1
  68. package/src/styles/modals/_create-device.scss +113 -0
  69. package/src/utils/__special_exceptions.ts +8 -0
  70. package/src/utils/analyseNodeByPatterns.ts +10 -6
  71. package/src/utils/nodeTree.ts +0 -115
  72. package/dist/build-components/GlobalProvider/GlobalContext.d.ts +0 -28
  73. package/dist/build-components/GlobalProvider/GlobalProvider.d.ts +0 -5
  74. package/dist/build-components/GlobalProvider/GlobalProviderProps.generated.d.ts +0 -60
  75. package/dist/build-components/GlobalProvider/globalProviderUtils.d.ts +0 -28
  76. package/dist/build-components/GlobalProvider/useGlobalNavigation.d.ts +0 -19
  77. package/dist/build-components/GlobalProvider/useGlobalProviderLogic.d.ts +0 -15
  78. package/dist/build-components/TermsProvider/TermsProvider.d.ts +0 -5
  79. package/dist/build-components/TermsProvider/TermsProviderProps.generated.d.ts +0 -55
  80. package/src/assets/samples/global-onboard-flow.json +0 -735
  81. package/src/build-components/GlobalProvider/GlobalContext.ts +0 -48
  82. package/src/build-components/GlobalProvider/GlobalProvider.tsx +0 -51
  83. package/src/build-components/GlobalProvider/GlobalProviderProps.generated.ts +0 -78
  84. package/src/build-components/GlobalProvider/globalProviderUtils.ts +0 -204
  85. package/src/build-components/GlobalProvider/pattern.json +0 -55
  86. package/src/build-components/GlobalProvider/useGlobalNavigation.ts +0 -65
  87. package/src/build-components/GlobalProvider/useGlobalProviderLogic.ts +0 -172
  88. package/src/build-components/TermsProvider/TermsProvider.tsx +0 -45
  89. package/src/build-components/TermsProvider/TermsProviderProps.generated.ts +0 -82
  90. package/src/build-components/TermsProvider/pattern.json +0 -35
  91. package/src/patterns/event-constants.json +0 -19
  92. package/src/styles/components/_global-provider.scss +0 -131
@@ -88,7 +88,6 @@ export function AttributesEditorPanel({
88
88
  node={nodeForEditor}
89
89
  onChange={handleAttributesChange}
90
90
  projectColors={projectColors}
91
- projectRoot={attributes}
92
91
  />
93
92
  </div>
94
93
  );
@@ -13,6 +13,7 @@ type DeviceButtonProps = {
13
13
  onSelect: (device: Device) => void;
14
14
  isFavorite?: boolean;
15
15
  onToggleFavorite?: (device: Device, e: React.MouseEvent) => void;
16
+ onEdit?: (device: Device, e: React.MouseEvent) => void;
16
17
  };
17
18
 
18
19
  export function DeviceButton({
@@ -21,6 +22,7 @@ export function DeviceButton({
21
22
  onSelect,
22
23
  isFavorite,
23
24
  onToggleFavorite,
25
+ onEdit,
24
26
  }: DeviceButtonProps) {
25
27
  const platformIcon = platformIcons[device.platform];
26
28
  const aspect =
@@ -74,10 +76,41 @@ export function DeviceButton({
74
76
  {device.name} <br />
75
77
  {device.width}×{device.height} ({aspect})
76
78
  {platformIcon && (
77
- <span>
79
+ <span className="editor-device-button__platform">
78
80
  <img src={platformIcon} alt="" aria-hidden="true" />
79
81
  </span>
80
82
  )}
83
+ {onEdit && (
84
+ <span
85
+ className="editor-device-button__edit"
86
+ onClick={(e) => {
87
+ e.stopPropagation();
88
+ onEdit(device, e);
89
+ }}
90
+ title="Edit device"
91
+ style={{
92
+ position: 'absolute',
93
+ bottom: 4,
94
+ left: 4,
95
+ cursor: 'pointer',
96
+ opacity: 0.5,
97
+ }}
98
+ >
99
+ <svg
100
+ width="12"
101
+ height="12"
102
+ viewBox="0 0 24 24"
103
+ fill="none"
104
+ stroke="currentColor"
105
+ strokeWidth="2"
106
+ strokeLinecap="round"
107
+ strokeLinejoin="round"
108
+ >
109
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
110
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
111
+ </svg>
112
+ </span>
113
+ )}
81
114
  </button>
82
115
  );
83
116
  }
@@ -11,6 +11,7 @@ import { DeviceSelectorModal } from '../modals/DeviceSelectorModal';
11
11
  import { toast } from 'react-toastify';
12
12
  import { getSamples } from '../assets/samples/getSamples';
13
13
  import type { Project } from '../types/Project';
14
+ import { useCustomDeviceStore } from '../store/customDeviceStore';
14
15
 
15
16
  const devices = getDevices();
16
17
  interface EditorHeaderProps {
@@ -53,14 +54,31 @@ export function EditorHeader({
53
54
  toggleFavoriteDevice: s.toggleFavoriteDevice,
54
55
  }));
55
56
 
57
+ const { customDevices } = useCustomDeviceStore();
56
58
  const headerDevices = useMemo(() => {
59
+ // 1. Get all potential devices
60
+ const allAvailable = [...customDevices, ...devices];
61
+
62
+ // 2. Filter favorites
57
63
  const favs = favoriteDevices
58
- .map((name) => devices.find((d) => d.name === name))
64
+ .map((name) => allAvailable.find((d) => d.name === name))
59
65
  .filter((d): d is Device => d !== undefined);
60
66
 
61
- const others = devices.filter((d) => !favoriteDevices.includes(d.name));
62
- return [...favs, ...others].slice(0, 5);
63
- }, [favoriteDevices]);
67
+ // 3. Get others (custom first, then default)
68
+ const others = allAvailable.filter(
69
+ (d) => !favoriteDevices.includes(d.name),
70
+ );
71
+
72
+ // Combine and limit to 5
73
+ const top5 = [...favs, ...others].slice(0, 5);
74
+
75
+ // If selected device is not in the top 5, append it at the end
76
+ if (selectedDevice && !top5.some((d) => d.name === selectedDevice.name)) {
77
+ return [...top5, selectedDevice];
78
+ }
79
+
80
+ return top5;
81
+ }, [favoriteDevices, customDevices, selectedDevice]);
64
82
 
65
83
  const sortedSamples = useMemo(() => {
66
84
  const weight = (t?: Project['type']) => {
@@ -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 devices.filter((device) =>
41
+ return allDevices.filter((device) =>
33
42
  device.name.toLowerCase().includes(searchTerm.toLowerCase()),
34
43
  );
35
- }, [devices, searchTerm]);
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
  }
@@ -1,4 +1,7 @@
1
- import { convertIOSPeriodUnit } from './periodLocalizationKeys';
1
+ import {
2
+ convertIOSPeriodUnit,
3
+ normalizeIOSPeriod,
4
+ } from './periodLocalizationKeys';
2
5
  import { iapLogger } from '../logger';
3
6
  import {
4
7
  calculateDiscount,
@@ -60,10 +63,13 @@ export function extractIOSParams(
60
63
  const currency = product.currency || product.currencyCode || '';
61
64
  const localizedPrice = product.localizedPrice || '';
62
65
 
63
- const periodUnit = convertIOSPeriodUnit(
64
- product.subscriptionPeriodUnitIOS || 'MONTH',
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 periodValue = String(product.subscriptionPeriodNumberIOS || 1);
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 =
@@ -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
+ );
@@ -94,7 +94,7 @@
94
94
  }
95
95
  }
96
96
 
97
- .editor-device-button span {
97
+ .editor-device-button__platform {
98
98
  position: absolute;
99
99
  bottom: 4px;
100
100
  right: 4px;
@@ -106,7 +106,7 @@
106
106
  overflow: hidden;
107
107
  }
108
108
 
109
- .editor-device-button img {
109
+ .editor-device-button__platform img {
110
110
  position: absolute;
111
111
  top: 50%;
112
112
  left: 50%;
@@ -117,6 +117,16 @@
117
117
  height: auto;
118
118
  }
119
119
 
120
+ .editor-device-button__edit {
121
+ transition:
122
+ opacity 0.2s ease,
123
+ transform 0.2s ease;
124
+ &:hover {
125
+ opacity: 1 !important;
126
+ transform: scale(1.1);
127
+ }
128
+ }
129
+
120
130
  .editor-header__actions {
121
131
  margin-left: auto; /* push actions to the far right */
122
132
  display: flex;
@@ -16,7 +16,6 @@
16
16
  @use './components/mockos-router';
17
17
  @use './components/bottom-bar';
18
18
  @use './components/onboard';
19
- @use './components/global-provider';
20
19
 
21
20
  @use './components/webview';
22
21
  @use './components/checkbox';
@@ -33,5 +32,6 @@
33
32
  @use './modals/benefit-presets-modal';
34
33
  @use './modals/inspect-modal';
35
34
  @use './modals/prompt-manager-modal';
35
+ @use './modals/create-device';
36
36
 
37
37
  @use './utilities/carousel';