@wix/himalaya-cli 0.1608.0 → 0.1610.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.
Files changed (36) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/app-config.d.ts +7 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/catalog-adapters/wix-video.d.ts +20 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/catalog-pricing-contract.d.ts +1 -1
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/index.d.ts +1 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/mapping-source.d.ts +1 -1
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments-contract.d.ts +11 -0
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/wix-platform/src/index.d.ts +14 -0
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/wix-platform/src/user-props.d.ts +8 -0
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/iap-verticals/index.d.ts +1 -0
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/iap-verticals/videos.d.ts +5 -0
  11. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/index.d.ts +1 -1
  12. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/online-programs/src/index.d.ts +122 -1
  13. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/video/src/index.d.ts +6 -5
  14. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +25 -4
  15. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +25 -5
  16. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +8 -3
  17. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-online-programs.mjs +118 -0
  18. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-platform.mjs +65 -0
  19. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-video.mjs +3 -3
  20. package/dist/cli.mjs +157 -46
  21. package/dist/preview-assets.generated.bin +0 -0
  22. package/guides/form-submit.service.ts +22 -2
  23. package/guides/iap-pricing.md +1 -0
  24. package/guides/iap-products.md +18 -7
  25. package/guides/iap-screen-adoption.md +2 -1
  26. package/guides/qa-builds.md +14 -1
  27. package/guides/recipe-dynamic-list.md +16 -0
  28. package/guides/recipe-form-submit.md +32 -2
  29. package/guides/recipe-four-state.md +2 -1
  30. package/guides/sdk-reference.json +144 -10
  31. package/guides/web-page-sdk-reference.md +1 -1
  32. package/guides/wix-platform-usage.md +38 -0
  33. package/package.json +2 -2
  34. package/templates/branded-lite/dev/index.ts +107 -3
  35. package/templates/branded-lite/tier5-src/himi-branded-lite-video/index.ts +62 -7
  36. package/templates/branded-lite/tier5-src/himi-branded-lite-videos/index.ts +82 -26
@@ -79085,6 +79085,20 @@ const SCREENS = runtimeSetting({
79085
79085
  "collapseWhenEmpty": true
79086
79086
  }
79087
79087
  }
79088
+ },
79089
+ {
79090
+ "id": "",
79091
+ "body": {
79092
+ "package": "himalaya",
79093
+ "type": "Text",
79094
+ "props": {
79095
+ "text": "$item.priceLine",
79096
+ "style": "caption",
79097
+ "color": "textOnAccent",
79098
+ "maxLines": 1,
79099
+ "collapseWhenEmpty": true
79100
+ }
79101
+ }
79088
79102
  }
79089
79103
  ]
79090
79104
  }
@@ -79163,6 +79177,20 @@ const SCREENS = runtimeSetting({
79163
79177
  "collapseWhenEmpty": true
79164
79178
  }
79165
79179
  }
79180
+ },
79181
+ {
79182
+ "id": "",
79183
+ "body": {
79184
+ "package": "himalaya",
79185
+ "type": "Text",
79186
+ "props": {
79187
+ "text": "$item.priceLine",
79188
+ "style": "caption",
79189
+ "color": "accentPrimary",
79190
+ "maxLines": 1,
79191
+ "collapseWhenEmpty": true
79192
+ }
79193
+ }
79166
79194
  }
79167
79195
  ]
79168
79196
  }
@@ -81541,7 +81569,9 @@ const SCREENS = runtimeSetting({
81541
81569
  "integrity": "",
81542
81570
  "contentType": "application/javascript+ts-bundle",
81543
81571
  "declaredCapabilities": [
81544
- "wix_api"
81572
+ "wix_api",
81573
+ "iap",
81574
+ "app_info"
81545
81575
  ],
81546
81576
  "minBinaryVersion": "0.1.0",
81547
81577
  "bundleVersion": "0.0.1"
@@ -81715,6 +81745,20 @@ const SCREENS = runtimeSetting({
81715
81745
  "collapseWhenEmpty": true
81716
81746
  }
81717
81747
  }
81748
+ },
81749
+ {
81750
+ "id": "",
81751
+ "body": {
81752
+ "package": "himalaya",
81753
+ "type": "Text",
81754
+ "props": {
81755
+ "text": "$item.priceLine",
81756
+ "style": "footnote",
81757
+ "color": "accentPrimary",
81758
+ "maxLines": 1,
81759
+ "collapseWhenEmpty": true
81760
+ }
81761
+ }
81718
81762
  }
81719
81763
  ]
81720
81764
  }
@@ -81947,7 +81991,9 @@ const SCREENS = runtimeSetting({
81947
81991
  "integrity": "",
81948
81992
  "contentType": "application/javascript+ts-bundle",
81949
81993
  "declaredCapabilities": [
81950
- "wix_api"
81994
+ "wix_api",
81995
+ "iap",
81996
+ "app_info"
81951
81997
  ],
81952
81998
  "minBinaryVersion": "1.2.0",
81953
81999
  "bundleVersion": "0.0.1"
@@ -82036,6 +82082,62 @@ const SCREENS = runtimeSetting({
82036
82082
  }
82037
82083
  }
82038
82084
  },
82085
+ {
82086
+ "id": "himi_video.price",
82087
+ "body": {
82088
+ "package": "himalaya",
82089
+ "type": "Text",
82090
+ "props": {
82091
+ "text": "$service:himiBrandedVideo.priceText",
82092
+ "style": "title2",
82093
+ "color": "accentPrimary",
82094
+ "collapseWhenEmpty": true
82095
+ }
82096
+ }
82097
+ },
82098
+ {
82099
+ "id": "himi_video.price_note",
82100
+ "body": {
82101
+ "package": "himalaya",
82102
+ "type": "Text",
82103
+ "props": {
82104
+ "text": "$service:himiBrandedVideo.priceNote",
82105
+ "style": "caption",
82106
+ "color": "textSecondary",
82107
+ "collapseWhenEmpty": true
82108
+ }
82109
+ }
82110
+ },
82111
+ {
82112
+ "id": "himi_video.purchase_note",
82113
+ "body": {
82114
+ "package": "himalaya",
82115
+ "type": "Text",
82116
+ "props": {
82117
+ "text": "$service:himiBrandedVideo.purchaseNote",
82118
+ "style": "footnote",
82119
+ "color": "textSecondary",
82120
+ "collapseWhenEmpty": true
82121
+ }
82122
+ }
82123
+ },
82124
+ {
82125
+ "id": "himi_video.price_retry",
82126
+ "body": {
82127
+ "package": "himalaya",
82128
+ "type": "Button",
82129
+ "props": {
82130
+ "title": "Try again",
82131
+ "style": "secondary",
82132
+ "action": {
82133
+ "actionId": "tier5.himiBrandedVideo.refreshPrice"
82134
+ }
82135
+ }
82136
+ },
82137
+ "modifiers": {
82138
+ "hiddenPath": "$service:himiBrandedVideo.hidePriceRetry"
82139
+ }
82140
+ },
82039
82141
  {
82040
82142
  "id": "",
82041
82143
  "body": {
@@ -82232,7 +82334,9 @@ const SCREENS = runtimeSetting({
82232
82334
  "integrity": "",
82233
82335
  "contentType": "application/javascript+ts-bundle",
82234
82336
  "declaredCapabilities": [
82235
- "wix_api"
82337
+ "wix_api",
82338
+ "iap",
82339
+ "app_info"
82236
82340
  ],
82237
82341
  "minBinaryVersion": "1.2.0",
82238
82342
  "bundleVersion": "0.0.1"
@@ -1,5 +1,11 @@
1
+ // App Store prices come from the shared IAP catalog layer (@himalaya/iap): a video with a one-time Sale
2
+ // price, in a channel that does not require a subscription, is priced as `wixVideo` under its id. Prices
3
+ // only: buying a video stays off (VIDEOS_APPLE_SALES_ENABLED), so the screen says so instead of a Buy.
1
4
  import { descriptionBody } from "../_shared/rich-body.js";
2
5
  import { defineService } from "@himalaya/state";
6
+ import { iapPriceFields, wixVideoItem, type IapCatalogItem } from "@himalaya/iap";
7
+ import { createIapCommerce } from "@himalaya/iap-checkout";
8
+ import { iapPriceCopy, resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
3
9
  import { LOADING, loaded, loadFailed, needsSignIn, type LoadFlags } from "../_shared/branded-load-state.js";
4
10
  import { copyParam, stashCopy } from "../_shared/himi-copy.js";
5
11
  import {
@@ -7,12 +13,14 @@ import {
7
13
  } from "@himalaya/wix-video";
8
14
  import type { MediaItem } from "@himalaya/core";
9
15
 
10
- interface VideoRow { id: string; channelId: string; title: string; subtitle: string; meta: string; coverUrl: string; hasCover: boolean; noCover: boolean }
16
+ interface VideoRow { id: string; channelId: string; title: string; subtitle: string; meta: string; coverUrl: string; hasCover: boolean; noCover: boolean; priceLine: string }
11
17
  interface S extends LoadFlags {
12
18
  id: string; channelId: string;
13
19
  title: string; description: string; videos: VideoRow[];
14
20
  items: MediaItem[]; hidePlayer: boolean; hideNotFound: boolean; noEntity: boolean; hideRefresh: boolean; emptyText: string; message: string;
15
21
  playbackState: "loading" | "ready" | "processing" | "denied" | "unavailable" | "error";
22
+ /** This run's App Store price of the video, its loading/unavailable label, or "". */
23
+ priceText: string; priceNote: string; purchaseNote: string; hidePriceRetry: boolean;
16
24
  }
17
25
  const COPY = { prefix: "video", empty: "No videos in this channel yet.", error: "Videos couldn't be loaded.", signIn: "Sign in to watch videos.", sample: () => [] };
18
26
  function input(key: string): string {
@@ -20,21 +28,56 @@ function input(key: string): string {
20
28
  return typeof value === "string" ? value : "";
21
29
  }
22
30
  let generation = 0;
23
- const initial: S = { ...LOADING, id: "", channelId: "", title: "", description: "", videos: [], items: [], hidePlayer: true, hideNotFound: true, noEntity: false, hideRefresh: false, emptyText: "", message: "", playbackState: "loading" };
31
+ const initial: S = { ...LOADING, id: "", channelId: "", title: "", description: "", videos: [], items: [], hidePlayer: true, hideNotFound: true, noEntity: false, hideRefresh: false, emptyText: "", message: "", playbackState: "loading",
32
+ priceText: "", priceNote: "", purchaseNote: "", hidePriceRetry: true };
33
+
34
+ // ── App Store prices ─────
35
+ // One writer: `load` keeps what it read in `shown` and the priced items here; every repaint goes
36
+ // through the synchronous `publish`, which reads the latest content and prices together.
37
+ const commerce = createIapCommerce({ onChange: () => { void svc.publish(); } });
38
+ const pricing = commerce.pricing;
39
+ let shown: S = { ...initial };
40
+ /** The item the detail prices: a locked video sold once. Rows carry their own. */
41
+ let detailItem: IapCatalogItem | undefined;
42
+ let rowItems: Array<IapCatalogItem | undefined> = [];
43
+
44
+ async function priceContent(): Promise<void> {
45
+ const items = [...(detailItem ? [detailItem] : []), ...rowItems.flatMap((item) => item ?? [])];
46
+ if (items.length) await commerce.loadPrices(items);
47
+ void svc.publish();
48
+ }
49
+
50
+ function priced(state: S): S {
51
+ const copy = iapPriceCopy("video");
52
+ const videos = state.videos.map((row, i) => ({ ...row, priceLine: rowItems[i] ? iapPriceFields(pricing.view(rowItems[i]!.reference), copy).priceText : "" }));
53
+ if (!detailItem) return { ...state, videos, priceText: "", priceNote: "", purchaseNote: "", hidePriceRetry: true };
54
+ const view = pricing.view(detailItem.reference);
55
+ if (view.route === "disabled") return { ...state, videos, priceText: "", priceNote: "", purchaseNote: "", hidePriceRetry: true };
56
+ const fields = iapPriceFields(view, copy);
57
+ return { ...state, videos, priceText: fields.priceText, priceNote: fields.priceNote, purchaseNote: fields.actionNote,
58
+ // An incomplete host scope (a failed app-identity read) reads as non-retryable; Retry re-reads it.
59
+ hidePriceRetry: fields.hidePriceRetry && commerce.scopeComplete() };
60
+ }
24
61
  function unavailable(s: S, kind: S["playbackState"], message: string): S {
25
62
  return { ...s, ...loaded("videos", [], { ...COPY, empty: "" }), items: [], hidePlayer: true, hideNotFound: kind !== "unavailable", noEntity: kind === "unavailable", hideRefresh: kind === "unavailable", emptyText: "", message, playbackState: kind };
26
63
  }
27
- const svc = defineService<S, { reset: (s: S) => S; load: (s: S, arg?: unknown) => Promise<S | undefined> }>("himiBrandedVideo", {
64
+ type Actions = { reset: (s: S) => S; publish: (s: S) => S; refreshPrice: (s: S) => Promise<undefined>; start: (s: S) => S; load: (s: S, arg?: unknown) => Promise<undefined> };
65
+ const svc = defineService<S, Actions>("himiBrandedVideo", {
28
66
  state: initial,
29
67
  notHydrated: Object.keys(initial) as (keyof S & string)[],
30
68
  actions: {
31
- reset: () => ({ ...initial }),
69
+ reset: () => { shown = { ...initial }; detailItem = undefined; rowItems = []; return { ...initial }; },
70
+ publish: () => priced(shown),
71
+ start: () => ({ ...priced(shown), _poll: pricing.pollDirective("himiBrandedVideo.refreshPrice") } as S),
72
+ refreshPrice: async () => { await priceContent(); return undefined; },
32
73
  load: async (_s, arg) => {
33
74
  stashCopy(arg);
34
75
  const request = ++generation;
35
76
  // Revoke the previous item immediately while current access is rechecked.
36
77
  await svc.reset();
37
78
  let next: S = { ...initial, id: input("id"), channelId: input("channelId") };
79
+ let nextDetail: IapCatalogItem | undefined;
80
+ let nextRows: Array<IapCatalogItem | undefined> = [];
38
81
  try {
39
82
  const { id, channelId } = next;
40
83
  const channelScreen = input("screenId") === "himi_video_channel";
@@ -45,9 +88,11 @@ const svc = defineService<S, { reset: (s: S) => S; load: (s: S, arg?: unknown) =
45
88
  const untitled = copyParam("video.untitled", "Video");
46
89
  next = { ...next, ...loaded("videos", videos.map(v => {
47
90
  const card = videoCard({ channel, video: v }, untitled);
48
- return { id: v.id, channelId: id, title: card.title, meta: card.duration, coverUrl: card.coverUrl, hasCover: card.hasCover, noCover: card.noCover, subtitle:
91
+ return { id: v.id, channelId: id, title: card.title, meta: card.duration, coverUrl: card.coverUrl, hasCover: card.hasCover, noCover: card.noCover, priceLine: "", subtitle:
49
92
  v.content?.status !== "READY" ? copyParam("video.processingText", "Video is processing.") : "" };
50
93
  }), COPY), playbackState: "ready" };
94
+ // A video the viewer can already watch (bought, rented, or through the channel) is not priced.
95
+ nextRows = next.videos.map((row) => { const v = videos.find((video) => video.id === row.id); return v && !canWatchVideo(channel, v) ? wixVideoItem(channel, v) : undefined; });
51
96
  } else {
52
97
  const data = await getVideo(channelId, id);
53
98
  const channel = data?.channel;
@@ -59,7 +104,10 @@ const svc = defineService<S, { reset: (s: S) => S; load: (s: S, arg?: unknown) =
59
104
  if (video.content?.status !== "READY") next = isVideoProcessing(video) ?
60
105
  unavailable(next, "processing", copyParam("video.processingText", "Video is processing. Try again later.")) :
61
106
  unavailable(next, "unavailable", copyParam("video.unavailableText", "This video is unavailable."));
62
- else if (!canWatchVideo(channel, video)) next = unavailable(next, "denied", copyParam("video.deniedText", "Your current membership or purchase does not allow this video."));
107
+ else if (!canWatchVideo(channel, video)) {
108
+ next = unavailable(next, "denied", copyParam("video.deniedText", "Your current membership or purchase does not allow this video."));
109
+ nextDetail = wixVideoItem(channel, video);
110
+ }
63
111
  else {
64
112
  let item: MediaItem | undefined;
65
113
  if (video.source === "YOUTUBE") {
@@ -81,10 +129,17 @@ const svc = defineService<S, { reset: (s: S) => S; load: (s: S, arg?: unknown) =
81
129
  denied ? unavailable(next, "denied", copyParam("video.deniedText", "Your current membership or purchase does not allow this video.")) :
82
130
  { ...next, ...loadFailed("videos", error, COPY), playbackState: "error", items: [], hidePlayer: true };
83
131
  }
84
- if (request === generation) return next;
132
+ if (request !== generation) return undefined;
133
+ shown = next;
134
+ detailItem = nextDetail;
135
+ rowItems = nextRows;
136
+ void svc.publish();
137
+ void priceContent();
85
138
  return undefined;
86
139
  },
87
140
  },
88
141
  });
142
+ resetUnhydratedKeysOnOlderRuntime(svc, initial, Object.keys(initial) as (keyof S & string)[]);
143
+ void svc.start();
89
144
  svc.load();
90
145
  export { svc };
@@ -8,11 +8,19 @@
8
8
  // channels taking turns). A failed query is an error with Retry (or Sign in), an
9
9
  // empty one a real empty state; sample channels appear only in the host's explicit
10
10
  // sample preview (_shared/branded-load-state.ts).
11
- import { defineService } from "../_shared/collection-service.js";
11
+ //
12
+ // App Store prices come from the shared IAP catalog layer (@himalaya/iap): a carousel video with a
13
+ // one-time Sale price is priced as `wixVideo`. An app that sells nothing through the App Store reads
14
+ // every video as `disabled` and shows no price line. The list sells nothing.
15
+ import { defineService as register } from "@himalaya/state";
16
+ import { decorateCollection } from "../_shared/collection-service.js";
12
17
  import { LOADING, loaded, loadFailed, type LoadCopy, type LoadFlags } from "../_shared/branded-load-state.js";
13
18
  import { copyParam } from "../_shared/himi-copy.js";
19
+ import { iapPriceCopy, resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
14
20
  import { videoCaptionOf, withCaption, type CaptionFlags, type VideoCaption } from "../_shared/video-caption.js";
15
- import { channelCoverUrl, latestVideos, listChannels, videoCard, type WixVideoCard, type WixVideoChannel } from "@himalaya/wix-video";
21
+ import { iapPriceFields, wixVideoItem, type IapCatalogItem } from "@himalaya/iap";
22
+ import { createIapCommerce } from "@himalaya/iap-checkout";
23
+ import { canWatchVideo, channelCoverUrl, latestVideos, listChannels, videoCard, type WixVideoCard, type WixVideoChannel, type WixVideoInChannel } from "@himalaya/wix-video";
16
24
 
17
25
  interface Row { id: string; title: string; subtitle: string; coverUrl: string; hasCover: boolean; noCover: boolean }
18
26
  interface S extends LoadFlags {
@@ -21,7 +29,7 @@ interface S extends LoadFlags {
21
29
  noChannels: boolean;
22
30
  /** The carousel section: up to CAROUSEL_SIZE ready videos across the channels, each
23
31
  * stamped with the caption placement the descriptor asked for (`videoCaption`). */
24
- latestVideos: (WixVideoCard & CaptionFlags)[]; noLatestVideos: boolean;
32
+ latestVideos: (WixVideoCard & CaptionFlags & { priceLine: string })[]; noLatestVideos: boolean;
25
33
  isMock: boolean; notMock: boolean; /** #2849 — hiddenWhen() hides on TRUE, so these are inverted: the empty-state banner
26
34
  * shows only when the site really has none, and the unavailable one only when the
27
35
  * query failed. A failed query must never render the sentence that claims the
@@ -57,33 +65,81 @@ const COPY: LoadCopy<Row> = {
57
65
  sample: () => MOCK.map((r) => ({ ...r })),
58
66
  };
59
67
 
68
+ const SERVICE = "himiBrandedVideos";
60
69
  let generation = 0;
61
70
  /** The latest placement an appear asked for; a param-less reload keeps it. */
62
71
  let caption: VideoCaption = "overlay";
63
- const svc = defineService<S, { load: (s: S, arg?: unknown) => Promise<S | undefined> }>("himiBrandedVideos", {
64
- state: { ...LOADING, channels: [], emptyText: "", noChannels: true, latestVideos: [], noLatestVideos: true },
65
- actions: {
66
- load: async (s, arg) => {
67
- const request = ++generation;
68
- const asked = (arg as { videoCaption?: unknown } | undefined)?.videoCaption;
69
- if (asked !== undefined) caption = videoCaptionOf(asked);
70
- try {
71
- const entities = await listChannels({ videosPerChannel: PER_CHANNEL });
72
- const untitled = copyParam("videos.untitledVideo", "Video");
73
- const cards = withCaption(latestVideos(entities, CAROUSEL_SIZE).map((entry) => videoCard(entry, untitled)), caption);
74
- if (request === generation) {
75
- const rows = loaded("channels", entities.map(norm), COPY);
76
- return { ...s, ...rows, noChannels: rows.channels.length === 0, latestVideos: cards, noLatestVideos: cards.length === 0 };
77
- }
78
- } catch (err) {
79
- if (request === generation) {
80
- const rows = loadFailed("channels", err, COPY);
81
- return { ...s, ...rows, noChannels: rows.channels.length === 0, latestVideos: [], noLatestVideos: true };
82
- }
72
+
73
+ // ── Content and App Store prices ─────
74
+ // One writer: `load` keeps the carousel here and every repaint goes through the synchronous
75
+ // `pricingPublish`, which reads the latest content and prices together.
76
+ const commerce = createIapCommerce({ onChange: () => { void svc.pricingPublish(); } });
77
+ const pricing = commerce.pricing;
78
+ const PRICING_NOT_HYDRATED = ["latestVideos"] as const;
79
+ let carousel: Array<{ card: WixVideoCard & CaptionFlags; item?: IapCatalogItem }> = [];
80
+
81
+ function carouselOf(entries: WixVideoInChannel[], untitled: string) {
82
+ const cards = withCaption(entries.map((entry) => videoCard(entry, untitled)), caption);
83
+ // A video the viewer can already watch (bought, rented, or through the channel) is not priced.
84
+ return cards.map((card, i) => {
85
+ const { channel, video } = entries[i];
86
+ return { card, item: canWatchVideo(channel, video) ? undefined : wixVideoItem(channel, video) };
87
+ });
88
+ }
89
+
90
+ async function priceCarousel(): Promise<void> {
91
+ const items = carousel.flatMap((entry) => entry.item ?? []);
92
+ if (items.length) await commerce.loadPrices(items);
93
+ void svc.pricingPublish();
94
+ }
95
+
96
+ function pricedState(state: S): S {
97
+ const copy = iapPriceCopy("videos");
98
+ const cards = carousel.map(({ card, item }) => ({ ...card, priceLine: item ? iapPriceFields(pricing.view(item.reference), copy).priceText : "" }));
99
+ return { ...state, latestVideos: cards, noLatestVideos: cards.length === 0 };
100
+ }
101
+
102
+ type Actions = {
103
+ load: (s: S, arg?: unknown) => Promise<S | undefined>;
104
+ pricingPublish: (s: S) => S;
105
+ pricingStart: (s: S) => S;
106
+ pricingRefresh: (s: S) => Promise<undefined>;
107
+ };
108
+
109
+ const actions: Actions = {
110
+ load: async (s, arg) => {
111
+ const request = ++generation;
112
+ const asked = (arg as { videoCaption?: unknown } | undefined)?.videoCaption;
113
+ if (asked !== undefined) caption = videoCaptionOf(asked);
114
+ try {
115
+ const entities = await listChannels({ videosPerChannel: PER_CHANNEL });
116
+ const untitled = copyParam("videos.untitledVideo", "Video");
117
+ if (request === generation) {
118
+ carousel = carouselOf(latestVideos(entities, CAROUSEL_SIZE), untitled);
119
+ void priceCarousel();
120
+ const rows = loaded("channels", entities.map(norm), COPY);
121
+ return pricedState({ ...s, ...rows, noChannels: rows.channels.length === 0 });
83
122
  }
84
- return undefined;
85
- },
123
+ } catch (err) {
124
+ if (request === generation) {
125
+ carousel = [];
126
+ const rows = loadFailed("channels", err, COPY);
127
+ return pricedState({ ...s, ...rows, noChannels: rows.channels.length === 0 });
128
+ }
129
+ }
130
+ return undefined;
86
131
  },
87
- });
132
+ pricingPublish: pricedState,
133
+ pricingStart: (s) => ({ ...pricedState(s), _poll: pricing.pollDirective(`${SERVICE}.pricingRefresh`) }),
134
+ pricingRefresh: async () => { await priceCarousel(); return undefined; },
135
+ };
136
+
137
+ const INITIAL: S = { ...LOADING, channels: [], emptyText: "", noChannels: true, latestVideos: [], noLatestVideos: true } as S;
138
+ // The collection decorator's own `defineService` drops `notHydrated`, so the decorated definition is
139
+ // registered directly: this run's prices must never be restored from a cold-boot snapshot.
140
+ const decorated = decorateCollection(SERVICE, { state: INITIAL, actions } as unknown as Parameters<typeof decorateCollection>[1]);
141
+ const svc = register<S, Actions>(SERVICE, { ...decorated, notHydrated: [...PRICING_NOT_HYDRATED] } as unknown as Parameters<typeof register<S, Actions>>[1]);
142
+ resetUnhydratedKeysOnOlderRuntime(svc, decorated.state as unknown as S, PRICING_NOT_HYDRATED);
143
+ void svc.pricingStart();
88
144
  svc.load();
89
145
  export { svc };