@developer_tribe/react-builder 1.2.44-test.1 → 1.2.44-test.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attributes-editor/Field.d.ts +3 -1
- package/dist/attributes-editor/attributesEditorModelTypes.d.ts +3 -0
- package/dist/attributes-editor/useAttributesEditorModel.d.ts +1 -1
- package/dist/build-components/FormSubmitButton/FormSubmitButtonProps.generated.d.ts +8 -3
- package/dist/build-components/GlobalProvider/globalProviderUtils.d.ts +4 -13
- package/dist/build-components/GlobalProvider/useGlobalProviderLogic.d.ts +15 -0
- package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +8 -3
- package/dist/build-components/SystemButton/SystemButtonProps.generated.d.ts +8 -3
- package/dist/build-components/SystemButton/usePlacementButtonEvents.d.ts +9 -2
- package/dist/build-components/patterns.generated.d.ts +15 -9
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +1 -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/utils/nodeTree.d.ts +18 -0
- package/package.json +1 -1
- package/scripts/prebuild/utils/validateAllComponentsOrThrow.js +68 -4
- package/src/assets/meta.json +1 -1
- package/src/assets/samples/global-onboard-flow.json +7 -1
- package/src/assets/samples/terms-and-privacy-no-form.json +1 -1
- package/src/assets/samples/terms-and-privacy.json +1 -1
- package/src/attributes-editor/AttributesEditorView.tsx +3 -0
- package/src/attributes-editor/Field.tsx +91 -2
- package/src/attributes-editor/attributesEditorModelTypes.ts +3 -0
- package/src/attributes-editor/useAttributesEditorModel.ts +8 -0
- package/src/build-components/FormCheckbox/FormCheckbox.tsx +3 -1
- package/src/build-components/FormSubmitButton/FormSubmitButton.tsx +3 -0
- package/src/build-components/FormSubmitButton/FormSubmitButtonProps.generated.ts +26 -3
- package/src/build-components/GlobalProvider/GlobalProvider.tsx +4 -144
- package/src/build-components/GlobalProvider/globalProviderUtils.ts +79 -38
- package/src/build-components/GlobalProvider/useGlobalNavigation.ts +0 -5
- package/src/build-components/GlobalProvider/useGlobalProviderLogic.ts +172 -0
- package/src/build-components/OnboardButton/OnboardButton.tsx +3 -0
- package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +26 -3
- package/src/build-components/OnboardButton/pattern.json +5 -3
- package/src/build-components/SystemButton/SystemButton.tsx +3 -0
- package/src/build-components/SystemButton/SystemButtonProps.generated.ts +26 -3
- package/src/build-components/SystemButton/pattern.json +5 -3
- package/src/build-components/SystemButton/usePlacementButtonEvents.ts +22 -9
- package/src/build-components/patterns.generated.ts +45 -9
- package/src/components/AttributesEditorPanel.tsx +1 -0
- package/src/patterns/event-constants.json +19 -0
- package/src/utils/nodeTree.ts +115 -0
|
@@ -1,155 +1,15 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react';
|
|
2
2
|
import type { GlobalProviderComponentProps } from './GlobalProviderProps.generated';
|
|
3
|
-
import type { GlobalContextValue, GlobalPage } from './GlobalContext';
|
|
4
3
|
import { GlobalContext } from './GlobalContext';
|
|
5
|
-
import {
|
|
6
|
-
buildPages,
|
|
7
|
-
loadProgress,
|
|
8
|
-
normalizeSkipConditions,
|
|
9
|
-
persistProgress,
|
|
10
|
-
resolveEffectivePage,
|
|
11
|
-
} from './globalProviderUtils';
|
|
4
|
+
import { useGlobalProviderLogic } from './useGlobalProviderLogic';
|
|
12
5
|
import RenderNode from '../RenderNode.generated';
|
|
13
|
-
import useNode from '../useNode';
|
|
14
6
|
import { useLogRender } from '../../utils/useLogRender';
|
|
15
|
-
import type { NodeData } from '../../types/Node';
|
|
16
7
|
|
|
17
8
|
function GlobalProvider({ node }: GlobalProviderComponentProps) {
|
|
18
9
|
useLogRender('GlobalProvider');
|
|
19
|
-
node = useNode(node);
|
|
20
10
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const attributeKey = node.key ?? generatedId;
|
|
24
|
-
const attrs = node.attributes;
|
|
25
|
-
const shouldPersist = attrs?.persistProgress === true;
|
|
26
|
-
|
|
27
|
-
const childNodes = useMemo((): NodeData[] => {
|
|
28
|
-
const raw = node.children;
|
|
29
|
-
if (!raw) return [];
|
|
30
|
-
if (Array.isArray(raw)) return raw as NodeData[];
|
|
31
|
-
return [raw as NodeData];
|
|
32
|
-
}, [node.children]);
|
|
33
|
-
|
|
34
|
-
// skipConditions is stored as SkipConditionEntry[] in the schema (array of
|
|
35
|
-
// {pageKey, conditionKey} objects). Normalize to a lookup map here.
|
|
36
|
-
const skipConditions = useMemo(
|
|
37
|
-
() => normalizeSkipConditions(attrs?.skipConditions),
|
|
38
|
-
[attrs?.skipConditions],
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
const pages = useMemo(
|
|
42
|
-
() => buildPages(childNodes, skipConditions),
|
|
43
|
-
[childNodes, skipConditions],
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
const storageKey = attributeKey;
|
|
47
|
-
|
|
48
|
-
const [conditions, setConditions] = useState<Record<string, boolean>>(() => {
|
|
49
|
-
if (shouldPersist) {
|
|
50
|
-
return loadProgress(storageKey)?.conditions ?? {};
|
|
51
|
-
}
|
|
52
|
-
return {};
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const [pageStack, setPageStack] = useState<string[]>(() => {
|
|
56
|
-
const firstEffective = (() => {
|
|
57
|
-
if (shouldPersist) {
|
|
58
|
-
const saved = loadProgress(storageKey);
|
|
59
|
-
if (saved?.currentPageKey) return saved.currentPageKey;
|
|
60
|
-
}
|
|
61
|
-
const requestedKey =
|
|
62
|
-
typeof attrs?.initialPage === 'string'
|
|
63
|
-
? attrs.initialPage
|
|
64
|
-
: (pages[0]?.key ?? '');
|
|
65
|
-
return resolveEffectivePage(requestedKey, pages, {});
|
|
66
|
-
})();
|
|
67
|
-
return firstEffective ? [firstEffective] : [];
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const currentPageKey = pageStack[pageStack.length - 1] ?? pages[0]?.key ?? '';
|
|
71
|
-
|
|
72
|
-
// Keep ref of latest conditions for persist calls in callbacks
|
|
73
|
-
const conditionsRef = useRef(conditions);
|
|
74
|
-
conditionsRef.current = conditions;
|
|
75
|
-
|
|
76
|
-
const setCondition = useCallback(
|
|
77
|
-
(key: string, value: boolean) => {
|
|
78
|
-
setConditions((prev) => {
|
|
79
|
-
const next = { ...prev, [key]: value };
|
|
80
|
-
if (shouldPersist) {
|
|
81
|
-
const currentKey = pageStack[pageStack.length - 1] ?? '';
|
|
82
|
-
persistProgress(storageKey, currentKey, next);
|
|
83
|
-
}
|
|
84
|
-
return next;
|
|
85
|
-
});
|
|
86
|
-
},
|
|
87
|
-
[shouldPersist, storageKey, pageStack],
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
const navigate = useCallback(
|
|
91
|
-
(key: string) => {
|
|
92
|
-
const effective = resolveEffectivePage(key, pages, conditionsRef.current);
|
|
93
|
-
setPageStack((prev) => {
|
|
94
|
-
const last = prev[prev.length - 1];
|
|
95
|
-
if (last === effective) return prev;
|
|
96
|
-
const next = [...prev, effective];
|
|
97
|
-
if (shouldPersist) {
|
|
98
|
-
persistProgress(storageKey, effective, conditionsRef.current);
|
|
99
|
-
}
|
|
100
|
-
return next;
|
|
101
|
-
});
|
|
102
|
-
},
|
|
103
|
-
[pages, shouldPersist, storageKey],
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
const goNext = useCallback(() => {
|
|
107
|
-
const currentIdx = pages.findIndex((p) => p.key === currentPageKey);
|
|
108
|
-
if (currentIdx === -1 || currentIdx >= pages.length - 1) return;
|
|
109
|
-
const nextPage = pages[currentIdx + 1];
|
|
110
|
-
if (!nextPage) return;
|
|
111
|
-
navigate(nextPage.key);
|
|
112
|
-
}, [pages, currentPageKey, navigate]);
|
|
113
|
-
|
|
114
|
-
const goBack = useCallback((): boolean => {
|
|
115
|
-
if (pageStack.length <= 1) return false;
|
|
116
|
-
setPageStack((prev) => {
|
|
117
|
-
const next = prev.slice(0, -1);
|
|
118
|
-
const prevKey = next[next.length - 1] ?? '';
|
|
119
|
-
if (shouldPersist) {
|
|
120
|
-
persistProgress(storageKey, prevKey, conditionsRef.current);
|
|
121
|
-
}
|
|
122
|
-
return next;
|
|
123
|
-
});
|
|
124
|
-
return true;
|
|
125
|
-
}, [pageStack.length, shouldPersist, storageKey]);
|
|
126
|
-
|
|
127
|
-
const contextValue = useMemo<GlobalContextValue>(
|
|
128
|
-
() => ({
|
|
129
|
-
currentPageKey,
|
|
130
|
-
pages,
|
|
131
|
-
pageStack,
|
|
132
|
-
navigate,
|
|
133
|
-
goNext,
|
|
134
|
-
goBack,
|
|
135
|
-
conditions,
|
|
136
|
-
setCondition,
|
|
137
|
-
}),
|
|
138
|
-
[
|
|
139
|
-
currentPageKey,
|
|
140
|
-
pages,
|
|
141
|
-
pageStack,
|
|
142
|
-
navigate,
|
|
143
|
-
goNext,
|
|
144
|
-
goBack,
|
|
145
|
-
conditions,
|
|
146
|
-
setCondition,
|
|
147
|
-
],
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
const activePage: GlobalPage | undefined = pages.find(
|
|
151
|
-
(p) => p.key === currentPageKey,
|
|
152
|
-
);
|
|
11
|
+
const { attributeName, attributeKey, attrs, activePage, contextValue } =
|
|
12
|
+
useGlobalProviderLogic({ node });
|
|
153
13
|
|
|
154
14
|
const animationClass = (() => {
|
|
155
15
|
const a = activePage?.animation;
|
|
@@ -11,22 +11,32 @@ const TYPE_KEY_MAP: Record<string, string> = {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Derives a stable page key for a child node.
|
|
15
|
-
* Priority: node.key
|
|
16
|
-
*
|
|
17
|
-
* Using node.key avoids adding unknown attributes to child components that
|
|
18
|
-
* would fail builder validation.
|
|
14
|
+
* Derives a stable and unique page key for a child node.
|
|
15
|
+
* Priority: node.key → TYPE_KEY_MAP → "${type}-${index}"
|
|
19
16
|
*/
|
|
20
|
-
export function derivePageKey(
|
|
17
|
+
export function derivePageKey(
|
|
18
|
+
node: NodeData,
|
|
19
|
+
index: number,
|
|
20
|
+
existingKeys: Set<string>,
|
|
21
|
+
): string {
|
|
22
|
+
let baseKey = '';
|
|
21
23
|
if (typeof node.key === 'string' && node.key.trim()) {
|
|
22
|
-
|
|
24
|
+
baseKey = node.key.trim();
|
|
25
|
+
} else {
|
|
26
|
+
baseKey = TYPE_KEY_MAP[node.type] || node.type?.toLowerCase?.() || 'page';
|
|
23
27
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
|
|
29
|
+
let finalKey = baseKey;
|
|
30
|
+
let counter = 1;
|
|
31
|
+
|
|
32
|
+
// Ensure uniqueness among siblings
|
|
33
|
+
while (existingKeys.has(finalKey)) {
|
|
34
|
+
finalKey = `${baseKey}-${index + counter}`;
|
|
35
|
+
counter++;
|
|
27
36
|
}
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
|
|
38
|
+
existingKeys.add(finalKey);
|
|
39
|
+
return finalKey;
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
/** Runtime shape of a SkipConditionEntry (mirrors the types block in pattern.json). */
|
|
@@ -37,8 +47,6 @@ export interface SkipConditionEntry {
|
|
|
37
47
|
|
|
38
48
|
/**
|
|
39
49
|
* Converts SkipConditionEntry[] (from pattern schema) to a lookup map for fast access.
|
|
40
|
-
* Array format is used in pattern.json because the schema system doesn't support
|
|
41
|
-
* plain Record/object attribute types.
|
|
42
50
|
*/
|
|
43
51
|
export function normalizeSkipConditions(raw: unknown): Record<string, string> {
|
|
44
52
|
if (!Array.isArray(raw)) return {};
|
|
@@ -60,17 +68,14 @@ export function normalizeSkipConditions(raw: unknown): Record<string, string> {
|
|
|
60
68
|
|
|
61
69
|
/**
|
|
62
70
|
* Builds the page list for GlobalProvider.
|
|
63
|
-
*
|
|
64
|
-
* @param children - direct child nodes of GlobalProvider
|
|
65
|
-
* @param skipConditions - lookup map of pageKey → conditionKey, already normalized
|
|
66
|
-
* from SkipConditionEntry[] via normalizeSkipConditions().
|
|
67
71
|
*/
|
|
68
72
|
export function buildPages(
|
|
69
73
|
children: NodeData[],
|
|
70
74
|
skipConditions: Record<string, string> = {},
|
|
71
75
|
): GlobalPage[] {
|
|
76
|
+
const existingKeys = new Set<string>();
|
|
72
77
|
return children.map((node, index) => {
|
|
73
|
-
const key = derivePageKey(node, index);
|
|
78
|
+
const key = derivePageKey(node, index, existingKeys);
|
|
74
79
|
const skipIf = skipConditions[key] ?? undefined;
|
|
75
80
|
const animation =
|
|
76
81
|
typeof (node.attributes as Record<string, unknown>)?.animation ===
|
|
@@ -92,10 +97,11 @@ export function resolveEffectivePage(
|
|
|
92
97
|
targetKey: string,
|
|
93
98
|
pages: GlobalPage[],
|
|
94
99
|
conditions: Record<string, boolean>,
|
|
95
|
-
): string {
|
|
100
|
+
): string | null {
|
|
96
101
|
const targetIdx = pages.findIndex((p) => p.key === targetKey);
|
|
97
102
|
if (targetIdx === -1) {
|
|
98
|
-
|
|
103
|
+
// If target not found, start from the first non-skipped page
|
|
104
|
+
return resolveFirstNonSkippedPage(pages, conditions);
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
// Walk forward from target, skipping pages whose condition is met
|
|
@@ -108,34 +114,73 @@ export function resolveEffectivePage(
|
|
|
108
114
|
return page.key;
|
|
109
115
|
}
|
|
110
116
|
|
|
111
|
-
//
|
|
112
|
-
|
|
117
|
+
// If all remaining pages are skipped, return null instead of falling back to the last page.
|
|
118
|
+
// This allows the caller to handle the "no available pages" state explicitly.
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolveFirstNonSkippedPage(
|
|
123
|
+
pages: GlobalPage[],
|
|
124
|
+
conditions: Record<string, boolean>,
|
|
125
|
+
): string | null {
|
|
126
|
+
for (const page of pages) {
|
|
127
|
+
if (!(page.skipIf && conditions[page.skipIf] === true)) {
|
|
128
|
+
return page.key;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
113
132
|
}
|
|
114
133
|
|
|
115
134
|
const STORAGE_KEY_PREFIX = 'global-provider-progress';
|
|
116
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Internal storage abstraction. In a real RN environment, this would be swapped
|
|
138
|
+
* for AsyncStorage. For now, it defaults to localStorage with safe checks.
|
|
139
|
+
*/
|
|
140
|
+
const storage = {
|
|
141
|
+
getItem: (key: string) => {
|
|
142
|
+
try {
|
|
143
|
+
return typeof localStorage !== 'undefined'
|
|
144
|
+
? localStorage.getItem(key)
|
|
145
|
+
: null;
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
setItem: (key: string, value: string) => {
|
|
151
|
+
try {
|
|
152
|
+
if (typeof localStorage !== 'undefined') localStorage.setItem(key, value);
|
|
153
|
+
} catch {
|
|
154
|
+
// ignore
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
removeItem: (key: string) => {
|
|
158
|
+
try {
|
|
159
|
+
if (typeof localStorage !== 'undefined') localStorage.removeItem(key);
|
|
160
|
+
} catch {
|
|
161
|
+
// ignore
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
117
166
|
export function persistProgress(
|
|
118
167
|
storageKey: string,
|
|
119
168
|
currentPageKey: string,
|
|
120
169
|
conditions: Record<string, boolean>,
|
|
121
170
|
): void {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
);
|
|
127
|
-
} catch {
|
|
128
|
-
// localStorage unavailable (SSR, private browsing etc.)
|
|
129
|
-
}
|
|
171
|
+
storage.setItem(
|
|
172
|
+
`${STORAGE_KEY_PREFIX}-${storageKey}`,
|
|
173
|
+
JSON.stringify({ currentPageKey, conditions }),
|
|
174
|
+
);
|
|
130
175
|
}
|
|
131
176
|
|
|
132
177
|
export function loadProgress(storageKey: string): {
|
|
133
178
|
currentPageKey: string;
|
|
134
179
|
conditions: Record<string, boolean>;
|
|
135
180
|
} | null {
|
|
181
|
+
const raw = storage.getItem(`${STORAGE_KEY_PREFIX}-${storageKey}`);
|
|
182
|
+
if (!raw) return null;
|
|
136
183
|
try {
|
|
137
|
-
const raw = localStorage.getItem(`${STORAGE_KEY_PREFIX}-${storageKey}`);
|
|
138
|
-
if (!raw) return null;
|
|
139
184
|
const parsed = JSON.parse(raw) as unknown;
|
|
140
185
|
if (
|
|
141
186
|
parsed &&
|
|
@@ -155,9 +200,5 @@ export function loadProgress(storageKey: string): {
|
|
|
155
200
|
}
|
|
156
201
|
|
|
157
202
|
export function clearProgress(storageKey: string): void {
|
|
158
|
-
|
|
159
|
-
localStorage.removeItem(`${STORAGE_KEY_PREFIX}-${storageKey}`);
|
|
160
|
-
} catch {
|
|
161
|
-
// localStorage unavailable
|
|
162
|
-
}
|
|
203
|
+
storage.removeItem(`${STORAGE_KEY_PREFIX}-${storageKey}`);
|
|
163
204
|
}
|
|
@@ -36,8 +36,6 @@ export function useGlobalNavigation(): (target: string) => boolean {
|
|
|
36
36
|
(target: string): boolean => {
|
|
37
37
|
if (globalCtx) {
|
|
38
38
|
// Direct page key match
|
|
39
|
-
console.log('target', target);
|
|
40
|
-
console.log('globalCtx.pages', globalCtx.pages);
|
|
41
39
|
if (globalCtx.pages.some((p) => p.key === target)) {
|
|
42
40
|
globalCtx.navigate(target);
|
|
43
41
|
return true;
|
|
@@ -45,18 +43,15 @@ export function useGlobalNavigation(): (target: string) => boolean {
|
|
|
45
43
|
|
|
46
44
|
// Alias resolution: map incoming target to canonical page key
|
|
47
45
|
const canonicalKey = GLOBAL_ROUTE_ALIASES[target];
|
|
48
|
-
console.log('canonicalKey', canonicalKey);
|
|
49
46
|
if (
|
|
50
47
|
canonicalKey &&
|
|
51
48
|
globalCtx.pages.some((p) => p.key === canonicalKey)
|
|
52
49
|
) {
|
|
53
|
-
console.log('navigate to canonicalKey', canonicalKey);
|
|
54
50
|
globalCtx.navigate(canonicalKey);
|
|
55
51
|
return true;
|
|
56
52
|
}
|
|
57
53
|
}
|
|
58
54
|
|
|
59
|
-
console.log('fallback to MockOSContext', target);
|
|
60
55
|
// Fallback: delegate to MockOSContext
|
|
61
56
|
if (mockOS) {
|
|
62
57
|
mockOS.navigation(target as RouteType);
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { useCallback, useId, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import type { GlobalContextValue } from './GlobalContext';
|
|
3
|
+
import {
|
|
4
|
+
buildPages,
|
|
5
|
+
loadProgress,
|
|
6
|
+
normalizeSkipConditions,
|
|
7
|
+
persistProgress,
|
|
8
|
+
resolveEffectivePage,
|
|
9
|
+
} from './globalProviderUtils';
|
|
10
|
+
import useNode from '../useNode';
|
|
11
|
+
import type { NodeData } from '../../types/Node';
|
|
12
|
+
|
|
13
|
+
export interface UseGlobalProviderLogicOptions {
|
|
14
|
+
node: NodeData;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useGlobalProviderLogic({
|
|
18
|
+
node: rawNode,
|
|
19
|
+
}: UseGlobalProviderLogicOptions) {
|
|
20
|
+
const node = useNode(rawNode);
|
|
21
|
+
|
|
22
|
+
const generatedId = useId();
|
|
23
|
+
const attributeName = node.sourceType ?? node.type ?? 'GlobalProvider';
|
|
24
|
+
const attributeKey = node.key ?? generatedId;
|
|
25
|
+
const attrs = node.attributes;
|
|
26
|
+
const shouldPersist = attrs?.persistProgress === true;
|
|
27
|
+
|
|
28
|
+
const childNodes = useMemo((): NodeData[] => {
|
|
29
|
+
const raw = node.children;
|
|
30
|
+
if (!raw) return [];
|
|
31
|
+
if (Array.isArray(raw)) return raw as NodeData[];
|
|
32
|
+
return [raw as NodeData];
|
|
33
|
+
}, [node.children]);
|
|
34
|
+
|
|
35
|
+
const skipConditions = useMemo(
|
|
36
|
+
() => normalizeSkipConditions(attrs?.skipConditions),
|
|
37
|
+
[attrs?.skipConditions],
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const pages = useMemo(
|
|
41
|
+
() => buildPages(childNodes, skipConditions),
|
|
42
|
+
[childNodes, skipConditions],
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const storageKey = attributeKey;
|
|
46
|
+
|
|
47
|
+
const [conditions, setConditions] = useState<Record<string, boolean>>(() => {
|
|
48
|
+
if (shouldPersist) {
|
|
49
|
+
return loadProgress(storageKey)?.conditions ?? {};
|
|
50
|
+
}
|
|
51
|
+
return {};
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const [pageStack, setPageStack] = useState<string[]>(() => {
|
|
55
|
+
const saved = shouldPersist ? loadProgress(storageKey) : null;
|
|
56
|
+
const initialConditions = saved?.conditions ?? {};
|
|
57
|
+
|
|
58
|
+
const firstEffective = (() => {
|
|
59
|
+
if (saved?.currentPageKey) {
|
|
60
|
+
return resolveEffectivePage(
|
|
61
|
+
saved.currentPageKey,
|
|
62
|
+
pages,
|
|
63
|
+
initialConditions,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const requestedKey =
|
|
68
|
+
typeof attrs?.initialPage === 'string'
|
|
69
|
+
? attrs.initialPage
|
|
70
|
+
: (pages[0]?.key ?? '');
|
|
71
|
+
return resolveEffectivePage(requestedKey, pages, initialConditions);
|
|
72
|
+
})();
|
|
73
|
+
return firstEffective ? [firstEffective] : [];
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const currentPageKey = pageStack[pageStack.length - 1] ?? '';
|
|
77
|
+
|
|
78
|
+
const conditionsRef = useRef(conditions);
|
|
79
|
+
conditionsRef.current = conditions;
|
|
80
|
+
|
|
81
|
+
const setCondition = useCallback(
|
|
82
|
+
(key: string, value: boolean) => {
|
|
83
|
+
setConditions((prev) => {
|
|
84
|
+
const next = { ...prev, [key]: value };
|
|
85
|
+
if (shouldPersist) {
|
|
86
|
+
const currentKey = pageStack[pageStack.length - 1] ?? '';
|
|
87
|
+
persistProgress(storageKey, currentKey, next);
|
|
88
|
+
}
|
|
89
|
+
return next;
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
[shouldPersist, storageKey, pageStack],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const navigate = useCallback(
|
|
96
|
+
(key: string) => {
|
|
97
|
+
const effective = resolveEffectivePage(key, pages, conditionsRef.current);
|
|
98
|
+
if (!effective) return;
|
|
99
|
+
|
|
100
|
+
setPageStack((prev) => {
|
|
101
|
+
const last = prev[prev.length - 1];
|
|
102
|
+
if (last === effective) return prev;
|
|
103
|
+
const next = [...prev, effective];
|
|
104
|
+
if (shouldPersist) {
|
|
105
|
+
persistProgress(storageKey, effective, conditionsRef.current);
|
|
106
|
+
}
|
|
107
|
+
return next;
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
[pages, shouldPersist, storageKey],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const goNext = useCallback(() => {
|
|
114
|
+
const currentIdx = pages.findIndex((p) => p.key === currentPageKey);
|
|
115
|
+
if (currentIdx === -1 || currentIdx >= pages.length - 1) return;
|
|
116
|
+
const nextPage = pages[currentIdx + 1];
|
|
117
|
+
if (!nextPage) return;
|
|
118
|
+
navigate(nextPage.key);
|
|
119
|
+
}, [pages, currentPageKey, navigate]);
|
|
120
|
+
|
|
121
|
+
const goBack = useCallback((): boolean => {
|
|
122
|
+
if (pageStack.length <= 1) return false;
|
|
123
|
+
setPageStack((prev) => {
|
|
124
|
+
const next = prev.slice(0, -1);
|
|
125
|
+
const prevKey = next[next.length - 1] ?? '';
|
|
126
|
+
if (shouldPersist) {
|
|
127
|
+
persistProgress(storageKey, prevKey, conditionsRef.current);
|
|
128
|
+
}
|
|
129
|
+
return next;
|
|
130
|
+
});
|
|
131
|
+
return true;
|
|
132
|
+
}, [pageStack.length, shouldPersist, storageKey]);
|
|
133
|
+
|
|
134
|
+
const activePage = useMemo(
|
|
135
|
+
() => pages.find((p) => p.key === currentPageKey),
|
|
136
|
+
[pages, currentPageKey],
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const contextValue = useMemo<GlobalContextValue>(
|
|
140
|
+
() => ({
|
|
141
|
+
currentPageKey,
|
|
142
|
+
pages,
|
|
143
|
+
pageStack,
|
|
144
|
+
navigate,
|
|
145
|
+
goNext,
|
|
146
|
+
goBack,
|
|
147
|
+
conditions,
|
|
148
|
+
setCondition,
|
|
149
|
+
}),
|
|
150
|
+
[
|
|
151
|
+
currentPageKey,
|
|
152
|
+
pages,
|
|
153
|
+
pageStack,
|
|
154
|
+
navigate,
|
|
155
|
+
goNext,
|
|
156
|
+
goBack,
|
|
157
|
+
conditions,
|
|
158
|
+
setCondition,
|
|
159
|
+
],
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
node,
|
|
164
|
+
attributeName,
|
|
165
|
+
attributeKey,
|
|
166
|
+
attrs,
|
|
167
|
+
pages,
|
|
168
|
+
currentPageKey,
|
|
169
|
+
activePage,
|
|
170
|
+
contextValue,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type PlacementEventObject,
|
|
14
14
|
} from '../SystemButton/usePlacementButtonEvents';
|
|
15
15
|
import { useGlobalNavigation } from '../GlobalProvider/useGlobalNavigation';
|
|
16
|
+
import { useGlobalContext } from '../GlobalProvider/GlobalContext';
|
|
16
17
|
|
|
17
18
|
export function OnboardButton({ node }: OnboardButtonComponentProps) {
|
|
18
19
|
useLogRender('OnboardButton');
|
|
@@ -23,6 +24,7 @@ export function OnboardButton({ node }: OnboardButtonComponentProps) {
|
|
|
23
24
|
const context = useMockOSContext();
|
|
24
25
|
const mockPermissionManager = useMockPermission(context);
|
|
25
26
|
const globalNavigate = useGlobalNavigation();
|
|
27
|
+
const globalCtx = useGlobalContext();
|
|
26
28
|
const generatedId = useId();
|
|
27
29
|
const attributeKey = node.key ?? generatedId;
|
|
28
30
|
const attrs = node.attributes;
|
|
@@ -68,6 +70,7 @@ export function OnboardButton({ node }: OnboardButtonComponentProps) {
|
|
|
68
70
|
mockPermissionManager.requestPermission(permission),
|
|
69
71
|
onNavigateWithoutPlacement,
|
|
70
72
|
globalNavigate,
|
|
73
|
+
setCondition: globalCtx?.setCondition,
|
|
71
74
|
});
|
|
72
75
|
|
|
73
76
|
return (
|
|
@@ -2,7 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
import type { NodeData } from '../../types/Node';
|
|
4
4
|
|
|
5
|
-
export type TypeOptionType =
|
|
5
|
+
export type TypeOptionType =
|
|
6
|
+
| 'Permission'
|
|
7
|
+
| 'Navigate'
|
|
8
|
+
| 'Placement'
|
|
9
|
+
| 'SetCondition';
|
|
10
|
+
export type PermissionOptionType =
|
|
11
|
+
| 'notification'
|
|
12
|
+
| 'camera'
|
|
13
|
+
| 'microphone'
|
|
14
|
+
| 'location'
|
|
15
|
+
| 'photos'
|
|
16
|
+
| 'contacts'
|
|
17
|
+
| 'att'
|
|
18
|
+
| 'rating'
|
|
19
|
+
| 'GDPR';
|
|
20
|
+
export type PlacementKeyOptionType =
|
|
21
|
+
| 'terms'
|
|
22
|
+
| 'onboard'
|
|
23
|
+
| 'paywall'
|
|
24
|
+
| 'subscription'
|
|
25
|
+
| 'home';
|
|
26
|
+
export type ConditionKeyOptionType = 'termsAccepted';
|
|
6
27
|
export type TestIDOptionType = 'onboardSkip' | 'onboardNext';
|
|
7
28
|
export type AnimationOptionType =
|
|
8
29
|
| 'simple-animation'
|
|
@@ -29,10 +50,12 @@ export type PositionOptionType = 'relative' | 'absolute';
|
|
|
29
50
|
|
|
30
51
|
export interface EventObjectGenerated {
|
|
31
52
|
type?: TypeOptionType;
|
|
32
|
-
permission?:
|
|
53
|
+
permission?: PermissionOptionType;
|
|
33
54
|
navigate_to?: string;
|
|
34
55
|
targetIndex?: number;
|
|
35
|
-
placementKey?:
|
|
56
|
+
placementKey?: PlacementKeyOptionType;
|
|
57
|
+
conditionKey?: ConditionKeyOptionType;
|
|
58
|
+
value?: boolean;
|
|
36
59
|
}
|
|
37
60
|
|
|
38
61
|
export interface OnboardButtonStyleGenerated {
|
|
@@ -32,11 +32,13 @@
|
|
|
32
32
|
},
|
|
33
33
|
"types": {
|
|
34
34
|
"EventObject": {
|
|
35
|
-
"type":
|
|
36
|
-
"permission": "
|
|
35
|
+
"type": "$ref:event-constants.eventTypes",
|
|
36
|
+
"permission": "$ref:event-constants.permissionTypes",
|
|
37
37
|
"navigate_to": "string",
|
|
38
38
|
"targetIndex": "number",
|
|
39
|
-
"placementKey": "
|
|
39
|
+
"placementKey": "$ref:event-constants.placementKeys",
|
|
40
|
+
"conditionKey": "$ref:event-constants.conditionKeys",
|
|
41
|
+
"value": "boolean"
|
|
40
42
|
}
|
|
41
43
|
},
|
|
42
44
|
"meta": {
|
|
@@ -9,6 +9,7 @@ import { useMockOSContext, useMockPermission } from '../../mockOS';
|
|
|
9
9
|
import { useLocalize } from '../../hooks/useLocalize';
|
|
10
10
|
import { usePlacementButtonEvents } from './usePlacementButtonEvents';
|
|
11
11
|
import { useGlobalNavigation } from '../GlobalProvider/useGlobalNavigation';
|
|
12
|
+
import { useGlobalContext } from '../GlobalProvider/GlobalContext';
|
|
12
13
|
|
|
13
14
|
export type SystemButtonOptionalProps = {
|
|
14
15
|
onClick?: () => void;
|
|
@@ -26,6 +27,7 @@ export function SystemButton({
|
|
|
26
27
|
const context = useMockOSContext();
|
|
27
28
|
const mockPermissionManager = useMockPermission(context);
|
|
28
29
|
const globalNavigate = useGlobalNavigation();
|
|
30
|
+
const globalCtx = useGlobalContext();
|
|
29
31
|
const generatedId = useId();
|
|
30
32
|
const attributeKey = node.key ?? generatedId;
|
|
31
33
|
const attrs = node.attributes;
|
|
@@ -40,6 +42,7 @@ export function SystemButton({
|
|
|
40
42
|
requestPermission: (permission) =>
|
|
41
43
|
mockPermissionManager.requestPermission(permission),
|
|
42
44
|
globalNavigate,
|
|
45
|
+
setCondition: globalCtx?.setCondition,
|
|
43
46
|
});
|
|
44
47
|
|
|
45
48
|
const handleClick = onClickProp ?? placementClick;
|
|
@@ -2,7 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
import type { NodeData } from '../../types/Node';
|
|
4
4
|
|
|
5
|
-
export type TypeOptionType =
|
|
5
|
+
export type TypeOptionType =
|
|
6
|
+
| 'Permission'
|
|
7
|
+
| 'Navigate'
|
|
8
|
+
| 'Placement'
|
|
9
|
+
| 'SetCondition';
|
|
10
|
+
export type PermissionOptionType =
|
|
11
|
+
| 'notification'
|
|
12
|
+
| 'camera'
|
|
13
|
+
| 'microphone'
|
|
14
|
+
| 'location'
|
|
15
|
+
| 'photos'
|
|
16
|
+
| 'contacts'
|
|
17
|
+
| 'att'
|
|
18
|
+
| 'rating'
|
|
19
|
+
| 'GDPR';
|
|
20
|
+
export type PlacementKeyOptionType =
|
|
21
|
+
| 'terms'
|
|
22
|
+
| 'onboard'
|
|
23
|
+
| 'paywall'
|
|
24
|
+
| 'subscription'
|
|
25
|
+
| 'home';
|
|
26
|
+
export type ConditionKeyOptionType = 'termsAccepted';
|
|
6
27
|
export type FlexDirectionOptionType = 'row' | 'column';
|
|
7
28
|
export type FlexWrapOptionType = 'nowrap' | 'wrap' | 'wrap-reverse';
|
|
8
29
|
export type AlignItemsOptionType =
|
|
@@ -22,10 +43,12 @@ export type PositionOptionType = 'relative' | 'absolute';
|
|
|
22
43
|
|
|
23
44
|
export interface EventObjectGenerated {
|
|
24
45
|
type?: TypeOptionType;
|
|
25
|
-
permission?:
|
|
46
|
+
permission?: PermissionOptionType;
|
|
26
47
|
navigate_to?: string;
|
|
27
48
|
targetIndex?: number;
|
|
28
|
-
placementKey?:
|
|
49
|
+
placementKey?: PlacementKeyOptionType;
|
|
50
|
+
conditionKey?: ConditionKeyOptionType;
|
|
51
|
+
value?: boolean;
|
|
29
52
|
}
|
|
30
53
|
|
|
31
54
|
export interface SystemButtonStyleGenerated {
|