summaryrefslogtreecommitdiff
path: root/src/jet/models
diff options
context:
space:
mode:
authorrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
committerrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
commitbce557cc2dc767628bed6aac87301a1be7c5431b (patch)
treeb51a051228d01fe3306cd7626d4a96768aadb944 /src/jet/models
init commit
Diffstat (limited to 'src/jet/models')
-rw-r--r--src/jet/models/error-page.ts15
-rw-r--r--src/jet/models/external-action.ts7
-rw-r--r--src/jet/models/flow-action.ts28
-rw-r--r--src/jet/models/page.ts177
-rw-r--r--src/jet/models/static-message-page.ts33
5 files changed, 260 insertions, 0 deletions
diff --git a/src/jet/models/error-page.ts b/src/jet/models/error-page.ts
new file mode 100644
index 0000000..80bcdf5
--- /dev/null
+++ b/src/jet/models/error-page.ts
@@ -0,0 +1,15 @@
+import { GenericPage } from '@jet-app/app-store/api/models';
+import type { Opt } from '@jet/environment';
+
+export class ErrorPage extends GenericPage {
+ constructor({ error }: { error: Opt<Error> }) {
+ super([]);
+ this.error = error;
+ }
+
+ // Used in our type guards to narrow a `Page` down to a `ErrorPage`
+ pageType: string = 'errorPage';
+
+ // The browser `Error`, used to determine which message to display to the user
+ error: Opt<Error>;
+}
diff --git a/src/jet/models/external-action.ts b/src/jet/models/external-action.ts
new file mode 100644
index 0000000..25dbd14
--- /dev/null
+++ b/src/jet/models/external-action.ts
@@ -0,0 +1,7 @@
+import type { Action, ExternalUrlAction } from '@jet-app/app-store/api/models';
+
+export function isExternalUrlAction(
+ action: Action,
+): action is ExternalUrlAction {
+ return action.$kind === 'ExternalUrlAction';
+}
diff --git a/src/jet/models/flow-action.ts b/src/jet/models/flow-action.ts
new file mode 100644
index 0000000..d5edb40
--- /dev/null
+++ b/src/jet/models/flow-action.ts
@@ -0,0 +1,28 @@
+import type { Intent } from '@jet/environment/dispatching';
+import { FlowAction } from '@jet-app/app-store/api/models';
+
+export const FLOW_ACTION_KIND: FlowAction['$kind'] = 'flowAction';
+
+/**
+ * Creates a FlowAction For a given destination.
+ *
+ * Note: this is only here temporarily as a convenience for the "web" client, to be used
+ * while the upstream `FlowAction` is represented as a class that needs to be constructed,
+ * so those details are abstracted away from our codebase. Once `FlowAction` has been
+ * migrated to a POJO, there should be a factory-function provided that we should leverage
+ * instead
+ *
+ * @param destination Destination of the `FlowAction`
+ */
+export function makeFlowAction(destination: Intent<unknown>): FlowAction {
+ const action = new FlowAction(
+ // This data is only used by the Jet app's `PageRouter` architecture, which is not
+ // relevant for us. We should safely be able to pass an arbitrary value here.
+ 'page',
+ );
+
+ // The important part for the "web" client router: setting the `destination`
+ action.destination = destination;
+
+ return action;
+}
diff --git a/src/jet/models/page.ts b/src/jet/models/page.ts
new file mode 100644
index 0000000..a05e59f
--- /dev/null
+++ b/src/jet/models/page.ts
@@ -0,0 +1,177 @@
+import type {
+ ArticlePage,
+ ChartsHubPage,
+ GenericPage,
+ SearchLandingPage,
+ SearchResultsPage,
+ ShelfBasedProductPage,
+ TopChartsPage,
+ TodayPage,
+ SeeAllPage,
+} from '@jet-app/app-store/api/models';
+import { StaticMessagePage } from '~/jet/models/static-message-page';
+import { isObject } from '~/utils/types';
+import { ErrorPage } from './error-page';
+import type { WebRenderablePage } from 'node_modules/@jet-app/app-store/src/api/models/web-renderable-page';
+
+/**
+ * The union of every type of page that the App Store Onyx app can render
+ */
+export type Page = (
+ | ArticlePage
+ | ChartsHubPage
+ | GenericPage
+ | SearchLandingPage
+ | SearchResultsPage
+ | ShelfBasedProductPage
+ | StaticMessagePage
+ | TopChartsPage
+ | TodayPage
+ | ErrorPage
+) &
+ // TS needs to be told this explicitly, even though all the above implement this
+ WebRenderablePage;
+
+/**
+ * Detects if {@linkcode page} is actually an {@linkcode AppEventDetailPage}
+ */
+export function isAppEventDetailPage(page: Page): page is GenericPage {
+ return (
+ 'shelves' in page &&
+ page.shelves.some(({ contentType }) => contentType === 'appEventDetail')
+ );
+}
+
+/**
+ * Detects if {@linkcode page} is actually an {@linkcode ArticlePage}
+ */
+export function isArticlePage(page: Page): page is ArticlePage {
+ return 'card' in page && 'shelves' in page;
+}
+
+/**
+ * Detects if {@linkcode page} is actually a {@linkcode ChartsHubPage}
+ */
+export function isChartsHubPage(page: Page): page is ChartsHubPage {
+ return 'charts' in page;
+}
+
+/**
+ * Detects if {@linkcode page} is actually a {@linkcode GenericPage}
+ */
+export function isGenericPage(page: Page): page is GenericPage {
+ return 'shelves' in page;
+}
+
+/**
+ * Detects if {@linkcode page} is actually a {@linkcode ShelfBasedProductPage}
+ */
+export function isShelfBasedProductPage(
+ page: Page,
+): page is ShelfBasedProductPage {
+ return 'shelfMapping' in page && !('seeAllType' in page);
+}
+
+/**
+ * Detects if {@linkcode page} is actually a {@linkcode SeeAllPage}
+ */
+export function isSeeAllPage(page: Page): page is SeeAllPage {
+ return 'seeAllType' in page;
+}
+
+/**
+ * Detects if {@linkcode page} is actually a {@linkcode SearchLandingPage}
+ */
+export function isSearchLandingPage(page: Page): page is SearchLandingPage {
+ return 'adIncidents' in page;
+}
+
+/**
+ * Detects if {@linkcode page} is actually a {@linkcode SearchResultsPage}
+ */
+export function isSearchResultsPage(page: Page): page is SearchResultsPage {
+ return 'searchClearAction' in page || 'searchCancelAction' in page;
+}
+
+/**
+ * Detects if {@linkcode page} is actually a {@linkcode TopChartsPage}
+ */
+export function isTopChartsPage(page: Page): page is TopChartsPage {
+ return 'segments' in page && 'categories' in page;
+}
+
+/**
+ * Detects if {@linkcode page} is actually a {@linkcode TodayPage}
+ */
+export function isTodayPage(page: Page): page is TodayPage {
+ return 'titleDetail' in page;
+}
+
+/**
+ * Detects if {@linkcode page} is actually a {@linkcode StaticMessagePage}
+ */
+export function isStaticMessagePage(
+ page: GenericPage,
+): page is StaticMessagePage {
+ return 'pageType' in page && page.pageType === 'staticMessagePage';
+}
+
+export function isErrorPage(page: GenericPage) {
+ return 'pageType' in page && page.pageType === 'errorPage';
+}
+
+/**
+ * Type-guard that determines if the provided {@linkcode page} matches a renderable {@linkcode Page} definition
+ */
+export function isPage(page: unknown): page is Page {
+ if (!isObject(page)) {
+ return false;
+ }
+
+ return [
+ isAppEventDetailPage,
+ isArticlePage,
+ isChartsHubPage,
+ isGenericPage,
+ isShelfBasedProductPage,
+ isSearchLandingPage,
+ isSearchResultsPage,
+ isTopChartsPage,
+ isTodayPage,
+ isErrorPage,
+ isSeeAllPage,
+ ].some((specificPageTypePredicate) =>
+ specificPageTypePredicate(
+ // This type-cast reflects the fact that we don't really know if `page` is really a `Page`,
+ // but that we're going to use the type-guards of our `Page` members to see if `page` looks
+ // like one of them
+ page as Page,
+ ),
+ );
+}
+
+/**
+ * Type-assertion that determines if the provided {@linkcode page} matches a renderable {@linkcode Page} definition
+ */
+export function assertIsPage(page: unknown): asserts page is Page {
+ if (!isPage(page)) {
+ throw new Error(
+ 'The view-model for the dispatched `Intent` does not match a known renderable shape',
+ );
+ }
+}
+
+/**
+ * Detects if {@linkcode page} has the Vision Pro pathname in it's URL.
+ */
+export function hasVisionProUrl(page: GenericPage) {
+ if (!page.canonicalURL) {
+ return false;
+ }
+
+ const url = new URL(page.canonicalURL);
+ return (
+ url.pathname.includes('/vision/apps-and-games') ||
+ url.pathname.includes('/vision/arcade')
+ );
+}
diff --git a/src/jet/models/static-message-page.ts b/src/jet/models/static-message-page.ts
new file mode 100644
index 0000000..91dafb0
--- /dev/null
+++ b/src/jet/models/static-message-page.ts
@@ -0,0 +1,33 @@
+import { GenericPage } from '@jet-app/app-store/api/models';
+
+const contentTypes = [
+ 'win-back',
+ 'carrier',
+ 'invoice',
+ 'contingent-price',
+] as const;
+
+export type ContentType = (typeof contentTypes)[number];
+
+export class StaticMessagePage extends GenericPage {
+ constructor({
+ titleLocKey,
+ contentType,
+ }: {
+ titleLocKey: string;
+ contentType: ContentType;
+ }) {
+ super([]);
+ this.titleLocKey = titleLocKey;
+ this.contentType = contentType;
+ }
+
+ titleLocKey?: string;
+
+ // Used to indicate which type of content the page needs to show, used to pull in the proper
+ // LOC keys when rendering
+ contentType: ContentType;
+
+ // Used in our type guards to narrow a `Page` down to a `StaticMessagePage`
+ pageType: string = 'staticMessagePage';
+}