@revenuecat/purchases-ui-js 4.8.6 → 4.8.8
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/button/ButtonNode.svelte +26 -16
- package/dist/components/button/ButtonNodeTestWrapper.svelte +19 -1
- package/dist/components/button/ButtonNodeTestWrapper.svelte.d.ts +3 -0
- package/dist/components/button/button-input-gating.d.ts +3 -0
- package/dist/components/button/button-input-gating.js +6 -0
- package/dist/components/footer/Footer.svelte +1 -4
- package/dist/components/input-text/InputText.svelte +16 -1
- package/dist/components/input-text/InputTextTestWrapper.svelte +18 -3
- package/dist/components/input-text/InputTextTestWrapper.svelte.d.ts +3 -1
- package/dist/components/paywall/Paywall.stories.svelte +27 -0
- package/dist/components/paywall/Paywall.svelte +40 -13
- package/dist/components/paywall/Paywall.svelte.d.ts +5 -0
- package/dist/components/paywall/fixtures/transparent-footer-paywall.d.ts +7 -0
- package/dist/components/paywall/fixtures/transparent-footer-paywall.js +59 -0
- package/dist/components/web-view/WebView.svelte +8 -2
- package/dist/components/workflows/Screen.svelte +7 -0
- package/dist/components/workflows/Screen.svelte.d.ts +5 -0
- package/dist/components/workflows/Workflow.svelte +7 -0
- package/dist/components/workflows/Workflow.svelte.d.ts +5 -0
- package/dist/stores/inputValidation.d.ts +1 -1
- package/dist/stores/inputValidation.js +2 -2
- package/dist/stores/paywall.d.ts +6 -0
- package/dist/types/components/button.d.ts +6 -0
- package/dist/types/components/input-text.d.ts +1 -1
- package/dist/types.d.ts +9 -3
- package/package.json +1 -1
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
PackageSelectionSheetInteractionData,
|
|
11
11
|
} from "../../types/paywall-component-interaction";
|
|
12
12
|
import { getActiveStateProps } from "../../utils/style-utils";
|
|
13
|
-
import { readable } from "svelte/store";
|
|
13
|
+
import { fromStore, readable } from "svelte/store";
|
|
14
|
+
import { isWorkflowButtonInputGated } from "./button-input-gating";
|
|
14
15
|
|
|
15
16
|
const props: ButtonProps = $props();
|
|
16
17
|
type SheetAction = Extract<
|
|
@@ -35,13 +36,8 @@
|
|
|
35
36
|
const validationContext = getInputValidationContext();
|
|
36
37
|
const { getLocalizedString } = getLocalizationContext();
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// Button is disabled when it's a workflow (continue) or complete_workflow action and inputs aren't satisfied
|
|
42
|
-
const isDisabled = $derived(
|
|
43
|
-
(action.type === "workflow" || action.type === "complete_workflow") &&
|
|
44
|
-
!$inputSatisfied,
|
|
39
|
+
const inputSatisfied = fromStore(
|
|
40
|
+
validationContext?.isSatisfied ?? readable(true),
|
|
45
41
|
);
|
|
46
42
|
|
|
47
43
|
const getButtonInteractionData = (): ButtonInteractionData => {
|
|
@@ -142,7 +138,15 @@
|
|
|
142
138
|
});
|
|
143
139
|
|
|
144
140
|
const onclick = () => {
|
|
145
|
-
if (
|
|
141
|
+
if (
|
|
142
|
+
isWorkflowButtonInputGated(
|
|
143
|
+
action.type,
|
|
144
|
+
props.require_valid_inputs,
|
|
145
|
+
inputSatisfied.current,
|
|
146
|
+
)
|
|
147
|
+
) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
146
150
|
// For "workflow" actions, pass the component's own id so Workflow.svelte
|
|
147
151
|
// can look it up in the step's triggers array (component ID → action ID).
|
|
148
152
|
const actionId = action.type === "workflow" ? props.id : undefined;
|
|
@@ -177,14 +181,20 @@
|
|
|
177
181
|
return true;
|
|
178
182
|
}
|
|
179
183
|
});
|
|
180
|
-
|
|
181
|
-
const style = $derived(
|
|
182
|
-
isDisabled
|
|
183
|
-
? { opacity: "0.4", "pointer-events": "none", cursor: "not-allowed" }
|
|
184
|
-
: undefined,
|
|
185
|
-
);
|
|
186
184
|
</script>
|
|
187
185
|
|
|
188
186
|
{#if visible}
|
|
189
|
-
|
|
187
|
+
{@const isDisabled = isWorkflowButtonInputGated(
|
|
188
|
+
action.type,
|
|
189
|
+
props.require_valid_inputs,
|
|
190
|
+
inputSatisfied.current,
|
|
191
|
+
)}
|
|
192
|
+
<Stack
|
|
193
|
+
{...props.stack}
|
|
194
|
+
{onclick}
|
|
195
|
+
style={isDisabled
|
|
196
|
+
? { opacity: "0.4", "pointer-events": "none", cursor: "not-allowed" }
|
|
197
|
+
: undefined}
|
|
198
|
+
testId={`button-${action.type}`}
|
|
199
|
+
/>
|
|
190
200
|
{/if}
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
import { setLocalizationContext } from "../../stores/localization";
|
|
3
3
|
import { setPackageInfoContext } from "../../stores/packageInfo";
|
|
4
4
|
import { setPaywallContext } from "../../stores/paywall";
|
|
5
|
+
import {
|
|
6
|
+
createInputValidationContext,
|
|
7
|
+
setInputValidationContext,
|
|
8
|
+
} from "../../stores/inputValidation";
|
|
5
9
|
import { setVariablesContext } from "../../stores/variables";
|
|
6
10
|
import { writable, readable } from "svelte/store";
|
|
7
11
|
import ButtonNode from "./ButtonNode.svelte";
|
|
@@ -9,9 +13,23 @@
|
|
|
9
13
|
|
|
10
14
|
interface Props extends ButtonProps {
|
|
11
15
|
hideBackButtons?: boolean;
|
|
16
|
+
/** When set, seeds the shared input validation context (default: satisfied). */
|
|
17
|
+
inputsSatisfied?: boolean;
|
|
18
|
+
onSatisfactionChange?: (satisfied: boolean) => void;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
|
-
const {
|
|
21
|
+
const {
|
|
22
|
+
hideBackButtons = false,
|
|
23
|
+
inputsSatisfied = true,
|
|
24
|
+
onSatisfactionChange,
|
|
25
|
+
...buttonProps
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
const validationContext = createInputValidationContext(inputsSatisfied);
|
|
29
|
+
setInputValidationContext(validationContext);
|
|
30
|
+
if (onSatisfactionChange) {
|
|
31
|
+
validationContext.isSatisfied.subscribe(onSatisfactionChange);
|
|
32
|
+
}
|
|
15
33
|
|
|
16
34
|
setLocalizationContext(() => ({
|
|
17
35
|
defaultLocale: "en_US",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ButtonProps } from "../../types/components/button";
|
|
2
2
|
interface Props extends ButtonProps {
|
|
3
3
|
hideBackButtons?: boolean;
|
|
4
|
+
/** When set, seeds the shared input validation context (default: satisfied). */
|
|
5
|
+
inputsSatisfied?: boolean;
|
|
6
|
+
onSatisfactionChange?: (satisfied: boolean) => void;
|
|
4
7
|
}
|
|
5
8
|
declare const ButtonNodeTestWrapper: import("svelte").Component<Props, {}, "">;
|
|
6
9
|
type ButtonNodeTestWrapper = ReturnType<typeof ButtonNodeTestWrapper>;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Action } from "../../types/components/button";
|
|
2
|
+
/** Whether a workflow / complete_workflow button should be disabled. */
|
|
3
|
+
export declare function isWorkflowButtonInputGated(actionType: Action["type"], requireValidInputs: boolean | undefined, inputsSatisfied: boolean): boolean;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Whether a workflow / complete_workflow button should be disabled. */
|
|
2
|
+
export function isWorkflowButtonInputGated(actionType, requireValidInputs, inputsSatisfied) {
|
|
3
|
+
return (requireValidInputs !== false &&
|
|
4
|
+
(actionType === "workflow" || actionType === "complete_workflow") &&
|
|
5
|
+
!inputsSatisfied);
|
|
6
|
+
}
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Stack from "../stack/Stack.svelte";
|
|
3
3
|
import type { FooterProps } from "../../types/components/footer";
|
|
4
|
-
import { STICKY_OVERLAY_Z_INDEX } from "../../utils/constants";
|
|
5
4
|
|
|
6
5
|
const { stack }: FooterProps = $props();
|
|
7
6
|
</script>
|
|
8
7
|
|
|
9
|
-
<div
|
|
8
|
+
<div>
|
|
10
9
|
<Stack {...stack} />
|
|
11
10
|
</div>
|
|
12
11
|
|
|
13
12
|
<style>
|
|
14
13
|
div {
|
|
15
|
-
position: sticky;
|
|
16
|
-
bottom: 0;
|
|
17
14
|
width: 100%;
|
|
18
15
|
|
|
19
16
|
display: flex;
|
|
@@ -73,6 +73,12 @@
|
|
|
73
73
|
|
|
74
74
|
function commitValue(input: HTMLInputElement) {
|
|
75
75
|
const value = input.value.trim();
|
|
76
|
+
if (keyboard_type === "password") {
|
|
77
|
+
// Sensitive value: never emit through the persisted onInputChanged path.
|
|
78
|
+
// Hosts keep it in memory only (no storage, analytics, or attributes).
|
|
79
|
+
onSensitiveInputChanged?.(field_id, value);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
76
82
|
if (reserved_attribute) {
|
|
77
83
|
if (!onReservedAttributeChanged) {
|
|
78
84
|
console.error("onReservedAttributeChanged is not set");
|
|
@@ -118,6 +124,7 @@
|
|
|
118
124
|
const {
|
|
119
125
|
uiConfig,
|
|
120
126
|
onInputChanged,
|
|
127
|
+
onSensitiveInputChanged,
|
|
121
128
|
onReservedAttributeChanged,
|
|
122
129
|
selectedPackageId,
|
|
123
130
|
} = getPaywallContext();
|
|
@@ -196,6 +203,8 @@
|
|
|
196
203
|
return "number";
|
|
197
204
|
case "email":
|
|
198
205
|
return "email";
|
|
206
|
+
case "password":
|
|
207
|
+
return "password";
|
|
199
208
|
case "tel":
|
|
200
209
|
return "tel";
|
|
201
210
|
case "text":
|
|
@@ -205,6 +214,12 @@
|
|
|
205
214
|
}
|
|
206
215
|
});
|
|
207
216
|
|
|
217
|
+
// `password` is not a valid `inputmode`; masking is handled by `type` above, so
|
|
218
|
+
// fall back to the default text keyboard for it.
|
|
219
|
+
const inputMode = $derived.by((): HTMLInputAttributes["inputmode"] =>
|
|
220
|
+
keyboard_type === "password" ? "text" : keyboard_type,
|
|
221
|
+
);
|
|
222
|
+
|
|
208
223
|
const isVisible = $derived(
|
|
209
224
|
evaluateVisibilityConditions(
|
|
210
225
|
{
|
|
@@ -232,7 +247,7 @@
|
|
|
232
247
|
{type}
|
|
233
248
|
{placeholder}
|
|
234
249
|
{required}
|
|
235
|
-
inputmode={
|
|
250
|
+
inputmode={inputMode}
|
|
236
251
|
autocapitalize={capitalize}
|
|
237
252
|
style={inputStyle}
|
|
238
253
|
{oninput}
|
|
@@ -10,15 +10,28 @@
|
|
|
10
10
|
import { setVariablesContext } from "../../stores/variables";
|
|
11
11
|
import { readable, writable } from "svelte/store";
|
|
12
12
|
import InputText from "./InputText.svelte";
|
|
13
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
InputTextProps,
|
|
15
|
+
ReservedAttribute,
|
|
16
|
+
} from "../../types/components/input-text";
|
|
14
17
|
|
|
15
18
|
interface Props extends InputTextProps {
|
|
16
19
|
onInputChanged?: (fieldId: string, value: string) => void;
|
|
20
|
+
onSensitiveInputChanged?: (fieldId: string, value: string) => void;
|
|
21
|
+
onReservedAttributeChanged?: (
|
|
22
|
+
reservedAttribute: ReservedAttribute,
|
|
23
|
+
value: string,
|
|
24
|
+
) => void;
|
|
17
25
|
onSatisfactionChange?: (satisfied: boolean) => void;
|
|
18
26
|
}
|
|
19
27
|
|
|
20
|
-
const {
|
|
21
|
-
|
|
28
|
+
const {
|
|
29
|
+
onInputChanged,
|
|
30
|
+
onSensitiveInputChanged,
|
|
31
|
+
onReservedAttributeChanged,
|
|
32
|
+
onSatisfactionChange,
|
|
33
|
+
...inputTextProps
|
|
34
|
+
}: Props = $props();
|
|
22
35
|
|
|
23
36
|
const validationContext = createInputValidationContext();
|
|
24
37
|
setInputValidationContext(validationContext);
|
|
@@ -43,6 +56,8 @@
|
|
|
43
56
|
emitComponentInteraction: () => {},
|
|
44
57
|
onButtonAction: () => {},
|
|
45
58
|
onInputChanged,
|
|
59
|
+
onSensitiveInputChanged,
|
|
60
|
+
onReservedAttributeChanged,
|
|
46
61
|
uiConfig: { app: { fonts: {} } } as never,
|
|
47
62
|
hideBackButtons: false,
|
|
48
63
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type { InputTextProps } from "../../types/components/input-text";
|
|
1
|
+
import type { InputTextProps, ReservedAttribute } from "../../types/components/input-text";
|
|
2
2
|
interface Props extends InputTextProps {
|
|
3
3
|
onInputChanged?: (fieldId: string, value: string) => void;
|
|
4
|
+
onSensitiveInputChanged?: (fieldId: string, value: string) => void;
|
|
5
|
+
onReservedAttributeChanged?: (reservedAttribute: ReservedAttribute, value: string) => void;
|
|
4
6
|
onSatisfactionChange?: (satisfied: boolean) => void;
|
|
5
7
|
}
|
|
6
8
|
declare const InputTextTestWrapper: import("svelte").Component<Props, {}, "">;
|
|
@@ -39,6 +39,11 @@
|
|
|
39
39
|
DUELINGUE_PAYWALL,
|
|
40
40
|
} from "./fixtures/express-purchase-button-paywall";
|
|
41
41
|
import { CustomVariableValue } from "../../types/variables";
|
|
42
|
+
import {
|
|
43
|
+
paywallWithTransparentFooter,
|
|
44
|
+
paywallWithCenteredBodyAndFooter,
|
|
45
|
+
paywallWithHeaderAndFooter,
|
|
46
|
+
} from "./fixtures/transparent-footer-paywall";
|
|
42
47
|
|
|
43
48
|
const { Story } = defineMeta({
|
|
44
49
|
title: "Example/Paywall",
|
|
@@ -480,6 +485,28 @@
|
|
|
480
485
|
}}
|
|
481
486
|
/>
|
|
482
487
|
|
|
488
|
+
<Story
|
|
489
|
+
name="Sticky Footer - transparent (overlaps content)"
|
|
490
|
+
args={{
|
|
491
|
+
paywallData: paywallWithTransparentFooter,
|
|
492
|
+
}}
|
|
493
|
+
/>
|
|
494
|
+
|
|
495
|
+
<Story
|
|
496
|
+
name="Sticky Footer - centered body above footer"
|
|
497
|
+
decorators={[viewportDecorator(500, 500, 0)]}
|
|
498
|
+
args={{
|
|
499
|
+
paywallData: paywallWithCenteredBodyAndFooter,
|
|
500
|
+
}}
|
|
501
|
+
/>
|
|
502
|
+
|
|
503
|
+
<Story
|
|
504
|
+
name="Sticky Footer - with header"
|
|
505
|
+
args={{
|
|
506
|
+
paywallData: paywallWithHeaderAndFooter,
|
|
507
|
+
}}
|
|
508
|
+
/>
|
|
509
|
+
|
|
483
510
|
<Story
|
|
484
511
|
name="Header"
|
|
485
512
|
args={{
|
|
@@ -93,6 +93,11 @@
|
|
|
93
93
|
value: string,
|
|
94
94
|
actionId?: string,
|
|
95
95
|
) => void;
|
|
96
|
+
/**
|
|
97
|
+
* Called for sensitive inputs (keyboard_type: "password") instead of
|
|
98
|
+
* onInputChanged, so hosts can keep the value in memory only.
|
|
99
|
+
*/
|
|
100
|
+
onSensitiveInputChanged?: (fieldId: string, value: string) => void;
|
|
96
101
|
onReservedAttributeChanged?: (
|
|
97
102
|
reservedAttribute: ReservedAttribute,
|
|
98
103
|
value: string,
|
|
@@ -141,6 +146,7 @@
|
|
|
141
146
|
uiConfig,
|
|
142
147
|
walletButtonRender,
|
|
143
148
|
onInputChanged,
|
|
149
|
+
onSensitiveInputChanged,
|
|
144
150
|
onReservedAttributeChanged,
|
|
145
151
|
hideBackButtons = false,
|
|
146
152
|
maxContentWidth,
|
|
@@ -309,6 +315,7 @@
|
|
|
309
315
|
onNavigateToUrl: onNavigateToUrlClicked,
|
|
310
316
|
onButtonAction,
|
|
311
317
|
onInputChanged,
|
|
318
|
+
onSensitiveInputChanged,
|
|
312
319
|
onReservedAttributeChanged,
|
|
313
320
|
walletButtonRender,
|
|
314
321
|
uiConfig,
|
|
@@ -352,6 +359,13 @@
|
|
|
352
359
|
const pullContentUnderHeader = !!header && firstComponentIsFullWidthImage;
|
|
353
360
|
|
|
354
361
|
let headerHeight = $state(0);
|
|
362
|
+
let footerHeight = $state(0);
|
|
363
|
+
|
|
364
|
+
const contentColumnStyle = $derived(
|
|
365
|
+
maxContentWidth
|
|
366
|
+
? `max-width: ${maxContentWidth}; margin-inline: auto;`
|
|
367
|
+
: "",
|
|
368
|
+
);
|
|
355
369
|
</script>
|
|
356
370
|
|
|
357
371
|
<svelte:boundary onerror={onError}>
|
|
@@ -366,19 +380,16 @@
|
|
|
366
380
|
<Header {...header} />
|
|
367
381
|
</div>
|
|
368
382
|
{/if}
|
|
369
|
-
<div
|
|
370
|
-
class="paywall-content"
|
|
371
|
-
style={[
|
|
372
|
-
maxContentWidth
|
|
373
|
-
? `max-width: ${maxContentWidth}; margin-inline: auto;`
|
|
374
|
-
: "",
|
|
375
|
-
]
|
|
376
|
-
.filter(Boolean)
|
|
377
|
-
.join(" ")}
|
|
378
|
-
>
|
|
383
|
+
<div class="paywall-content" style={contentColumnStyle}>
|
|
379
384
|
<Stack
|
|
380
385
|
{...stack}
|
|
381
386
|
class="paywall-content-scroll"
|
|
387
|
+
padding={sticky_footer
|
|
388
|
+
? {
|
|
389
|
+
...stack.padding,
|
|
390
|
+
bottom: (stack.padding?.bottom ?? 0) + footerHeight,
|
|
391
|
+
}
|
|
392
|
+
: stack.padding}
|
|
382
393
|
style={{
|
|
383
394
|
height: "auto",
|
|
384
395
|
...(pullContentUnderHeader
|
|
@@ -386,10 +397,19 @@
|
|
|
386
397
|
: {}),
|
|
387
398
|
}}
|
|
388
399
|
/>
|
|
389
|
-
{#if sticky_footer}
|
|
390
|
-
<Footer {...sticky_footer} />
|
|
391
|
-
{/if}
|
|
392
400
|
</div>
|
|
401
|
+
{#if sticky_footer}
|
|
402
|
+
<!-- Overlaps the content so translucent footers show it; sticky rather than
|
|
403
|
+
absolute so it stays visible when an ancestor scrollport is shorter than
|
|
404
|
+
the paywall. -->
|
|
405
|
+
<div
|
|
406
|
+
class="footer-wrapper"
|
|
407
|
+
style="z-index: {STICKY_OVERLAY_Z_INDEX}; margin-top: -{footerHeight}px; {contentColumnStyle}"
|
|
408
|
+
bind:clientHeight={footerHeight}
|
|
409
|
+
>
|
|
410
|
+
<Footer {...sticky_footer} />
|
|
411
|
+
</div>
|
|
412
|
+
{/if}
|
|
393
413
|
</div>
|
|
394
414
|
|
|
395
415
|
{#if sheet}
|
|
@@ -422,6 +442,13 @@
|
|
|
422
442
|
width: 100%;
|
|
423
443
|
}
|
|
424
444
|
|
|
445
|
+
.footer-wrapper {
|
|
446
|
+
position: sticky;
|
|
447
|
+
bottom: 0;
|
|
448
|
+
width: 100%;
|
|
449
|
+
flex-shrink: 0;
|
|
450
|
+
}
|
|
451
|
+
|
|
425
452
|
.paywall-content {
|
|
426
453
|
z-index: 2;
|
|
427
454
|
width: 100%;
|
|
@@ -44,6 +44,11 @@ interface Props {
|
|
|
44
44
|
hideBackButtons?: boolean;
|
|
45
45
|
walletButtonRender?: WalletButtonRender;
|
|
46
46
|
onInputChanged?: (fieldId: string, value: string, actionId?: string) => void;
|
|
47
|
+
/**
|
|
48
|
+
* Called for sensitive inputs (keyboard_type: "password") instead of
|
|
49
|
+
* onInputChanged, so hosts can keep the value in memory only.
|
|
50
|
+
*/
|
|
51
|
+
onSensitiveInputChanged?: (fieldId: string, value: string) => void;
|
|
47
52
|
onReservedAttributeChanged?: (reservedAttribute: ReservedAttribute, value: string) => void;
|
|
48
53
|
maxContentWidth?: string;
|
|
49
54
|
initialInputSelections?: InitialInputSelections;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PaywallData } from "../../../types/paywall";
|
|
2
|
+
/** Translucent sticky footer over long scrollable content. */
|
|
3
|
+
export declare const paywallWithTransparentFooter: PaywallData;
|
|
4
|
+
/** Small body that must center in the space above the footer, not the whole screen. */
|
|
5
|
+
export declare const paywallWithCenteredBodyAndFooter: PaywallData;
|
|
6
|
+
/** Header and sticky footer together: the footer overlay must not affect the header. */
|
|
7
|
+
export declare const paywallWithHeaderAndFooter: PaywallData;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { paywallWithFooter, paywallWithHeader } from "../../../stories/fixtures";
|
|
2
|
+
function footerStack(paywall) {
|
|
3
|
+
const footer = paywall.components_config.base.sticky_footer;
|
|
4
|
+
if (!footer) {
|
|
5
|
+
throw new Error("Fixture has no sticky footer");
|
|
6
|
+
}
|
|
7
|
+
return footer.stack;
|
|
8
|
+
}
|
|
9
|
+
function lastTextComponent(components) {
|
|
10
|
+
return components.findLast((component) => component.type === "text");
|
|
11
|
+
}
|
|
12
|
+
/** Duplicates the last text component so the content is tall enough to scroll. */
|
|
13
|
+
function appendScrollableContent(paywall, count) {
|
|
14
|
+
const components = paywall.components_config.base.stack.components;
|
|
15
|
+
const lastText = lastTextComponent(components);
|
|
16
|
+
if (!lastText) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
for (let index = 0; index < count; index++) {
|
|
20
|
+
const copy = structuredClone(lastText);
|
|
21
|
+
copy.id = `scroll-filler-${index}`;
|
|
22
|
+
components.push(copy);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function setFooterBackground(paywall, hex) {
|
|
26
|
+
footerStack(paywall).background_color = {
|
|
27
|
+
light: { type: "hex", value: hex },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Translucent sticky footer over long scrollable content. */
|
|
31
|
+
export const paywallWithTransparentFooter = (() => {
|
|
32
|
+
const paywall = structuredClone(paywallWithFooter);
|
|
33
|
+
appendScrollableContent(paywall, 25);
|
|
34
|
+
setFooterBackground(paywall, "#057C5BAA");
|
|
35
|
+
return paywall;
|
|
36
|
+
})();
|
|
37
|
+
/** Small body that must center in the space above the footer, not the whole screen. */
|
|
38
|
+
export const paywallWithCenteredBodyAndFooter = (() => {
|
|
39
|
+
const paywall = structuredClone(paywallWithFooter);
|
|
40
|
+
const stack = paywall.components_config.base.stack;
|
|
41
|
+
const lastText = lastTextComponent(stack.components);
|
|
42
|
+
stack.components = lastText ? [lastText] : [];
|
|
43
|
+
stack.dimension.distribution = "center";
|
|
44
|
+
setFooterBackground(paywall, "#057C5BAA");
|
|
45
|
+
return paywall;
|
|
46
|
+
})();
|
|
47
|
+
/** Header and sticky footer together: the footer overlay must not affect the header. */
|
|
48
|
+
export const paywallWithHeaderAndFooter = (() => {
|
|
49
|
+
const paywall = structuredClone(paywallWithFooter);
|
|
50
|
+
appendScrollableContent(paywall, 15);
|
|
51
|
+
const headerDonor = structuredClone(paywallWithHeader);
|
|
52
|
+
paywall.components_config.base.header =
|
|
53
|
+
headerDonor.components_config.base.header;
|
|
54
|
+
paywall.components_localizations.en_US = {
|
|
55
|
+
...paywall.components_localizations.en_US,
|
|
56
|
+
...headerDonor.components_localizations.en_US,
|
|
57
|
+
};
|
|
58
|
+
return paywall;
|
|
59
|
+
})();
|
|
@@ -56,17 +56,23 @@
|
|
|
56
56
|
let fitWidth = $state<number | undefined>(undefined);
|
|
57
57
|
let fitHeight = $state<number | undefined>(undefined);
|
|
58
58
|
|
|
59
|
+
const fitAxisFallback = (axis: typeof props.size.width): number =>
|
|
60
|
+
axis.type === "fit" && typeof axis.default === "number"
|
|
61
|
+
? axis.default
|
|
62
|
+
: FIT_FALLBACK_SIZE_PX;
|
|
63
|
+
|
|
59
64
|
const style = $derived.by(() => {
|
|
60
65
|
const { width, height } = props.size;
|
|
61
66
|
const widthFit = width.type === "fit";
|
|
62
67
|
const heightFit = height.type === "fit";
|
|
63
68
|
// Pin a `fit` axis with an explicit size. `flex-shrink: 0` keeps flex parents
|
|
64
69
|
// from collapsing the box; avoid mirroring the size in `min-*` or it beats `max-*`.
|
|
70
|
+
// Prefer author-declared `default` when present; else the hard 300px fallback.
|
|
65
71
|
const fitWidthPx = widthFit
|
|
66
|
-
? clampFitSize(fitWidth ??
|
|
72
|
+
? clampFitSize(fitWidth ?? fitAxisFallback(width))
|
|
67
73
|
: null;
|
|
68
74
|
const fitHeightPx = heightFit
|
|
69
|
-
? clampFitSize(fitHeight ??
|
|
75
|
+
? clampFitSize(fitHeight ?? fitAxisFallback(height))
|
|
70
76
|
: null;
|
|
71
77
|
return css({
|
|
72
78
|
width: fitWidthPx != null ? px(fitWidthPx) : mapSize(width),
|
|
@@ -32,6 +32,11 @@
|
|
|
32
32
|
value: string,
|
|
33
33
|
actionId?: string,
|
|
34
34
|
) => void;
|
|
35
|
+
/**
|
|
36
|
+
* Called for sensitive inputs (keyboard_type: "password") instead of
|
|
37
|
+
* onInputChanged, so hosts can keep the value in memory only.
|
|
38
|
+
*/
|
|
39
|
+
onSensitiveInputChanged?: (fieldId: string, value: string) => void;
|
|
35
40
|
onReservedAttributeChanged?: (
|
|
36
41
|
reservedAttribute: ReservedAttribute,
|
|
37
42
|
value: string,
|
|
@@ -61,6 +66,7 @@
|
|
|
61
66
|
infoPerPackage,
|
|
62
67
|
initialInputSelections = {},
|
|
63
68
|
onInputChanged,
|
|
69
|
+
onSensitiveInputChanged,
|
|
64
70
|
onReservedAttributeChanged,
|
|
65
71
|
onCompleteWorkflowNavigate,
|
|
66
72
|
onNavigateToUrlClicked,
|
|
@@ -100,6 +106,7 @@
|
|
|
100
106
|
{onComponentInteraction}
|
|
101
107
|
{onPurchaseClicked}
|
|
102
108
|
{onInputChanged}
|
|
109
|
+
{onSensitiveInputChanged}
|
|
103
110
|
{onReservedAttributeChanged}
|
|
104
111
|
{walletButtonRender}
|
|
105
112
|
{safeAreaFallbackColor}
|
|
@@ -23,6 +23,11 @@ interface Props {
|
|
|
23
23
|
infoPerPackage?: Record<string, PackageInfo>;
|
|
24
24
|
initialInputSelections?: InitialInputSelections;
|
|
25
25
|
onInputChanged?: (fieldId: string, value: string, actionId?: string) => void;
|
|
26
|
+
/**
|
|
27
|
+
* Called for sensitive inputs (keyboard_type: "password") instead of
|
|
28
|
+
* onInputChanged, so hosts can keep the value in memory only.
|
|
29
|
+
*/
|
|
30
|
+
onSensitiveInputChanged?: (fieldId: string, value: string) => void;
|
|
26
31
|
onReservedAttributeChanged?: (reservedAttribute: ReservedAttribute, value: string) => void;
|
|
27
32
|
onCompleteWorkflowNavigate?: (args: CompleteWorkflowNavigateArgs) => void | Promise<void>;
|
|
28
33
|
onNavigateToUrlClicked?: (url: string) => void;
|
|
@@ -49,6 +49,11 @@
|
|
|
49
49
|
value: string,
|
|
50
50
|
actionId?: string,
|
|
51
51
|
) => void;
|
|
52
|
+
/**
|
|
53
|
+
* Called for sensitive inputs (keyboard_type: "password") instead of
|
|
54
|
+
* onInputChanged, so hosts can keep the value in memory only.
|
|
55
|
+
*/
|
|
56
|
+
onSensitiveInputChanged?: (fieldId: string, value: string) => void;
|
|
52
57
|
onReservedAttributeChanged?: (
|
|
53
58
|
reservedAttribute: ReservedAttribute,
|
|
54
59
|
value: string,
|
|
@@ -84,6 +89,7 @@
|
|
|
84
89
|
onPurchaseClicked,
|
|
85
90
|
onActionTriggered,
|
|
86
91
|
onInputChanged,
|
|
92
|
+
onSensitiveInputChanged,
|
|
87
93
|
onReservedAttributeChanged,
|
|
88
94
|
onCompleteWorkflowNavigate,
|
|
89
95
|
onNavigateToUrlClicked,
|
|
@@ -303,6 +309,7 @@
|
|
|
303
309
|
{onPurchaseClicked}
|
|
304
310
|
onActionTriggered={handleActionTriggered}
|
|
305
311
|
{onInputChanged}
|
|
312
|
+
{onSensitiveInputChanged}
|
|
306
313
|
{onReservedAttributeChanged}
|
|
307
314
|
{onCompleteWorkflowNavigate}
|
|
308
315
|
{onNavigateToUrlClicked}
|
|
@@ -21,6 +21,11 @@ interface Props {
|
|
|
21
21
|
onPurchaseClicked?: (packageId: string, actionId: string) => void | Promise<void>;
|
|
22
22
|
onActionTriggered?: (actionId: string) => void;
|
|
23
23
|
onInputChanged?: (fieldId: string, value: string, actionId?: string) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Called for sensitive inputs (keyboard_type: "password") instead of
|
|
26
|
+
* onInputChanged, so hosts can keep the value in memory only.
|
|
27
|
+
*/
|
|
28
|
+
onSensitiveInputChanged?: (fieldId: string, value: string) => void;
|
|
24
29
|
onReservedAttributeChanged?: (reservedAttribute: ReservedAttribute, value: string) => void;
|
|
25
30
|
onCompleteWorkflowNavigate?: (args: CompleteWorkflowNavigateArgs) => void | Promise<void>;
|
|
26
31
|
onNavigateToUrlClicked?: (url: string) => void;
|
|
@@ -8,7 +8,7 @@ interface InputValidationContext {
|
|
|
8
8
|
isSatisfied: Readable<boolean>;
|
|
9
9
|
updateSatisfaction: (satisfied: boolean) => void;
|
|
10
10
|
}
|
|
11
|
-
export declare function createInputValidationContext(): InputValidationContext;
|
|
11
|
+
export declare function createInputValidationContext(initiallySatisfied?: boolean): InputValidationContext;
|
|
12
12
|
export declare function setInputValidationContext(context: InputValidationContext): void;
|
|
13
13
|
export declare function getInputValidationContext(): InputValidationContext | undefined;
|
|
14
14
|
export declare function setInitialInputSelectionsContext(selections: InitialInputSelections): void;
|
|
@@ -2,8 +2,8 @@ import { getContext, setContext } from "svelte";
|
|
|
2
2
|
import { writable } from "svelte/store";
|
|
3
3
|
const key = Symbol("inputValidation");
|
|
4
4
|
const initialSelectionsKey = Symbol("initialInputSelections");
|
|
5
|
-
export function createInputValidationContext() {
|
|
6
|
-
const isSatisfied = writable(
|
|
5
|
+
export function createInputValidationContext(initiallySatisfied = true) {
|
|
6
|
+
const isSatisfied = writable(initiallySatisfied);
|
|
7
7
|
const updateSatisfaction = (satisfied) => {
|
|
8
8
|
isSatisfied.set(satisfied);
|
|
9
9
|
};
|
package/dist/stores/paywall.d.ts
CHANGED
|
@@ -22,6 +22,12 @@ type PaywallContext = Readonly<{
|
|
|
22
22
|
emitComponentInteraction: (data: ComponentInteractionData) => void;
|
|
23
23
|
onNavigateToUrl?: (url: string) => void;
|
|
24
24
|
onInputChanged?: (fieldId: string, value: string, actionId?: string) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Called for sensitive inputs (keyboard_type: "password") instead of
|
|
27
|
+
* onInputChanged, so hosts can keep the value in memory only and never
|
|
28
|
+
* persist, log, or forward it.
|
|
29
|
+
*/
|
|
30
|
+
onSensitiveInputChanged?: (fieldId: string, value: string) => void;
|
|
25
31
|
onReservedAttributeChanged?: (reservedAttribute: ReservedAttribute, value: string) => void;
|
|
26
32
|
walletButtonRender?: WalletButtonRender;
|
|
27
33
|
onWalletButtonReady?: (walletButtonAvailable?: boolean) => void;
|
|
@@ -63,5 +63,11 @@ export interface ButtonProps extends BaseComponent {
|
|
|
63
63
|
stack: StackProps;
|
|
64
64
|
transition?: null;
|
|
65
65
|
overrides?: Overrides<ButtonProps>;
|
|
66
|
+
/**
|
|
67
|
+
* When false, workflow / complete_workflow buttons stay enabled even if
|
|
68
|
+
* required inputs on the screen are empty. Hosts (e.g. rc-workflows) may set
|
|
69
|
+
* this at runtime for auth cross-links; it is not persisted in paywall JSON.
|
|
70
|
+
*/
|
|
71
|
+
require_valid_inputs?: boolean;
|
|
66
72
|
}
|
|
67
73
|
export {};
|
|
@@ -4,7 +4,7 @@ import type { BaseComponent } from "../base";
|
|
|
4
4
|
import type { ColorGradientScheme } from "../colors";
|
|
5
5
|
import type { Overrides } from "../overrides";
|
|
6
6
|
export type InputTextCapitalizeType = "none" | "sentences" | "words" | "characters";
|
|
7
|
-
export type InputTextKeyboardType = "decimal" | "email" | "numeric" | "tel" | "text" | "url";
|
|
7
|
+
export type InputTextKeyboardType = "decimal" | "email" | "numeric" | "password" | "tel" | "text" | "url";
|
|
8
8
|
export type ReservedAttribute = "$email" | "$displayName" | "$phoneNumber";
|
|
9
9
|
export interface InputTextProps extends BaseComponent {
|
|
10
10
|
type: "input_text";
|
package/dist/types.d.ts
CHANGED
|
@@ -48,11 +48,17 @@ type RelativeSize = {
|
|
|
48
48
|
type: "relative";
|
|
49
49
|
value: number;
|
|
50
50
|
};
|
|
51
|
-
type
|
|
52
|
-
type: "fit"
|
|
51
|
+
type FitSize = {
|
|
52
|
+
type: "fit";
|
|
53
53
|
value?: null;
|
|
54
|
+
/** Placeholder px until the content reports its measured size. */
|
|
55
|
+
default?: number;
|
|
54
56
|
};
|
|
55
|
-
|
|
57
|
+
type FillSize = {
|
|
58
|
+
type: "fill";
|
|
59
|
+
value?: null;
|
|
60
|
+
};
|
|
61
|
+
export type Size = FixedSize | RelativeSize | FitSize | FillSize;
|
|
56
62
|
export type SizeType = {
|
|
57
63
|
width: Size;
|
|
58
64
|
height: Size;
|