adapty 0.6.0 → 0.6.1-beta.0
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/README.md +28 -0
- package/dist/commands/auth/login.d.ts +2 -0
- package/dist/commands/auth/login.js +13 -2
- package/dist/commands/flows/config/preview.d.ts +18 -0
- package/dist/commands/flows/config/preview.js +74 -0
- package/dist/commands/flows/config/validate.d.ts +20 -0
- package/dist/commands/flows/config/validate.js +81 -0
- package/dist/lib/api-schemas.d.ts +15 -0
- package/dist/lib/app-url.d.ts +10 -0
- package/dist/lib/app-url.js +33 -0
- package/dist/lib/preview.d.ts +27 -0
- package/dist/lib/preview.js +48 -0
- package/oclif.manifest.json +1193 -1053
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -256,6 +256,33 @@ and the outcome shows up in `adapty asa automations runs`.
|
|
|
256
256
|
| `--page` | Page number (default: 1) |
|
|
257
257
|
| `--page-size` | Items per page (default: 20, max: 100; `asa` commands: default 100, max 1000) |
|
|
258
258
|
|
|
259
|
+
## Paywall Preview
|
|
260
|
+
|
|
261
|
+
`adapty flows config preview <config_file>` turns a local flow config into a render URL. On a TTY it opens the
|
|
262
|
+
browser; piped or with `--json` it prints the URL alone. Screenshotting is the caller's job — the CLI only
|
|
263
|
+
builds the URL.
|
|
264
|
+
|
|
265
|
+
> **Small configs only.** This is a quick-look escape hatch. The config travels in the URL fragment, and past
|
|
266
|
+
> roughly **32KB of pretty-printed JSON** the render page becomes slow and unreliable. Trim to the screen you
|
|
267
|
+
> care about, or preview the saved flow in the dashboard builder instead.
|
|
268
|
+
|
|
269
|
+
> **That URL is huge.** Even a config the page renders happily produces thousands of characters, and a 668KB
|
|
270
|
+
> flow yields ~113,000. Nobody should read it — **agents especially should never let it into their context.**
|
|
271
|
+
> Pipe it into whatever captures the screenshot:
|
|
272
|
+
>
|
|
273
|
+
> ```sh
|
|
274
|
+
> adapty flows config preview flow.json --screen scr_abc | node capture.mjs --out shot.png
|
|
275
|
+
>
|
|
276
|
+
> # or, for a tool that wants a flag instead of stdin:
|
|
277
|
+
> node capture.mjs --url "$(adapty flows config preview flow.json --screen scr_abc)" --out shot.png
|
|
278
|
+
> ```
|
|
279
|
+
>
|
|
280
|
+
> Prefer the pipe: it has no size limit, while an argument is capped by the shell (~1MB, so a config around
|
|
281
|
+
> 6MB).
|
|
282
|
+
|
|
283
|
+
See [skills/adapty-cli/references/cli-commands.md](skills/adapty-cli/references/cli-commands.md#preview) for
|
|
284
|
+
the flags and the size ceiling.
|
|
285
|
+
|
|
259
286
|
## Environment Variables
|
|
260
287
|
|
|
261
288
|
| Variable | Description |
|
|
@@ -263,6 +290,7 @@ and the outcome shows up in `adapty asa automations runs`.
|
|
|
263
290
|
| `ADAPTY_TOKEN` | Override stored auth token |
|
|
264
291
|
| `ADAPTY_API_URL` | Override Developer API base URL (default: `https://api-admin.adapty.io/api/v1/developer`) |
|
|
265
292
|
| `ADAPTY_ASA_API_URL` | Override Apple Search Ads base URL (default: `https://api-asa-admin.adapty.io/api/v1/cli`) |
|
|
293
|
+
| `ADAPTY_APP_URL` | Override dashboard base URL (default: `https://app.adapty.io`). Used by `flows config preview` for the fixed `/flow-preview` route, and by `auth login` to keep the verification link on that host |
|
|
266
294
|
|
|
267
295
|
The two API URLs are independent: pointing `ADAPTY_API_URL` at a staging host leaves `adapty asa` on the ASA
|
|
268
296
|
default, and the other way round.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
2
|
import open from 'open';
|
|
3
3
|
import { ApiClient } from '../../lib/api-client.js';
|
|
4
|
+
import { onAppHost } from '../../lib/app-url.js';
|
|
4
5
|
import { buildUserAgent } from '../../lib/client-from-config.js';
|
|
5
6
|
import { readConfig, writeConfig } from '../../lib/config.js';
|
|
6
7
|
import { ApiError } from '../../lib/errors.js';
|
|
@@ -27,11 +28,12 @@ export default class AuthLogin extends Command {
|
|
|
27
28
|
catch (error) {
|
|
28
29
|
this.error(error instanceof Error ? error.message : 'Failed to initiate auth flow', { exit: 1 });
|
|
29
30
|
}
|
|
31
|
+
const verificationUrl = this.verificationUrl(device.verification_uri_complete);
|
|
30
32
|
this.log(`\nYour code: ${device.user_code}\n`);
|
|
31
|
-
this.log(`If browser doesn't open, visit: ${
|
|
33
|
+
this.log(`If browser doesn't open, visit: ${verificationUrl}\n`);
|
|
32
34
|
if (process.stdin.isTTY === true) {
|
|
33
35
|
try {
|
|
34
|
-
await open(
|
|
36
|
+
await open(verificationUrl);
|
|
35
37
|
}
|
|
36
38
|
catch {
|
|
37
39
|
// browser open failed silently — URL already printed
|
|
@@ -106,4 +108,13 @@ export default class AuthLogin extends Command {
|
|
|
106
108
|
process.removeListener('SIGINT', onSignal);
|
|
107
109
|
}
|
|
108
110
|
}
|
|
111
|
+
/** Keeps the browser on the configured dashboard host, when ADAPTY_APP_URL asks for one. */
|
|
112
|
+
verificationUrl(issuedUrl) {
|
|
113
|
+
try {
|
|
114
|
+
return onAppHost(issuedUrl);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
this.error(error instanceof Error ? error.message : String(error), { exit: 2 });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
109
120
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export interface PreviewResult {
|
|
3
|
+
render_url: string;
|
|
4
|
+
}
|
|
5
|
+
export default class FlowsConfigPreview extends Command {
|
|
6
|
+
static args: {
|
|
7
|
+
config_file: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
8
|
+
};
|
|
9
|
+
static description: string;
|
|
10
|
+
static enableJsonFlag: boolean;
|
|
11
|
+
static examples: string[];
|
|
12
|
+
static flags: {
|
|
13
|
+
device: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
orientation: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
screen: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
};
|
|
17
|
+
run(): Promise<PreviewResult>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import open from 'open';
|
|
5
|
+
import { APP_URL_ENV_VAR } from '../../../lib/app-url.js';
|
|
6
|
+
import { buildRenderUrl, DEFAULT_DEVICE_ID, DEFAULT_ORIENTATION, normalizePreviewConfig, ORIENTATIONS, } from '../../../lib/preview.js';
|
|
7
|
+
function describeError(error) {
|
|
8
|
+
return error instanceof Error ? error.message : String(error);
|
|
9
|
+
}
|
|
10
|
+
export default class FlowsConfigPreview extends Command {
|
|
11
|
+
static args = {
|
|
12
|
+
config_file: Args.string({ description: 'Path to a local flow config JSON file', required: true }),
|
|
13
|
+
};
|
|
14
|
+
static description = `Build a render URL for a local flow config and open it. A quick-look escape hatch for small configs: the whole config rides in the URL fragment, and past roughly 32KB of pretty-printed JSON the render page gets slow and unreliable. Opens the browser on a TTY; when piped or with --json, prints the URL alone — and that URL is long (~113K characters for a 668KB flow), so pipe it into whatever takes the screenshot ("| node capture.mjs") or pass it with --url and command substitution. Never print or read it: agents burn context for zero information. The render host comes from $${APP_URL_ENV_VAR}.`;
|
|
15
|
+
static enableJsonFlag = true;
|
|
16
|
+
static examples = [
|
|
17
|
+
'<%= config.bin %> flows config preview ./config.json',
|
|
18
|
+
'<%= config.bin %> flows config preview ./config.json --screen welcome --device ipad-pro --orientation landscape',
|
|
19
|
+
'# pipe the URL straight into a screenshot tool, never print it\n<%= config.bin %> flows config preview ./config.json | node capture.mjs --out shot.png',
|
|
20
|
+
'# or pass it as an argument, for tools that want a flag\nnode capture.mjs --url "$(<%= config.bin %> flows config preview ./config.json)" --out shot.png',
|
|
21
|
+
];
|
|
22
|
+
static flags = {
|
|
23
|
+
device: Flags.string({ default: DEFAULT_DEVICE_ID, description: 'Device frame to render in' }),
|
|
24
|
+
orientation: Flags.string({
|
|
25
|
+
default: DEFAULT_ORIENTATION,
|
|
26
|
+
description: 'Device orientation to render in',
|
|
27
|
+
options: [...ORIENTATIONS],
|
|
28
|
+
}),
|
|
29
|
+
screen: Flags.string({ description: "Screen ID to render (default: the flow's first screen)" }),
|
|
30
|
+
};
|
|
31
|
+
async run() {
|
|
32
|
+
const { args, flags } = await this.parse(FlowsConfigPreview);
|
|
33
|
+
const configPath = resolve(args.config_file);
|
|
34
|
+
let raw;
|
|
35
|
+
try {
|
|
36
|
+
raw = JSON.parse(await readFile(configPath, 'utf8'));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
this.error(`Could not read config file ${configPath}: ${describeError(error)}`, { exit: 2 });
|
|
40
|
+
}
|
|
41
|
+
let payload;
|
|
42
|
+
try {
|
|
43
|
+
payload = normalizePreviewConfig(raw);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
this.error(describeError(error), { exit: 2 });
|
|
47
|
+
}
|
|
48
|
+
let renderUrl;
|
|
49
|
+
try {
|
|
50
|
+
renderUrl = buildRenderUrl({ device: flags.device, orientation: flags.orientation, screen: flags.screen }, payload);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
this.error(describeError(error), { exit: 2 });
|
|
54
|
+
}
|
|
55
|
+
const result = { render_url: renderUrl };
|
|
56
|
+
if (this.jsonEnabled())
|
|
57
|
+
return result;
|
|
58
|
+
// Piped output stays a bare URL so it composes — the whole config rides in the fragment, which
|
|
59
|
+
// is why a TTY gets the browser opened instead of a screenful of base64.
|
|
60
|
+
if (process.stdout.isTTY !== true) {
|
|
61
|
+
this.log(renderUrl);
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
const target = [flags.screen ?? 'first screen', flags.device, flags.orientation].join(', ');
|
|
65
|
+
try {
|
|
66
|
+
await open(renderUrl);
|
|
67
|
+
this.log(`Opened the preview in your browser (${target}).`);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
this.log(renderUrl);
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { FlowConfigValidationDTO } from '../../../lib/api-schemas.js';
|
|
3
|
+
export default class FlowsConfigValidate extends Command {
|
|
4
|
+
static args: {
|
|
5
|
+
flow_id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static enableJsonFlag: boolean;
|
|
9
|
+
static examples: string[];
|
|
10
|
+
static flags: {
|
|
11
|
+
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'config-file': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
source: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
app: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<FlowConfigValidationDTO>;
|
|
17
|
+
private parseJson;
|
|
18
|
+
private readConfig;
|
|
19
|
+
private readStdin;
|
|
20
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { createAuthenticatedClient } from '../../../lib/client-from-config.js';
|
|
4
|
+
import { appFlag, isValidUuid } from '../../../lib/flags.js';
|
|
5
|
+
import { printResponse } from '../../../lib/output.js';
|
|
6
|
+
export default class FlowsConfigValidate extends Command {
|
|
7
|
+
static args = {
|
|
8
|
+
flow_id: Args.string({ description: 'Flow ID (UUID)', required: true }),
|
|
9
|
+
};
|
|
10
|
+
static description = 'Check whether a builder config is publishable (advisory; does not save). Exits non-zero when the config is not publishable.';
|
|
11
|
+
static enableJsonFlag = true;
|
|
12
|
+
static examples = [
|
|
13
|
+
'<%= config.bin %> flows config validate --app UUID FLOW_UUID --config-file config.json',
|
|
14
|
+
'cat config.json | <%= config.bin %> flows config validate --app UUID FLOW_UUID --config-file -',
|
|
15
|
+
'<%= config.bin %> flows config validate --app UUID FLOW_UUID --config \'{"screens":[],"locales":[]}\'',
|
|
16
|
+
];
|
|
17
|
+
static flags = {
|
|
18
|
+
...appFlag,
|
|
19
|
+
config: Flags.string({
|
|
20
|
+
description: 'Builder config as a JSON string',
|
|
21
|
+
exactlyOne: ['config', 'config-file'],
|
|
22
|
+
}),
|
|
23
|
+
'config-file': Flags.string({
|
|
24
|
+
description: 'JSON file with the builder config, or - to read stdin',
|
|
25
|
+
exactlyOne: ['config', 'config-file'],
|
|
26
|
+
}),
|
|
27
|
+
source: Flags.string({
|
|
28
|
+
default: 'adapty-cli',
|
|
29
|
+
description: 'Caller attribution, sent as X-Adapty-Source (e.g. byo-cli)',
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
async run() {
|
|
33
|
+
const { args, flags } = await this.parse(FlowsConfigValidate);
|
|
34
|
+
if (!isValidUuid(args.flow_id)) {
|
|
35
|
+
this.error('Invalid flow ID format.', { exit: 2 });
|
|
36
|
+
}
|
|
37
|
+
const config = await this.readConfig(flags);
|
|
38
|
+
const body = { config };
|
|
39
|
+
const client = await createAuthenticatedClient(this.config);
|
|
40
|
+
const result = await client.post(`/apps/${flags.app}/flows/${args.flow_id}/config/validate`, body, undefined, { headers: { 'X-Adapty-Source': flags.source } });
|
|
41
|
+
this.log(result.valid ? 'Config is publishable.' : 'Config is NOT publishable.');
|
|
42
|
+
printResponse(result, this.log.bind(this));
|
|
43
|
+
// Advisory endpoint always returns HTTP 200; surface the verdict as an exit code so scripts and agents
|
|
44
|
+
// can gate on it. The JSON/`valid` field stays the source of truth for programmatic consumers.
|
|
45
|
+
if (!result.valid) {
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
parseJson(raw, label) {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(raw);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
this.error(`Invalid ${label} JSON: ${error instanceof Error ? error.message : String(error)}`, { exit: 2 });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async readConfig(flags) {
|
|
59
|
+
if (flags.config !== undefined) {
|
|
60
|
+
return this.parseJson(flags.config, '--config');
|
|
61
|
+
}
|
|
62
|
+
const path = flags['config-file'];
|
|
63
|
+
if (path === undefined) {
|
|
64
|
+
this.error('Provide --config or --config-file.', { exit: 2 });
|
|
65
|
+
}
|
|
66
|
+
let raw;
|
|
67
|
+
try {
|
|
68
|
+
raw = path === '-' ? await this.readStdin() : await readFile(path, 'utf8');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
this.error(`Could not read ${path}.`, { exit: 2 });
|
|
72
|
+
}
|
|
73
|
+
return this.parseJson(raw, path);
|
|
74
|
+
}
|
|
75
|
+
async readStdin() {
|
|
76
|
+
const chunks = [];
|
|
77
|
+
for await (const chunk of process.stdin)
|
|
78
|
+
chunks.push(chunk);
|
|
79
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -101,6 +101,21 @@ export interface FlowConfigWriteRequestDTO {
|
|
|
101
101
|
expected_updated_at?: null | number;
|
|
102
102
|
remote_configs?: FlowRemoteConfigDTO[];
|
|
103
103
|
}
|
|
104
|
+
export interface FlowConfigValidateRequestDTO {
|
|
105
|
+
config: Record<string, unknown>;
|
|
106
|
+
}
|
|
107
|
+
export interface FlowConfigIssueDTO {
|
|
108
|
+
/** Machine code, relayed from the transformer; absent until it reports path-level diagnostics. */
|
|
109
|
+
code?: null | string;
|
|
110
|
+
message: string;
|
|
111
|
+
/** Location of the issue; absent until the transformer reports path-level diagnostics. */
|
|
112
|
+
path?: null | string;
|
|
113
|
+
severity: string;
|
|
114
|
+
}
|
|
115
|
+
export interface FlowConfigValidationDTO {
|
|
116
|
+
issues: FlowConfigIssueDTO[];
|
|
117
|
+
valid: boolean;
|
|
118
|
+
}
|
|
104
119
|
export interface SegmentDTO {
|
|
105
120
|
description: null | string;
|
|
106
121
|
id: string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const APP_URL_ENV_VAR = "ADAPTY_APP_URL";
|
|
2
|
+
export declare const DEFAULT_APP_URL = "https://app.adapty.io";
|
|
3
|
+
/** Builds a dashboard URL for a fixed route, e.g. the flow preview page. */
|
|
4
|
+
export declare function appUrl(path: string): URL;
|
|
5
|
+
/**
|
|
6
|
+
* Moves a link the API issued (the device-flow verification URI) onto the configured dashboard
|
|
7
|
+
* host, so pointing the CLI at a local or staging dashboard keeps the browser there too. Without
|
|
8
|
+
* ADAPTY_APP_URL the link is left exactly as issued — the API is free to serve it from any host.
|
|
9
|
+
*/
|
|
10
|
+
export declare function onAppHost(issuedUrl: string): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const APP_URL_ENV_VAR = 'ADAPTY_APP_URL';
|
|
2
|
+
export const DEFAULT_APP_URL = 'https://app.adapty.io';
|
|
3
|
+
/** Dashboard origin: ADAPTY_APP_URL when set, production otherwise. Only its origin is used. */
|
|
4
|
+
function appBaseUrl() {
|
|
5
|
+
const base = process.env[APP_URL_ENV_VAR] ?? DEFAULT_APP_URL;
|
|
6
|
+
try {
|
|
7
|
+
return new URL(base);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
throw new Error(`Invalid ${APP_URL_ENV_VAR}: ${base}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Builds a dashboard URL for a fixed route, e.g. the flow preview page. */
|
|
14
|
+
export function appUrl(path) {
|
|
15
|
+
return new URL(path, appBaseUrl());
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Moves a link the API issued (the device-flow verification URI) onto the configured dashboard
|
|
19
|
+
* host, so pointing the CLI at a local or staging dashboard keeps the browser there too. Without
|
|
20
|
+
* ADAPTY_APP_URL the link is left exactly as issued — the API is free to serve it from any host.
|
|
21
|
+
*/
|
|
22
|
+
export function onAppHost(issuedUrl) {
|
|
23
|
+
if (!process.env[APP_URL_ENV_VAR])
|
|
24
|
+
return issuedUrl;
|
|
25
|
+
let issued;
|
|
26
|
+
try {
|
|
27
|
+
issued = new URL(issuedUrl);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return issuedUrl;
|
|
31
|
+
}
|
|
32
|
+
return appUrl(`${issued.pathname}${issued.search}${issued.hash}`).toString();
|
|
33
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** The render route is fixed; only its host is configurable, via ADAPTY_APP_URL. */
|
|
2
|
+
export declare const PREVIEW_PATH = "/flow-preview";
|
|
3
|
+
export declare const DEFAULT_DEVICE_ID = "iphone-14";
|
|
4
|
+
/** Orientations the render page accepts; anything else falls back to its own default. */
|
|
5
|
+
export declare const ORIENTATIONS: readonly ["landscape", "portrait"];
|
|
6
|
+
export declare const DEFAULT_ORIENTATION = "portrait";
|
|
7
|
+
/** Payload the render page expects. Wire format shared with the UI, hence camelCase. */
|
|
8
|
+
export interface PreviewPayload {
|
|
9
|
+
flow: Record<string, unknown>;
|
|
10
|
+
remoteConfigs: unknown[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Accepts either a dashboard-api envelope (`{config, remote_configs, ...}`) or a bare
|
|
14
|
+
* builder config, and returns the shape the render page injects.
|
|
15
|
+
*
|
|
16
|
+
* `screens` must be an array: that is exactly what the page's own payload guard checks
|
|
17
|
+
* before it treats the payload as a builder draft, so a config without it would be
|
|
18
|
+
* rejected there instead — better to say so here, against the file the user named.
|
|
19
|
+
*/
|
|
20
|
+
export declare function normalizePreviewConfig(raw: unknown): PreviewPayload;
|
|
21
|
+
export interface RenderTarget {
|
|
22
|
+
device: string;
|
|
23
|
+
orientation: string;
|
|
24
|
+
/** Omitted lets the render page fall back to the flow's first screen. */
|
|
25
|
+
screen?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function buildRenderUrl(target: RenderTarget, payload: PreviewPayload): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { gzipSync } from 'node:zlib';
|
|
2
|
+
import { appUrl } from './app-url.js';
|
|
3
|
+
/** The render route is fixed; only its host is configurable, via ADAPTY_APP_URL. */
|
|
4
|
+
export const PREVIEW_PATH = '/flow-preview';
|
|
5
|
+
export const DEFAULT_DEVICE_ID = 'iphone-14';
|
|
6
|
+
/** Orientations the render page accepts; anything else falls back to its own default. */
|
|
7
|
+
export const ORIENTATIONS = ['landscape', 'portrait'];
|
|
8
|
+
export const DEFAULT_ORIENTATION = 'portrait';
|
|
9
|
+
function isRecord(value) {
|
|
10
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Accepts either a dashboard-api envelope (`{config, remote_configs, ...}`) or a bare
|
|
14
|
+
* builder config, and returns the shape the render page injects.
|
|
15
|
+
*
|
|
16
|
+
* `screens` must be an array: that is exactly what the page's own payload guard checks
|
|
17
|
+
* before it treats the payload as a builder draft, so a config without it would be
|
|
18
|
+
* rejected there instead — better to say so here, against the file the user named.
|
|
19
|
+
*/
|
|
20
|
+
export function normalizePreviewConfig(raw) {
|
|
21
|
+
if (!isRecord(raw)) {
|
|
22
|
+
throw new Error('Config file must contain a JSON object.');
|
|
23
|
+
}
|
|
24
|
+
const envelopeConfig = isRecord(raw.config) ? raw.config : null;
|
|
25
|
+
const flow = envelopeConfig ?? raw;
|
|
26
|
+
if (!Array.isArray(flow.screens)) {
|
|
27
|
+
throw new TypeError('Unrecognized config file. Expected a dashboard-api envelope with a `config` object, or a bare builder config — either way `screens` must be an array.');
|
|
28
|
+
}
|
|
29
|
+
const remoteConfigs = envelopeConfig && Array.isArray(raw.remote_configs) ? raw.remote_configs : [];
|
|
30
|
+
return { flow, remoteConfigs };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Fragment wire format shared with the render page: bare `base64url(gzip(utf8(JSON)))`, no
|
|
34
|
+
* prefix — the page compresses unconditionally too, so there is no plain shape to mark it
|
|
35
|
+
* apart from. Node's base64url already omits the `=` padding the page strips.
|
|
36
|
+
*/
|
|
37
|
+
function encodeConfigFragment(payload) {
|
|
38
|
+
return gzipSync(Buffer.from(JSON.stringify(payload), 'utf8')).toString('base64url');
|
|
39
|
+
}
|
|
40
|
+
export function buildRenderUrl(target, payload) {
|
|
41
|
+
const url = appUrl(PREVIEW_PATH);
|
|
42
|
+
if (target.screen)
|
|
43
|
+
url.searchParams.set('screen', target.screen);
|
|
44
|
+
url.searchParams.set('device', target.device);
|
|
45
|
+
url.searchParams.set('orientation', target.orientation);
|
|
46
|
+
url.hash = `config=${encodeConfigFragment(payload)}`;
|
|
47
|
+
return url.toString();
|
|
48
|
+
}
|