@flamingo-stack/openframe-frontend-core 0.0.721 → 0.0.723
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/assets/index.cjs +0 -6
- package/dist/assets/index.d.ts +0 -1
- package/dist/assets/index.d.ts.map +1 -1
- package/dist/assets/index.js +1 -5
- package/dist/{chunk-I2254QN6.cjs → chunk-2MC4DRSC.cjs} +3 -3
- package/dist/{chunk-UXU7Y5BH.js → chunk-2VOPQJNR.js} +3 -3
- package/dist/{chunk-QYS4YVEP.cjs → chunk-3RIKO2O2.cjs} +31 -31
- package/dist/{chunk-KFOAV77K.js → chunk-5D2K7TFT.js} +1 -1
- package/dist/{chunk-GGBYZKMS.cjs → chunk-6QTGLBJH.cjs} +5 -5
- package/dist/{chunk-5QXE5HMV.js → chunk-7JXCLUU7.js} +3 -3
- package/dist/{chunk-LAXKEBDD.js → chunk-A7JPMNHF.js} +7 -7
- package/dist/{chunk-7Z5FMQYR.cjs → chunk-ABSWD4PQ.cjs} +14 -14
- package/dist/{chunk-742BGXNX.js → chunk-EWED6ISQ.js} +1 -1
- package/dist/{chunk-S63QBBT7.js → chunk-F6JAGSKV.js} +5 -5
- package/dist/{chunk-QLTXT562.cjs → chunk-FDWYBJYD.cjs} +6 -6
- package/dist/{chunk-2P2M27EW.js → chunk-G6YNPBWV.js} +2219 -2202
- package/dist/{chunk-QQCFPMAA.js → chunk-GRDJR2Q7.js} +1 -1
- package/dist/{chunk-O5EHOJZD.cjs → chunk-GYUFKNOQ.cjs} +7 -7
- package/dist/{chunk-OKE5BIQH.cjs → chunk-HP4UUBJC.cjs} +8 -8
- package/dist/{chunk-HXB7SEYG.js → chunk-IIBTYCEN.js} +7 -16
- package/dist/{chunk-GCK75D7F.cjs → chunk-JENNI7XU.cjs} +56 -31
- package/dist/{chunk-ZLUFEWZZ.js → chunk-JL7AHSFE.js} +6 -6
- package/dist/{chunk-JDBZ4ZHU.js → chunk-KS3YLYVL.js} +2 -2
- package/dist/{chunk-6AJIOW3S.cjs → chunk-KSADTFNZ.cjs} +37 -37
- package/dist/{chunk-D2DH6VZL.cjs → chunk-KSZRALRL.cjs} +3 -3
- package/dist/{chunk-RJMWJAJO.cjs → chunk-KY4WQYYG.cjs} +19 -19
- package/dist/{chunk-IYVFSLWJ.js → chunk-KZQO5EJQ.js} +4 -0
- package/dist/{chunk-IMTY3GRR.cjs → chunk-LV6UJD2K.cjs} +2 -2
- package/dist/{chunk-UZTOI6FJ.cjs → chunk-MWLVYYRX.cjs} +619 -602
- package/dist/{chunk-44Q6MV7C.js → chunk-N4VCU6XP.js} +1 -1
- package/dist/{chunk-F7ORKKEF.cjs → chunk-NESUUHPV.cjs} +83 -83
- package/dist/{chunk-BYVVE7HP.cjs → chunk-NFABAOMD.cjs} +34 -36
- package/dist/{chunk-3R32ZNA2.cjs → chunk-OHC2MPUO.cjs} +35 -35
- package/dist/{chunk-RCI3PALX.js → chunk-OVG6MV3X.js} +6 -6
- package/dist/{chunk-HJMSNOBT.cjs → chunk-OWRD2GZU.cjs} +258 -267
- package/dist/{chunk-VFICDEYJ.cjs → chunk-P7MDECYI.cjs} +41 -41
- package/dist/{chunk-NPSJE7J3.js → chunk-PKS2QQJU.js} +29 -4
- package/dist/{chunk-57UTI7ZH.js → chunk-QBXEF7D5.js} +4 -4
- package/dist/{chunk-74N742ZG.js → chunk-SBCJFJ6Q.js} +4 -4
- package/dist/{chunk-XXO52RZR.cjs → chunk-TDSLKKX7.cjs} +18 -18
- package/dist/{chunk-F5BPGWGT.cjs → chunk-TKFTWM3V.cjs} +5 -1
- package/dist/{chunk-GYAXV3MZ.js → chunk-UYMTAFHG.js} +4 -6
- package/dist/{chunk-XE2K53GH.cjs → chunk-VGXAZEQP.cjs} +17 -17
- package/dist/{chunk-3LY7V3RF.js → chunk-VIFVSFYY.js} +2 -2
- package/dist/{chunk-LODMNSQY.cjs → chunk-WSMOTZRJ.cjs} +11 -11
- package/dist/{chunk-CO3FOZIH.js → chunk-WVA7SCLY.js} +1 -1
- package/dist/{chunk-ZZEKXGFS.js → chunk-WY4SLTMZ.js} +2 -2
- package/dist/{chunk-NJ5TBTJG.js → chunk-XTG7W75B.js} +5 -5
- package/dist/{chunk-NMPVK53I.cjs → chunk-Y5H3M7OM.cjs} +23 -23
- package/dist/{chunk-E6SMMMKV.js → chunk-YGIUWQEN.js} +9 -9
- package/dist/{chunk-RQZQHLNS.cjs → chunk-YQOJOW2Z.cjs} +66 -66
- package/dist/{chunk-LGKMR7NZ.js → chunk-YUSA7IPD.js} +3 -3
- package/dist/{cloud-tenants-HZHKRUIW.cjs → cloud-tenants-3TVX7U23.cjs} +10 -10
- package/dist/{cloud-tenants-Q4TCJS5Y.js → cloud-tenants-N6KV2T7X.js} +9 -9
- package/dist/components/case-studies/index.cjs +23 -23
- package/dist/components/case-studies/index.js +10 -10
- package/dist/components/chat/index.cjs +7 -7
- package/dist/components/chat/index.js +6 -6
- package/dist/components/contact/index.cjs +11 -11
- package/dist/components/contact/index.js +10 -10
- package/dist/components/docs/index.cjs +14 -14
- package/dist/components/docs/index.js +13 -13
- package/dist/components/embeds/index.cjs +11 -11
- package/dist/components/embeds/index.js +10 -10
- package/dist/components/faq/index.cjs +11 -11
- package/dist/components/faq/index.js +10 -10
- package/dist/components/features/index.cjs +10 -10
- package/dist/components/features/index.js +9 -9
- package/dist/components/features/remote-session/remote-access-request-card.d.ts.map +1 -1
- package/dist/components/help-center-pages/download-apps-page.d.ts +19 -6
- package/dist/components/help-center-pages/download-apps-page.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +170 -151
- package/dist/components/help-center-pages/index.d.ts +1 -1
- package/dist/components/help-center-pages/index.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.js +88 -69
- package/dist/components/help-center-pages/trust-center-page.d.ts +34 -1
- package/dist/components/help-center-pages/trust-center-page.d.ts.map +1 -1
- package/dist/components/help-center-pages/trust-center-sections.d.ts +1 -1
- package/dist/components/help-center-pages/trust-center-sections.d.ts.map +1 -1
- package/dist/components/index.cjs +66 -64
- package/dist/components/index.js +23 -21
- package/dist/components/meeting-scheduler/index.cjs +47 -47
- package/dist/components/meeting-scheduler/index.js +9 -9
- package/dist/components/navigation/index.cjs +13 -13
- package/dist/components/navigation/index.js +12 -12
- package/dist/components/onboarding-guides/index.cjs +13 -13
- package/dist/components/onboarding-guides/index.js +12 -12
- package/dist/components/product-demo/index.cjs +8 -8
- package/dist/components/product-demo/index.js +8 -8
- package/dist/components/related-content/index.cjs +5 -5
- package/dist/components/related-content/index.js +4 -4
- package/dist/components/tickets/index.cjs +14 -14
- package/dist/components/tickets/index.js +13 -13
- package/dist/components/ui/data-attribution.d.ts +3 -1
- package/dist/components/ui/data-attribution.d.ts.map +1 -1
- package/dist/components/ui/download-button.d.ts +19 -0
- package/dist/components/ui/download-button.d.ts.map +1 -0
- package/dist/components/ui/index.cjs +12 -10
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +11 -9
- package/dist/components/ui/mobile-app-qr.d.ts +13 -15
- package/dist/components/ui/mobile-app-qr.d.ts.map +1 -1
- package/dist/{devices-NOUGVTES.js → devices-275UZZUS.js} +9 -9
- package/dist/{devices-6XHUFIC6.cjs → devices-MAALZ7DX.cjs} +11 -11
- package/dist/index.cjs +19 -19
- package/dist/index.js +23 -23
- package/dist/{knowledge-2YTUXQ2Z.js → knowledge-THXC2KC3.js} +9 -9
- package/dist/{knowledge-ANEKPMO2.cjs → knowledge-YMMULPPK.cjs} +11 -11
- package/dist/{logs-WVQIONUT.js → logs-GM2XBHYT.js} +9 -9
- package/dist/{logs-U3IB3HUM.cjs → logs-IBUKQOHS.cjs} +18 -18
- package/dist/{policies-UFRN2EQF.cjs → policies-J5GJXD6G.cjs} +14 -14
- package/dist/{policies-66EGDW2H.js → policies-NKMRK6RR.js} +9 -9
- package/dist/{remote-session-6WEUJZDC.js → remote-session-WCLR47RN.js} +4 -4
- package/dist/{remote-session-A75X5NPC.cjs → remote-session-Z2YCUPHU.cjs} +6 -6
- package/dist/{software-update-3ZD3MXF3.js → software-update-EKKMKDWI.js} +9 -9
- package/dist/{software-update-2MHKL76D.cjs → software-update-JHC4T4SE.cjs} +14 -14
- package/dist/{tickets-board-IFWMIZ35.cjs → tickets-board-R5OQSEEC.cjs} +8 -8
- package/dist/{tickets-board-MYXG3V72.js → tickets-board-U3RF3AO4.js} +3 -3
- package/dist/types/downloads.d.ts +16 -0
- package/dist/types/downloads.d.ts.map +1 -1
- package/dist/types/index.cjs +27 -2
- package/dist/types/index.js +25 -3
- package/dist/types/trust-center.d.ts +19 -2
- package/dist/types/trust-center.d.ts.map +1 -1
- package/dist/utils/format.d.ts +11 -0
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/index.cjs +4 -9
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +4 -7
- package/dist/utils/mobile-app.cjs +0 -9
- package/dist/utils/mobile-app.d.ts +8 -25
- package/dist/utils/mobile-app.d.ts.map +1 -1
- package/dist/utils/mobile-app.js +1 -7
- package/package.json +1 -1
- package/src/assets/index.ts +0 -1
- package/src/components/features/remote-session/remote-access-request-card.tsx +6 -6
- package/src/components/help-center-pages/download-apps-page.tsx +53 -29
- package/src/components/help-center-pages/index.ts +1 -1
- package/src/components/help-center-pages/trust-center-page.tsx +50 -29
- package/src/components/help-center-pages/trust-center-sections.tsx +2 -2
- package/src/components/ui/data-attribution.tsx +19 -6
- package/src/components/ui/download-button.tsx +34 -0
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/mobile-app-qr.tsx +17 -22
- package/src/types/downloads.ts +17 -0
- package/src/types/trust-center.ts +52 -4
- package/src/utils/format.ts +15 -0
- package/src/utils/index.ts +1 -1
- package/src/utils/mobile-app.ts +8 -31
- package/dist/assets/mobile-app-qr.d.ts +0 -17
- package/dist/assets/mobile-app-qr.d.ts.map +0 -1
- package/src/assets/mobile-app-qr.ts +0 -19
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile-app.d.ts","sourceRoot":"","sources":["../../src/utils/mobile-app.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,uEAAuE;AACvE,eAAO,MAAM,kBAAkB,cAAc,CAAC;AAE9C
|
|
1
|
+
{"version":3,"file":"mobile-app.d.ts","sourceRoot":"","sources":["../../src/utils/mobile-app.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,uEAAuE;AACvE,eAAO,MAAM,kBAAkB,cAAc,CAAC;AAE9C;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,cAAc,EACrB,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACpC,cAAc,CAAC,EAAE,MAAM,GACtB,MAAM,GAAG,IAAI,CAKf"}
|
package/dist/utils/mobile-app.js
CHANGED
|
@@ -24,12 +24,6 @@ function detectVisitorOs(userAgent, hints = {}) {
|
|
|
24
24
|
|
|
25
25
|
// src/utils/mobile-app.ts
|
|
26
26
|
var DOWNLOAD_PAGE_PATH = "/download";
|
|
27
|
-
var DOWNLOAD_PAGE_STORE_PARAM = "store";
|
|
28
|
-
var MOBILE_APP_INSTALL_URL = `https://www.flamingo.run${DOWNLOAD_PAGE_PATH}?${DOWNLOAD_PAGE_STORE_PARAM}=1`;
|
|
29
|
-
var MOBILE_APP_INSTALL_HOST_PATH = MOBILE_APP_INSTALL_URL.replace(/^https:\/\/(www\.)?/, "").replace(
|
|
30
|
-
/\?.*$/,
|
|
31
|
-
""
|
|
32
|
-
);
|
|
33
27
|
function resolveMobileStoreUrl(links, userAgent, maxTouchPoints) {
|
|
34
28
|
const os = detectVisitorOs(userAgent, { maxTouchPoints });
|
|
35
29
|
if (os === "ios") return links.appStoreUrl;
|
|
@@ -37,4 +31,4 @@ function resolveMobileStoreUrl(links, userAgent, maxTouchPoints) {
|
|
|
37
31
|
return null;
|
|
38
32
|
}
|
|
39
33
|
|
|
40
|
-
export { DOWNLOAD_PAGE_PATH,
|
|
34
|
+
export { DOWNLOAD_PAGE_PATH, resolveMobileStoreUrl };
|
package/package.json
CHANGED
package/src/assets/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { formatUrlForDisplay } from '../../../utils/format';
|
|
3
4
|
import { ExternalLinkIcon, InfoCircleIcon } from '../../icons-v2-generated';
|
|
4
5
|
import { Button } from '../../ui/button/button';
|
|
5
6
|
import { RemoteSessionOrgLogo } from './remote-session-parts';
|
|
@@ -21,11 +22,6 @@ export interface RemoteAccessRequestCardProps {
|
|
|
21
22
|
onOpenSite?: (url: string) => void;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
/** `https://www.techflow.com/` -> `www.techflow.com`. */
|
|
25
|
-
function displaySiteUrl(url: string): string {
|
|
26
|
-
return url.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
25
|
/** The end user's consent dialog for a remote access request: who is asking, what they get, Decline / Allow Access. */
|
|
30
26
|
export function RemoteAccessRequestCard({
|
|
31
27
|
party,
|
|
@@ -47,7 +43,11 @@ export function RemoteAccessRequestCard({
|
|
|
47
43
|
<RemoteSessionOrgLogo name={organizationName} logoUrl={party.organizationLogoUrl} sizePx={64} />
|
|
48
44
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
49
45
|
<p className="w-full truncate text-ods-text-primary text-h3">{organizationName}</p>
|
|
50
|
-
{siteUrl &&
|
|
46
|
+
{siteUrl && (
|
|
47
|
+
<p className="w-full truncate text-ods-text-secondary text-h4">
|
|
48
|
+
{formatUrlForDisplay(siteUrl, { keepWww: true })}
|
|
49
|
+
</p>
|
|
50
|
+
)}
|
|
51
51
|
</div>
|
|
52
52
|
{openableSite && onOpenSite && (
|
|
53
53
|
<Button variant="outline" size="icon" aria-label="Open website" onClick={() => onOpenSite(openableSite)}>
|
|
@@ -8,8 +8,14 @@
|
|
|
8
8
|
* when a visitor looks for another system). In a card the installer is THE
|
|
9
9
|
* action (the other architecture is a quieter button) and each package
|
|
10
10
|
* manager is one line under it, with its copy button.
|
|
11
|
-
* - The mobile app: the store badges, and the install QR code beside them.
|
|
12
|
-
*
|
|
11
|
+
* - The mobile app: the store badges, and the install QR code beside them. The
|
|
12
|
+
* code and the address it encodes are the server's (`mobile.install`): the
|
|
13
|
+
* website's install link, which the server answers with a redirect to the
|
|
14
|
+
* phone's store. This page redirects nobody, and with no install link from
|
|
15
|
+
* the server it draws no code.
|
|
16
|
+
* - `web`: the way in for someone who installs nothing (the website's trial
|
|
17
|
+
* signup). The host names the words and the button; the page draws the card,
|
|
18
|
+
* in the frame of every other card on it.
|
|
13
19
|
*
|
|
14
20
|
* DATA: all of it is the server's answer (`DownloadsPublic`), read by
|
|
15
21
|
* `useDownloads` against `endpoint` (default `DOWNLOADS_API_PATH`; embedders
|
|
@@ -26,7 +32,7 @@
|
|
|
26
32
|
* The chrome is the canonical `PageShell` + frozen `PageLayout`.
|
|
27
33
|
*/
|
|
28
34
|
|
|
29
|
-
import {
|
|
35
|
+
import type { ComponentType, ReactNode } from 'react';
|
|
30
36
|
import { useRouter } from '../../embed-shims/next-navigation';
|
|
31
37
|
import { useCopyToClipboard } from '../../hooks/use-copy-to-clipboard';
|
|
32
38
|
import { useDownloads } from '../../hooks/use-downloads';
|
|
@@ -44,7 +50,7 @@ import {
|
|
|
44
50
|
type MobileAppLinks,
|
|
45
51
|
} from '../../types/downloads';
|
|
46
52
|
import { cn } from '../../utils/cn';
|
|
47
|
-
import {
|
|
53
|
+
import { formatUrlForDisplay } from '../../utils/format';
|
|
48
54
|
import { DESKTOP_OSES, type DesktopOs } from '../../utils/visitor-os';
|
|
49
55
|
import { CommandBox } from '../features/command-box';
|
|
50
56
|
import { AppleLogoIcon } from '../icons-v2-generated/brand-logos/apple-logo-icon';
|
|
@@ -78,8 +84,16 @@ export interface DownloadAppsPageProps {
|
|
|
78
84
|
openStoresInNewTab?: boolean;
|
|
79
85
|
/** Called when a visitor downloads an installer or copies an install command (the host's analytics). */
|
|
80
86
|
onDownloadAction?: (event: DownloadActionEvent) => void;
|
|
81
|
-
/** The
|
|
82
|
-
|
|
87
|
+
/** The closing card, under the mobile app: using OpenFrame without installing anything. Default: none. */
|
|
88
|
+
web?: DownloadAppsWebOption;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The "no install" card: the host's words and its own button (the page holds no signup link). */
|
|
92
|
+
export interface DownloadAppsWebOption {
|
|
93
|
+
title: string;
|
|
94
|
+
description: string;
|
|
95
|
+
/** The host's button (the website's trial signup). */
|
|
96
|
+
action: ReactNode;
|
|
83
97
|
}
|
|
84
98
|
|
|
85
99
|
const SECTIONS_CLASS = 'flex flex-col gap-[var(--spacing-system-xlf)]';
|
|
@@ -206,6 +220,24 @@ function SystemCard({
|
|
|
206
220
|
);
|
|
207
221
|
}
|
|
208
222
|
|
|
223
|
+
/** The closing card: one line of what the web offers, and the host's button at the far end. */
|
|
224
|
+
function WebCard({ web }: { web: DownloadAppsWebOption }) {
|
|
225
|
+
return (
|
|
226
|
+
<section
|
|
227
|
+
aria-labelledby="download-web"
|
|
228
|
+
className={cn(CARD_CLASS, 'content-md:flex-row content-md:items-center content-md:justify-between')}
|
|
229
|
+
>
|
|
230
|
+
<div className="flex flex-col gap-[var(--spacing-system-xsf)]">
|
|
231
|
+
<h2 id="download-web" className="m-0 text-ods-text-primary text-h3">
|
|
232
|
+
{web.title}
|
|
233
|
+
</h2>
|
|
234
|
+
<p className="m-0 text-ods-text-secondary text-h4">{web.description}</p>
|
|
235
|
+
</div>
|
|
236
|
+
<div className="shrink-0">{web.action}</div>
|
|
237
|
+
</section>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
209
241
|
/** A section's cards while the server's answer loads: the same grid and card frame, a button's box in each. */
|
|
210
242
|
function CardsSkeleton({ label }: { label: string }) {
|
|
211
243
|
return (
|
|
@@ -248,7 +280,7 @@ export function DownloadAppsPage({
|
|
|
248
280
|
showDesktop = true,
|
|
249
281
|
openStoresInNewTab = true,
|
|
250
282
|
onDownloadAction,
|
|
251
|
-
|
|
283
|
+
web,
|
|
252
284
|
}: DownloadAppsPageProps) {
|
|
253
285
|
const { data, isLoading, error, reload } = useDownloads({ endpoint, initialData });
|
|
254
286
|
// The page adapts to the server's answer: a section exists only for what it names.
|
|
@@ -258,15 +290,6 @@ export function DownloadAppsPage({
|
|
|
258
290
|
// Until the answer lands, a host that offers the desktop app is titled for it.
|
|
259
291
|
const offersDesktop = showDesktop && (!data || systems.length > 0);
|
|
260
292
|
|
|
261
|
-
useEffect(() => {
|
|
262
|
-
// The install QR code arrives with this parameter: a phone or a tablet goes
|
|
263
|
-
// straight on to its store. `replace`, so Back returns to whatever sent the
|
|
264
|
-
// visitor here. Anyone else who opens the page on a phone reads it.
|
|
265
|
-
if (!mobile || !new URLSearchParams(window.location.search).has(DOWNLOAD_PAGE_STORE_PARAM)) return;
|
|
266
|
-
const storeUrl = resolveMobileStoreUrl(mobile, navigator.userAgent, navigator.maxTouchPoints);
|
|
267
|
-
if (storeUrl) window.location.replace(storeUrl);
|
|
268
|
-
}, [mobile]);
|
|
269
|
-
|
|
270
293
|
let body: ReactNode;
|
|
271
294
|
if (error && !data) {
|
|
272
295
|
body = <LoadError message="Could not load the downloads" onRetry={reload} />;
|
|
@@ -298,7 +321,7 @@ export function DownloadAppsPage({
|
|
|
298
321
|
|
|
299
322
|
{mobile && hasMobile && (
|
|
300
323
|
<Section id="download-mobile" title="Mobile app">
|
|
301
|
-
<div className={
|
|
324
|
+
<div className={cardsGridClass(mobile.install ? 2 : 1)}>
|
|
302
325
|
<div className={CARD_CLASS}>
|
|
303
326
|
<CardHeader title={storeCardTitle(mobile)} />
|
|
304
327
|
<p className="m-0 text-ods-text-secondary text-h4">Get alerts and respond to tickets on the go.</p>
|
|
@@ -308,18 +331,19 @@ export function DownloadAppsPage({
|
|
|
308
331
|
openInNewTab={openStoresInNewTab}
|
|
309
332
|
/>
|
|
310
333
|
</div>
|
|
311
|
-
|
|
312
|
-
<div
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
334
|
+
{mobile.install && (
|
|
335
|
+
<div
|
|
336
|
+
className={cn(CARD_CLASS, 'content-md:flex-row content-md:items-center content-md:justify-between')}
|
|
337
|
+
>
|
|
338
|
+
<div className="flex flex-col gap-[var(--spacing-system-sf)]">
|
|
339
|
+
<CardHeader title="Scan with your phone" />
|
|
340
|
+
<p className="m-0 text-ods-text-secondary text-h4">
|
|
341
|
+
Point your camera at the code, or open {formatUrlForDisplay(mobile.install.url)} on your phone.
|
|
342
|
+
</p>
|
|
343
|
+
</div>
|
|
344
|
+
<MobileAppQr install={mobile.install} className="h-[144px] w-[144px] shrink-0" />
|
|
321
345
|
</div>
|
|
322
|
-
|
|
346
|
+
)}
|
|
323
347
|
</div>
|
|
324
348
|
</Section>
|
|
325
349
|
)}
|
|
@@ -336,7 +360,7 @@ export function DownloadAppsPage({
|
|
|
336
360
|
>
|
|
337
361
|
<div className={SECTIONS_CLASS}>
|
|
338
362
|
{body}
|
|
339
|
-
{
|
|
363
|
+
{web && <WebCard web={web} />}
|
|
340
364
|
</div>
|
|
341
365
|
</DownloadAppsChrome>
|
|
342
366
|
);
|
|
@@ -22,7 +22,7 @@ export { ProductReleasesListPage, type ProductReleasesListPageProps } from './pr
|
|
|
22
22
|
export { DeliveryPage, type DeliveryPageProps } from './delivery-page';
|
|
23
23
|
export { OnboardingGuidesCatalogPage, type OnboardingGuidesCatalogPageProps } from './onboarding-guides-catalog-page';
|
|
24
24
|
export { TrustCenterPage, TrustCenterPageSkeleton, type TrustCenterPageProps } from './trust-center-page';
|
|
25
|
-
export { DownloadAppsPage, type DownloadAppsPageProps } from './download-apps-page';
|
|
25
|
+
export { DownloadAppsPage, type DownloadAppsPageProps, type DownloadAppsWebOption } from './download-apps-page';
|
|
26
26
|
|
|
27
27
|
// Existing full-pages re-exported for a single Help Center import site.
|
|
28
28
|
export { FaqDocumentPage, type FaqDocumentPageProps } from '../faq';
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* ONE page, not tabs (2026 trust-center practice: buyers skim and Ctrl-F, and a
|
|
7
7
|
* questionnaire needs a deep link per section): hero with the monitoring status
|
|
8
|
-
* and the
|
|
8
|
+
* (with the page's PDF as a quiet link at its end) and the Request access action, then anchored sections in reading order
|
|
9
9
|
* (`TRUST_CENTER_SECTIONS`: AI & data use → compliance → controls → documents →
|
|
10
10
|
* subprocessors → FAQ → questions) with a sticky section rail on desktop
|
|
11
11
|
* (`StickySectionNav` + `useScrollSpy`, the vendor-page / DocViewer pattern).
|
|
@@ -34,11 +34,12 @@ import { useSelfFetch } from '../../hooks/use-self-fetch';
|
|
|
34
34
|
import {
|
|
35
35
|
TRUST_CENTER_API_PATH,
|
|
36
36
|
TRUST_CENTER_CACHE_SECONDS,
|
|
37
|
-
TRUST_CENTER_SECTIONS,
|
|
38
37
|
TRUST_CENTER_TAGLINE,
|
|
39
38
|
TRUST_CENTER_TITLE,
|
|
40
|
-
|
|
39
|
+
trustCenterMonitoringStatus,
|
|
41
40
|
trustCenterDocumentUrl,
|
|
41
|
+
trustCenterPdfUrl,
|
|
42
|
+
visibleTrustCenterSections,
|
|
42
43
|
type TrustCenterControlsPage,
|
|
43
44
|
type TrustCenterPublic,
|
|
44
45
|
type TrustCenterSectionId,
|
|
@@ -50,6 +51,7 @@ import { PageShell } from '../layout/article-detail-layout';
|
|
|
50
51
|
import { PageLayout } from '../layout/page-layout';
|
|
51
52
|
import { StickySectionNav } from '../navigation/sticky-section-nav';
|
|
52
53
|
import { DataAttribution } from '../ui/data-attribution';
|
|
54
|
+
import { DownloadButton } from '../ui/download-button';
|
|
53
55
|
import { EntityImage } from '../ui/entity-image';
|
|
54
56
|
import { LoadError } from '../ui/error-state';
|
|
55
57
|
import { StatusIndicator } from '../ui/status-indicator';
|
|
@@ -85,6 +87,13 @@ export interface TrustCenterPageProps {
|
|
|
85
87
|
subtitle?: string;
|
|
86
88
|
/** The FAQ section's "ask the assistant" card (`FaqSection`'s `ask`): the host states its topic. Absent: no card. */
|
|
87
89
|
ask?: FaqSectionProps['ask'];
|
|
90
|
+
/**
|
|
91
|
+
* Host content shown after the trust sections and BEFORE the FAQ (the hub's
|
|
92
|
+
* own "why we build in the open" sections). The FAQ is the last thing on a
|
|
93
|
+
* page, so with this the page draws: trust sections, this content at the
|
|
94
|
+
* page's full width, then the FAQ. Absent: the FAQ closes the sections column.
|
|
95
|
+
*/
|
|
96
|
+
beforeFaq?: ReactNode;
|
|
88
97
|
}
|
|
89
98
|
|
|
90
99
|
/** One section's page content. Its `h2` is the `TRUST_CENTER_SECTIONS` label — never a literal here. */
|
|
@@ -93,20 +102,6 @@ interface TrustSectionView {
|
|
|
93
102
|
render: (data: TrustCenterPublic) => ReactNode;
|
|
94
103
|
}
|
|
95
104
|
|
|
96
|
-
/** Sections that have content, in page order. */
|
|
97
|
-
function visibleSections(data: TrustCenterPublic): Array<(typeof TRUST_CENTER_SECTIONS)[number]> {
|
|
98
|
-
const hasContent: Record<TrustCenterSectionId, boolean> = {
|
|
99
|
-
ai: data.aiPractices.length > 0,
|
|
100
|
-
compliance: data.frameworks.length > 0,
|
|
101
|
-
controls: data.controlDomains.length > 0,
|
|
102
|
-
documents: data.documents.length > 0,
|
|
103
|
-
subprocessors: data.subprocessors.length > 0,
|
|
104
|
-
faq: data.faqs.length > 0,
|
|
105
|
-
contact: true,
|
|
106
|
-
};
|
|
107
|
-
return TRUST_CENTER_SECTIONS.filter(section => hasContent[section.id]);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
105
|
/**
|
|
111
106
|
* The Controls section's first answer, from the page's own server data: the
|
|
112
107
|
* first category's controls with no query — exactly what the controls endpoint
|
|
@@ -136,12 +131,9 @@ function monitoringStatus(
|
|
|
136
131
|
hydrated: boolean,
|
|
137
132
|
nowMs: number,
|
|
138
133
|
): { status: 'success' | 'pending' | 'missing'; label: string } {
|
|
139
|
-
if (!data.connected) return { status: 'missing', label: 'Live control monitoring is not enabled yet' };
|
|
140
134
|
// Before mount the clock is the server's, so the text claims nothing either way.
|
|
141
|
-
if (!hydrated) return { status: 'missing', label: 'Checking monitoring status' };
|
|
142
|
-
return
|
|
143
|
-
? { status: 'success', label: 'Controls continuously monitored' }
|
|
144
|
-
: { status: 'pending', label: 'Monitoring paused' };
|
|
135
|
+
if (data.connected && !hydrated) return { status: 'missing', label: 'Checking monitoring status' };
|
|
136
|
+
return trustCenterMonitoringStatus(data, nowMs);
|
|
145
137
|
}
|
|
146
138
|
|
|
147
139
|
export function TrustCenterPage({
|
|
@@ -152,6 +144,7 @@ export function TrustCenterPage({
|
|
|
152
144
|
title = TRUST_CENTER_TITLE,
|
|
153
145
|
subtitle = TRUST_CENTER_TAGLINE,
|
|
154
146
|
ask,
|
|
147
|
+
beforeFaq,
|
|
155
148
|
}: TrustCenterPageProps) {
|
|
156
149
|
const { data, isLoading, error, reload } = useSelfFetch<TrustCenterPublic>(endpoint, {
|
|
157
150
|
initialData,
|
|
@@ -180,7 +173,7 @@ export function TrustCenterPage({
|
|
|
180
173
|
documentTitle: null,
|
|
181
174
|
});
|
|
182
175
|
|
|
183
|
-
const sections = useMemo(() => (data ?
|
|
176
|
+
const sections = useMemo(() => (data ? visibleTrustCenterSections(data) : []), [data]);
|
|
184
177
|
// Anchors. The scroll spy lights the rail from whatever scrolls the sections
|
|
185
178
|
// (the window, or a host shell's `<main overflow-y-auto>` — OpenFrame's
|
|
186
179
|
// `AppLayout`) and keeps the URL's hash on the section in view (`syncHash`;
|
|
@@ -205,9 +198,15 @@ export function TrustCenterPage({
|
|
|
205
198
|
const actions = hasGatedDocuments
|
|
206
199
|
? [{ label: 'Request access', variant: 'accent' as const, onClick: () => openRequest(null) }]
|
|
207
200
|
: undefined;
|
|
201
|
+
// The page as a file (the hub's `pdf` route beside `endpoint`). An occasional
|
|
202
|
+
// utility on a read-only page, so it is NOT a header action (a button there,
|
|
203
|
+
// and a fixed bar on a phone, are for the page's task: Request access). It is
|
|
204
|
+
// the shared small `DownloadButton` at the end of the status line under the
|
|
205
|
+
// title, at every width.
|
|
206
|
+
const download = <DownloadButton label="Download PDF" href={trustCenterPdfUrl(endpoint)} />;
|
|
208
207
|
|
|
209
208
|
// Section id → its content. Titles come from `TRUST_CENTER_SECTIONS`; which
|
|
210
|
-
// sections show comes from `
|
|
209
|
+
// sections show comes from `visibleTrustCenterSections` — this map only says what each renders.
|
|
211
210
|
const views: Record<TrustCenterSectionId, TrustSectionView> = {
|
|
212
211
|
ai: {
|
|
213
212
|
lead: TRUST_SECTION_LEADS.ai,
|
|
@@ -239,6 +238,11 @@ export function TrustCenterPage({
|
|
|
239
238
|
contact: { render: () => <ContactSection onContact={() => openRequest(null)} /> },
|
|
240
239
|
};
|
|
241
240
|
|
|
241
|
+
// With host content before it, the FAQ leaves the sections column and closes the page (below).
|
|
242
|
+
const faqSection = sections.find(section => section.id === 'faq');
|
|
243
|
+
const faqLast = Boolean(beforeFaq) && Boolean(faqSection);
|
|
244
|
+
const columnSections = faqLast ? sections.filter(section => section.id !== 'faq') : sections;
|
|
245
|
+
|
|
242
246
|
let body: ReactNode;
|
|
243
247
|
if (error && !data) {
|
|
244
248
|
body = <LoadError message="Could not load the trust center" onRetry={reload} />;
|
|
@@ -264,13 +268,16 @@ export function TrustCenterPage({
|
|
|
264
268
|
}
|
|
265
269
|
source={data.dataSource.name}
|
|
266
270
|
lastUpdated={data.syncedAt}
|
|
271
|
+
action={download}
|
|
267
272
|
/>
|
|
268
|
-
) :
|
|
273
|
+
) : (
|
|
274
|
+
download
|
|
275
|
+
)}
|
|
269
276
|
</div>
|
|
270
277
|
|
|
271
278
|
<div className={TRUST_BODY_GRID_CLASS}>
|
|
272
279
|
<div className={TRUST_SECTIONS_COLUMN_CLASS}>
|
|
273
|
-
{
|
|
280
|
+
{columnSections.map(section => {
|
|
274
281
|
const view = views[section.id];
|
|
275
282
|
return (
|
|
276
283
|
<TrustSection key={section.id} id={section.id} title={section.label} lead={view.lead}>
|
|
@@ -302,9 +309,23 @@ export function TrustCenterPage({
|
|
|
302
309
|
}
|
|
303
310
|
|
|
304
311
|
return (
|
|
305
|
-
|
|
306
|
-
{
|
|
307
|
-
|
|
312
|
+
<>
|
|
313
|
+
<TrustCenterChrome shell={shell} title={title} subtitle={subtitle} backButton={backButton} actions={actions}>
|
|
314
|
+
{body}
|
|
315
|
+
</TrustCenterChrome>
|
|
316
|
+
{beforeFaq}
|
|
317
|
+
{faqLast && faqSection && data ? (
|
|
318
|
+
// The page's own content box at its FULL width (no section-rail column): a closing FAQ takes the
|
|
319
|
+
// same space the FAQ block takes at the foot of every other page.
|
|
320
|
+
<div className={shell ? 'bg-ods-bg' : undefined}>
|
|
321
|
+
<div className={shell ? 'page-shell-content mx-auto max-w-[1920px]' : 'page-shell-content'}>
|
|
322
|
+
<TrustSection id={faqSection.id} title={faqSection.label} lead={views.faq.lead}>
|
|
323
|
+
{views.faq.render(data)}
|
|
324
|
+
</TrustSection>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
) : null}
|
|
328
|
+
</>
|
|
308
329
|
);
|
|
309
330
|
}
|
|
310
331
|
|
|
@@ -72,7 +72,7 @@ const BODY_TEXT = 'text-h6 text-ods-text-secondary';
|
|
|
72
72
|
* beside the section rail.
|
|
73
73
|
*/
|
|
74
74
|
export const TRUST_STATUS_ROW_CLASS =
|
|
75
|
-
'flex flex-col gap-[var(--spacing-system-
|
|
75
|
+
'flex flex-col gap-[var(--spacing-system-xsf)] content-md:flex-row content-md:items-center content-md:justify-between';
|
|
76
76
|
export const TRUST_BODY_GRID_CLASS =
|
|
77
77
|
'grid grid-cols-1 gap-[var(--spacing-system-xl)] content-lg:grid-cols-[minmax(0,1fr)_12rem]';
|
|
78
78
|
export const TRUST_SECTIONS_COLUMN_CLASS = 'flex min-w-0 flex-col gap-[var(--spacing-system-xxl)]';
|
|
@@ -675,7 +675,7 @@ export function TrustCenterSkeleton() {
|
|
|
675
675
|
<TextSkeleton.Body className="w-44" />
|
|
676
676
|
</div>
|
|
677
677
|
{/* `DataAttribution`'s own layout: two lines below `sm`, one row above it. */}
|
|
678
|
-
<div className="flex shrink-0 flex-col gap-[var(--spacing-system-
|
|
678
|
+
<div className="flex shrink-0 flex-col gap-[var(--spacing-system-xsf)] content-sm:flex-row content-sm:items-center content-sm:gap-[var(--spacing-system-sf)]">
|
|
679
679
|
<div className="flex items-center gap-[var(--spacing-system-xsf)]">
|
|
680
680
|
<UnifiedSkeleton className="size-4 rounded" />
|
|
681
681
|
<TextSkeleton.Body className="w-36" />
|
|
@@ -8,6 +8,20 @@ export interface DataAttributionProps {
|
|
|
8
8
|
source: string;
|
|
9
9
|
/** ISO instant of the last successful sync, or null when it never ran. */
|
|
10
10
|
lastUpdated: string | null;
|
|
11
|
+
/** One control at the end of the line: a way to take this data away (a `DownloadButton` for a file of it). */
|
|
12
|
+
action?: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const LINE_CLASS =
|
|
16
|
+
'flex shrink-0 flex-col gap-[var(--spacing-system-xsf)] text-ods-text-primary text-h6 content-sm:flex-row content-sm:items-center content-sm:gap-[var(--spacing-system-sf)]';
|
|
17
|
+
const ITEM_CLASS = 'flex items-center gap-[var(--spacing-system-xsf)]';
|
|
18
|
+
|
|
19
|
+
function Separator() {
|
|
20
|
+
return (
|
|
21
|
+
<span className="hidden text-ods-text-secondary content-sm:inline" aria-hidden="true">
|
|
22
|
+
·
|
|
23
|
+
</span>
|
|
24
|
+
);
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
/**
|
|
@@ -16,17 +30,16 @@ export interface DataAttributionProps {
|
|
|
16
30
|
* UTC (`formatDate`), so a server-rendered page and its hydrated copy print the
|
|
17
31
|
* same text.
|
|
18
32
|
*/
|
|
19
|
-
export function DataAttribution({ icon, source, lastUpdated }: DataAttributionProps) {
|
|
33
|
+
export function DataAttribution({ icon, source, lastUpdated, action }: DataAttributionProps) {
|
|
20
34
|
return (
|
|
21
|
-
<div className=
|
|
22
|
-
<div className=
|
|
35
|
+
<div className={LINE_CLASS}>
|
|
36
|
+
<div className={ITEM_CLASS}>
|
|
23
37
|
{icon}
|
|
24
38
|
<span>Data synced from {source}</span>
|
|
25
39
|
</div>
|
|
26
|
-
<
|
|
27
|
-
·
|
|
28
|
-
</span>
|
|
40
|
+
<Separator />
|
|
29
41
|
<span className="text-ods-text-secondary">Last updated: {lastUpdated ? formatDate(lastUpdated) : 'Never'}</span>
|
|
42
|
+
{action ? <div className="flex content-sm:pl-[var(--spacing-system-xs)]">{action}</div> : null}
|
|
30
43
|
</div>
|
|
31
44
|
);
|
|
32
45
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Download01Icon } from '../icons-v2-generated/interface/download-01-icon';
|
|
2
|
+
import { Button, type ButtonProps } from './button/button';
|
|
3
|
+
|
|
4
|
+
export type DownloadButtonProps = Omit<ButtonProps, 'leftIcon' | 'children'> & {
|
|
5
|
+
/** What is downloaded, as the button says it ("Download PDF"). */
|
|
6
|
+
label: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* THE download button: a regular small `Button` in the quiet `link` variant
|
|
11
|
+
* (no surface, secondary text like the facts it sits beside, the accent on
|
|
12
|
+
* hover) with the download glyph, wherever a surface hands the reader a file of
|
|
13
|
+
* what it shows (the Trust Center's PDF, the margin report's PDF). One look and
|
|
14
|
+
* one size, so a download reads the same on every page and never louder than
|
|
15
|
+
* the page's own action.
|
|
16
|
+
*
|
|
17
|
+
* With `href` it is a link that opens in a new tab (an attachment downloads
|
|
18
|
+
* there without leaving the page). With `onClick` the host fetches the file
|
|
19
|
+
* itself and passes `loading` while it does.
|
|
20
|
+
*/
|
|
21
|
+
export function DownloadButton({ label, variant = 'link', size = 'small', href, ...props }: DownloadButtonProps) {
|
|
22
|
+
return (
|
|
23
|
+
<Button
|
|
24
|
+
variant={variant}
|
|
25
|
+
size={size}
|
|
26
|
+
leftIcon={<Download01Icon aria-hidden="true" />}
|
|
27
|
+
href={href}
|
|
28
|
+
{...(href ? { openInNewTab: true, prefetch: false } : {})}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
{label}
|
|
32
|
+
</Button>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -108,6 +108,7 @@ export * from './device-card';
|
|
|
108
108
|
export * from './device-card-compact';
|
|
109
109
|
export * from './entity-image';
|
|
110
110
|
export * from './data-attribution';
|
|
111
|
+
export * from './download-button';
|
|
111
112
|
export * from './compliance-badge';
|
|
112
113
|
export * from './feature-card';
|
|
113
114
|
export * from './feature-list';
|
|
@@ -1,40 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { MobileAppInstallLink } from '../../types/downloads';
|
|
2
2
|
import { cn } from '../../utils/cn';
|
|
3
|
-
import { MOBILE_APP_INSTALL_URL } from '../../utils/mobile-app';
|
|
4
3
|
|
|
5
4
|
export interface MobileAppQrProps {
|
|
5
|
+
/** The server's install link: the address and the artwork that encodes it. */
|
|
6
|
+
install: MobileAppInstallLink;
|
|
6
7
|
/** Sizes the code; the caller owns its footprint. */
|
|
7
8
|
className?: string;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
* The install QR code, drawn inline
|
|
12
|
-
*
|
|
12
|
+
* The install QR code, drawn inline: the modules are the ODS accent colour and
|
|
13
|
+
* the background is transparent, so the code sits directly on whatever surface
|
|
14
|
+
* it is placed on. The artwork and the address it encodes are the server's
|
|
15
|
+
* (`MobileAppInstallLink`): this component holds neither.
|
|
13
16
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* margin
|
|
18
|
-
*
|
|
19
|
-
* That polarity holds only under the dark theme, where `text-ods-bg` is the dark
|
|
20
|
-
* value and `bg-ods-bg-inverted` the light one; both flip under `.theme-light`.
|
|
21
|
-
* Do not render this inside a light-theme scope: an inverted code is silently
|
|
22
|
-
* unscannable rather than visibly broken.
|
|
23
|
-
*
|
|
24
|
-
* The artwork is the asset `MOBILE_APP_QR_PATH` (`assets/mobile-app-qr`), which
|
|
25
|
-
* encodes {@link MOBILE_APP_INSTALL_URL}; the regeneration note lives there.
|
|
17
|
+
* On a dark surface that is a light-on-dark code. Phone cameras read both
|
|
18
|
+
* polarities; what they need is contrast between the accent and the surface, and
|
|
19
|
+
* the quiet zone, which the server bakes into the viewBox as a transparent
|
|
20
|
+
* margin. Place it on a plain surface (a card, the page), never over an image
|
|
21
|
+
* or a surface close to the accent in brightness.
|
|
26
22
|
*/
|
|
27
|
-
export function MobileAppQr({ className }: MobileAppQrProps) {
|
|
23
|
+
export function MobileAppQr({ install, className }: MobileAppQrProps) {
|
|
28
24
|
return (
|
|
29
25
|
<svg
|
|
30
|
-
viewBox={`0 0 ${
|
|
31
|
-
fill="none"
|
|
26
|
+
viewBox={`0 0 ${install.qrViewBoxSize} ${install.qrViewBoxSize}`}
|
|
32
27
|
shapeRendering="crispEdges"
|
|
33
28
|
role="img"
|
|
34
|
-
aria-label={`QR code for ${
|
|
35
|
-
className={cn('h-[120px] w-[120px] text-ods-
|
|
29
|
+
aria-label={`QR code for ${install.url}`}
|
|
30
|
+
className={cn('h-[120px] w-[120px] text-ods-accent', className)}
|
|
36
31
|
>
|
|
37
|
-
<path
|
|
32
|
+
<path fill="currentColor" d={install.qrPath} />
|
|
38
33
|
</svg>
|
|
39
34
|
);
|
|
40
35
|
}
|
package/src/types/downloads.ts
CHANGED
|
@@ -44,10 +44,27 @@ export interface AppDownload {
|
|
|
44
44
|
label: string | null;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* The install link a phone opens to reach its store, and the QR code that
|
|
49
|
+
* encodes it. Both are the server's: the link is an address on the website
|
|
50
|
+
* (answered there with a redirect by device), and the artwork is drawn from it
|
|
51
|
+
* on the server, so the two can never disagree and the lib names no host.
|
|
52
|
+
*/
|
|
53
|
+
export interface MobileAppInstallLink {
|
|
54
|
+
/** The absolute address the code encodes. */
|
|
55
|
+
url: string;
|
|
56
|
+
/** The side of the artwork's square viewBox, quiet zone included. */
|
|
57
|
+
qrViewBoxSize: number;
|
|
58
|
+
/** The dark modules as one SVG path of filled rectangles (`fill` draws them). */
|
|
59
|
+
qrPath: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
/** Where the mobile app is installed from. A store the server names no listing for is null, and is not offered. */
|
|
48
63
|
export interface MobileAppLinks {
|
|
49
64
|
appStoreUrl: string | null;
|
|
50
65
|
googlePlayUrl: string | null;
|
|
66
|
+
/** The install link and its QR code. Absent or null: no code is drawn. */
|
|
67
|
+
install?: MobileAppInstallLink | null;
|
|
51
68
|
}
|
|
52
69
|
|
|
53
70
|
/**
|