@developer_tribe/react-builder 1.2.43 → 1.2.44-test.1
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/build-components/Checkbox/Checkbox.d.ts +6 -0
- package/dist/build-components/Checkbox/CheckboxProps.generated.d.ts +67 -0
- package/dist/build-components/FormCheckbox/FormCheckbox.d.ts +3 -0
- package/dist/build-components/FormCheckbox/FormCheckboxProps.generated.d.ts +69 -0
- package/dist/build-components/FormErrorText/FormErrorText.d.ts +3 -0
- package/dist/build-components/FormErrorText/FormErrorTextProps.generated.d.ts +61 -0
- package/dist/build-components/FormProvider/FormProvider.d.ts +11 -0
- package/dist/build-components/FormProvider/FormProviderProps.generated.d.ts +55 -0
- package/dist/build-components/FormSubmitButton/FormSubmitButton.d.ts +2 -0
- package/dist/build-components/FormSubmitButton/FormSubmitButtonProps.generated.d.ts +73 -0
- package/dist/build-components/GlobalProvider/GlobalContext.d.ts +28 -0
- package/dist/build-components/GlobalProvider/GlobalProvider.d.ts +5 -0
- package/dist/build-components/GlobalProvider/GlobalProviderProps.generated.d.ts +60 -0
- package/dist/build-components/GlobalProvider/globalProviderUtils.d.ts +37 -0
- package/dist/build-components/GlobalProvider/useGlobalNavigation.d.ts +19 -0
- package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +17 -10
- package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +2 -0
- package/dist/build-components/PaywallProvider/PaywallProviderProps.generated.d.ts +2 -0
- package/dist/build-components/SystemButton/SystemButton.d.ts +7 -0
- package/dist/build-components/SystemButton/SystemButtonProps.generated.d.ts +71 -0
- package/dist/build-components/SystemButton/usePlacementButtonEvents.d.ts +28 -0
- package/dist/build-components/TermsProvider/TermsProvider.d.ts +5 -0
- package/dist/build-components/TermsProvider/TermsProviderProps.generated.d.ts +55 -0
- package/dist/build-components/WebView/WebView.d.ts +2 -0
- package/dist/build-components/WebView/WebViewProps.generated.d.ts +59 -0
- package/dist/build-components/index.d.ts +10 -1
- package/dist/build-components/patterns.generated.d.ts +5639 -1686
- 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 +5 -5
- package/dist/index.web.cjs.js.map +1 -1
- package/dist/index.web.d.ts +1 -0
- package/dist/index.web.esm.js +4 -4
- package/dist/index.web.esm.js.map +1 -1
- package/dist/mockOS/context/MockOSContextBase.d.ts +3 -1
- package/dist/styles.css +1 -1
- package/dist/types/PreviewConfig.d.ts +1 -1
- package/package.json +2 -1
- package/src/assets/meta.json +1 -1
- package/src/assets/prompt-scheme-onboard.generated.ts +1 -1
- package/src/assets/prompt-scheme-paywall.generated.ts +1 -1
- package/src/assets/samples/getSamples.ts +7 -0
- package/src/assets/samples/global-onboard-flow.json +729 -0
- package/src/assets/samples/terms-and-privacy-no-form.json +108 -0
- package/src/assets/samples/terms-and-privacy.json +130 -0
- package/src/build-components/Checkbox/Checkbox.tsx +165 -0
- package/src/build-components/Checkbox/CheckboxProps.generated.ts +84 -0
- package/src/build-components/Checkbox/pattern.json +83 -0
- package/src/build-components/FormCheckbox/FormCheckbox.tsx +106 -0
- package/src/build-components/FormCheckbox/FormCheckboxProps.generated.ts +86 -0
- package/src/build-components/FormCheckbox/pattern.json +39 -0
- package/src/build-components/FormErrorText/FormErrorText.tsx +34 -0
- package/src/build-components/FormErrorText/FormErrorTextProps.generated.ts +78 -0
- package/src/build-components/FormErrorText/pattern.json +21 -0
- package/src/build-components/FormProvider/FormProvider.tsx +131 -0
- package/src/build-components/FormProvider/FormProviderProps.generated.ts +72 -0
- package/src/build-components/FormProvider/pattern.json +33 -0
- package/src/build-components/FormSubmitButton/FormSubmitButton.tsx +49 -0
- package/src/build-components/FormSubmitButton/FormSubmitButtonProps.generated.ts +91 -0
- package/src/build-components/FormSubmitButton/pattern.json +33 -0
- package/src/build-components/GlobalProvider/GlobalContext.ts +48 -0
- package/src/build-components/GlobalProvider/GlobalProvider.tsx +191 -0
- package/src/build-components/GlobalProvider/GlobalProviderProps.generated.ts +78 -0
- package/src/build-components/GlobalProvider/globalProviderUtils.ts +163 -0
- package/src/build-components/GlobalProvider/pattern.json +55 -0
- package/src/build-components/GlobalProvider/useGlobalNavigation.ts +70 -0
- package/src/build-components/OnboardButton/OnboardButton.tsx +41 -36
- package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +17 -10
- package/src/build-components/OnboardButton/pattern.json +5 -4
- package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +12 -0
- package/src/build-components/OnboardProvider/pattern.json +9 -1
- package/src/build-components/PaywallProvider/PaywallProviderProps.generated.ts +12 -0
- package/src/build-components/PaywallProvider/pattern.json +9 -1
- package/src/build-components/RenderNode.generated.tsx +46 -1
- package/src/build-components/SystemButton/SystemButton.tsx +71 -0
- package/src/build-components/SystemButton/SystemButtonProps.generated.ts +89 -0
- package/src/build-components/SystemButton/pattern.json +61 -0
- package/src/build-components/SystemButton/usePlacementButtonEvents.ts +101 -0
- package/src/build-components/TermsProvider/TermsProvider.tsx +45 -0
- package/src/build-components/TermsProvider/TermsProviderProps.generated.ts +82 -0
- package/src/build-components/TermsProvider/pattern.json +35 -0
- package/src/build-components/WebView/WebView.tsx +149 -0
- package/src/build-components/WebView/WebViewProps.generated.ts +76 -0
- package/src/build-components/WebView/pattern.json +71 -0
- package/src/build-components/index.ts +45 -0
- package/src/build-components/patterns.generated.ts +5701 -1559
- package/src/index.web.ts +3 -0
- package/src/mockOS/components/MockOSRouter.tsx +21 -0
- package/src/mockOS/context/MockOSContext.tsx +7 -0
- package/src/mockOS/context/MockOSContextBase.ts +4 -0
- package/src/styles/components/_checkbox.scss +19 -0
- package/src/styles/components/_global-provider.scss +131 -0
- package/src/styles/components/_webview.scss +52 -0
- package/src/styles/index.scss +4 -0
- package/src/types/PreviewConfig.ts +19 -0
- package/src/utils/analyseNodeByPatterns.ts +5 -2
- package/src/utils/projectColors.ts +4 -0
- package/src/.DS_Store +0 -0
- package/src/assets/.DS_Store +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import type { NodeData } from '../../types/Node';
|
|
3
|
+
|
|
4
|
+
/** Supported transition animations when navigating to this page (platform-style names).
|
|
5
|
+
* rn stack transition animations
|
|
6
|
+
*/
|
|
7
|
+
export const GLOBAL_PAGE_ANIMATIONS = [
|
|
8
|
+
'default',
|
|
9
|
+
'fade',
|
|
10
|
+
'fade_from_bottom',
|
|
11
|
+
'fade_from_right',
|
|
12
|
+
'reveal_from_bottom',
|
|
13
|
+
'scale_from_center',
|
|
14
|
+
'slide_from_right',
|
|
15
|
+
'slide_from_left',
|
|
16
|
+
'slide_from_bottom',
|
|
17
|
+
'none',
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
export type GlobalPageAnimationType = (typeof GLOBAL_PAGE_ANIMATIONS)[number];
|
|
21
|
+
|
|
22
|
+
export interface GlobalPage {
|
|
23
|
+
key: string;
|
|
24
|
+
node: NodeData;
|
|
25
|
+
/** Condition key: this page is skipped when conditions[skipIf] === true */
|
|
26
|
+
skipIf?: string;
|
|
27
|
+
/** Optional transition animation when this page is shown (push/enter). */
|
|
28
|
+
animation?: string;
|
|
29
|
+
index: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface GlobalContextValue {
|
|
33
|
+
currentPageKey: string;
|
|
34
|
+
pages: GlobalPage[];
|
|
35
|
+
pageStack: string[];
|
|
36
|
+
navigate: (key: string) => void;
|
|
37
|
+
goNext: () => void;
|
|
38
|
+
goBack: () => boolean;
|
|
39
|
+
/** Runtime boolean conditions (e.g. termsAccepted) */
|
|
40
|
+
conditions: Record<string, boolean>;
|
|
41
|
+
setCondition: (key: string, value: boolean) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const GlobalContext = createContext<GlobalContextValue | null>(null);
|
|
45
|
+
|
|
46
|
+
export function useGlobalContext(): GlobalContextValue | null {
|
|
47
|
+
return useContext(GlobalContext);
|
|
48
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import React, { useCallback, useId, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import type { GlobalProviderComponentProps } from './GlobalProviderProps.generated';
|
|
3
|
+
import type { GlobalContextValue, GlobalPage } from './GlobalContext';
|
|
4
|
+
import { GlobalContext } from './GlobalContext';
|
|
5
|
+
import {
|
|
6
|
+
buildPages,
|
|
7
|
+
loadProgress,
|
|
8
|
+
normalizeSkipConditions,
|
|
9
|
+
persistProgress,
|
|
10
|
+
resolveEffectivePage,
|
|
11
|
+
} from './globalProviderUtils';
|
|
12
|
+
import RenderNode from '../RenderNode.generated';
|
|
13
|
+
import useNode from '../useNode';
|
|
14
|
+
import { useLogRender } from '../../utils/useLogRender';
|
|
15
|
+
import type { NodeData } from '../../types/Node';
|
|
16
|
+
|
|
17
|
+
function GlobalProvider({ node }: GlobalProviderComponentProps) {
|
|
18
|
+
useLogRender('GlobalProvider');
|
|
19
|
+
node = useNode(node);
|
|
20
|
+
|
|
21
|
+
const generatedId = useId();
|
|
22
|
+
const attributeName = node.sourceType ?? node.type ?? 'GlobalProvider';
|
|
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
|
+
);
|
|
153
|
+
|
|
154
|
+
const animationClass = (() => {
|
|
155
|
+
const a = activePage?.animation;
|
|
156
|
+
if (a === 'default') return 'global-provider-page--default';
|
|
157
|
+
if (!a || a === 'none') return ''; // No modifier: no animation at all
|
|
158
|
+
return `global-provider-page--${a}`;
|
|
159
|
+
})();
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<GlobalContext.Provider value={contextValue}>
|
|
163
|
+
<div
|
|
164
|
+
attribute-name={attributeName}
|
|
165
|
+
attribute-key={attributeKey}
|
|
166
|
+
{...(attrs?.testID ? { 'data-testid': attrs.testID } : {})}
|
|
167
|
+
style={{
|
|
168
|
+
flex: 1,
|
|
169
|
+
display: 'flex',
|
|
170
|
+
flexDirection: 'column',
|
|
171
|
+
height: '100%',
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
{activePage ? (
|
|
175
|
+
<div
|
|
176
|
+
key={activePage.key}
|
|
177
|
+
className={
|
|
178
|
+
animationClass
|
|
179
|
+
? `global-provider-page ${animationClass}`
|
|
180
|
+
: 'global-provider-page'
|
|
181
|
+
}
|
|
182
|
+
>
|
|
183
|
+
<RenderNode node={activePage.node} />
|
|
184
|
+
</div>
|
|
185
|
+
) : null}
|
|
186
|
+
</div>
|
|
187
|
+
</GlobalContext.Provider>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export default React.memo(GlobalProvider);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/* AUTO-GENERATED FILE - DO NOT EDIT */
|
|
2
|
+
|
|
3
|
+
import type { NodeData } from '../../types/Node';
|
|
4
|
+
|
|
5
|
+
export type FlexDirectionOptionType = 'row' | 'column';
|
|
6
|
+
export type FlexWrapOptionType = 'nowrap' | 'wrap' | 'wrap-reverse';
|
|
7
|
+
export type AlignItemsOptionType =
|
|
8
|
+
| 'flex-start'
|
|
9
|
+
| 'center'
|
|
10
|
+
| 'flex-end'
|
|
11
|
+
| 'stretch'
|
|
12
|
+
| 'baseline';
|
|
13
|
+
export type JustifyContentOptionType =
|
|
14
|
+
| 'flex-start'
|
|
15
|
+
| 'center'
|
|
16
|
+
| 'flex-end'
|
|
17
|
+
| 'space-between'
|
|
18
|
+
| 'space-around'
|
|
19
|
+
| 'space-evenly';
|
|
20
|
+
export type PositionOptionType = 'relative' | 'absolute';
|
|
21
|
+
|
|
22
|
+
export interface SkipConditionEntryGenerated {
|
|
23
|
+
pageKey?: string;
|
|
24
|
+
conditionKey?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GlobalProviderStyleGenerated {
|
|
28
|
+
flexDirection?: FlexDirectionOptionType;
|
|
29
|
+
flexWrap?: FlexWrapOptionType;
|
|
30
|
+
alignItems?: AlignItemsOptionType;
|
|
31
|
+
justifyContent?: JustifyContentOptionType;
|
|
32
|
+
gap?: string;
|
|
33
|
+
padding?: string;
|
|
34
|
+
paddingHorizontal?: string;
|
|
35
|
+
paddingVertical?: string;
|
|
36
|
+
paddingTop?: string;
|
|
37
|
+
paddingBottom?: string;
|
|
38
|
+
paddingLeft?: string;
|
|
39
|
+
paddingRight?: string;
|
|
40
|
+
margin?: string;
|
|
41
|
+
marginHorizontal?: string;
|
|
42
|
+
marginVertical?: string;
|
|
43
|
+
marginTop?: string;
|
|
44
|
+
marginBottom?: string;
|
|
45
|
+
marginLeft?: string;
|
|
46
|
+
marginRight?: string;
|
|
47
|
+
backgroundColor?: string;
|
|
48
|
+
borderRadius?: string;
|
|
49
|
+
width?: string;
|
|
50
|
+
minWidth?: string;
|
|
51
|
+
maxWidth?: string;
|
|
52
|
+
height?: string;
|
|
53
|
+
minHeight?: string;
|
|
54
|
+
maxHeight?: string;
|
|
55
|
+
flex?: number;
|
|
56
|
+
position?: PositionOptionType;
|
|
57
|
+
top?: string;
|
|
58
|
+
bottom?: string;
|
|
59
|
+
left?: string;
|
|
60
|
+
right?: string;
|
|
61
|
+
zIndex?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface GlobalProviderPropsGenerated {
|
|
65
|
+
child: string;
|
|
66
|
+
attributes: {
|
|
67
|
+
styles?: GlobalProviderStyleGenerated;
|
|
68
|
+
scrollable?: boolean;
|
|
69
|
+
testID?: string;
|
|
70
|
+
initialPage?: string;
|
|
71
|
+
persistProgress?: boolean;
|
|
72
|
+
skipConditions?: SkipConditionEntryGenerated[];
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface GlobalProviderComponentProps {
|
|
77
|
+
node: NodeData<GlobalProviderPropsGenerated['attributes']>;
|
|
78
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { NodeData } from '../../types/Node';
|
|
2
|
+
import type { GlobalPage } from './GlobalContext';
|
|
3
|
+
|
|
4
|
+
/** Maps node type to a canonical page key when no explicit pageKey attribute is provided. */
|
|
5
|
+
const TYPE_KEY_MAP: Record<string, string> = {
|
|
6
|
+
TermsProvider: 'terms',
|
|
7
|
+
OnboardProvider: 'onboard',
|
|
8
|
+
PaywallProvider: 'paywall',
|
|
9
|
+
FormProvider: 'form',
|
|
10
|
+
CarouselProvider: 'carousel',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Derives a stable page key for a child node.
|
|
15
|
+
* Priority: node.key (standard NodeData field) → TYPE_KEY_MAP → "${type}-{index}"
|
|
16
|
+
*
|
|
17
|
+
* Using node.key avoids adding unknown attributes to child components that
|
|
18
|
+
* would fail builder validation.
|
|
19
|
+
*/
|
|
20
|
+
export function derivePageKey(node: NodeData, index: number): string {
|
|
21
|
+
if (typeof node.key === 'string' && node.key.trim()) {
|
|
22
|
+
return node.key.trim();
|
|
23
|
+
}
|
|
24
|
+
const mapped = TYPE_KEY_MAP[node.type];
|
|
25
|
+
if (mapped) {
|
|
26
|
+
return mapped;
|
|
27
|
+
}
|
|
28
|
+
const typeSlug = node.type?.toLowerCase?.() ?? 'page';
|
|
29
|
+
return `${typeSlug}-${index}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Runtime shape of a SkipConditionEntry (mirrors the types block in pattern.json). */
|
|
33
|
+
export interface SkipConditionEntry {
|
|
34
|
+
pageKey: string;
|
|
35
|
+
conditionKey: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 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
|
+
*/
|
|
43
|
+
export function normalizeSkipConditions(raw: unknown): Record<string, string> {
|
|
44
|
+
if (!Array.isArray(raw)) return {};
|
|
45
|
+
const result: Record<string, string> = {};
|
|
46
|
+
for (const entry of raw) {
|
|
47
|
+
if (
|
|
48
|
+
entry &&
|
|
49
|
+
typeof entry === 'object' &&
|
|
50
|
+
typeof (entry as SkipConditionEntry).pageKey === 'string' &&
|
|
51
|
+
typeof (entry as SkipConditionEntry).conditionKey === 'string'
|
|
52
|
+
) {
|
|
53
|
+
result[(entry as SkipConditionEntry).pageKey] = (
|
|
54
|
+
entry as SkipConditionEntry
|
|
55
|
+
).conditionKey;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 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
|
+
*/
|
|
68
|
+
export function buildPages(
|
|
69
|
+
children: NodeData[],
|
|
70
|
+
skipConditions: Record<string, string> = {},
|
|
71
|
+
): GlobalPage[] {
|
|
72
|
+
return children.map((node, index) => {
|
|
73
|
+
const key = derivePageKey(node, index);
|
|
74
|
+
const skipIf = skipConditions[key] ?? undefined;
|
|
75
|
+
const animation =
|
|
76
|
+
typeof (node.attributes as Record<string, unknown>)?.animation ===
|
|
77
|
+
'string'
|
|
78
|
+
? (node.attributes as Record<string, string>).animation
|
|
79
|
+
: undefined;
|
|
80
|
+
return {
|
|
81
|
+
key,
|
|
82
|
+
node,
|
|
83
|
+
skipIf,
|
|
84
|
+
animation,
|
|
85
|
+
index,
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Returns the first non-skipped page key, starting from the requested key. */
|
|
91
|
+
export function resolveEffectivePage(
|
|
92
|
+
targetKey: string,
|
|
93
|
+
pages: GlobalPage[],
|
|
94
|
+
conditions: Record<string, boolean>,
|
|
95
|
+
): string {
|
|
96
|
+
const targetIdx = pages.findIndex((p) => p.key === targetKey);
|
|
97
|
+
if (targetIdx === -1) {
|
|
98
|
+
return pages[0]?.key ?? targetKey;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Walk forward from target, skipping pages whose condition is met
|
|
102
|
+
for (let i = targetIdx; i < pages.length; i++) {
|
|
103
|
+
const page = pages[i];
|
|
104
|
+
if (!page) break;
|
|
105
|
+
if (page.skipIf && conditions[page.skipIf] === true) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
return page.key;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// All remaining pages are skipped — return last page key anyway
|
|
112
|
+
return pages[pages.length - 1]?.key ?? targetKey;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const STORAGE_KEY_PREFIX = 'global-provider-progress';
|
|
116
|
+
|
|
117
|
+
export function persistProgress(
|
|
118
|
+
storageKey: string,
|
|
119
|
+
currentPageKey: string,
|
|
120
|
+
conditions: Record<string, boolean>,
|
|
121
|
+
): void {
|
|
122
|
+
try {
|
|
123
|
+
localStorage.setItem(
|
|
124
|
+
`${STORAGE_KEY_PREFIX}-${storageKey}`,
|
|
125
|
+
JSON.stringify({ currentPageKey, conditions }),
|
|
126
|
+
);
|
|
127
|
+
} catch {
|
|
128
|
+
// localStorage unavailable (SSR, private browsing etc.)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function loadProgress(storageKey: string): {
|
|
133
|
+
currentPageKey: string;
|
|
134
|
+
conditions: Record<string, boolean>;
|
|
135
|
+
} | null {
|
|
136
|
+
try {
|
|
137
|
+
const raw = localStorage.getItem(`${STORAGE_KEY_PREFIX}-${storageKey}`);
|
|
138
|
+
if (!raw) return null;
|
|
139
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
140
|
+
if (
|
|
141
|
+
parsed &&
|
|
142
|
+
typeof parsed === 'object' &&
|
|
143
|
+
!Array.isArray(parsed) &&
|
|
144
|
+
typeof (parsed as Record<string, unknown>).currentPageKey === 'string'
|
|
145
|
+
) {
|
|
146
|
+
return parsed as {
|
|
147
|
+
currentPageKey: string;
|
|
148
|
+
conditions: Record<string, boolean>;
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
// parse error
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function clearProgress(storageKey: string): void {
|
|
158
|
+
try {
|
|
159
|
+
localStorage.removeItem(`${STORAGE_KEY_PREFIX}-${storageKey}`);
|
|
160
|
+
} catch {
|
|
161
|
+
// localStorage unavailable
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"types": {
|
|
4
|
+
"SkipConditionEntry": {
|
|
5
|
+
"pageKey": "string",
|
|
6
|
+
"conditionKey": "string"
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"pattern": {
|
|
10
|
+
"type": "GlobalProvider",
|
|
11
|
+
"title": "title",
|
|
12
|
+
"description": "description",
|
|
13
|
+
"children": "node",
|
|
14
|
+
"extends": "View",
|
|
15
|
+
"attributes": {
|
|
16
|
+
"initialPage": "string",
|
|
17
|
+
"persistProgress": "boolean",
|
|
18
|
+
"skipConditions": "SkipConditionEntry[]"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"defaults": {
|
|
22
|
+
"styles": {
|
|
23
|
+
"flex": 1
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"meta": {
|
|
27
|
+
"desiredParent": ["root"],
|
|
28
|
+
"label": "Global Provider",
|
|
29
|
+
"description": "Top-level provider that manages multi-page flow (Terms → Onboard → Paywall). Each direct child is treated as a page.",
|
|
30
|
+
"attributes": {
|
|
31
|
+
"initialPage": {
|
|
32
|
+
"label": "Initial Page Key",
|
|
33
|
+
"description": "The page key to show first. Defaults to the first child.",
|
|
34
|
+
"category": "other",
|
|
35
|
+
"specialCategory": null,
|
|
36
|
+
"sort": 1
|
|
37
|
+
},
|
|
38
|
+
"persistProgress": {
|
|
39
|
+
"label": "Persist Progress",
|
|
40
|
+
"description": "Save current page and conditions to localStorage so the user continues from where they left off.",
|
|
41
|
+
"category": "other",
|
|
42
|
+
"specialCategory": null,
|
|
43
|
+
"sort": 2
|
|
44
|
+
},
|
|
45
|
+
"skipConditions": {
|
|
46
|
+
"label": "Skip Conditions",
|
|
47
|
+
"description": "List of page skip rules. Each entry maps a pageKey to a conditionKey: when conditions[conditionKey] is true, that page is skipped on navigate.",
|
|
48
|
+
"category": "other",
|
|
49
|
+
"specialCategory": null,
|
|
50
|
+
"sort": 3
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"styles": {}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useGlobalContext } from './GlobalContext';
|
|
3
|
+
import { useMockOSContext } from '../../mockOS/context/MockOSContextBase';
|
|
4
|
+
import type { RouteType } from '../../mockOS/context/MockOSContextBase';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Maps route/placement aliases to their canonical GlobalProvider page keys.
|
|
8
|
+
*
|
|
9
|
+
* Add entries here when a new alias should resolve to an existing page key.
|
|
10
|
+
* Keys are the incoming target strings (from JSON events / usePlacementButtonEvents).
|
|
11
|
+
* Values are the canonical page key that GlobalContext.navigate() will receive.
|
|
12
|
+
*
|
|
13
|
+
* Exported so other modules (e.g. a navigation-flow visualiser) can inspect
|
|
14
|
+
* which aliases exist without duplicating this logic.
|
|
15
|
+
*/
|
|
16
|
+
export const GLOBAL_ROUTE_ALIASES: Record<string, string> = {
|
|
17
|
+
subscription: 'paywall',
|
|
18
|
+
paywall: 'paywall',
|
|
19
|
+
onboard: 'onboard',
|
|
20
|
+
terms: 'terms',
|
|
21
|
+
home: 'home',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns a navigate function that first attempts to route within GlobalContext
|
|
26
|
+
* (when a GlobalProvider is active), then falls back to MockOSContext navigation.
|
|
27
|
+
*
|
|
28
|
+
* Used by SystemButton and OnboardButton so that events defined in JSON work
|
|
29
|
+
* transparently whether rendered inside a GlobalProvider or standalone.
|
|
30
|
+
*/
|
|
31
|
+
export function useGlobalNavigation(): (target: string) => boolean {
|
|
32
|
+
const globalCtx = useGlobalContext();
|
|
33
|
+
const mockOS = useMockOSContext();
|
|
34
|
+
|
|
35
|
+
return useCallback(
|
|
36
|
+
(target: string): boolean => {
|
|
37
|
+
if (globalCtx) {
|
|
38
|
+
// Direct page key match
|
|
39
|
+
console.log('target', target);
|
|
40
|
+
console.log('globalCtx.pages', globalCtx.pages);
|
|
41
|
+
if (globalCtx.pages.some((p) => p.key === target)) {
|
|
42
|
+
globalCtx.navigate(target);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Alias resolution: map incoming target to canonical page key
|
|
47
|
+
const canonicalKey = GLOBAL_ROUTE_ALIASES[target];
|
|
48
|
+
console.log('canonicalKey', canonicalKey);
|
|
49
|
+
if (
|
|
50
|
+
canonicalKey &&
|
|
51
|
+
globalCtx.pages.some((p) => p.key === canonicalKey)
|
|
52
|
+
) {
|
|
53
|
+
console.log('navigate to canonicalKey', canonicalKey);
|
|
54
|
+
globalCtx.navigate(canonicalKey);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log('fallback to MockOSContext', target);
|
|
60
|
+
// Fallback: delegate to MockOSContext
|
|
61
|
+
if (mockOS) {
|
|
62
|
+
mockOS.navigation(target as RouteType);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return false;
|
|
67
|
+
},
|
|
68
|
+
[globalCtx, mockOS],
|
|
69
|
+
);
|
|
70
|
+
}
|