@wix/himalaya-cli 0.1466.0 → 0.1468.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/dist/cli.mjs +22 -6
- package/guides/qa-builds.md +78 -0
- package/package.json +2 -2
- package/templates/beauty-salon/tier5-src/_shared/host-events.ts +27 -0
- package/templates/beauty-salon/tier5-src/himi-booking-time/index.ts +13 -0
- package/templates/branded-lite/tier5-src/_shared/host-events.ts +27 -0
- package/templates/branded-lite/tier5-src/himi-booking-time/index.ts +13 -0
- package/templates/gym-fitness/tier5-src/_shared/host-events.ts +27 -0
- package/templates/gym-fitness/tier5-src/himi-booking-time/index.ts +13 -0
- package/templates/yoga-studio/tier5-src/_shared/host-events.ts +27 -0
- package/templates/yoga-studio/tier5-src/himi-booking-time/index.ts +13 -0
package/dist/cli.mjs
CHANGED
|
@@ -8127,7 +8127,6 @@ function resolveTranslation(value, catalog3, sourceCatalog) {
|
|
|
8127
8127
|
var BINDING_SIGILS, BINDING_SIGIL_RE, TRANSLATION_PREFIX, TRANSLATION_KEY_RE;
|
|
8128
8128
|
var init_bindings = __esm({
|
|
8129
8129
|
"../../core/ts/src/bindings.ts"() {
|
|
8130
|
-
"use strict";
|
|
8131
8130
|
BINDING_SIGILS = ["service", "flag", "item", "t"];
|
|
8132
8131
|
BINDING_SIGIL_RE = new RegExp(
|
|
8133
8132
|
`^\\$(?:${BINDING_SIGILS.filter((s2) => s2 !== "t").join("|")})\\b|^\\$t:`
|
|
@@ -185850,7 +185849,8 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
185850
185849
|
"from-base44-cli",
|
|
185851
185850
|
"no-browser",
|
|
185852
185851
|
"refresh-wix",
|
|
185853
|
-
"empty-app-ok"
|
|
185852
|
+
"empty-app-ok",
|
|
185853
|
+
"aab"
|
|
185854
185854
|
]);
|
|
185855
185855
|
var SHELL_ENUM = SHELL_NAMES2.join("|");
|
|
185856
185856
|
var SWITCH_FLAGS = new Set([...BOOLEAN_FLAGS].filter((name) => name !== "deep"));
|
|
@@ -186355,7 +186355,7 @@ var USAGE2 = `himi \u2014 the Himalaya authoring CLI (remote dev loop)
|
|
|
186355
186355
|
minBinaryVersion \u2014 all named in the output.
|
|
186356
186356
|
himi dist [--app <a>] [--platform ios,android] [--release <id>] [--allow-draft] [--target <t>]
|
|
186357
186357
|
[--shell branded-lite|owner-lite|brandy|base44|player|demo-app] [--signing wix-managed|byo]
|
|
186358
|
-
[--android-signing qa-keystore|wix-keystore|debug]
|
|
186358
|
+
[--android-signing qa-keystore|wix-keystore|debug] [--aab]
|
|
186359
186359
|
[--borrow-ios-id <bundleId>] [--no-wait] [--force] [--plan]
|
|
186360
186360
|
build an INSTALLABLE QA binary (signed IPA / APK) on
|
|
186361
186361
|
the Wix Job Service Mac pool and hand back an install
|
|
@@ -186390,6 +186390,13 @@ var USAGE2 = `himi \u2014 the Himalaya authoring CLI (remote dev loop)
|
|
|
186390
186390
|
wix-keystore (the pool's own Wix key, when the machine
|
|
186391
186391
|
carries one \u2014 fails loudly if not), or debug (the old
|
|
186392
186392
|
per-machine key; QA must uninstall first).
|
|
186393
|
+
--aab builds an Android App Bundle (.aab) instead of
|
|
186394
|
+
an APK, to upload to Google Play yourself: you get a
|
|
186395
|
+
download link and its sha256, and no install link or
|
|
186396
|
+
QR (a bundle does not install on a device). Android
|
|
186397
|
+
only. Signed with the Wix production key (wix-keystore,
|
|
186398
|
+
the upload key Branded apps use); any other
|
|
186399
|
+
--android-signing is refused. The default stays an APK.
|
|
186393
186400
|
--borrow-ios-id is an
|
|
186394
186401
|
iOS-only interim: sign under an internal enterprise id the
|
|
186395
186402
|
pool already has a profile for (e.g. ent.wix.time.dev); the
|
|
@@ -186836,7 +186843,7 @@ var COMMANDS2 = [
|
|
|
186836
186843
|
{ name: "patch", summary: 'change ONE item of a published app \u2014 a screen descriptor, a Tier-5 bundle, tokens, or the app-config baseline \u2014 from a RAW FILE, with no content package and no build. The low-level door: it does not lint your package, because there is no package here; `himi push --only <selector>` is the same edit with the full build + lint gate and is what to reach for when the package is in hand. Derives a new release from the app\'s current LIVE release, or from `--from-release <id>`. An explicit `--draft` or `--live` is REQUIRED \u2014 a patch has no default blast radius \u2014 and `--notes` is required with `--live` on a screen or app-config patch, because off-repo `gitSha` is "dev" and the ledger notes are then the only durable record of bytes that correspond to no commit. A branch does not merge: activating one derived from an older release reverts every item the patch does not move, so that case is refused until `--accept-regressions`. A bundle patch must repoint a screen (`--for-screen`) or the bytes land under a manifest key nothing reads. It cannot ADD a screen (screens are git-only, RFC 0012), never claims a slug \u2014 the app must already be published \u2014 and carries no attestation, source archive, assets, icon or minBinaryVersion, each named in the output.', required: "screen <id> --file <descriptor.json> [--bundle <sha256|none>] | bundle --file <bundle.js> (--for-screen <id>[,<id>] | --no-rebind) | tokens --file <tokens.json> | app-config --file <app-config.json> ; (--draft|--live) --app <a> [--from-release <id>] [--accept-regressions] [--label <n>] [--notes <t>] [--force] [--allow-binary-gap] [--target <t>] (--control-token)" },
|
|
186837
186844
|
{ name: "flags", summary: "read and change the app's OPERATIONAL flags (RFC 0013) \u2014 `list` prints what the DEVICE door serves, `set <key> --definition` creates or replaces one, `ramp <key>` moves its rollout share or kills it, `rm <key>` deletes it and echoes the definition back so the removal is reversible. Every write is followed by a read of the same door the device uses, and the answer is reported as `servedNow`: flags are stored in the app's git-committed flags.json, so a control plane with no checkout accepts a write it will never serve.", required: "list|set|ramp|rm [<key>] [--definition <json|@file>] [--percent 0-100] [--enabled true|false] [--app <a>] (--control-token)" },
|
|
186838
186845
|
{ name: "visibility", summary: "read (no value) or set whether the public registry (/v1/apps + the Himi Player picker) LISTS the app. `public` lists it for everyone; `unlisted` hides it from listings while direct reads by app id \u2014 deep links, previews \u2014 keep working. App from --app or the content dir. Setting it needs no republish.", required: "[public|unlisted] [--app <a>] [--content <dir>] (--control-token)" },
|
|
186839
|
-
{ name: "dist", summary: "build an INSTALLABLE QA binary (signed IPA / APK) on the Wix Job Service Mac pool and return an install link \u2014 the serve plane brokers the job, the pool builds it. WAITS for the build by default (~4 min) and prints the install link; `--no-wait` returns a jobId immediately instead. Omitting `--release` builds whatever is LIVE for the app. A named draft is refused unless `--allow-draft` makes that exceptional build explicit. `status <jobId>` / `watch <jobId>` / `list --app <a>` / `cancel <jobId>` inspect and stop builds (`watch` reports up front whether the job mints its own signing identity or borrows one, so a doomed iOS build is cancellable in seconds rather than after the archive). Off-repo, name a target \u2014 `--target wix` for a self-serve Wix login or `--target bo` for the internal employee path (the in-repo default is a local dev plane that isn't reachable off-repo); self-serve callers may build their OWN apps under a tighter budget (2 in flight / 5 per 24h), while a manage-only owner-role credential is refused. `--shell` picks the native host to sign into (branded-lite | owner-lite | brandy | base44 | player | demo-app) and defaults to the content package's himalaya.content.json `shell` (templates carry the one they were written for), else branded-lite. The per-app shells ADD the release's declared capabilities to the binary, while Himi Player and demo-app refuse a release declaring one they lack; `--plan` previews it without building; `brandy` is the site shell \u2014 the customer's own website as native-framed tabs. `--signing` defaults to `wix-managed` \u2014 the Wix Apple account mints a per-app signing identity for the build, so the caller provisions no Apple credentials of their own; `--signing byo --site <id>` reuses an existing customer ad-hoc profile and certificate for iOS; requires an owned site and a completed personal signing binding. `--android-signing` is the Android counterpart and a SEPARATE axis (a keystore, not an Apple account): `qa-keystore` (default \u2014 the repo's committed QA key, identical on every pool machine, so a build installs over the previous one), `wix-keystore` (the Wix key the pool machine carries; the build FAILS naming the paths searched if it carries none, rather than quietly signing with something else), or `debug` (the old per-machine key \u2014 QA must uninstall any previous build first). NOT the normal way to ship: screens, Tier-5 bundles, tokens and app-config all go over the air with `himi push` in seconds \u2014 a binary trip is only needed for a NATIVE delta (a new SDUI component, a new declared capability/entitlement, a new bundle id, or a minBinaryVersion floor bump). Refuses to build an app whose launcher icon is missing, a placeholder, or one a store would reject \u2014 the one thing a binary carries that no later OTA push can fix; `--force` builds anyway", required: "[--app <a>] [--platform ios,android] [--release <id>] [--allow-draft] [--target <t>] [--shell branded-lite|owner-lite|brandy|base44|player|demo-app] [--signing wix-managed|byo] [--site <id>] [--android-signing qa-keystore|wix-keystore|debug] [--borrow-ios-id <bundleId>] [--no-wait] [--force] [--plan] | status <jobId> [--target <t>] | watch <jobId> [--target <t>] [--interval <ms>] | list --app <a> [--target <t>] | cancel <jobId> [--target <t>]" },
|
|
186846
|
+
{ name: "dist", summary: "build an INSTALLABLE QA binary (signed IPA / APK) on the Wix Job Service Mac pool and return an install link \u2014 the serve plane brokers the job, the pool builds it. WAITS for the build by default (~4 min) and prints the install link; `--no-wait` returns a jobId immediately instead. Omitting `--release` builds whatever is LIVE for the app. A named draft is refused unless `--allow-draft` makes that exceptional build explicit. `status <jobId>` / `watch <jobId>` / `list --app <a>` / `cancel <jobId>` inspect and stop builds (`watch` reports up front whether the job mints its own signing identity or borrows one, so a doomed iOS build is cancellable in seconds rather than after the archive). Off-repo, name a target \u2014 `--target wix` for a self-serve Wix login or `--target bo` for the internal employee path (the in-repo default is a local dev plane that isn't reachable off-repo); self-serve callers may build their OWN apps under a tighter budget (2 in flight / 5 per 24h), while a manage-only owner-role credential is refused. `--shell` picks the native host to sign into (branded-lite | owner-lite | brandy | base44 | player | demo-app) and defaults to the content package's himalaya.content.json `shell` (templates carry the one they were written for), else branded-lite. The per-app shells ADD the release's declared capabilities to the binary, while Himi Player and demo-app refuse a release declaring one they lack; `--plan` previews it without building; `brandy` is the site shell \u2014 the customer's own website as native-framed tabs. `--signing` defaults to `wix-managed` \u2014 the Wix Apple account mints a per-app signing identity for the build, so the caller provisions no Apple credentials of their own; `--signing byo --site <id>` reuses an existing customer ad-hoc profile and certificate for iOS; requires an owned site and a completed personal signing binding. `--android-signing` is the Android counterpart and a SEPARATE axis (a keystore, not an Apple account): `qa-keystore` (default \u2014 the repo's committed QA key, identical on every pool machine, so a build installs over the previous one), `wix-keystore` (the Wix key the pool machine carries; the build FAILS naming the paths searched if it carries none, rather than quietly signing with something else), or `debug` (the old per-machine key \u2014 QA must uninstall any previous build first). `--aab` builds an Android App Bundle (.aab) for the owner to upload to Google Play instead of an APK \u2014 a download link and sha256, no install link (a bundle does not install on a device); Android only, signed with the Wix production key (`wix-keystore`, the upload key Branded apps use) \u2014 any other `--android-signing` is refused, and it is limited to internal callers (a self-serve caller gets 403 android_format_not_allowed). NOT the normal way to ship: screens, Tier-5 bundles, tokens and app-config all go over the air with `himi push` in seconds \u2014 a binary trip is only needed for a NATIVE delta (a new SDUI component, a new declared capability/entitlement, a new bundle id, or a minBinaryVersion floor bump). Refuses to build an app whose launcher icon is missing, a placeholder, or one a store would reject \u2014 the one thing a binary carries that no later OTA push can fix; `--force` builds anyway", required: "[--app <a>] [--platform ios,android] [--release <id>] [--allow-draft] [--target <t>] [--shell branded-lite|owner-lite|brandy|base44|player|demo-app] [--signing wix-managed|byo] [--site <id>] [--android-signing qa-keystore|wix-keystore|debug] [--aab] [--borrow-ios-id <bundleId>] [--no-wait] [--force] [--plan] | status <jobId> [--target <t>] | watch <jobId> [--target <t>] [--interval <ms>] | list --app <a> [--target <t>] | cancel <jobId> [--target <t>]" },
|
|
186840
186847
|
{ name: "testflight", summary: 'build, sign and upload an app to TestFlight (internal testers \u2014 no Beta App Review), then WAIT until App Store Connect finishes processing it. Sugar for `store upload --track testflight`. Prompts first: a build number is consumed permanently on a real Apple account and cannot be reused \u2014 `--yes` for CI, `--dry-run` to see the plan without touching anything. The wait is the real check, not a nicety: fastlane prints "Successfully uploaded" for a binary App Store Connect then rejects in processing, and a rejected binary leaves NO build record at all (the reason is emailed). Public App Store review is deliberately NOT automatable here \u2014 `--track appstore` is refused; use /submit-ios-app when you mean to go public', required: "<app> [--track testflight|internal] [--profile <name>] [--yes] [--dry-run] [--skip-build] [--no-wait] [--json]" },
|
|
186841
186848
|
{ name: "store", summary: "the App Store Connect group behind `testflight`. `preflight <app>` checks readiness offline (icon, entitlements, encryption flag, version, store.config ids, listing + review record); `upload <app>` is what `testflight` runs; `status <app>` reads live build + processing state. `--profile` selects WHICH App Store Connect account: an app built for a Wix customer ships under the CUSTOMER's Apple team (Apple guideline 4.2.6 forbids an app-generation service submitting on a client's behalf), declared per-app as ios.credentialProfile in store.config.yaml and resolved from ~/.himalaya/store-credentials/<name>.json \u2014 the .p8 itself never enters the repo. `iap draft|plan|apply|status|connect|screenshots` registers an app owner's digital products as in-app purchases (non-consumables, or consumable price tiers): draft reads the Wix catalog and writes the manifest, plan and status only read, apply is the one App Store mutation boundary, connect readies the Wix site to sell them (purchases app, In-App Purchase key, appleIAP provider, product mappings; --preview-tag routes only its Serve calls to a deploy preview), screenshots renders each product's purchase screen from the app (--content) as a bound iPhone shows it to a signed-in member and names the 1290\xD72796 PNG as the item's App Review screenshot, and none of them builds, signs or needs a checkout (`himi docs iap-products`)", required: "preflight <app> [--profile <n>] | upload <app> [--track <t>] [--profile <n>] [--yes] [--dry-run] | status <app> [--profile <n>] | iap draft --site <msid> --profile <n> --apple-app <id> --bundle <id> --base-territory <t> --out <f> [--territories <a,b>] [--locale <l>] [--mode per-item|tiers] [--merge <f>] [--report <f>] [--json] | iap plan --manifest <f> --profile <n> --out <f> [--json] | iap apply --plan <f> --profile <n> --out <f> [--yes] [--limit <n>] [--json] | iap status --manifest <f> --profile <n> [--out <f>] [--json] | iap connect --manifest <f> --profile <n> [--iap-key <AuthKey.p8> --iap-key-id <id>] [--content <dir>] [--preview-tag dp-<tag>] [--dry-run] [--yes] [--out <f>] [--json] | iap screenshots --manifest <f> --content <dir> [--profile <n>] [--out-dir <d>] [--only <id,\u2026>] [--force] [--dry-run] [--json]" },
|
|
186842
186849
|
{ name: "guide", summary: "print the Himalaya overview + headless dev loop + available topics (offline)", required: "[--json]" },
|
|
@@ -193474,6 +193481,11 @@ ${label3}`);
|
|
|
193474
193481
|
};
|
|
193475
193482
|
render(" Himi Player", player);
|
|
193476
193483
|
if (job.status !== "SUCCESS") return;
|
|
193484
|
+
if (job.platform === "android" && job.androidFormat === "aab") {
|
|
193485
|
+
io2.log(" android bundle");
|
|
193486
|
+
io2.log(" QR omitted (an .aab is uploaded to Google Play; it does not install on a device)");
|
|
193487
|
+
return;
|
|
193488
|
+
}
|
|
193477
193489
|
const shortUrl = `${origin.replace(/\/+$/, "")}/v1/dist/${encodeURIComponent(jobId)}/install`;
|
|
193478
193490
|
const artifact = links[String(job.platform)];
|
|
193479
193491
|
const installUrl = typeof artifact?.url === "string" ? artifact.url : void 0;
|
|
@@ -193753,7 +193765,10 @@ ${label3}`);
|
|
|
193753
193765
|
// keystore on Android" — the normal combination — with one command.
|
|
193754
193766
|
// flags["android-signing"], NOT flags.androidSigning: parseFlags does no kebab→camel
|
|
193755
193767
|
// conversion, so the camel form reads undefined and the flag is a silent no-op.
|
|
193756
|
-
...str10(flags["android-signing"]) ? { androidSigning: str10(flags["android-signing"]) } : {}
|
|
193768
|
+
...str10(flags["android-signing"]) ? { androidSigning: str10(flags["android-signing"]) } : {},
|
|
193769
|
+
// A Play bundle instead of an APK. The plane validates it (Android only, link destination,
|
|
193770
|
+
// a stable key) so every caller of the route gets the same refusal.
|
|
193771
|
+
...flags.aab === true ? { androidFormat: "aab" } : {}
|
|
193757
193772
|
}, { wixAuthorization });
|
|
193758
193773
|
} catch (e2) {
|
|
193759
193774
|
return distUnreachable(e2);
|
|
@@ -193830,10 +193845,11 @@ ${label3}`);
|
|
|
193830
193845
|
done.push(rec);
|
|
193831
193846
|
if (!flags.json) {
|
|
193832
193847
|
const artifact = rec.artifact;
|
|
193848
|
+
const isAab = rec.platform === "android" && rec.androidFormat === "aab";
|
|
193833
193849
|
const installUrl = rec.platform === "ios" ? artifact?.installPageUrl : artifact?.downloadUrl ?? artifact?.installLink;
|
|
193834
193850
|
const handoff = rec.customerAppStore;
|
|
193835
193851
|
const handoffText = typeof handoff?.status === "string" ? ` testflight ${handoff.status}${handoff.error ? ` (${handoff.error})` : ""}` : "";
|
|
193836
|
-
io2.log(` ${rec.platform} ${rec.status}${handoffText}${installUrl ? ` install \u2192 ${installUrl}` : ""}`);
|
|
193852
|
+
io2.log(` ${rec.platform} ${rec.status}${handoffText}${installUrl ? isAab ? ` .aab for Google Play (not installable) \u2192 ${installUrl}` : ` install \u2192 ${installUrl}` : ""}`);
|
|
193837
193853
|
if (artifact?.caveat) io2.log(` \u26A0 ${artifact.caveat}`);
|
|
193838
193854
|
}
|
|
193839
193855
|
}
|
package/guides/qa-builds.md
CHANGED
|
@@ -1026,6 +1026,81 @@ that block off the first Android `himi dist` run to settle the question with a m
|
|
|
1026
1026
|
than an assumption, the way the iOS identities were settled on 2026-07-27 after two wrong
|
|
1027
1027
|
diagnoses.
|
|
1028
1028
|
|
|
1029
|
+
### An Android App Bundle for Google Play — `--aab`
|
|
1030
|
+
|
|
1031
|
+
An APK installs on a device. Google Play wants an **Android App Bundle** (`.aab`) for a new app,
|
|
1032
|
+
and a bundle installs on nothing. `--aab` builds the bundle instead, for an owner who downloads the
|
|
1033
|
+
Wix-built binary and uploads it to Play Console themselves:
|
|
1034
|
+
|
|
1035
|
+
```sh
|
|
1036
|
+
himi login --dpx --target bo # an INTERNAL caller: self-serve callers get 403 (see below)
|
|
1037
|
+
himi dist --app <app> --platform android --aab --wait --target bo
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
- **What you get:** `artifact.downloadUrl` (presigned, 7 days) and `artifact.sha256`. No
|
|
1041
|
+
`installLink`, no `installPageUrl`, no QR, and `GET /v1/dist/<jobId>/install` answers 404 for it —
|
|
1042
|
+
a link that hands a phone a file it cannot open is worse than none. The record carries
|
|
1043
|
+
`androidFormat: "aab"`.
|
|
1044
|
+
- **Android only.** The default stays an APK, and iOS is untouched. It travels as
|
|
1045
|
+
`androidFormat: "aab"` on `POST /v1/admin/dist` (every `/v1/owner/dist` caller is self-serve, so
|
|
1046
|
+
that route always answers `403 android_format_not_allowed`), and reaches the pool as
|
|
1047
|
+
`DIST_ANDROID_FORMAT=aab` in the `command` blob (see
|
|
1048
|
+
[why](#why-the-jobs-parameters-ride-in-command-not-envvars)). Refusals, all typed 400s before
|
|
1049
|
+
any pool time is spent: `invalid_android_format` (anything but `apk` / `aab`),
|
|
1050
|
+
`android_format_requires_link` (any destination but `link`), `android_format_requires_android`
|
|
1051
|
+
(an iOS-only build, where the flag would be dropped in silence), and
|
|
1052
|
+
`android_format_requires_wix_keystore` (any `--android-signing` other than `wix-keystore`), and
|
|
1053
|
+
`android_format_shell_unsupported` (the `owner-lite` shell, whose gradle build refuses a store
|
|
1054
|
+
bundle without a real Firebase config and customer identity). A mixed `--platform ios,android`
|
|
1055
|
+
applies it to the Android leg only.
|
|
1056
|
+
- **How the pool builds it.** `run-dist` runs store-release's `bundleRelease` task
|
|
1057
|
+
(`bundleReleaseArgs` / `bundleReleaseEnv`, the same task the Play lane runs) with **AGP injected
|
|
1058
|
+
signing**, with the **Wix production key** (`wix-keystore`: `~/.signing/oneapp-production.keystore`,
|
|
1059
|
+
alias `WixOne`), resolved by `resolveAndroidSigner` and verified there. The pool refuses any
|
|
1060
|
+
other signer for a bundle — a pool-side `DIST_ANDROID_KEYSTORE` included. The keystore path and alias are `-P` flags; the passwords ride in the environment as
|
|
1061
|
+
`ORG_GRADLE_PROJECT_android.injected.signing.{store,key}.password`, never in argv. `jarsigner
|
|
1062
|
+
-verify` must say `jar verified.` before anything is uploaded. The seed gate and the binary-size
|
|
1063
|
+
budget run on the `.aab` as they do on an APK. `DIST_ANDROID_BUNDLE_TASK` overrides the task
|
|
1064
|
+
(`DIST_ANDROID_TASK` stays the APK one), and `DIST_ANDROID_FLAVOR` picks among flavored bundles.
|
|
1065
|
+
Only the task's own gradle module is searched, so a sibling module's stale bundle is never taken.
|
|
1066
|
+
An **ambiguous** choice fails the build instead of guessing: the bundle's applicationId becomes
|
|
1067
|
+
the app's permanent Play package.
|
|
1068
|
+
- **What it skips, and why.** `apksigner` and the re-sign step (a bundle is JAR-signed at build
|
|
1069
|
+
time, and the injected config overrides any `signingConfig`); the 16 KB check (its ZIP half is about an installed APK,
|
|
1070
|
+
which Play generates; its ELF half reads the APK `lib/` layout — Play checks alignment on
|
|
1071
|
+
upload); and `--reuse-artifact` (that is an APK).
|
|
1072
|
+
- **Version skew.** The pool runs the release's own `run-dist`, and one older than `--aab` ignores
|
|
1073
|
+
`DIST_ANDROID_FORMAT` and builds an APK. So the pool reports what it built (`androidFormat` on
|
|
1074
|
+
the result callback, the `androidSigningUsed` pattern), and serve-control turns a `SUCCESS` that
|
|
1075
|
+
does not say `aab` into **`FAILURE`** with `android_format_not_built: …`, recording
|
|
1076
|
+
`androidFormat: "apk"`, and drops the artifact link. The verdict is read from that error on every
|
|
1077
|
+
read, so a status refresh that races the callback cannot bring back `SUCCESS`. **An old runner
|
|
1078
|
+
never signs with the Wix key:** for a bundle the plane dispatches `DIST_ANDROID_SIGNING=qa-keystore`,
|
|
1079
|
+
and the new run-dist overrides it with `wix-keystore`, derived from `DIST_ANDROID_FORMAT=aab`. A
|
|
1080
|
+
run-dist older than `--aab` therefore signs an ordinary QA APK, never an installable APK carrying
|
|
1081
|
+
the Wix production certificate. The QA key is named rather than left out, because a named key
|
|
1082
|
+
outranks a pool machine's `DIST_ANDROID_USE_WIX_KEYSTORE=1`. A bundle must also report `androidSigningUsed: "wix-keystore"`; one signed
|
|
1083
|
+
with any other key, or that reports none, fails as `android_format_wrong_signer` with its link
|
|
1084
|
+
dropped. Rebuild from a release published on a newer commit. The other direction fails too: an
|
|
1085
|
+
APK job whose pool reports a bundle is `android_format_not_requested`, with its link dropped. A
|
|
1086
|
+
brokered job reads the format from its OWN parameters only, so a machine with
|
|
1087
|
+
`DIST_ANDROID_FORMAT=aab` in its environment no longer turns an APK job into a bundle; the check
|
|
1088
|
+
stays for a runner older than that rule. A later delivery that passes every check (a retry on a
|
|
1089
|
+
clean machine) clears the verdict.
|
|
1090
|
+
|
|
1091
|
+
**Why the Wix production key, and only for a bundle.** There are two Android keys: the committed
|
|
1092
|
+
QA keystore and the Wix production key (WixOne). WixOne is the Play **upload** key every Branded
|
|
1093
|
+
app's bundle carries (`mobile-apps-engine` refuses any other certificate for a Play build), with
|
|
1094
|
+
Play App Signing on, so Google re-signs what users install. An `.aab` therefore signs with it —
|
|
1095
|
+
only its `WixOne` entry of `oneapp-production.keystore`: the alias is pinned for a bundle, whatever
|
|
1096
|
+
alias the pool's signing files name. Because the first upload binds a Play listing to that key for good, a **self-serve** caller
|
|
1097
|
+
gets no bundle at all (`403 android_format_not_allowed`): `--aab` is for internal callers until a
|
|
1098
|
+
Containers store build checks the app, its owner and its package name. A self-serve **APK** with
|
|
1099
|
+
`wix-keystore` keeps `403 android_signing_not_allowed`, unchanged. For the owner, the Play listing is bound to the WixOne upload certificate, so later
|
|
1100
|
+
updates come from Wix builds too; leaving Wix needs an upload-key reset through Play Console
|
|
1101
|
+
support. A pool machine without the key fails the build, naming the paths it searched, rather than
|
|
1102
|
+
signing with another key.
|
|
1103
|
+
|
|
1029
1104
|
### The iOS lane lives in one shared place
|
|
1030
1105
|
|
|
1031
1106
|
`fastlane dist` runs with `cwd = <app>/ios`, so it needs `<app>/ios/fastlane/Fastfile`. That file is
|
|
@@ -1118,6 +1193,9 @@ baking the live asset.
|
|
|
1118
1193
|
or `DIST_ANDROID_USE_WIX_KEYSTORE=1` names one, and a named key wins — `apksigner` replaces the
|
|
1119
1194
|
signature block — because the job record is written at dispatch and would otherwise name a key
|
|
1120
1195
|
that is not on the binary.
|
|
1196
|
+
- **Android, `--aab`** → a **presigned `.aab` download URL** and its sha256, and **no install
|
|
1197
|
+
link**: a bundle is uploaded to Google Play, it does not install on a device. See
|
|
1198
|
+
[the App Bundle section](#an-android-app-bundle-for-google-play----aab).
|
|
1121
1199
|
- **How Android picks up the branding.** `prepare:customer` writes an **xcconfig** for iOS (bundle
|
|
1122
1200
|
id + display name) but on Android only tokens, the icon drawable and a colour XML. Android's
|
|
1123
1201
|
`applicationId` and app label come from the **contract JSON itself**, read at gradle-configure
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1468.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
|
|
6
6
|
"keywords": [
|
|
@@ -71,5 +71,5 @@
|
|
|
71
71
|
"artifactId": "himalaya-cli"
|
|
72
72
|
}
|
|
73
73
|
},
|
|
74
|
-
"falconPackageHash": "
|
|
74
|
+
"falconPackageHash": "dd16fbdf6eef44d6e5c3095a1d25fc071a62d7eb1df54f9bdb1444a6"
|
|
75
75
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Host events — what a member screen tells the app hosting it about something the member did
|
|
2
|
+
// (a booking confirmed), so a host such as Symphony can say so in its chat and refresh what it shows.
|
|
3
|
+
//
|
|
4
|
+
// A worker dispatches `host.event` with `{ type, ...fields }` (string values only: a dispatch carries
|
|
5
|
+
// one string params map). An embed (`HimalayaEmbed`) hands it to the host's `onAction` as an action
|
|
6
|
+
// with that id; anywhere else nothing handles it and it is dropped. It is a notice, never a request:
|
|
7
|
+
// the host owns what happens next, and the worker never waits on it or depends on its answer.
|
|
8
|
+
|
|
9
|
+
import { dispatch } from "@himalaya/dispatch";
|
|
10
|
+
|
|
11
|
+
export const HOST_EVENT_ACTION_ID = "host.event";
|
|
12
|
+
|
|
13
|
+
/** The events a member screen reports. */
|
|
14
|
+
export type HostEventType = "booking.created";
|
|
15
|
+
|
|
16
|
+
/** Tell the host about [type]. Never throws: a host that does not listen must not break the screen. */
|
|
17
|
+
export async function emitHostEvent(type: HostEventType, fields: Record<string, string | undefined> = {}): Promise<void> {
|
|
18
|
+
const params: Record<string, string> = { type };
|
|
19
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
20
|
+
if (typeof value === "string" && value !== "") params[key] = value;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
await dispatch(HOST_EVENT_ACTION_ID, params);
|
|
24
|
+
} catch {
|
|
25
|
+
// Not embedded, or the host has no handler: nothing to tell.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -16,6 +16,7 @@ import { formatServicePrice, serviceIsChargeable } from "../_shared/wix-service-
|
|
|
16
16
|
import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
17
17
|
import { upsert as dbUpsert, remove as dbRemove, enqueue as dbEnqueue, type DbEntity } from "@himalaya/local-db";
|
|
18
18
|
import { isBridgeUnavailable } from "../_shared/owner-load-error.js";
|
|
19
|
+
import { emitHostEvent } from "../_shared/host-events.js";
|
|
19
20
|
import { wixMediaUrl, WIX_DETAIL_IMAGE } from "@himalaya/wix-media";
|
|
20
21
|
|
|
21
22
|
// Offline-first (data-offline): booking a slot is WRITE-BEHIND against the
|
|
@@ -635,6 +636,15 @@ const svc = defineService<S, {
|
|
|
635
636
|
if (!s.selectedSlotId) return derive({ ...s, message: copyParam("time.bookNeedsSlot", "Choose one time first.") });
|
|
636
637
|
const slot = slotsById.get(s.selectedSlotId);
|
|
637
638
|
if (!slot) return derive({ ...s, message: copyParam("time.bookSlotGone", "That time is no longer available. Try again.") });
|
|
639
|
+
// A confirmed booking is reported to the app hosting this screen (an embed's `onAction`), so a
|
|
640
|
+
// host chat can say so and refresh what it shows. Dropped where nothing hosts the screen.
|
|
641
|
+
const reportBooked = (bookingId: string | undefined) => emitHostEvent("booking.created", {
|
|
642
|
+
bookingId,
|
|
643
|
+
serviceId: s.id,
|
|
644
|
+
serviceName: s.serviceName,
|
|
645
|
+
start: slot.startDate,
|
|
646
|
+
end: slot.endDate,
|
|
647
|
+
});
|
|
638
648
|
// PAID service — FULLY NATIVE in-app payment (mirrors Wix's RN apps): NO
|
|
639
649
|
// hosted-checkout WebView / browser redirect. The chain is
|
|
640
650
|
// createBooking → ecom.createCheckout(booking line item) → ecom.createOrder
|
|
@@ -669,6 +679,7 @@ const svc = defineService<S, {
|
|
|
669
679
|
const orderRes = hosted ? { paymentGatewayOrderId: "" } : await createOrder(checkoutId);
|
|
670
680
|
const snapshotId = orderRes.paymentGatewayOrderId ?? "";
|
|
671
681
|
if (!snapshotId && !hosted) {
|
|
682
|
+
await reportBooked(bookingId);
|
|
672
683
|
return derive({ ...pending, bookingInFlight: false, successMessage: copyParam("time.successMessage", "Booked. Your confirmation is ready in My Bookings."), message: "" });
|
|
673
684
|
}
|
|
674
685
|
// 4. Collect payment IN-APP — wallet when this app has one wired, otherwise
|
|
@@ -690,6 +701,7 @@ const svc = defineService<S, {
|
|
|
690
701
|
},
|
|
691
702
|
});
|
|
692
703
|
if (pay.kind === "paid") {
|
|
704
|
+
await reportBooked(bookingId);
|
|
693
705
|
return derive({ ...pending, bookingInFlight: false, successMessage: copyParam("time.successMessage", "Booked. Your confirmation is ready in My Bookings."), message: "" });
|
|
694
706
|
}
|
|
695
707
|
if (pay.kind === "handoff") {
|
|
@@ -728,6 +740,7 @@ const svc = defineService<S, {
|
|
|
728
740
|
// Reconcile the optimistic local id with the server-assigned booking id.
|
|
729
741
|
const realId = created.booking?.id;
|
|
730
742
|
if (realId && realId !== localId) { await cacheDrop(localId); await cachePut(localRow(realId)); }
|
|
743
|
+
await reportBooked(realId);
|
|
731
744
|
return derive({
|
|
732
745
|
...pending,
|
|
733
746
|
bookingInFlight: false,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Host events — what a member screen tells the app hosting it about something the member did
|
|
2
|
+
// (a booking confirmed), so a host such as Symphony can say so in its chat and refresh what it shows.
|
|
3
|
+
//
|
|
4
|
+
// A worker dispatches `host.event` with `{ type, ...fields }` (string values only: a dispatch carries
|
|
5
|
+
// one string params map). An embed (`HimalayaEmbed`) hands it to the host's `onAction` as an action
|
|
6
|
+
// with that id; anywhere else nothing handles it and it is dropped. It is a notice, never a request:
|
|
7
|
+
// the host owns what happens next, and the worker never waits on it or depends on its answer.
|
|
8
|
+
|
|
9
|
+
import { dispatch } from "@himalaya/dispatch";
|
|
10
|
+
|
|
11
|
+
export const HOST_EVENT_ACTION_ID = "host.event";
|
|
12
|
+
|
|
13
|
+
/** The events a member screen reports. */
|
|
14
|
+
export type HostEventType = "booking.created";
|
|
15
|
+
|
|
16
|
+
/** Tell the host about [type]. Never throws: a host that does not listen must not break the screen. */
|
|
17
|
+
export async function emitHostEvent(type: HostEventType, fields: Record<string, string | undefined> = {}): Promise<void> {
|
|
18
|
+
const params: Record<string, string> = { type };
|
|
19
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
20
|
+
if (typeof value === "string" && value !== "") params[key] = value;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
await dispatch(HOST_EVENT_ACTION_ID, params);
|
|
24
|
+
} catch {
|
|
25
|
+
// Not embedded, or the host has no handler: nothing to tell.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -16,6 +16,7 @@ import { formatServicePrice, serviceIsChargeable } from "../_shared/wix-service-
|
|
|
16
16
|
import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
17
17
|
import { upsert as dbUpsert, remove as dbRemove, enqueue as dbEnqueue, type DbEntity } from "@himalaya/local-db";
|
|
18
18
|
import { isBridgeUnavailable } from "../_shared/owner-load-error.js";
|
|
19
|
+
import { emitHostEvent } from "../_shared/host-events.js";
|
|
19
20
|
import { wixMediaUrl, WIX_DETAIL_IMAGE } from "@himalaya/wix-media";
|
|
20
21
|
|
|
21
22
|
// Offline-first (data-offline): booking a slot is WRITE-BEHIND against the
|
|
@@ -635,6 +636,15 @@ const svc = defineService<S, {
|
|
|
635
636
|
if (!s.selectedSlotId) return derive({ ...s, message: copyParam("time.bookNeedsSlot", "Choose one time first.") });
|
|
636
637
|
const slot = slotsById.get(s.selectedSlotId);
|
|
637
638
|
if (!slot) return derive({ ...s, message: copyParam("time.bookSlotGone", "That time is no longer available. Try again.") });
|
|
639
|
+
// A confirmed booking is reported to the app hosting this screen (an embed's `onAction`), so a
|
|
640
|
+
// host chat can say so and refresh what it shows. Dropped where nothing hosts the screen.
|
|
641
|
+
const reportBooked = (bookingId: string | undefined) => emitHostEvent("booking.created", {
|
|
642
|
+
bookingId,
|
|
643
|
+
serviceId: s.id,
|
|
644
|
+
serviceName: s.serviceName,
|
|
645
|
+
start: slot.startDate,
|
|
646
|
+
end: slot.endDate,
|
|
647
|
+
});
|
|
638
648
|
// PAID service — FULLY NATIVE in-app payment (mirrors Wix's RN apps): NO
|
|
639
649
|
// hosted-checkout WebView / browser redirect. The chain is
|
|
640
650
|
// createBooking → ecom.createCheckout(booking line item) → ecom.createOrder
|
|
@@ -669,6 +679,7 @@ const svc = defineService<S, {
|
|
|
669
679
|
const orderRes = hosted ? { paymentGatewayOrderId: "" } : await createOrder(checkoutId);
|
|
670
680
|
const snapshotId = orderRes.paymentGatewayOrderId ?? "";
|
|
671
681
|
if (!snapshotId && !hosted) {
|
|
682
|
+
await reportBooked(bookingId);
|
|
672
683
|
return derive({ ...pending, bookingInFlight: false, successMessage: copyParam("time.successMessage", "Booked. Your confirmation is ready in My Bookings."), message: "" });
|
|
673
684
|
}
|
|
674
685
|
// 4. Collect payment IN-APP — wallet when this app has one wired, otherwise
|
|
@@ -690,6 +701,7 @@ const svc = defineService<S, {
|
|
|
690
701
|
},
|
|
691
702
|
});
|
|
692
703
|
if (pay.kind === "paid") {
|
|
704
|
+
await reportBooked(bookingId);
|
|
693
705
|
return derive({ ...pending, bookingInFlight: false, successMessage: copyParam("time.successMessage", "Booked. Your confirmation is ready in My Bookings."), message: "" });
|
|
694
706
|
}
|
|
695
707
|
if (pay.kind === "handoff") {
|
|
@@ -728,6 +740,7 @@ const svc = defineService<S, {
|
|
|
728
740
|
// Reconcile the optimistic local id with the server-assigned booking id.
|
|
729
741
|
const realId = created.booking?.id;
|
|
730
742
|
if (realId && realId !== localId) { await cacheDrop(localId); await cachePut(localRow(realId)); }
|
|
743
|
+
await reportBooked(realId);
|
|
731
744
|
return derive({
|
|
732
745
|
...pending,
|
|
733
746
|
bookingInFlight: false,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Host events — what a member screen tells the app hosting it about something the member did
|
|
2
|
+
// (a booking confirmed), so a host such as Symphony can say so in its chat and refresh what it shows.
|
|
3
|
+
//
|
|
4
|
+
// A worker dispatches `host.event` with `{ type, ...fields }` (string values only: a dispatch carries
|
|
5
|
+
// one string params map). An embed (`HimalayaEmbed`) hands it to the host's `onAction` as an action
|
|
6
|
+
// with that id; anywhere else nothing handles it and it is dropped. It is a notice, never a request:
|
|
7
|
+
// the host owns what happens next, and the worker never waits on it or depends on its answer.
|
|
8
|
+
|
|
9
|
+
import { dispatch } from "@himalaya/dispatch";
|
|
10
|
+
|
|
11
|
+
export const HOST_EVENT_ACTION_ID = "host.event";
|
|
12
|
+
|
|
13
|
+
/** The events a member screen reports. */
|
|
14
|
+
export type HostEventType = "booking.created";
|
|
15
|
+
|
|
16
|
+
/** Tell the host about [type]. Never throws: a host that does not listen must not break the screen. */
|
|
17
|
+
export async function emitHostEvent(type: HostEventType, fields: Record<string, string | undefined> = {}): Promise<void> {
|
|
18
|
+
const params: Record<string, string> = { type };
|
|
19
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
20
|
+
if (typeof value === "string" && value !== "") params[key] = value;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
await dispatch(HOST_EVENT_ACTION_ID, params);
|
|
24
|
+
} catch {
|
|
25
|
+
// Not embedded, or the host has no handler: nothing to tell.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -16,6 +16,7 @@ import { formatServicePrice, serviceIsChargeable } from "../_shared/wix-service-
|
|
|
16
16
|
import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
17
17
|
import { upsert as dbUpsert, remove as dbRemove, enqueue as dbEnqueue, type DbEntity } from "@himalaya/local-db";
|
|
18
18
|
import { isBridgeUnavailable } from "../_shared/owner-load-error.js";
|
|
19
|
+
import { emitHostEvent } from "../_shared/host-events.js";
|
|
19
20
|
import { wixMediaUrl, WIX_DETAIL_IMAGE } from "@himalaya/wix-media";
|
|
20
21
|
|
|
21
22
|
// Offline-first (data-offline): booking a slot is WRITE-BEHIND against the
|
|
@@ -635,6 +636,15 @@ const svc = defineService<S, {
|
|
|
635
636
|
if (!s.selectedSlotId) return derive({ ...s, message: copyParam("time.bookNeedsSlot", "Choose one time first.") });
|
|
636
637
|
const slot = slotsById.get(s.selectedSlotId);
|
|
637
638
|
if (!slot) return derive({ ...s, message: copyParam("time.bookSlotGone", "That time is no longer available. Try again.") });
|
|
639
|
+
// A confirmed booking is reported to the app hosting this screen (an embed's `onAction`), so a
|
|
640
|
+
// host chat can say so and refresh what it shows. Dropped where nothing hosts the screen.
|
|
641
|
+
const reportBooked = (bookingId: string | undefined) => emitHostEvent("booking.created", {
|
|
642
|
+
bookingId,
|
|
643
|
+
serviceId: s.id,
|
|
644
|
+
serviceName: s.serviceName,
|
|
645
|
+
start: slot.startDate,
|
|
646
|
+
end: slot.endDate,
|
|
647
|
+
});
|
|
638
648
|
// PAID service — FULLY NATIVE in-app payment (mirrors Wix's RN apps): NO
|
|
639
649
|
// hosted-checkout WebView / browser redirect. The chain is
|
|
640
650
|
// createBooking → ecom.createCheckout(booking line item) → ecom.createOrder
|
|
@@ -669,6 +679,7 @@ const svc = defineService<S, {
|
|
|
669
679
|
const orderRes = hosted ? { paymentGatewayOrderId: "" } : await createOrder(checkoutId);
|
|
670
680
|
const snapshotId = orderRes.paymentGatewayOrderId ?? "";
|
|
671
681
|
if (!snapshotId && !hosted) {
|
|
682
|
+
await reportBooked(bookingId);
|
|
672
683
|
return derive({ ...pending, bookingInFlight: false, successMessage: copyParam("time.successMessage", "Booked. Your confirmation is ready in My Bookings."), message: "" });
|
|
673
684
|
}
|
|
674
685
|
// 4. Collect payment IN-APP — wallet when this app has one wired, otherwise
|
|
@@ -690,6 +701,7 @@ const svc = defineService<S, {
|
|
|
690
701
|
},
|
|
691
702
|
});
|
|
692
703
|
if (pay.kind === "paid") {
|
|
704
|
+
await reportBooked(bookingId);
|
|
693
705
|
return derive({ ...pending, bookingInFlight: false, successMessage: copyParam("time.successMessage", "Booked. Your confirmation is ready in My Bookings."), message: "" });
|
|
694
706
|
}
|
|
695
707
|
if (pay.kind === "handoff") {
|
|
@@ -728,6 +740,7 @@ const svc = defineService<S, {
|
|
|
728
740
|
// Reconcile the optimistic local id with the server-assigned booking id.
|
|
729
741
|
const realId = created.booking?.id;
|
|
730
742
|
if (realId && realId !== localId) { await cacheDrop(localId); await cachePut(localRow(realId)); }
|
|
743
|
+
await reportBooked(realId);
|
|
731
744
|
return derive({
|
|
732
745
|
...pending,
|
|
733
746
|
bookingInFlight: false,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Host events — what a member screen tells the app hosting it about something the member did
|
|
2
|
+
// (a booking confirmed), so a host such as Symphony can say so in its chat and refresh what it shows.
|
|
3
|
+
//
|
|
4
|
+
// A worker dispatches `host.event` with `{ type, ...fields }` (string values only: a dispatch carries
|
|
5
|
+
// one string params map). An embed (`HimalayaEmbed`) hands it to the host's `onAction` as an action
|
|
6
|
+
// with that id; anywhere else nothing handles it and it is dropped. It is a notice, never a request:
|
|
7
|
+
// the host owns what happens next, and the worker never waits on it or depends on its answer.
|
|
8
|
+
|
|
9
|
+
import { dispatch } from "@himalaya/dispatch";
|
|
10
|
+
|
|
11
|
+
export const HOST_EVENT_ACTION_ID = "host.event";
|
|
12
|
+
|
|
13
|
+
/** The events a member screen reports. */
|
|
14
|
+
export type HostEventType = "booking.created";
|
|
15
|
+
|
|
16
|
+
/** Tell the host about [type]. Never throws: a host that does not listen must not break the screen. */
|
|
17
|
+
export async function emitHostEvent(type: HostEventType, fields: Record<string, string | undefined> = {}): Promise<void> {
|
|
18
|
+
const params: Record<string, string> = { type };
|
|
19
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
20
|
+
if (typeof value === "string" && value !== "") params[key] = value;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
await dispatch(HOST_EVENT_ACTION_ID, params);
|
|
24
|
+
} catch {
|
|
25
|
+
// Not embedded, or the host has no handler: nothing to tell.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -16,6 +16,7 @@ import { formatServicePrice, serviceIsChargeable } from "../_shared/wix-service-
|
|
|
16
16
|
import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
17
17
|
import { upsert as dbUpsert, remove as dbRemove, enqueue as dbEnqueue, type DbEntity } from "@himalaya/local-db";
|
|
18
18
|
import { isBridgeUnavailable } from "../_shared/owner-load-error.js";
|
|
19
|
+
import { emitHostEvent } from "../_shared/host-events.js";
|
|
19
20
|
import { wixMediaUrl, WIX_DETAIL_IMAGE } from "@himalaya/wix-media";
|
|
20
21
|
|
|
21
22
|
// Offline-first (data-offline): booking a slot is WRITE-BEHIND against the
|
|
@@ -635,6 +636,15 @@ const svc = defineService<S, {
|
|
|
635
636
|
if (!s.selectedSlotId) return derive({ ...s, message: copyParam("time.bookNeedsSlot", "Choose one time first.") });
|
|
636
637
|
const slot = slotsById.get(s.selectedSlotId);
|
|
637
638
|
if (!slot) return derive({ ...s, message: copyParam("time.bookSlotGone", "That time is no longer available. Try again.") });
|
|
639
|
+
// A confirmed booking is reported to the app hosting this screen (an embed's `onAction`), so a
|
|
640
|
+
// host chat can say so and refresh what it shows. Dropped where nothing hosts the screen.
|
|
641
|
+
const reportBooked = (bookingId: string | undefined) => emitHostEvent("booking.created", {
|
|
642
|
+
bookingId,
|
|
643
|
+
serviceId: s.id,
|
|
644
|
+
serviceName: s.serviceName,
|
|
645
|
+
start: slot.startDate,
|
|
646
|
+
end: slot.endDate,
|
|
647
|
+
});
|
|
638
648
|
// PAID service — FULLY NATIVE in-app payment (mirrors Wix's RN apps): NO
|
|
639
649
|
// hosted-checkout WebView / browser redirect. The chain is
|
|
640
650
|
// createBooking → ecom.createCheckout(booking line item) → ecom.createOrder
|
|
@@ -669,6 +679,7 @@ const svc = defineService<S, {
|
|
|
669
679
|
const orderRes = hosted ? { paymentGatewayOrderId: "" } : await createOrder(checkoutId);
|
|
670
680
|
const snapshotId = orderRes.paymentGatewayOrderId ?? "";
|
|
671
681
|
if (!snapshotId && !hosted) {
|
|
682
|
+
await reportBooked(bookingId);
|
|
672
683
|
return derive({ ...pending, bookingInFlight: false, successMessage: copyParam("time.successMessage", "Booked. Your confirmation is ready in My Bookings."), message: "" });
|
|
673
684
|
}
|
|
674
685
|
// 4. Collect payment IN-APP — wallet when this app has one wired, otherwise
|
|
@@ -690,6 +701,7 @@ const svc = defineService<S, {
|
|
|
690
701
|
},
|
|
691
702
|
});
|
|
692
703
|
if (pay.kind === "paid") {
|
|
704
|
+
await reportBooked(bookingId);
|
|
693
705
|
return derive({ ...pending, bookingInFlight: false, successMessage: copyParam("time.successMessage", "Booked. Your confirmation is ready in My Bookings."), message: "" });
|
|
694
706
|
}
|
|
695
707
|
if (pay.kind === "handoff") {
|
|
@@ -728,6 +740,7 @@ const svc = defineService<S, {
|
|
|
728
740
|
// Reconcile the optimistic local id with the server-assigned booking id.
|
|
729
741
|
const realId = created.booking?.id;
|
|
730
742
|
if (realId && realId !== localId) { await cacheDrop(localId); await cachePut(localRow(realId)); }
|
|
743
|
+
await reportBooked(realId);
|
|
731
744
|
return derive({
|
|
732
745
|
...pending,
|
|
733
746
|
bookingInFlight: false,
|