From 1051281a4c31f9ece33ebc092070dc2c1b5433fc Mon Sep 17 00:00:00 2001 From: dunegxn Date: Thu, 10 Sep 2026 21:29:01 +0530 Subject: [PATCH 1/7] +/live --- app/live/layout.tsx | 38 + app/live/live.css | 1053 +++++++++++++++++++++ app/live/opengraph-image.tsx | 92 ++ app/live/page.tsx | 101 ++ components/global/announcement-banner.tsx | 7 + components/global/nav-bar.tsx | 4 +- components/live/content.ts | 203 ++++ components/live/device-frame.tsx | 18 + components/live/flow-context.tsx | 66 ++ components/live/hero.tsx | 129 +++ components/live/icon-sprite.tsx | 165 ++++ components/live/mock-data.ts | 163 ++++ components/live/phone-tabbar.tsx | 43 + components/live/rich-text.tsx | 15 + components/live/step-auth.tsx | 124 +++ components/live/step-checkout.tsx | 380 ++++++++ components/live/step-install.tsx | 151 +++ components/live/step-orders.tsx | 357 +++++++ components/live/step-section.tsx | 50 + components/live/step-settings.tsx | 270 ++++++ components/live/step-shop.tsx | 110 +++ components/live/step-wallet.tsx | 116 +++ components/live/theme-context.tsx | 50 + public/live/home-hero-flags.png | Bin 0 -> 10879 bytes public/live/home-hero-sunburst.png | Bin 0 -> 20896 bytes 25 files changed, 3703 insertions(+), 2 deletions(-) create mode 100644 app/live/layout.tsx create mode 100644 app/live/live.css create mode 100644 app/live/opengraph-image.tsx create mode 100644 app/live/page.tsx create mode 100644 components/live/content.ts create mode 100644 components/live/device-frame.tsx create mode 100644 components/live/flow-context.tsx create mode 100644 components/live/hero.tsx create mode 100644 components/live/icon-sprite.tsx create mode 100644 components/live/mock-data.ts create mode 100644 components/live/phone-tabbar.tsx create mode 100644 components/live/rich-text.tsx create mode 100644 components/live/step-auth.tsx create mode 100644 components/live/step-checkout.tsx create mode 100644 components/live/step-install.tsx create mode 100644 components/live/step-orders.tsx create mode 100644 components/live/step-section.tsx create mode 100644 components/live/step-settings.tsx create mode 100644 components/live/step-shop.tsx create mode 100644 components/live/step-wallet.tsx create mode 100644 components/live/theme-context.tsx create mode 100644 public/live/home-hero-flags.png create mode 100644 public/live/home-hero-sunburst.png diff --git a/app/live/layout.tsx b/app/live/layout.tsx new file mode 100644 index 0000000..d99df57 --- /dev/null +++ b/app/live/layout.tsx @@ -0,0 +1,38 @@ +import type { Metadata } from "next"; +import { Anybody, Lexend, JetBrains_Mono } from "next/font/google"; +import type { ReactNode } from "react"; + +import "./live.css"; + +const anybody = Anybody({ + subsets: ["latin"], + display: "swap", + variable: "--live-font-anybody", +}); + +const lexend = Lexend({ + subsets: ["latin"], + display: "swap", + variable: "--live-font-lexend", +}); + +const jetbrainsMono = JetBrains_Mono({ + subsets: ["latin"], + weight: ["500", "600"], + display: "swap", + variable: "--live-font-mono", +}); + +export const metadata: Metadata = { + title: "Get Kokio — download & guide", + description: + "Download Kokio for iOS and Android, and see exactly how it works: sign in with a passkey, buy an eSIM, install it, and manage everything from a wallet only you control.", +}; + +export default function LiveLayout({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/app/live/live.css b/app/live/live.css new file mode 100644 index 0000000..3501668 --- /dev/null +++ b/app/live/live.css @@ -0,0 +1,1053 @@ +.kokio-live, +.kokio-live * { + box-sizing: border-box; +} + +.kokio-live { + --chrome-bg: #131519; + --chrome-panel: #1a1d22; + --chrome-ink: #edf1f2; + --chrome-muted: #9aa6ac; + --chrome-faint: #7a858c; + --chrome-border: #2b3036; + --chrome-accent: #64d2ff; + --chrome-accent-fg: #06232b; + --chrome-code-bg: #21252b; + --chrome-danger: #e2846e; + --chrome-danger-soft: #2f201b; + --chrome-danger-border: #4a2e26; + + --app-bg: #000000; + --app-card: #ffd60a; + --app-card-fg: #000000; + --app-muted-fg: #aeaeb2; + --app-inactive: #777777; + --app-text: #ffffff; + --app-primary: #ff9f0a; + --app-primary-fg: #000000; + --app-secondary: #ffd60a; + --app-secondary-fg: #000000; + --app-muted: #46464b; + --app-surface: #1a1a1a; + --app-input-bg: #7676803d; + --app-success: #22c55e; + --app-info: #64d2ff; + --app-warning: #f59e0b; + --app-destructive: #ef4444; + --app-pay-btn: #ffd60a; + --app-golden: #ffaf01; + --app-link: #4a9eff; + --app-handle: rgba(60, 60, 67, 0.25); + --app-handle-arrow: rgba(60, 60, 67, 0.85); + --app-lpa-bg: #ffffff; + --app-highlight: #ffcc00; + --app-foreground2: #aeaeb2; + --app-hero-btn-bg: #ffffff; + --app-hero-btn-fg: #000000; + --app-shop-cta: #ffaf01; + --app-icon: #9ba1a6; + --app-item-bg: #1c1c1e; + --app-gradient-dark: #404040; + --app-wallet-balance-fg: #ffffff; + --app-modal-bg: rgba(60, 60, 60, 0.9); + + color-scheme: dark; + margin: 0; + background: var(--chrome-bg); + color: var(--chrome-ink); + font-family: + var(--live-font-lexend), + system-ui, + -apple-system, + sans-serif; + font-size: 16px; + line-height: 1.55; + -webkit-font-smoothing: antialiased; +} + +.kokio-live[data-theme="light"] { + color-scheme: light; + --chrome-bg: #eef1f3; + --chrome-panel: #ffffff; + --chrome-ink: #1c2b30; + --chrome-muted: #5b6770; + --chrome-faint: #626d76; /* darkened from the artifact's #8b969c — 2.67:1 failed WCAG AA badly on --chrome-bg, this is 4.66:1 (ticket #10) */ + --chrome-border: #d7dee1; + --chrome-accent: #1f6b78; /* darkened from the artifact's #2a8fa0 — 3.34:1 failed WCAG AA where used as text (callout strong), this is 5.39:1 (ticket #10) */ + --chrome-accent-fg: #ffffff; + --chrome-code-bg: #e4e9eb; + --chrome-danger: #c0392b; + --chrome-danger-soft: #fbe9e7; + --chrome-danger-border: #eec2ba; + + --app-bg: #a8d8e8; + --app-card: #ffffff; + --app-card-fg: #1a3d4f; + --app-muted-fg: #7abccc; + --app-inactive: #7abccc; + --app-text: #1a3d4f; + --app-primary: #2a8fa0; + --app-primary-fg: #ffffff; + --app-secondary: #e8614a; + --app-secondary-fg: #ffffff; + --app-muted: #c5e8f2; + --app-surface: #eef8fc; + --app-input-bg: #eef8fc; + --app-success: #22c55e; + --app-info: #64d2ff; + --app-warning: #f59e0b; + --app-destructive: #ef4444; + --app-pay-btn: #e8614a; + --app-golden: #e8614a; + --app-link: #2a8fa0; + --app-handle: rgba(60, 60, 67, 0.25); + --app-handle-arrow: rgba(60, 60, 67, 0.85); + --app-lpa-bg: var(--app-muted); + --app-highlight: #e8614a; + --app-foreground2: #4a8090; + --app-hero-btn-bg: #7abccc; + --app-hero-btn-fg: #ffffff; + --app-shop-cta: #7abccc; + --app-icon: #2a8fa0; + --app-item-bg: #ffffff; + --app-gradient-dark: #ffffff; + --app-wallet-balance-fg: #000000; + --app-modal-bg: rgba(255, 255, 255, 0.95); +} + +.kokio-live a { + color: inherit; +} +.kokio-live :focus-visible { + outline: 2px solid var(--chrome-accent); + outline-offset: 2px; + border-radius: 4px; +} +.kokio-live h1, +.kokio-live h2, +.kokio-live h3 { + font-family: var(--live-font-anybody), sans-serif; + text-wrap: balance; + margin: 0; +} +.kokio-live button { + font-family: inherit; +} +@media (prefers-reduced-motion: reduce) { + .kokio-live * { + animation-duration: 0.001ms !important; + transition-duration: 0.001ms !important; + } +} +.kokio-live [hidden] { + display: none !important; +} +@keyframes kokio-live-spin { + to { + transform: rotate(360deg); + } +} +.kokio-live .a-spinner { + width: 14px; + height: 14px; + border-radius: 50%; + border: 2px solid transparent; + border-top-color: currentColor; + border-right-color: currentColor; + animation: kokio-live-spin 0.7s linear infinite; + flex: none; +} + +.kokio-live .wrap { + max-width: 1080px; + margin: 0 auto; + padding: 0 24px; +} + +.kokio-live .masthead { + padding: 56px 0 32px; + border-bottom: 1px solid var(--chrome-border); +} +.kokio-live .masthead h1 { + font-size: clamp(30px, 5vw, 44px); + font-weight: 800; + letter-spacing: -0.01em; + margin-bottom: 12px; +} +.kokio-live .masthead p { + max-width: 64ch; + color: var(--chrome-muted); + font-size: 15.5px; + margin: 0 0 4px; +} +.kokio-live .masthead .note { + font-size: 12.5px; + color: var(--chrome-faint); + margin-top: 10px; +} +.kokio-live .masthead .note code { + font-family: var(--live-font-mono), monospace; + background: var(--chrome-code-bg); + padding: 1px 5px; + border-radius: 4px; +} + +.kokio-live .controls-outer { + position: sticky; + top: 0; + z-index: 6; + background: color-mix(in srgb, var(--chrome-bg) 90%, transparent); + backdrop-filter: blur(10px); + border-bottom: 1px solid var(--chrome-border); +} +.kokio-live .controls { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 14px 26px; + max-width: 1080px; + margin: 0 auto; + padding: 12px 24px; +} +.kokio-live .control-group { + display: flex; + align-items: center; + gap: 10px; +} +.kokio-live .control-label { + font-size: 10.5px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--chrome-faint); + white-space: nowrap; +} +.kokio-live .segmented { + display: inline-flex; + background: var(--chrome-code-bg); + border-radius: 9px; + padding: 3px; + gap: 2px; +} +.kokio-live .segmented button { + appearance: none; + border: none; + background: transparent; + color: var(--chrome-muted); + font-size: 12.5px; + font-weight: 600; + padding: 6px 12px; + border-radius: 7px; + cursor: pointer; + transition: + background 0.15s, + color 0.15s; +} +.kokio-live .segmented button:hover { + color: var(--chrome-ink); +} +.kokio-live .segmented button.active { + background: var(--chrome-accent); + color: var(--chrome-accent-fg); +} +.kokio-live .rail { + display: flex; + gap: 2px; + overflow-x: auto; + flex: 1; + min-width: 220px; +} +.kokio-live .rail a { + flex: 0 0 auto; + font-size: 12.5px; + font-weight: 600; + color: var(--chrome-muted); + padding: 7px 11px; + border-radius: 999px; + white-space: nowrap; + text-decoration: none; +} +.kokio-live .rail a:hover { + color: var(--chrome-ink); + background: var(--chrome-code-bg); +} + +.kokio-live .steps { + padding: 16px 0 8px; +} +.kokio-live .step { + display: grid; + grid-template-columns: 1fr; + gap: 32px; + padding: 40px 0; + border-bottom: 1px solid var(--chrome-border); + scroll-margin-top: 64px; +} +.kokio-live .step:last-child { + border-bottom: none; +} +@media (min-width: 900px) { + .kokio-live .step { + grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr); + align-items: start; + gap: 48px; + padding: 52px 0; + } + .kokio-live .step[data-alt="1"] .step-text { + order: 2; + } + .kokio-live .step[data-alt="1"] .step-visual { + order: 1; + } +} +.kokio-live .step-index { + font-family: var(--live-font-mono), monospace; + font-size: 12.5px; + color: var(--chrome-accent); + font-weight: 600; + letter-spacing: 0.05em; + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 8px; +} +.kokio-live .step-text h2 { + font-size: clamp(21px, 3vw, 26px); + font-weight: 800; + margin-bottom: 8px; +} +.kokio-live .step-summary { + color: var(--chrome-muted); + font-size: 14.5px; + max-width: 48ch; + margin: 0 0 20px; +} +.kokio-live .step-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 13px; + max-width: 50ch; + counter-reset: step-counter; +} +.kokio-live .step-list li { + display: flex; + gap: 12px; + align-items: baseline; +} +.kokio-live .step-list li::before { + counter-increment: step-counter; + content: counter(step-counter); + font-family: var(--live-font-mono), monospace; + font-size: 11.5px; + font-weight: 700; + color: var(--chrome-accent-fg); + background: var(--chrome-accent); + flex: 0 0 auto; + width: 20px; + height: 20px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + transform: translateY(2px); +} +.kokio-live .step-list li p { + margin: 0; + font-size: 14.5px; +} + +.kokio-live .callout { + margin-top: 20px; + padding: 13px 15px; + border-radius: 12px; + font-size: 13px; + line-height: 1.6; + max-width: 50ch; + background: var(--chrome-code-bg); + border: 1px solid var(--chrome-border); + color: var(--chrome-ink); +} +.kokio-live .callout strong { + color: var(--chrome-accent); +} +.kokio-live .callout--danger { + background: var(--chrome-danger-soft); + border-color: var(--chrome-danger-border); +} +.kokio-live .callout--danger strong { + color: var(--chrome-danger); +} +.kokio-live .soon-pill { + font-family: var(--live-font-mono), monospace; + font-size: 10.5px; + font-weight: 700; + letter-spacing: 0.03em; + color: var(--chrome-accent); + background: var(--chrome-code-bg); + border: 1px solid var(--chrome-border); + padding: 3px 9px; + border-radius: 999px; +} +.kokio-live .try-hint { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 11.5px; + color: var(--chrome-faint); + margin-top: 14px; +} +.kokio-live .try-hint svg { + width: 12px; + height: 12px; + flex: none; +} + +/* ---- device stage ---- */ +.kokio-live .stage { + background: var(--app-bg); + border-radius: 22px; + padding: 22px 16px; + border: 1px solid var(--chrome-border); + overflow-x: auto; + display: flex; + align-items: flex-start; +} +.kokio-live .phone { + width: 100%; + max-width: 330px; + margin: 0 auto; + font-family: var(--live-font-lexend), sans-serif; +} +.kokio-live .src-tag { + font-family: var(--live-font-mono), monospace; + font-size: 10px; + color: var(--chrome-faint); + text-align: center; + margin-bottom: 10px; +} + +.kokio-live footer.page-footer { + padding: 28px 24px 48px; + text-align: center; + color: var(--chrome-faint); + font-size: 12.5px; +} +.kokio-live footer.page-footer .footer-link { + color: var(--chrome-accent); + text-decoration: underline; +} + +/* ================= shared app-styled components ================= */ +.kokio-live .a-card { + background: var(--app-card); + color: var(--app-card-fg); + border-radius: 21px; + padding: 16px; +} +.kokio-live .esim-card { + background: var(--app-card); + color: var(--app-card-fg); + border-radius: 21px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 8px; + border: 1px solid var(--app-muted); +} +.kokio-live .a-statrow { + display: flex; + gap: 14px; + align-items: center; + flex-wrap: wrap; +} +.kokio-live .a-stat { + display: flex; + align-items: center; + gap: 3px; + color: var(--app-card-fg); + font-size: 14px; +} +.kokio-live .a-stat svg { + width: 18px; + height: 18px; + flex: none; +} +.kokio-live .a-stat b { + font-weight: 800; +} +.kokio-live .a-flag { + position: absolute; + top: -10px; + right: 30px; + font-size: 34px; + line-height: 1; + filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.25)); +} +.kokio-live .a-card-wrap { + position: relative; + width: 100%; +} +.kokio-live .a-country { + font-size: 20px; + font-weight: 700; + color: var(--app-card-fg); + padding-right: 90px; +} +.kokio-live .a-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + border: none; + border-radius: 10px; + padding: 10px 0; + font-size: 14px; + font-weight: 600; + cursor: pointer; + width: 100%; + font-family: inherit; +} +.kokio-live .a-btn svg { + width: 15px; + height: 15px; +} +.kokio-live .a-btn-primary { + background: var(--app-primary); + color: var(--app-primary-fg); +} +.kokio-live .a-btn-highlight { + background: var(--app-highlight); + color: #000000; + font-weight: 600; +} +.kokio-live .a-btn-fgborder { + background: transparent; + border: 1px solid var(--app-foreground2); + color: var(--app-foreground2); + font-weight: 400; +} +.kokio-live .a-view-btn { + all: unset; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + gap: 2px; + width: 100%; + padding: 8px; + border-radius: 16px; + margin-top: 8px; + cursor: pointer; + background: var(--app-shop-cta); +} +.kokio-live .a-view-btn b { + color: var(--app-card-fg); + font-size: 14px; +} +.kokio-live .a-view-btn .vb-right { + display: flex; + align-items: center; + gap: 2px; + padding-left: 16px; +} +.kokio-live .a-view-btn .vb-right svg { + width: 20px; + height: 20px; + stroke: var(--app-card-fg); + fill: none; + stroke-width: 1.8; +} +.kokio-live .a-view-btn .vb-right span { + color: var(--app-card-fg); + font-size: 14px; + font-weight: 800; +} +.kokio-live .a-btn-outline { + background: transparent; + border: 1px solid var(--app-muted); + color: var(--app-text); +} +.kokio-live .a-btn[disabled] { + opacity: 0.45; + cursor: not-allowed; +} +.kokio-live .a-pill { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.02em; + text-transform: uppercase; + padding: 3px 8px; + border-radius: 8px; +} +.kokio-live .a-muted { + color: var(--app-inactive); + font-size: 12.5px; +} +.kokio-live .a-sublabel { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--app-inactive); + margin: 14px 0 8px; +} + +.kokio-live .a-order-meta { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 4px 0; +} +.kokio-live .a-chevron { + margin-left: auto; + color: var(--app-muted-fg); + width: 14px; + height: 14px; +} + +.kokio-live .a-detail { + background: var(--app-surface); + border-radius: 12px; + margin: 6px 4px 0; + padding: 14px; + display: flex; + flex-direction: column; +} +.kokio-live .a-summary-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 0; +} +.kokio-live .a-summary-row .lbl { + font-size: 13px; + color: var(--app-inactive); +} +.kokio-live .a-summary-row .val { + font-size: 13px; + font-weight: 600; + color: var(--app-text); + font-variant-numeric: tabular-nums; +} +.kokio-live .a-summary-row .val.mono { + font-family: var(--live-font-mono), monospace; + font-size: 12.5px; + letter-spacing: 0.01em; +} +.kokio-live .a-usage-track { + height: 8px; + border-radius: 4px; + overflow: hidden; + background: var(--app-muted); + margin-top: -2px; + margin-bottom: 6px; +} +.kokio-live .a-usage-fill { + height: 8px; + border-radius: 4px; + transition: + width 0.2s ease, + background 0.2s ease; +} + +.kokio-live .a-copy-row { + display: flex; + align-items: center; + padding: 10px 0; + border-top: 1px solid color-mix(in srgb, var(--app-inactive) 30%, transparent); + cursor: pointer; +} +.kokio-live .a-copy-row .body { + flex: 1; + margin-right: 12px; +} +.kokio-live .a-copy-row .copy-label { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.02em; + text-transform: uppercase; + color: var(--app-inactive); + margin-bottom: 3px; +} +.kokio-live .a-copy-row .copy-value { + font-size: 13px; + color: var(--app-text); + line-height: 1.3; + font-family: var(--live-font-mono), monospace; + word-break: break-all; +} +.kokio-live .a-copy-row svg { + width: 18px; + height: 18px; + flex: none; + color: var(--app-muted-fg); +} +.kokio-live .a-copy-row.copied svg { + color: var(--app-success); +} + +.kokio-live .a-action-row { + display: flex; + gap: 8px; + margin-top: 8px; +} + +/* bottom tab bar */ +.kokio-live .a-tabbar { + display: flex; + justify-content: space-around; + align-items: center; + background: var(--app-card); + border-top: 1px solid var(--app-muted); + padding: 10px 6px 8px; +} +.kokio-live .a-tabbar-item { + all: unset; + display: flex; + flex-direction: column; + align-items: center; + gap: 3px; + cursor: pointer; + color: var(--app-inactive); + flex: 1; +} +.kokio-live .a-tabbar-item svg { + width: 21px; + height: 21px; + stroke: currentColor; + fill: none; + stroke-width: 1.8; +} +.kokio-live .a-tabbar-item span { + font-size: 9.5px; + font-weight: 600; +} +.kokio-live .a-tabbar-item.active { + color: var(--app-primary); +} +.kokio-live .a-tabbar-item.disabled { + opacity: 0.35; + cursor: not-allowed; +} + +/* home hero "shop card" */ +.kokio-live .a-hero { + border-radius: 21px; + overflow: hidden; + background: var(--app-card); + border: 1px solid var(--app-muted); +} +.kokio-live .a-hero-imgwrap { + position: relative; + width: 100%; + aspect-ratio: 378 / 196; + overflow: hidden; + background: var(--app-card); +} +.kokio-live .a-hero-bg { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + opacity: 0.5; +} +.kokio-live .a-hero-flagsimg { + position: relative; + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +.kokio-live .a-hero-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px 16px; +} +.kokio-live .a-hero-title { + font-size: 16px; + font-weight: 700; + color: var(--app-card-fg); +} +.kokio-live .a-hero-sub { + font-size: 10px; + font-weight: 400; + color: var(--app-card-fg); +} +.kokio-live .a-hero-btn { + all: unset; + cursor: pointer; + background: var(--app-hero-btn-bg); + color: var(--app-hero-btn-fg); + font-size: 16px; + font-weight: 500; + padding: 5px 24px; + border-radius: 40px; +} + +/* ---------------- /live's own hero (above the ported guide) ---------------- */ +.kokio-live .live-hero { + padding: 64px 0 40px; + border-bottom: 1px solid var(--chrome-border); +} +.kokio-live .live-hero-inner { + display: flex; + flex-direction: column; + gap: 24px; + align-items: flex-start; +} +/* Kokio mark inline at the start of the "Get Kokio" heading — the whole + * row is a link back to the site's homepage, same as a logo usually is. */ +.kokio-live .hero-title-row { + display: flex; + align-items: center; + gap: 14px; + text-decoration: none; + transition: opacity 0.15s; +} +.kokio-live .hero-title-row:hover { + opacity: 0.8; +} +.kokio-live .hero-title-mark { + width: 40px; + height: 40px; + flex: none; +} +@media (min-width: 640px) { + .kokio-live .hero-title-mark { + width: 52px; + height: 52px; + } +} + +/* iOS / Android download row — the actual App Store and Google Play + * logos (see icon-sprite.tsx), each with a small caption underneath. */ +.kokio-live .hero-icon-row { + display: flex; + align-items: center; + gap: 40px; + margin: 4px 0 2px; +} +.kokio-live .hero-icon-tile { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + text-decoration: none; + flex: none; + transition: opacity 0.15s; +} +.kokio-live .hero-icon-tile:hover { + opacity: 0.75; +} +.kokio-live .hero-icon-box { + width: 128px; + height: 128px; + border-radius: 32px; + display: flex; + align-items: center; + justify-content: center; +} +.kokio-live .hero-icon-box .hero-icon-glyph { + width: 58%; + height: 58%; +} +/* App Store's real icon presentation: the white "A" glyph on its blue + * rounded-square background, not a bare monochrome mark. */ +.kokio-live .hero-icon-box--appstore { + background: linear-gradient(160deg, #3fb6fa 0%, #0a6cff 100%); +} +.kokio-live .hero-icon-box--appstore .hero-icon-glyph { + color: #ffffff; +} +/* Play Store's real icon presentation: the tri-color triangle on its own + * white rounded-square background, same as the App Store tile beside it. */ +.kokio-live .hero-icon-box--playstore { + background: #ffffff; + border: 1px solid var(--chrome-border); +} +.kokio-live .hero-icon-caption { + font-size: 14px; + font-weight: 700; + color: var(--chrome-muted); +} +@media (max-width: 480px) { + .kokio-live .hero-icon-row { + gap: 24px; + } + .kokio-live .hero-icon-box { + width: 96px; + height: 96px; + border-radius: 24px; + } +} +.kokio-live .live-hero h1 { + font-size: clamp(32px, 6vw, 52px); + font-weight: 800; + letter-spacing: -0.01em; + margin-bottom: 14px; +} +.kokio-live .live-hero p { + max-width: 56ch; + color: var(--chrome-muted); + font-size: 17px; + line-height: 1.6; + margin: 0 0 10px; +} +.kokio-live .live-hero p:last-child { + margin-bottom: 0; +} +/* The 56ch cap above is too narrow for this longer sentence — it was + * wrapping into a ragged 3-line block with a short orphan last line. + * Let it use the hero column's own width instead. */ +.kokio-live .live-hero .hero-body { + max-width: none; +} +.kokio-live .guide-intro { + margin-top: 8px; + padding-top: 28px; + border-top: 1px solid var(--chrome-border); + width: 100%; +} +.kokio-live .guide-intro h2 { + font-size: clamp(26px, 4.5vw, 38px); + font-weight: 800; + letter-spacing: -0.01em; + margin-bottom: 8px; +} +.kokio-live .guide-intro p { + max-width: 56ch; + font-size: 15px; + color: var(--chrome-faint); + line-height: 1.5; +} +.kokio-live .store-badges { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 8px; +} +.kokio-live .store-badge { + display: inline-flex; + align-items: center; + gap: 10px; + background: var(--chrome-ink); + color: var(--chrome-bg); + border-radius: 12px; + padding: 10px 18px; + text-decoration: none; + font-weight: 700; + font-size: 14px; + transition: opacity 0.15s; +} +.kokio-live .store-badge:hover { + opacity: 0.85; +} +.kokio-live .store-badge svg { + width: 20px; + height: 20px; + flex: none; +} +.kokio-live .store-badge .lines { + display: flex; + flex-direction: column; + line-height: 1.2; +} +.kokio-live .store-badge .lines small { + font-size: 10.5px; + font-weight: 500; + opacity: 0.75; +} + +.kokio-live .sticky-download-bar { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 20; + display: flex; + gap: 10px; + padding: 10px 16px calc(10px + env(safe-area-inset-bottom)); + background: color-mix(in srgb, var(--chrome-panel) 96%, transparent); + backdrop-filter: blur(10px); + border-top: 1px solid var(--chrome-border); + transform: translateY(100%); + transition: transform 0.2s ease; +} +.kokio-live .sticky-download-bar.visible { + transform: translateY(0); +} +.kokio-live .sticky-download-bar .store-badge { + flex: 1; + justify-content: center; + padding: 10px 12px; +} +@media (min-width: 700px) { + .kokio-live .sticky-download-bar { + display: none; + } +} + +.kokio-live .handoff-divider { + display: flex; + align-items: center; + gap: 16px; + margin: 56px 0 0; + color: var(--chrome-faint); + font-family: var(--live-font-mono), monospace; + font-size: 11px; + letter-spacing: 0.1em; + text-transform: uppercase; +} +.kokio-live .handoff-divider::before, +.kokio-live .handoff-divider::after { + content: ""; + height: 1px; + background: var(--chrome-border); + flex: 1; +} + +/* ---------------- touch targets (ticket #10 / D10) ---------------- + The artifact's segmented controls, tab strips, and icon buttons were + sized for a mouse-driven desktop demo (~26-30px tall). On an actual + touch device that's under the ~44px target Apple/WCAG recommend, so + bump hit areas here on coarse-pointer (touch) devices only — desktop + mouse users still get the artifact's exact visual sizing untouched. */ +@media (pointer: coarse) { + .kokio-live .segmented button { + min-height: 44px; + padding-top: 10px; + padding-bottom: 10px; + } + .kokio-live .rail a { + min-height: 44px; + display: inline-flex; + align-items: center; + } + .kokio-live .a-tabbar-item { + min-height: 44px; + justify-content: center; + } + .kokio-live .a-hero-btn { + min-height: 44px; + padding-top: 10px; + padding-bottom: 10px; + display: inline-flex; + align-items: center; + } + .kokio-live .a-copy-row, + .kokio-live .a-summary-row { + min-height: 44px; + } + .kokio-live input[type="checkbox"] { + min-width: 20px; + min-height: 20px; + } + .kokio-live .store-badge { + min-height: 44px; + } +} diff --git a/app/live/opengraph-image.tsx b/app/live/opengraph-image.tsx new file mode 100644 index 0000000..834a7ca --- /dev/null +++ b/app/live/opengraph-image.tsx @@ -0,0 +1,92 @@ +import fs from "node:fs"; +import path from "node:path"; +import { ImageResponse } from "next/og"; + +import { + LOGO_PATHS, + OG_IMAGE_CONTENT_TYPE, + OG_IMAGE_SIZE, +} from "@/lib/og-image"; + +export const alt = "Get Kokio"; +export const size = OG_IMAGE_SIZE; +export const contentType = OG_IMAGE_CONTENT_TYPE; + +const anybodyBold = fs.readFileSync( + path.join(process.cwd(), "assets/fonts/Anybody-Bold.ttf") +); + +export default function Image() { + return new ImageResponse( + ( +
+ + {LOGO_PATHS.map((p) => ( + + ))} + + +
+ + Get Kokio. + + + No name, no email, no KYC — just a passkey and an eSIM. + +
+ + + kokio.app/live + +
+ ), + { + ...OG_IMAGE_SIZE, + fonts: [ + { + name: "Anybody", + data: anybodyBold, + weight: 700, + style: "normal", + }, + ], + } + ); +} diff --git a/app/live/page.tsx b/app/live/page.tsx new file mode 100644 index 0000000..5ef8c08 --- /dev/null +++ b/app/live/page.tsx @@ -0,0 +1,101 @@ +import { IconSprite } from "@/components/live/icon-sprite"; +import { LiveThemeProvider, ThemeToggle } from "@/components/live/theme-context"; +import { FlowProvider } from "@/components/live/flow-context"; +import { LiveHero, IOS_URL, ANDROID_URL } from "@/components/live/hero"; +import { StepSection } from "@/components/live/step-section"; +import { STEPS, SETTINGS_ABOUT, SETTINGS_CONTACT } from "@/components/live/content"; +import { ManifestoCTA } from "@/components/blog/manifesto-cta"; +import { StepAuthVisual } from "@/components/live/step-auth"; +import { StepShopVisual } from "@/components/live/step-shop"; +import { StepCheckoutVisual } from "@/components/live/step-checkout"; +import { StepOrdersVisual } from "@/components/live/step-orders"; +import { StepInstallVisual } from "@/components/live/step-install"; +import { StepWalletVisual } from "@/components/live/step-wallet"; +import { StepSettingsVisual } from "@/components/live/step-settings"; + +const STEP_VISUALS: Record = { + auth: StepAuthVisual, + shop: StepShopVisual, + checkout: StepCheckoutVisual, + orders: StepOrdersVisual, + install: StepInstallVisual, + wallet: StepWalletVisual, + settings: StepSettingsVisual, +}; + +const STEP_TRY_HINT_ICONS: Partial> = { + auth: "fingerprint", + shop: "cart", + checkout: "check", + orders: "chev-down", + install: "copy", +}; + +const RAIL_LABELS = [ + ["auth", "01 · Start"], + ["shop", "02 · Browse"], + ["checkout", "03 · Checkout"], + ["orders", "04 · Orders"], + ["install", "05 · Install"], + ["wallet", "06 · Wallet"], + ["settings", "07 · Settings"], +] as const; + +export default function LivePage() { + return ( + + + + + +
+
+ + +
+
+ +
+ {STEPS.map((step) => { + const Visual = STEP_VISUALS[step.id]; + return ( + + + + ); + })} +
+ +
+ +
+ + +
+
+ ); +} diff --git a/components/global/announcement-banner.tsx b/components/global/announcement-banner.tsx index 959c8b8..74882a9 100644 --- a/components/global/announcement-banner.tsx +++ b/components/global/announcement-banner.tsx @@ -1,11 +1,18 @@ +"use client"; + import React from "react"; import Image from "next/image"; +import { usePathname } from "next/navigation"; import Twitter from "@/assets/icons/twitter-fill.svg"; import { TWITTER_URL } from "@/config/site"; import { LAUNCH_NOTICE } from "@/lib/site-copy"; const AnnouncementBanner = () => { + + const pathname = usePathname(); + if (pathname?.startsWith("/live")) return null; + return (
diff --git a/components/global/nav-bar.tsx b/components/global/nav-bar.tsx index cb512f9..2c67364 100644 --- a/components/global/nav-bar.tsx +++ b/components/global/nav-bar.tsx @@ -54,9 +54,9 @@ export const Links = () => { const NavBar = () => { // /manifesto renders its own nav row inside the shared translucent panel - // (components/manifesto/manifesto-nav.tsx) instead of this global pill — see #25. + // (components/manifesto/manifesto-nav.tsx) instead of this global pill. const pathname = usePathname(); - if (pathname?.startsWith("/manifesto")) return null; + if (pathname?.startsWith("/manifesto") || pathname?.startsWith("/live")) return null; return (
diff --git a/components/live/content.ts b/components/live/content.ts new file mode 100644 index 0000000..97a95cb --- /dev/null +++ b/components/live/content.ts @@ -0,0 +1,203 @@ +/** + * Voice: manifesto/blog register (plain, confident, privacy-forward). + * Every claim here has been checked against released app. + * + * `**word**` inside strings is bold-parsed by — used to call out + * exact button labels, the same way the artifact does with . + */ + +// Kokio's Telegram community — same link used in Contact Support panel and the hero's "reach out" line, kept as one source so they can't drift. +const TELEGRAM_URL = "https://t.me/+Ru38DI2V69IyY2Y9"; + +export const HERO = { + title: "Get Kokio", + body: "Onboard via biometrics, pick a plan that suits your travel, pay with your go-to option, install at home and activate once you arrive at your destination.", + contactLinkText: "reach out", + contactHref: TELEGRAM_URL, + guideHeading: "Interactive Usage Guide", + guideLead: "Flip the theme, tap and explore, a simulation of Kokio app.", +}; + +export type StepContent = { + id: string; + index: string; + sourceTag: string; + alt: 0 | 1; + title: string; + summary: string; + items: string[]; + callout?: { tone?: "default" | "danger"; strong?: string; text: string }; + tryHint?: string; +}; + +export const STEPS: StepContent[] = [ + { + id: "auth", + index: "Step 01", + sourceTag: "Sign in", + alt: 0, + title: "Onboarding", + summary: + "Using biomterics(passkeys), Kokio never asks for your name, email, or any such personal information that can be used to indetify you irl.", + items: [ + "Open Kokio, Tap **New User** create and sign-in using passkeys.", + "Only use **Existing User** if you've reinstalled Kokio on the same device without deleting that `kokio.app` passkey from device password manager?", + "After the first time, You'll just have to **Log In** continue", + ], + callout: { + strong: "Guard the passkey", + text: "A passkey synced through your platform's password manager can restore your account on a new or reinstalled device, but if the passkey itself is gone(if you delete it), or your manager doesn't sync, there's no way back in. Deleting your account (see in Step 07) is the one action that truly has no recovery, by anyone, ever.", + }, + tryHint: 'Switch to "Sign in" below, then tap a button.', + }, + { + id: "shop", + index: "Step 02", + sourceTag: "Explore", + alt: 1, + title: "Pick a plan that suits your trip with well curated eSIM catalgoue with Local, Unlimited, Regional, Global and Special Plans", + summary: + "Jump straight to the Shop tab.", + items: [ + 'On Home, tap **Shop** or the cart icon in the tab bar, to open the shop.', + "Tap **Countries**, **Regions**, **Global**, or **Special** to browse plans in that category.", + "Every plan card shows its Days, GB, Minutes and SMS up front, next to the price. Tap **View** to open checkout and find more details about the plan there as well.", + ], + tryHint: "Tap Shop, then switch category tabs.", + }, + { + id: "checkout", + index: "Step 03", + sourceTag: "Checkout", + alt: 0, + title: "Pay however you already pay", + summary: + "Expand the plan to see exactly what you're buying, pay and install", + items: [ + "Tap the plan header, the country name and Days/GB, to expand it.", + "The expanded panel adds Plan Type, Top-Up availability, and Coverage; tap Coverage for the full list of countries and networks.", + "Check the box confirming your device is eSIM-compatible and network-enabled.", + "Pick whichever payment method you already reach for: **Credit Card**, **Apple Pay**, or **External Wallet** for crypto, via MoonPay. **Device Wallet** is coming soon.", + "Have a coupon? Fill and tap **Apply Coupon** to use it.", + "Already have an eSIM in use? Flip the **First eSIM / Top-up** toggle to **Top-up**, then tap the specific existing eSIM you want the new plan applied to, that selection is what makes it a top-up instead of a fresh purchase.", + "Once payment confirms, tap **Install eSIM** there to get the installation details (Step 05 covers it).", + ], + callout: { + text: 'Top-up only appears if you already hold a compatible eSIM, confirmed vendor-side, otherwise you\'ll see a plain "not compatible for top-up" instead of a guess.', + }, + tryHint: 'Switch to "Top-up" above to see the option.', + }, + { + id: "orders", + index: "Step 04", + sourceTag: "Orders, Payments and eSIM details", + alt: 1, + title: "Track your eSIM usage, order history, invoices and more details", + summary: + "The Orders tab follows the purchase. A separate eSIM view, reached from Home, follows the data.", + items: [ + "Open the **Orders** tab. Each order carries a status pill: Processing, Provisioning, eSIM Ready, Completed, plus a few in-between states if something needs a retry.", + "Nothing to do while it's Processing or Provisioning, give it a moment.", + "Tap an order to expand it. Once it reads **eSIM Ready**, an **Install eSIM** button appears.", + "Tap **Purchase Details** any time for a bottom sheet with ICCID, reference, payment method, invoice, and plan history.", + "For the eSIM's own status: Data Remaining, a usage bar, Ready to Install / Active / Unavailable / Deactivated, and its ICCID, check the active-eSIM card on **Home**, not the Orders tab. Different screen, same eSIM.", + ], + tryHint: "Tap any order below to expand it.", + }, + { + id: "install", + index: "Step 05", + sourceTag: "Install your eSIM", + alt: 0, + title: "Installation", + summary: + "Three tabs on the installation screen: Direct, QR, or Manual. You'll get on the fastest Direct Install first.", + items: [ + "From an order marked eSIM Ready, tap **Install eSIM**. You'll land on the **Direct** tab.", + "On **Direct**, iOS only for now, with Android support on the way, tap **Install eSIM** to hand off to Apple's native flow.", + "On **QR**, open your phone's own Settings (Cellular/Mobile Data → Add eSIM → Use QR Code) and scan it directly, or tap **Share QR code** to save or send the image somewhere you can scan it from.", + "On **Manual**, tap the **Activation code (LPA)** row to copy it to your clipboard, then paste it wherever your phone's own setup flow asks.", + "The Orders tab shows the install status too, any time you want to check.", + ], + callout: { + text: 'Kokio do not any personal info, so it cannot share this QR via email or messages to you. Share it to yourself through a channel you can open and scan, like messaging it to another device.', + }, + tryHint: "Switch tabs, or tap Copy.", + }, + { + id: "wallet", + index: "Step 06", + sourceTag: "Self custodial keyless wallet", + alt: 1, + title: "A wallet you own", + summary: + "A Device Wallet lives on your Home screen, the passkey never leaves your phone's own hardware.", + items: [ + "Before you deploy one, Home shows a **Device Wallet** card with a simple prompt: tap to create it.", + "Once deployed, that same card shows your balance and a shortened wallet address, with quick links to copy it or open it on the block explorer.", + "It's running on the Base Sepolia testnet today, ahead of mainnet.", + ], + callout: { + text: "Wallet will be enabled soon for purchases and more.", + }, + tryHint: "Tap the card below to deploy your wallet.", + }, + { + id: "settings", + index: "Step 07", + sourceTag: "Menu or settings", + alt: 0, + title: "Contact for support, About and more", + summary: + "Control the look, get support, read the real policy, and (if you ever want to) delete everything for good.", + items: [ + "Open **Settings**.", + "Tap **Privacy Policy** for kokio.app's actual policy page, loaded in place.", + "Tap **About** for the app version, what Kokio is built on, and a link to follow along.", + "Tap **Contact Support** to email or message on telegram to Kokio directly.", + "Tap the theme row to flip Dark/Light, try it below, it flips this whole guide too.", + "Tap **Logout** to end the session; you'll land back on the sign-in screen from Step 01.", + "Tap **Delete Account**, then type **DELETE** to arm the button and confirm.", + ], + callout: { + tone: "danger", + strong: "This cannot be undone.", + text: "Deleting your account is immediate. There's no recovery: not by you, not by Kokio. Any eSIM you've already paid for keeps working until it expires; deletion doesn't cancel or refund it. You lose in-app access to your order history, eSIM details, and wallet, and on-chain records can never be deleted by anyone. You'll also need to remove your passkey yourself.", + }, + }, +]; + +export const SETTINGS_ABOUT = { + lines: [ + "You are using official Kokio mobile app.", + "A mobile app to purchase eSIM data plans and subscriptions using crypto or fiat in over 200 countries.", + ], + basedOn: { + text: "Based on ", + linkLabel: "Open Source eSIM Wallet Suite", + href: "https://github.com/Blockchain-Powered-eSIM/Smart-Contract-Suite", + }, + privacyLine: + "Built with privacy first, friendly and practical design for the digital well-being and connectivity freedom of mobile users worldwide.", + website: { text: "Our website ", linkLabel: "kokio.app", href: "https://kokio.app" }, + // Only X is real (verified against source) — no Instagram link in the app. + socials: [{ label: "Follow us: @kokiodotapp", href: "https://x.com/kokiodotapp" }], +}; + +export const SETTINGS_CONTACT = { + email: "contact@kokio.app", + telegram: TELEGRAM_URL, +}; + +export const DELETE_ACCOUNT = { + title: "Delete account", + intro: + "This permanently deletes your account. It cannot be undone, not by you, and not by us. There is no recovery.", + bullets: [ + "Your eSIMs keep working. Any plan you have already paid for stays active until it expires. Deleting your account does not cancel or refund it.", + "You lose access to your order history, eSIM details, and wallet in this app.", + "On-chain records are permanent and cannot be deleted by anyone.", + "You will need to remove your passkey yourself.", + ], + confirmLabel: "Type DELETE to confirm.", +}; diff --git a/components/live/device-frame.tsx b/components/live/device-frame.tsx new file mode 100644 index 0000000..8f20658 --- /dev/null +++ b/components/live/device-frame.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; + +export function DeviceFrame({ + sourceTag, + children, +}: { + sourceTag: string; + children: ReactNode; +}) { + return ( +
+
{sourceTag}
+
+
{children}
+
+
+ ); +} diff --git a/components/live/flow-context.tsx b/components/live/flow-context.tsx new file mode 100644 index 0000000..24795b4 --- /dev/null +++ b/components/live/flow-context.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { createContext, useContext, useState, type ReactNode } from "react"; +import type { Plan } from "@/components/live/mock-data"; + +type FlowContextValue = { + shopScreen: "home" | "shop"; + setShopScreen: (screen: "home" | "shop") => void; + selectedPlan: Plan | null; + choosePlan: (plan: Plan) => void; + completedOrderId: string | null; + completedPlan: Plan | null; + completeOrder: (plan: Plan) => void; + goTo: (stepId: string) => void; +}; + +const FlowContext = createContext(null); + +export function FlowProvider({ children }: { children: ReactNode }) { + const [shopScreen, setShopScreen] = useState<"home" | "shop">("home"); + const [selectedPlan, setSelectedPlan] = useState(null); + const [completedOrderId, setCompletedOrderId] = useState(null); + const [completedPlan, setCompletedPlan] = useState(null); + + function goTo(stepId: string) { + document.getElementById(`step-${stepId}`)?.scrollIntoView({ behavior: "smooth" }); + } + + function choosePlan(plan: Plan) { + setSelectedPlan(plan); + goTo("checkout"); + } + + function completeOrder(plan: Plan) { + // Reuse the mock "global" order slot (the one that's already eSIM + // Ready / expandable / installable) to represent "the order you just + // placed" — display fields below are swapped to match the plan that + // was actually bought. + setCompletedPlan(plan); + setCompletedOrderId("global"); + goTo("orders"); + } + + return ( + + {children} + + ); +} + +export function useFlow() { + const ctx = useContext(FlowContext); + if (!ctx) throw new Error("useFlow must be used within FlowProvider"); + return ctx; +} diff --git a/components/live/hero.tsx b/components/live/hero.tsx new file mode 100644 index 0000000..79740a3 --- /dev/null +++ b/components/live/hero.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Image from "next/image"; +import Link from "next/link"; +import Logomark from "@/assets/logomark.svg"; +import { Icon } from "@/components/live/icon-sprite"; +import { HERO } from "@/components/live/content"; + +export const IOS_URL = "https://apps.apple.com/us/app/kokio/id6799362847"; +export const ANDROID_URL = "https://play.google.com/store/apps/details?id=app.kokio.mobile"; + +function trackDownload(platform: "ios" | "android") { + const w = window as unknown as { _paq?: unknown[][] }; + w._paq?.push(["trackEvent", "Live Page", "Download Click", platform]); +} + +function HeroIconRow() { + return ( + + ); +} + +function StoreBadgeLinks() { + return ( + <> + trackDownload("ios")} + > + + + Download on the + App Store + + + trackDownload("android")} + > + + + Get it on the + Play Store + + + + ); +} + +export function LiveHero() { + const [showSticky, setShowSticky] = useState(false); + + useEffect(() => { + const hero = document.querySelector(".live-hero"); + if (!hero) return; + const observer = new IntersectionObserver( + ([entry]) => setShowSticky(!entry.isIntersecting), + { rootMargin: "-1px 0px 0px 0px" } + ); + observer.observe(hero); + return () => observer.disconnect(); + }, []); + + return ( + <> +
+
+
+ + +

{HERO.title}

+ +

{HERO.body}

+

+ Feel free to{" "} + + {HERO.contactLinkText} + {" "} + for anything. +

+
+ +
+

{HERO.guideHeading}

+

{HERO.guideLead}

+
+
+
+
+ +
+ + ); +} diff --git a/components/live/icon-sprite.tsx b/components/live/icon-sprite.tsx new file mode 100644 index 0000000..1176b62 --- /dev/null +++ b/components/live/icon-sprite.tsx @@ -0,0 +1,165 @@ +import type { CSSProperties } from "react"; + +export function IconSprite() { + return ( + + ); +} + +export function Icon({ + name, + className, + style, +}: { + name: string; + className?: string; + style?: CSSProperties; +}) { + return ( + + ); +} diff --git a/components/live/mock-data.ts b/components/live/mock-data.ts new file mode 100644 index 0000000..07acc85 --- /dev/null +++ b/components/live/mock-data.ts @@ -0,0 +1,163 @@ +/** + * Simulated data for the /live guide , illustrative only, no real backend calls. + */ + +export type PlanCategory = "Countries" | "Regions" | "Global" | "Special"; + +export type Plan = { + country: string; + flag: string; + days: string; + gb: string; + price: string; +}; + +export const PLANS: Record = { + Countries: [ + { country: "Japan", flag: "🇯🇵", days: "7", gb: "5", price: "24.99" }, + { country: "France", flag: "🇫🇷", days: "15", gb: "10", price: "32.50" }, + { country: "Brazil", flag: "🇧🇷", days: "30", gb: "20", price: "44.00" }, + ], + Regions: [ + { country: "Europe", flag: "🇪🇺", days: "10", gb: "8", price: "28.00" }, + { country: "Southeast Asia", flag: "🌏", days: "15", gb: "12", price: "35.00" }, + { country: "North America", flag: "🌎", days: "7", gb: "6", price: "22.00" }, + ], + Global: [ + { country: "Global 5GB", flag: "🌐", days: "15", gb: "5", price: "26.00" }, + { country: "Global 10GB", flag: "🌐", days: "30", gb: "10", price: "42.00" }, + { country: "Global 20GB", flag: "🌐", days: "30", gb: "20", price: "58.00" }, + ], + Special: [ + { country: "3-Country Bundle", flag: "✨", days: "20", gb: "15", price: "39.00" }, + ], +}; + +export const CHECKOUT_RADIOS: { + id: string; + label: string; + sub: string; + disabled?: boolean; +}[] = [ + { id: "wallet", label: "Device Wallet", sub: "Coming soon", disabled: true }, + { id: "card", label: "Credit Card", sub: "Pay with a card" }, + { id: "apple", label: "Apple Pay", sub: "Pay with Apple Pay" }, + { id: "crypto", label: "External Wallet (via Moonpay)", sub: "Pay in crypto" }, +]; + +export const BASE_PRICE = 24.99; + +export const TOPUP_CANDIDATES = [ + { esimId: "japan-1", label: "Japan · 7 Days · 5GB" }, + { esimId: "global-1", label: "Global · 30 Days · 10GB" }, +]; + +export type OrderStatus = "Processing" | "Provisioning" | "eSIM Ready" | "Completed"; + +export type Order = { + id: string; + plan: string; + flag: string; + days: string; + gb: string; + mins: string; + sms: string; + pill: OrderStatus; + pillTone: "warning" | "success"; + expandable: boolean; + note?: string; + install?: boolean; + iccid?: string; + lpa?: string; + paymentMethod?: string; + supportRef?: string; + invoiceUrl?: string; + planHistory?: { planId: string; validity: number; purchaseDate: string }[]; +}; + +export const ORDERS: Order[] = [ + { + id: "france", + plan: "France", + flag: "🇫🇷", + days: "15", + gb: "10", + mins: "0", + sms: "0", + pill: "Processing", + pillTone: "warning", + expandable: false, + note: "No action needed while it finishes.", + }, + { + id: "global", + plan: "Global", + flag: "🌐", + days: "30", + gb: "10", + mins: "0", + sms: "0", + pill: "eSIM Ready", + pillTone: "warning", + expandable: true, + install: true, + iccid: "8988 2801 0000 4471 923", + lpa: "LPA:1$rsp.example.net$B4C5-D6E7-F8G9-H0I1", + paymentMethod: "Credit Card", + supportRef: "KOK-48291", + invoiceUrl: "#", + }, + { + id: "japan", + plan: "Japan", + flag: "🇯🇵", + days: "7", + gb: "5", + mins: "100", + sms: "50", + pill: "Completed", + pillTone: "success", + expandable: true, + install: false, + iccid: "8988 2801 0000 5512 044", + paymentMethod: "Apple Pay", + supportRef: "KOK-30144", + invoiceUrl: "#", + planHistory: [{ planId: "Japan 5GB / 7 days", validity: 7, purchaseDate: "2026-08-14" }], + }, +]; + +// The active eSIM card on Home (app/esim-detail.tsx) — a separate screen +// from Orders, per the source-accuracy correction in AppLiveTasks.md. +export const HOME_ESIM_DETAIL = { + plan: "Japan", + flag: "🇯🇵", + days: "7", + gb: "5", + remaining: "2.35 GB", + totalGb: 5, + usedPct: 47, + status: "Active" as const, + iccid: "8988 2801 0000 5512 044", +}; + +export const QR_PATTERN = [ + 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, + 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, +]; + +export const MANUAL_LPA = "LPA:1$rsp.example.net$A1B2-C3D4-E5F6-G7H8"; + +export const WALLET_ADDRESS_SHORT = "0x9bf...b1e2ef7"; +export const WALLET_ADDRESS_FULL = + "0x9bfbf5000f10121edc519bdc198f2fb93e16c4fd9c20846ff837e82a8b1e2ef7"; + +export const SETTINGS_MENU = [ + { icon: "call", label: "Contact", disabled: true }, + { icon: "lock", label: "Privacy Policy", href: "https://kokio.app/privacy-policy" }, + { icon: "info", label: "About", view: "about" as const }, + { icon: "headset", label: "Contact Support", view: "contact" as const }, + { icon: "theme", toggle: true }, + { icon: "logout", label: "Logout", view: "logout" as const }, + { icon: "trash", label: "Delete Account", view: "delete" as const }, +]; diff --git a/components/live/phone-tabbar.tsx b/components/live/phone-tabbar.tsx new file mode 100644 index 0000000..4a87c91 --- /dev/null +++ b/components/live/phone-tabbar.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { Icon } from "@/components/live/icon-sprite"; + +type TabId = "home" | "shop" | "wallet" | "orders" | "settings"; + +const TABS: { id: TabId; icon: string; label: string }[] = [ + { id: "home", icon: "home", label: "Home" }, + { id: "shop", icon: "cart", label: "Shop" }, + { id: "wallet", icon: "wallet", label: "Wallet" }, + { id: "orders", icon: "receipt", label: "Orders" }, + { id: "settings", icon: "menu", label: "Settings" }, +]; + +export function PhoneTabBar({ + active, + onNavigate, +}: { + active?: TabId; + onNavigate?: (tab: TabId) => void; +}) { + return ( +
+ {TABS.map((tab) => { + const isWallet = tab.id === "wallet"; + return ( + + ); + })} +
+ ); +} diff --git a/components/live/rich-text.tsx b/components/live/rich-text.tsx new file mode 100644 index 0000000..239ab44 --- /dev/null +++ b/components/live/rich-text.tsx @@ -0,0 +1,15 @@ +import { Fragment } from "react"; + +export function RichText({ text }: { text: string }) { + const parts = text.split(/(\*\*[^*]+\*\*)/g); + return ( + <> + {parts.map((part, i) => { + if (part.startsWith("**") && part.endsWith("**")) { + return {part.slice(2, -2)}; + } + return {part}; + })} + + ); +} diff --git a/components/live/step-auth.tsx b/components/live/step-auth.tsx new file mode 100644 index 0000000..4f300d5 --- /dev/null +++ b/components/live/step-auth.tsx @@ -0,0 +1,124 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import Image from "next/image"; +import Logomark from "@/assets/logomark.svg"; +import { DeviceFrame } from "@/components/live/device-frame"; +import { useFlow } from "@/components/live/flow-context"; + +type Mode = "new" | "returning"; + +/** + * Step 01 — Sign in (components/AuthenticationModal.tsx in the mobile app). + * Confirming a button walks through a fake "Verifying… / Passkey confirmed" sequence, then resets. + */ +export function StepAuthVisual() { + const flow = useFlow(); + const [mode, setMode] = useState("new"); + const [subtext, setSubtext] = useState("Choose how to get started"); + const timers = useRef[]>([]); + + useEffect(() => () => timers.current.forEach(clearTimeout), []); + + function baseSubtext(m: Mode) { + return m === "returning" ? "Log in to continue" : "Choose how to get started"; + } + + function handleSwitchMode(m: Mode) { + setMode(m); + setSubtext(baseSubtext(m)); + } + + function confirm() { + timers.current.forEach(clearTimeout); + setSubtext("Verifying your identity…"); + timers.current.push( + setTimeout(() => setSubtext("Passkey confirmed"), 850), + // Land on Home's shop card, same as the artifact — the visitor taps + // "Shop" themselves from there to open the browsing screen. + setTimeout(() => { + flow.setShopScreen("home"); + flow.goTo("shop"); + }, 1500), + setTimeout(() => setSubtext(baseSubtext(mode)), 2600) + ); + } + + return ( + +
+
+
+ + +
+
+
+ Kokio +
+ Authentication Required +
+
+ {subtext} +
+ + {mode !== "returning" ? ( +
+ + +
+ ) : ( + + )} + +
+ +
+
+
+ No account recovery without a synced passkey. +
+ Losing the passkey itself means losing access. +
+
+
+ ); +} diff --git a/components/live/step-checkout.tsx b/components/live/step-checkout.tsx new file mode 100644 index 0000000..09033a7 --- /dev/null +++ b/components/live/step-checkout.tsx @@ -0,0 +1,380 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { DeviceFrame } from "@/components/live/device-frame"; +import { Icon } from "@/components/live/icon-sprite"; +import { CHECKOUT_RADIOS, PLANS, TOPUP_CANDIDATES } from "@/components/live/mock-data"; +import { useFlow } from "@/components/live/flow-context"; + +type CouponState = "idle" | "validating" | "valid" | "applied"; + +const DEFAULT_PLAN = PLANS.Countries[0]; + +/** + * Step 03 — Checkout & payment (checkoutHeader/CheckoutHeader.tsx + + * screens/checkout/Checkout.tsx). Reflects whichever plan was actually + * picked in Step 02, coupon validation, top-up compatibility check, and + * the Pay confirmation are all fake timers. + */ +export function StepCheckoutVisual() { + const flow = useFlow(); + const plan = flow.selectedPlan ?? DEFAULT_PLAN; + const [headerOpen, setHeaderOpen] = useState(false); + const [selectedMethod, setSelectedMethod] = useState(null); + const [coupon, setCoupon] = useState(""); + const [couponState, setCouponState] = useState("idle"); + const [priorMode, setPriorMode] = useState<"first" | "existing">("first"); + const [topupChecking, setTopupChecking] = useState(false); + const [topupSelected, setTopupSelected] = useState(null); + const [esimOk, setEsimOk] = useState(false); + const [payStatus, setPayStatus] = useState(""); + const timers = useRef[]>([]); + + useEffect(() => () => timers.current.forEach(clearTimeout), []); + + const discountApplied = couponState === "applied"; + const basePrice = Number(plan.price); + const total = (discountApplied ? basePrice - 10 : basePrice).toFixed(2); + const canPay = esimOk && !!selectedMethod; + + function handleCouponChange(value: string) { + const upper = value.toUpperCase(); + setCoupon(upper); + setCouponState("idle"); + timers.current.forEach(clearTimeout); + if (upper.length === 8) { + setCouponState("validating"); + timers.current.push( + setTimeout(() => setCouponState("valid"), 700) + ); + } + } + + function handlePriorMode(mode: "first" | "existing") { + setPriorMode(mode); + if (mode === "first") { + setTopupSelected(null); + return; + } + setTopupChecking(true); + timers.current.push(setTimeout(() => setTopupChecking(false), 700)); + } + + function handlePay() { + if (!canPay) return; + const target = TOPUP_CANDIDATES.find((r) => r.esimId === topupSelected); + setPayStatus("Preparing your payment…"); + timers.current.push( + setTimeout(() => setPayStatus("Confirm with Face ID…"), 700), + setTimeout(() => { + setPayStatus( + target + ? `Top-up of ${plan.country} · ${plan.days} Days · ${plan.gb}GB is applied to ${target.label}.` + : "Order placed — check the Orders tab." + ); + }, 1500), + // Hand off to Orders with this purchase's row pre-expanded. + setTimeout(() => flow.completeOrder(plan), 2300), + setTimeout(() => setPayStatus(""), 4500) + ); + } + + return ( + +
+ + {plan.flag} + +
+
+ +
+ {plan.country} +
+
+
+ + + {plan.days} Days + + + + {plan.gb} GB + +
+
+ + {headerOpen && ( +
+
+ Plan Type + Data +
+
+ Top-Up Options + Available +
+
+ Coverage + + 1 country + +
+
+ IP Routing + N/A +
+
+ )} +
+ +
+
+ + +
+
+ +
+
+ Payment Method +
+
+ {CHECKOUT_RADIOS.map((r) => { + const selected = selectedMethod === r.id; + return ( + + ); + })} +
+ +
Discount
+ handleCouponChange(e.target.value)} + style={{ + width: "100%", + background: "var(--app-input-bg)", + border: "1px solid transparent", + borderRadius: 12, + padding: "10px 14px", + fontSize: 14, + color: "var(--app-text)", + fontFamily: "inherit", + }} + /> +
+ {couponState === "validating" && ( + + Validating coupon… + + )} + {(couponState === "valid" || couponState === "applied") && ( +
+ {couponState === "valid" ? ( + <> + + Balance: $10.00 KOKIO + + + + ) : ( + <> + Discount applied: -$10.00 + + + )} +
+ )} +
+ + {priorMode === "existing" && ( +
+ {topupChecking ? ( +
+ + Checking top-up compatibility… +
+ ) : ( +
+
+ Apply as Top-up +
+
+ Select an eSIM to top up, or leave unselected to buy a new one +
+
+ {TOPUP_CANDIDATES.map((r) => { + const selected = topupSelected === r.esimId; + return ( + + ); + })} +
+
+ )} +
+ )} + + + + +
+ {payStatus} +
+
+
+ ); +} diff --git a/components/live/step-install.tsx b/components/live/step-install.tsx new file mode 100644 index 0000000..174186f --- /dev/null +++ b/components/live/step-install.tsx @@ -0,0 +1,151 @@ +"use client"; + +import { useState } from "react"; +import { DeviceFrame } from "@/components/live/device-frame"; +import { Icon } from "@/components/live/icon-sprite"; +import { MANUAL_LPA, QR_PATTERN } from "@/components/live/mock-data"; +import { useFlow } from "@/components/live/flow-context"; + +type Tab = "QR" | "Direct" | "Manual"; + +/** + * Step 05 — Install (screens/esimInstallation/EsimInstallation.tsx). + * Tab strip order and default active tab are both **Direct** first, per + * explicit instruction — overrides the earlier QR-default accuracy note + * in AppLiveTasks.md's source-accuracy table. + * Install eSIM is always tappable and just hands off to Orders. + */ +export function StepInstallVisual() { + const flow = useFlow(); + const [tab, setTab] = useState("Direct"); + const [copied, setCopied] = useState(false); + + function copyLpa() { + try { + navigator.clipboard?.writeText(MANUAL_LPA); + } catch { + // clipboard access can be blocked — the visual "copied" state still runs + } + setCopied(true); + setTimeout(() => setCopied(false), 1200); + } + + return ( + <> + +
+ + 🇯🇵 + +
+
+ +
+ Install eSIM +
+
+
+ + + 7 Days + + + + 5 GB + +
+
+
+ +
+
+ + + +
+ + {tab === "QR" && ( +
+
+ {QR_PATTERN.map((v, i) => ( +
+ ))} +
+

+ Add it from Settings > Cellular/Mobile Data > Add eSIM > Use QR Code +

+ +
+ )} + + {tab === "Direct" && ( +
+

+ Hands off to Apple's native "Install eSIM" flow on this iPhone. +

+ +
+ )} + + {tab === "Manual" && ( +
+ +
+ )} +
+ + + ); +} diff --git a/components/live/step-orders.tsx b/components/live/step-orders.tsx new file mode 100644 index 0000000..3120340 --- /dev/null +++ b/components/live/step-orders.tsx @@ -0,0 +1,357 @@ +"use client"; + +import { useEffect, useState, type ReactNode } from "react"; +import { Icon } from "@/components/live/icon-sprite"; +import { ORDERS, HOME_ESIM_DETAIL, type Order } from "@/components/live/mock-data"; +import { useFlow } from "@/components/live/flow-context"; + +type CopyRow = { label: string; value: string; display?: string }; + +function PurchaseDetailsSheet({ order, onClose }: { order: Order; onClose: () => void }) { + const [copiedKey, setCopiedKey] = useState(null); + const rows: CopyRow[] = []; + if (order.iccid) rows.push({ label: "ICCID", value: order.iccid }); + if (order.supportRef) rows.push({ label: "Reference", value: order.supportRef }); + if (order.lpa) rows.push({ label: "LPA String", value: order.lpa, display: "Install using this code in SIM settings" }); + + function copy(key: string, value: string) { + try { + navigator.clipboard?.writeText(value); + } catch { + // clipboard access can be blocked — the visual "copied" state still runs + } + setCopiedKey(key); + setTimeout(() => setCopiedKey((k) => (k === key ? null : k)), 1200); + } + + useEffect(() => { + function onKeyDown(e: KeyboardEvent) { + if (e.key === "Escape") onClose(); + } + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [onClose]); + + return ( +
e.target === e.currentTarget && onClose()} + > +
+
+ +
+
+ Purchase Details + +
+
+ {rows.map((row) => ( + + ))} + {order.paymentMethod && ( +
+ Payment Method + {order.paymentMethod} +
+ )} + {order.invoiceUrl && ( +
+ Invoice + View invoice → +
+ )} + {order.planHistory?.length ? ( + <> +
+ Plan History +
+ {order.planHistory.map((entry) => ( +
+ {entry.planId} + + {entry.validity}d · {new Date(entry.purchaseDate).toLocaleDateString()} + +
+ ))} + + ) : null} +
+
+
+ ); +} + +/** Renders the order card as a real + ); +} + +function OrdersTabView() { + const flow = useFlow(); + const [openId, setOpenId] = useState(null); + const [pdOrder, setPdOrder] = useState(null); + + useEffect(() => { + if (flow.completedOrderId) setOpenId(flow.completedOrderId); + }, [flow.completedOrderId]); + + const orders = ORDERS.map((order) => + order.id === flow.completedOrderId && flow.completedPlan + ? { + ...order, + plan: flow.completedPlan.country, + flag: flow.completedPlan.flag, + days: flow.completedPlan.days, + gb: flow.completedPlan.gb, + } + : order + ); + + return ( +
+
+ {orders.map((order) => { + const expanded = openId === order.id; + return ( +
+ setOpenId(expanded ? null : order.id)} + > + + {order.flag} + +
+
+ {order.plan} +
+
+ + + {order.days} Days + + + + {order.gb} GB + + + + {order.mins} Mins + + + + {order.sms} SMS + +
+
+
+
+ + {order.pill} + + {order.expandable ? ( + + ) : order.note ? ( + + {order.note} + + ) : null} +
+ {expanded && ( +
+
+ {order.install && ( + + )} + +
+
+ )} +
+ ); + })} +
+ {pdOrder && setPdOrder(null)} />} +
+ ); +} + +function HomeEsimDetailView() { + const barColor = + HOME_ESIM_DETAIL.usedPct > 40 ? "var(--app-success)" : HOME_ESIM_DETAIL.usedPct > 15 ? "var(--app-warning)" : "var(--app-destructive)"; + return ( +
+ + {HOME_ESIM_DETAIL.flag} + +
+
+ {HOME_ESIM_DETAIL.plan} +
+
+ + + {HOME_ESIM_DETAIL.days} Days + + + + {HOME_ESIM_DETAIL.gb} GB + +
+
+
+
+ Data Remaining + {HOME_ESIM_DETAIL.remaining} +
+
+
+
+
+ Status + + {HOME_ESIM_DETAIL.status} + +
+
+ ICCID + {HOME_ESIM_DETAIL.iccid} +
+
+
+ ); +} + +/** + * Step 04 — Orders + the separate Home eSIM-detail screen. The artifact + * originally conflated these into one view; the mobile source shows + * they're two different screens (see AppLiveTasks.md accuracy table), so + * this mockup keeps them as an explicit toggle rather than pretending + * they're one expand panel. + */ +export function StepOrdersVisual() { + const [screen, setScreen] = useState<"orders" | "detail">("orders"); + + return ( +
+
Track your order
+
+
+
+
+ + +
+
+ {screen === "orders" ? : } +
+
+
+ ); +} diff --git a/components/live/step-section.tsx b/components/live/step-section.tsx new file mode 100644 index 0000000..a380bb8 --- /dev/null +++ b/components/live/step-section.tsx @@ -0,0 +1,50 @@ +import type { ReactNode } from "react"; +import { RichText } from "@/components/live/rich-text"; +import { Icon } from "@/components/live/icon-sprite"; +import type { StepContent } from "@/components/live/content"; + +export function StepSection({ + step, + tryHintIcon, + children, +}: { + step: StepContent; + tryHintIcon?: string; + children: ReactNode; +}) { + return ( +
+
+ {step.index} +

{step.title}

+

{step.summary}

+
    + {step.items.map((item, i) => ( +
  1. +

    + +

    +
  2. + ))} +
+ {step.callout && ( +
+ {step.callout.strong && {step.callout.strong} } + {step.callout.text} +
+ )} + {step.tryHint && ( +
+ {tryHintIcon && } + Try it: {step.tryHint} +
+ )} +
+
{children}
+
+ ); +} diff --git a/components/live/step-settings.tsx b/components/live/step-settings.tsx new file mode 100644 index 0000000..24987dd --- /dev/null +++ b/components/live/step-settings.tsx @@ -0,0 +1,270 @@ +"use client"; + +import { useState } from "react"; +import { DeviceFrame } from "@/components/live/device-frame"; +import { Icon } from "@/components/live/icon-sprite"; +import { useLiveTheme } from "@/components/live/theme-context"; +import { SETTINGS_MENU } from "@/components/live/mock-data"; +import { DELETE_ACCOUNT, SETTINGS_ABOUT, SETTINGS_CONTACT } from "@/components/live/content"; + +type Panel = "list" | "about" | "contact" | "delete"; + +const ABOUT_TEXT_STYLE = { + fontSize: 13.5, + lineHeight: 1.6, + color: "var(--app-text)", + opacity: 0.9, + margin: "0 0 12px", +} as const; + +/** + * Step 07 — Settings (app/(tabs)/settings.tsx + DeleteAccountModal.tsx). + */ +export function StepSettingsVisual() { + const { theme, setTheme } = useLiveTheme(); + const [panel, setPanel] = useState("list"); + const [deleteInput, setDeleteInput] = useState(""); + const [deleteStatus, setDeleteStatus] = useState(""); + const isDark = theme === "dark"; + + function resetDelete() { + setDeleteInput(""); + setDeleteStatus(""); + setPanel("list"); + } + + function confirmDelete() { + if (deleteInput.trim() !== "DELETE") return; + setDeleteStatus("Deleting your account…"); + setTimeout(() => setDeleteStatus("This is a preview — no account was deleted."), 700); + setTimeout(resetDelete, 2200); + } + + return ( + +
+ Settings +
+ + {panel === "list" && ( +
+ {SETTINGS_MENU.map((item, i) => { + const isToggle = "toggle" in item && item.toggle; + const iconName = isToggle ? (isDark ? "moon" : "sun") : item.icon; + const label = isToggle ? (isDark ? "Dark Mode" : "Light Mode") : item.label; + const disabled = "disabled" in item && item.disabled; + return ( + + ); + })} +
+ )} + + {panel === "about" && ( +
+ setPanel("list")} /> + {SETTINGS_ABOUT.lines.map((line) => ( +

+ {line} +

+ ))} +

+ {SETTINGS_ABOUT.basedOn.text} + + {SETTINGS_ABOUT.basedOn.linkLabel} + +

+

{SETTINGS_ABOUT.privacyLine}

+

+ {SETTINGS_ABOUT.website.text} + + {SETTINGS_ABOUT.website.linkLabel} + +

+
+ Follow us on socials + + {SETTINGS_ABOUT.socials.map((s) => ( + + + + ))} + +
+
+ )} + + {panel === "contact" && ( + + )} + + {panel === "delete" && ( +
+
{DELETE_ACCOUNT.title}
+

{DELETE_ACCOUNT.intro}

+
    + {DELETE_ACCOUNT.bullets.map((b) => ( +
  • + {b} +
  • + ))} +
+
{DELETE_ACCOUNT.confirmLabel}
+ setDeleteInput(e.target.value.toUpperCase())} + placeholder="DELETE" + autoCapitalize="characters" + style={{ + width: "100%", + background: "transparent", + border: "1px solid var(--app-foreground2)", + borderRadius: 14, + padding: "12px 14px", + fontSize: 15, + letterSpacing: 2, + color: "var(--app-text)", + fontFamily: "inherit", + }} + /> +
+ + +
+
+ {deleteStatus} +
+
+ )} +
+ ); +} + +function PanelHeader({ title, onClose }: { title: string; onClose: () => void }) { + return ( +
+ {title} + +
+ ); +} diff --git a/components/live/step-shop.tsx b/components/live/step-shop.tsx new file mode 100644 index 0000000..c6c6761 --- /dev/null +++ b/components/live/step-shop.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { useState } from "react"; +import { DeviceFrame } from "@/components/live/device-frame"; +import { PhoneTabBar } from "@/components/live/phone-tabbar"; +import { Icon } from "@/components/live/icon-sprite"; +import { PLANS, type PlanCategory } from "@/components/live/mock-data"; +import { useFlow } from "@/components/live/flow-context"; + +const CATEGORIES: PlanCategory[] = ["Countries", "Regions", "Global", "Special"]; + +/** Step 02 — Browse & compare (Home hero card + Shop tabs). + * The visitor here landing on the Home shop card — they tap Shop themselves + * to reach the browsing screen */ +export function StepShopVisual() { + const flow = useFlow(); + const screen = flow.shopScreen; + const setScreen = flow.setShopScreen; + const [category, setCategory] = useState("Countries"); + + return ( + +
+
+ {screen === "home" ? ( +
+
+ Home +
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + + {/* eslint-disable-next-line @next/next/no-img-element */} + +
+
+
+
Plan Your Next Adventure
+
The world awaits you!
+
+ +
+
+
+ ) : ( +
+
+ {CATEGORIES.map((cat) => ( + + ))} +
+ +
+ {PLANS[category].map((plan) => ( +
+ + {plan.flag} + +
+
+ {plan.country} +
+
+ + + {plan.days} Days + + + + {plan.gb} GB + +
+ +
+
+ ))} +
+
+ )} +
+ +
+ { + if (tab === "home" || tab === "shop") setScreen(tab); + }} + /> +
+
+
+ ); +} diff --git a/components/live/step-wallet.tsx b/components/live/step-wallet.tsx new file mode 100644 index 0000000..71f3ec4 --- /dev/null +++ b/components/live/step-wallet.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { useState } from "react"; +import { DeviceFrame } from "@/components/live/device-frame"; +import { Icon } from "@/components/live/icon-sprite"; +import { WALLET_ADDRESS_FULL, WALLET_ADDRESS_SHORT } from "@/components/live/mock-data"; + +/** + * Step 06 — Wallet, scoped to the Home "Device Wallet" card only (the app's Wallet tab is feature-flagged off and + * unreachable, so no Send/Receive/Deposit dashboard is shown here). + */ +export function StepWalletVisual() { + const [deploying, setDeploying] = useState(false); + const [deployed, setDeployed] = useState(false); + const [copied, setCopied] = useState(false); + + function deploy() { + setDeploying(true); + setTimeout(() => { + setDeploying(false); + setDeployed(true); + }, 900); + } + + function copyAddress() { + try { + navigator.clipboard?.writeText(WALLET_ADDRESS_FULL); + } catch { + // clipboard access can be blocked — the visual "copied" state still runs + } + setCopied(true); + setTimeout(() => setCopied(false), 1200); + } + + return ( + +
Device Wallet
+
+
+ Device Wallet + +
+ + {!deployed ? ( + + ) : ( +
+
Total balance
+
+ 0 + USD +
+
+ {WALLET_ADDRESS_SHORT} + + +
+
+ )} +
+ +
+ {[ + { icon: "arrow-up", label: "Send", bg: "var(--app-warning)" }, + { icon: "arrow-down", label: "Receive", bg: "var(--app-success)" }, + { icon: "deposit", label: "Deposit", bg: "var(--app-info)" }, + ].map((item) => ( +
+ + + + {item.label} + + Coming soon + +
+ ))} +
+
+ ); +} diff --git a/components/live/theme-context.tsx b/components/live/theme-context.tsx new file mode 100644 index 0000000..12783ae --- /dev/null +++ b/components/live/theme-context.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { createContext, useContext, useState, type ReactNode } from "react"; + +type LiveTheme = "light" | "dark"; + +const ThemeContext = createContext<{ + theme: LiveTheme; + setTheme: (t: LiveTheme) => void; +}>({ theme: "light", setTheme: () => {} }); + +export function LiveThemeProvider({ children }: { children: ReactNode }) { + const [theme, setTheme] = useState("light"); + return ( + +
+ {children} +
+
+ ); +} + +export function useLiveTheme() { + return useContext(ThemeContext); +} + +export function ThemeToggle() { + const { theme, setTheme } = useLiveTheme(); + return ( +
+ Theme +
+ + +
+
+ ); +} diff --git a/public/live/home-hero-flags.png b/public/live/home-hero-flags.png new file mode 100644 index 0000000000000000000000000000000000000000..af62c2f6de4b8f257d0796ed60cb80f270cb31f4 GIT binary patch literal 10879 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 literal 0 HcmV?d00001 diff --git a/public/live/home-hero-sunburst.png b/public/live/home-hero-sunburst.png new file mode 100644 index 0000000000000000000000000000000000000000..8b13353d3330dc9add6074fb3dd9d794e59f1bec GIT binary patch literal 20896 zcma%C1y@_o)5hK1wLtMw+@ZK@ao1wOiWLY}ye-fo#T`l^xVw9S;vO7|1`huRguJQS4O^Z?-B-5-4^mwg0eUzRCHZ6|g32qe*|0lOXO4$U>TA zk4t*_pgXD?EaZ^!!;9%UYFWDZ)X_$20a5n4kLEbdao3M zW9zqKcazsuI!zvgIn_~I`?&iG2)!0FmhvlFlq;i^+!7AsD5PT7N@=XB)8K1E^`2^a zD0&gbBGhOb2}{;7_p81OM&1oZawx?sqFV(yd7(c2fYwz+ zkq>z{Zn3lwZhm78WclBJ-a)UZT@!ZSbRT}sUZ$}VZPf_tm^gVg?^#NOkQDyLB$2Jn z<(osF@nF&*X`1A0Yg^|Roafsjlp4(k%*$)%gk8;%LKa}2Y7A@)j$bij&4vJwx|}*J zEQ8l}E05O|CRKW z&t_FxI+crR^b416ge+AC?)mlpABsN`*c8Rzz2+=BdVO1XFJC!6kkz^?aNWczqrWf} z)Kq_nevYsj+{<%FCdx3uy$659Yc+VavLP6y8dl;(qhDxD2CPbxDQV=@`+*c_d6yCO zOTX)A;-Fq0CA?g-Hc{1dSEGYBP)t7hh3FT$^=qon&sXK43*3q7rkCr0ti$o|WK)^r zj>M6f^F9sEt&DB-<-vz(2vQueH z2MqZh^lI$&EWH}>EFnw832}1zoXZ{7zD*E_zj#f0No;O<752+r%y%n4jull#!fek{MxY|nP{)9uE=3z9mX+w7M((4~9a4S;Tjl11e{(FNqK@bd$CTQdKnLfV$1UIs#Cw36haQ6l;vd zgbXYeGmSQ8_MaO<Wc(!Oah#mvuqq4bz_|>oJr;xZ? zf)85Oqp~170pOx8o|A@ktzr~wgujlb3k0$N#dz~nc9`d5L(3Fv>x~qB4+_h!Y zt&G#;XPdd#k{@XHdAK0xEJwe$8+XKG7GuqF*cOVnYtzLWoLzTVVFhK0a%;4Pr@c}z z^IxOfBPbz#ni*MCa`|~T@ zH6UeN!AZbB1f!14Znlqtt9m87d-zLbr|^x)4?BOQ#{_O28OKdGlHtmXO;6Zz686m` z1dfVKA2Xd-6;~R!4&Wb-flGQNcdYOkS4P^h#)r$2z00tDw6OXGtli>F&hxm&AUD^F zQeM>T&g13;%VrDYWA@W&)?#+_D*Fd$o5c4Ozd|_0A7ye%a^63iu%p>#)ie>9G^62m z7YTijnw#uf2ytkJi1tU}&adE5+a=4;PXnm38 z1CN-L!0Gd-rJj_<-1Wks0>#oK1O_aWdeY*(&&wu;%CFNVDS?hc)3z^@ob5Jut)A_@|NTa{$wLl^DX`-++kc(^nJEMiLg(F^7t7xirLiPbs8$^|x zG{gyl?lVe#jwzLmTPc7xor+%;`BKJ!khD*Acq`1#3FUVdfN8w=gdH`U z+9Li+Jw1t??)z<;)G1|yiO{G@(!d+^FJ?RCMOJ&_Z?%_gs>Q~LJ2kxL0v*dwEks(2 z0`p$ue*>$8l$G`Aeo#vX8?JkPnTWr|z6#;i+WSlRBCu6hhL%i$cH>XV3=HE|N4iAb z^h`wxjI6}y*C|wx#dmN~yWDZ*K}<8ipqA(Tsn$+0uWiRc|F&!J!AyZmBhQ7=&UedT zLmj{L+B~M5v1RxNnm?nNerGFAgbHI$a6w4w>nokX-QRT)vstsM6-_%QEzhE73a6fV zW?}3L(yTJSu)Tjc#TM)^j5e7-$oEHZ^wQlwBbn_`59gaHz*2TD$)SR03hrBwcou3v zD|sn1jfv7~$%@RcR`A=*kT72|wyP~#6MX}eIR1nKVHS!@^d7DRw9Duz{pSmg5@s`9 zyw0fegAZzYGuGkO$@#O2-3k(Ftn~aRU}l1v@$YfdDmAZ31`i)pItcqnEkJ5m5De%c zVz|YrTEr6>>(y4nFV_S#7spWUq&xd)ehE?9OC1$qrO2Dv!J@ zt2*y*b_ZdKo##K2!TNmb`YeVeAP}V*xYg zCa}nN-u~O4m0R~TH+Yrd$4IPwqeb$slyR^s075REUCb68UCAdRviJ1zE<|t{W&`Tf zSIo2bNlp!oN9?bnOHu)2k+QmCocq}N`mAT(lw&y&rzI;&juUvI^#r0)-uKLMz@89d zmgz?DUk=foDl4#@+vKm=EEf4;4vZ4>QC@81?tdr^x8fK#th*|rHs5~#RL41bo)v$- zDvb+qeczLgkTBa;kSd{7OhGG!iSDA!$+?;QWV1f5R4KAWy;4I1J>p@j?&GBp8~NV- z#0B4#%}+8~7o(6#EkEBGyq$V8*90VQm(0J_@+G;&_}EBoY7ORGXq~MN2dXsFwV0y9 z|C)y`Cl0T`W|Z-JSUp-)<5VTuCzJ-Zvm=E0bC`_2QNcx6$s#`QOI{)GvH#&4K9r#g z;NH3IXJNeSPESKhUgDn?PI5Z!E+bHlZ{8ukN2~kiBxg|_szuB+MNf9Cnk2*+pnxLh zgtW~{j`C!wb0_gjRUBh$jPl;mGeyU5p^*%GXI&^;lTr< zCyTj)DJ(w>2`WL6dwgwypd&6ZZDr;d$z#da_kKH+Ac|!_hoc-wS6YTe1T?66O;qAo zVK|5f#m`e&rM|(K!j@7+%O!V79^vn484ZXlFT^6^8_o{e!wPVjC&TKGYO8y>C*85h zKgDNC?`<#P*7o z_l~lj;aMfA?++Giy~PmCz`U5~2)JeiL=!4nG z*Nf>_I!EYqg3@4>go!4&k=0<%Hsago8(za|Hb=stC`Y zD5#{(^}m5Hr+;X%`xPt7rETR zl%%epA+t-%UXc(K5VQEPFgnRHcq^P^C$bWeDa<;5^I0)pzo;YR$O zEEM_H?1KU2;3Q;^1%y(+X=z0;T@SJeuIU6NWY=f6vH}dbM9sXfjtXQ`ULgQwDluG& zFZUTq^=}CMQ4VGn{eoaYCBG9RHj(L}OB%@WXVYNajR|(@;Y;>@xZoEL4V{C-GD5XL zD=Yuh($O#C^C}0RHRiRUw+{G2hB=LtM)dWO(m=9wt?R?D7h&ItA`bWFYCV|;Q>(Y6 zF5EFxmHtUN=PI3sz0Ck03Em27zBfeDwSJO1y67!0#_B?^PggKX#JXIx_(INT*9Khs zz`ggcxD1IX9TkkxOiyaD-?-Vv{zqh4e!f?dOPKs*XZBG@W9tk(EGcRooB1STn)ix1 z@<1OG!12*$8uqohp#m?Pv=G5n(t^Gg?&JeeY4(xrwC!_)$;s zNOzr2^f3DZ)a)m@<>EzbOrlRWO;REEoS$=87!8tM4JX_;4R!+>cVR*^x=0Y64&19A z7OC7CW{ln+tbkso8}cy~ zxiKi=aQsn8IQ906XS%%B)@29n9%X==`{~(tartOzyps3=`R$jWo1|0{UsI>HK4t)t zCT;6iB2lfT*>5!y#mqWejGg!-HlI6GY1GlAFk%QT4!TwG%65;`M`%X6uF->^=Qi}fDcp-ol`U5WQo(*GU+m;ZXr+# zDGtS_Q7NYbk_Q6Y#)vamyD@{&DnvS;<_hhtC_b3uYf1u%d^>o9sit*lSrlFW`LsgL zDLhiXFTipXx<91Euw9+S#Le&m$O078Ga{&7bok%PWz#>YeQRL~McBA=XG$2#6+;jm zj7oqE1>JLEL_Z$Jh92Hc91=a~8f(!mcVB+E##Eri4?^kT!1aVn_D{nSNaWh(ZIjg? zOq4ZO8`D{249T8>m)OypDSa0i)67?va$)a4f#BUs+fsbSh!3Nr)6cif^~87Cz9tv+ zI|8E|=i+4}ISO+OZ4sTfGO=F=jSZ=ep6haeL3;OO{oLBk$Q~T9ne7Z*Fxt%+$Hi*! z7TJ^1B%iS!it07OIHdPo%B6V)=TyL2Yj36~lns%s`-u@l4!nP!apW{YA&l`94bET$ zSfF8^M>HOtM!O!X2LdZzf9$hzslRT(7rf<>Px*Gb%Y=9@sc!^QR064^*b6)uW#KJp zx3o#OwA09DuWn==I$?cYfQjvCsHgF1{?ur>6A@6`0U+~i_^DRkA%H7nw zlOlm3H%2^5=uG~7ELA%W^fLg;HHIeu(N|~LkafByXY+1q)rv)!^v)>Td7I^1`uXWR z%u`w89U2Vuhr_o+EfUI(Vv5_C_0^Ad8YqQuCBL~yxIlt- zqT*A@iumBhd5=7U(3v;kOnx~2-I(9k?aw}+^9K0&Ij>G{XX`x9GC#ivX@-7~UK8(2 zLS#SderXEp7b!`k^Yjg}M;Axti+b2(2I*AW#j1yIh+2_|L~gClR?4PH!TJS$+#)l9 zcYRFW)?17gT6LTDPpd^c-(GT#l4A)l{YUUdjvw>HX7Znmj_}8a3y|PafyyxI(YVeF z{Lv-Qw(lI2Y9(X1fzT~*|9${0Y^sMASGmT*d)}s8${=w@+*P=S5A)ghmwm+lnZR+* z_E1F*0^P@VQ0%^52cE5LE*fkO^nWs6;r}|)+%n4>>S>6v)<}lD^L#FEK!I_`XoP*sc z2a}@xntirulgR_5yrL=%wVN$7e(WbTrDnielb${i zeDugxGi^ks_n=pCC;ID%Y0Ga$C}yjIuZAM^q2B5qg zf1;y(ez#H;g3|Xa5+t&?@#%Qk-M(;xwEsg~rY*R{;%%&2uwr{tx8l_+eq-!UmHik( zc;kIGv663RV`6+0I+JFa@Xk8(P0Qmq*cYQ3vw^2h8}<^l;^!%U)#bx%hadM%5lE`D7K?NJ9iKZg z_bZprY(op?N51-He+|W#sK)BiYZ=7aQ!b^!B4|}de-7#}zRNRqSErUU6~D#Ee7=sm zpYV-;N>6m0L)4IM@+lTg+v{3r*UTiX7KYp1k}fc|7lSdw%4A6$0vYk)sWyLL<%ZqxKy;4h?lL-#-N4!bDX%{>s+eah&*>l(FkBSVtZ z&vbQHSw9t@F*ur^Xf&3LMuy_%<}&fz#9g2e&HsUBz=M);0{aekoCXUf@Hg?mD19Q;@IN5c ztV)~-N!_Y82{tUZESu!j^Td&W zcETBLexA%m3~)-%?N~O)k;0&T7QlVRuG_?KcHtzx5&cdeMV4|GmoX+(osym!+AAY| z%YkKOcr+w9h@wvX&_t`=RloT?+lg;R;?NpkyqZUv0C0Q|Zc$ePj`WRW0XT_E7GC?T zNS_gp;AO?wOka=+&9uudm(OIHnA&}Y?H4ODoH*h&S3%ooPeiBpSJV6vi~0g3!+K-b zAvFJQk(GylyK~~k9E0~4vJtA&mybt&B}LzRkq=c4-n zgJn$te3admE&p`+mmuNt34!roQOaEqtXei<(BM5HHFE;&~>p< zg-&wluV05n3kS-{!n1xI#|HLuy!qcW)Q5M@98yYBw~zT~kjCdYHMfcX!aiZOLVz6XaHubn-f5LLs8m|o^}j?1{^0Uua0q4z>pY0U zTPfgiGqr<0?4RAgx9Ql>lyqydnOv<84BmC8T+NjEDw4N`+6=l6=DAiS{!g@KglPO9 zK5YOft%+pG=`iU(_4TZFaP9eo;s1QPhFrr|&)!FPXoEV3u;n3lQm5=-oqUniX#X-p z!HG#}TX(I&E~9kYInSOMr>|nH1qv7ec;6gB;BN?hrTQ74dzFeqlX1uWZG6^jf_*%r z-lXLFA~!XLgjfI(`f6V7GY{BpiT3bghDG0&(*@h(iD(mvq~)(o$9$F#+Z8VivF*`n z(ZG6ruhO|n7c$!Fscl2_$sMuBqwukJlFtcq1KZvALqwYj)-rF)&xhwG-!4qbG||Fp zQynRI&W}9b(f1SS1P=;CmL2}Df zZ;}XK-Z^8$CtWSyO~l4M85%l+0^Q@ptP*F&>?M8I-u!UPN&_xcz|I?QscosSWZH_> z3o6-;RpUl=JSooleR9`c)MQmMj_Cope&ikuJ|0$g+M4IBunW~tDMb(wA5l+79rosW&P$l1G;@f zw|!B-;^65vud?o&B<|4PNexYn>p_@6e^21yKl`8kr~{$Fcs zqN9?%6^ascJeZ79@}^OvI8i1os-eme+T7ZFX~ai4j^{bk7mLk@{!RxSPP1TF+cieY zZ>CClywap(!YM-k@bJd*At#G9sAT}{B^p5|*mIHkKB ztPZ+e<{IUE6zo5Ex^Sw@|Jf5k4c#ii zzR3zGnEGxFZ1{p=1!g)ZJM!HS8h%*!YtTgB?DAd*g*aDGJUX&ZP(u@g@ntL9P?s^v z&Zvcibz3yrwxGhIEw*^Y)U9sC!$x5{NGVJ-6vR?c;e zxS{PMYxepnVDHvgut0S@@+qm%`<4m4aCh`E^WK6Px{wldNBsW81m1`J7R61!mdQ3l zi%MAW^+PMDqBltgUDoNbr9CY0BtPF0;3S=|S22A~rH;nG*e=g7f97N8RYhE-(p%6= z7cvNQ)WO2#GP}$S{?F>QGXaB9}uzMcCVk>3jY zqOPSKthiZF5z%g0jd?>^_ACG*YCPgUB{=R@EB%GwiRlTZ8??BC{ys2SgSp|px3p|X9p z^MMebch^mbAfJWhxP)o#ErlEwXCK5~uLP22;yeWwL*F#Wyz%UU%dAW%EZ%##86Fds zd=;);T9bAF-4Ym^)io)`_zugQt(=?##KKPJ%Iah>(k5rT?=c|gpHLx%15@Hqtm%pt z(TeegGx46Mjq$6EKntgC+M#Ni7e-4yz~dg3!P_5xI`&}aRX>ndl!tfzT6s!Iqsl0T z7XJJh%eWW7WMDWi`>tK4u*XWobN-_P#0RxDfbSkd*KWUE`d{sj_t6kUT!vyYl~=Up z8$G_sBfZ`0_-p4G10R8=a6@h9r;q}2dp~ z*d$+fpc;lMSd>Uf8SgeE3)3nOBZQ;!pJzK~%$9Ts{Cco0KUOhEs>dmBURtlATziuOW9-vSK6ay!=u!*pm;B#=3v-S zeMJu3`p-l=WK-;D?;)Qbt?r_p%c35q=h+%T&^_$($_e!Fc(wVZW&(b;-vN$soXUWQ zbv?PD|MMJx%bv;L!=wSbhme=OM$e3Ytf=5{4D)BfU4?*(h6HhanVYcWG z*O0%S)ldRE4^KLyEm@Pl!-)&-@UVBD74VN)pwq)2B2{TO_Y)_!t!iJu3mGjs3XB%= zjVDQ>Adb~|TWjvD9yQ|<3)DM0n4=prd0_Nc>IpBh^vf{oD)MJVe!B->$87vtJGYkW zrOSgiagh6e?afJ3Zx?L3mPozfxt|1{7EBkLJzH)wCMp>yn!wM#M<=ebZPedxzlT?n z1D}8Oaytr`{HfmR=x=0KFWyR`c$v;@uADH}=z9NBdqcL>>i(e+Uvg|zR=JPy(Ou~o zU@{5hJNMV)wpi)4!VFtAQ$@Gp#5lxw!{KUGO?1y`cPx0n+#DK8Zy_msYp3&S@4umH zTOs0%@ZkRd>=TJmfVd;=@wPO(u)^|N&A1r3gNauE{)US+K0M7Q@BBBO7}2EFM$ax9 z?)wYofY+{EWt?KRJx*XTwV?HUM-s^4W|J63D(k#+$k8=bu(hK(nVM=9kf>SZ1)E9} zFWK9DvlOhFP3ZykbE5`JK@%SFP}l^MGp&txK7+4W9`9?{uetBaj^f&2k3B-*a>0#e zdDg4oO8J3Af{*Q5n{(dln8nEOVP`|Z-#u_MM?Y&?QWbzh1#&)mo^ z^gc`r$WJvHj)HEFK!nfbbL9G4g|O)d%6UBe2E_&}4565JHxva)bHepS9S;uJ!oJD^59WW05T z{rol6eBd@rXM;o5w~d<)HZo8T_8ywD`JR&aaY^SguW49|gjwi^as#CcG z{VTr|n~d1mbFX~@1X~PS0zXVs&^25%A1yltp>Xg6L;9g||61h_1iM~9H!gCfis&!x z;&VPo*Up?pThMkD?pFWq=Fs)}MU{*{K_>Tqc_l=sHy(jyNGlsNFF|5UCjm8;1|str zytUw!=9g`AjJ}l?J&4N=q!xleeilV+b(q=|d0m5vxw49Z1qcSV1zZ zxFx#(G(acH3tsLHS+TMzR~+EqN7R$vTEq2{uzK_ z9nr$EMX2vnpDqKvR;4UrJ*u8?)Seds`IZ04`c(IvKT#hrfF}y?e%c@a8)vi(KlCsh z_pVL5lsVKk?ZCYrV4hQt&40qIWP*RXcN9_pqNfUnZVeZbl{V*S)tV#XZex^UkV;mH$E6i2ORp@x|C#ykoM5&h2Fi z=k|z|5w#MKe0$aMhYXffK)6<)AIjJ){HN9BsB~vxsfB7oaUgLbiN6ne^T8Uf zAmLOXX1KL4HW!Hw{K^6Ryw=B%DB$&w`_ydf?ByEk7e>wF$$fqch1Xj61)Z$B24_q& zU}zv3M*k9fckp!uXYT)7n^~NLbZroJoaS$Mh@BZJfh=T(#qhn<87e5^M-K7Ie-#>-l5kg-F0+@i! zPA_7oN5*PpHPdAa1B$=E*TALpFU>#HWyQcNaGfdm8D!1qhX`Y=#|HmRolkn^)zFy^ zS74@8*H^I@HanOn2k>GB?(g4odh2li_r?E0Ed{;Cxv9s0nDqQ((8|asZU_JLhivJU zn9!DyZQOqJAwF;ngCyS0Nu%#!NPuj}qV{4%%vo1}OoQEDF?hfazwWL5GTRoH7%=?V zq0N7}%(C0;s9^)x;zc)EE8B2V7M?*1n7r^M!+W%MyZ^hnK5{a99;~D;)yfwk8pyk4 zgmfBj$FJQ)@!E7oMFP$2Wa{oB_l!o&NWyCHS*Rf|6DadE)zmS$7Yu{7Ou=twUM>XB zTD0?JW_{qaRn~$VwO>XbVqWeW&SZD)zBB*=SF*!p@S)-Mxvwzz&q?nwt5q}6pZ}Zz z#GE)OY*L%fZ;FRcd_lfpqMdOaedeO-f+3>M>wG7hOyNruFTtHnj~%vc9hoxC+HfaJ zksmVDRg5*HES{gc3*ez3Vtf27&sL8Ei+3&0Lg7BzI7@Kpb{)Vt{0}k|E%Zt2^g_?z z)z1JA%{kJG+>76tB6yqa*1v6oj5~P^db&35HXOPxTjvGWe3UkAcGm}*XOhl@M!gtz zhmzpojv2Ecd&nxAL)i66CpHsU&8FbR(|-onNRZALl|kZ_$%#Tw)y(?tt=90%vv>V^ z*Y$JGdubq8Q>NvRfIUAmbN>{mX(<|@9rioZV^iyKede*-C@|FCWEa=SLXLz<#tS%M zp%P9Ng5v2!5cAlRDbJs?9to)9XzaRNOdW-EWUImM>?+tNHYCBalZ}FAYlw3tW+yoy zsznErG3M@M>~Qx-rRCwXChSiMyvKf5YWUOeA2QU!-cyP|`d9&7{WpO@G_R-Ggg&|_ zx;$>Z-a3{c#^8^9k#yi2CU{oM3?XHhiUl~)5(Z|s`CNo}zTvMg--fjnoHY0eKTI}X z`#G5kHn^X;!y9X5@7_HN{Bu@X8(~&F8UYvdX~z&*<)JVZ0zJ*%gIJn`bgF?4JAh7dJtMxl&(H@dVX;b>HZevGy5;Jp<6Z}l=cK;g#aSbg>bps~CB{O93rOZ}73pR4DRi`#eBGFxYV?$D;ojiKmvhu^IKV+*hG zKr$MeFQKFCwBQj(+jv_hx;ThsmGmyxPB!1SZCxil*yO|7ZJ4MaI0^Yr$JpTpBuMyS zo&2hz@&k=7h~m)!A8AOfQKTzD<*{}CrjVJ#Um5b6~zIBVl zbGtiI*l?0YV1@R~@bH|W7L_7(=P^bv2*ezS6&83o9vDltR>nMrEb36RY(IVh=);`t06 zH#gJ2!*s->BW_xZeQw-oNK=L(JY={$bAK)EEH`IAU7r%ZE13TPARG6@J@Fs7_stZs z7kHbY!yVc*!M|}M;(Oq{szLsE`23DJ!j#dAHhTWuFSRkkaT@Gp`R?wq_1E*94V=l^ zwE0ep56$&Y^IH1+JffRkz{!yN)|(ATXvdAW*mijj z6}irJLpH{RiOljg!X3-N=By@5JQtD)NS6^NlQ-}HIxjgEt0!m+QR_m5K`D|Bwk6;@ zZYVkfYOsJG!}Fug*V|}+8Q!WEqsRbPaZ%ifP_OjNiKMXQRR)o(&C7PGm7QjhX6gOU zsuAe6y4C&E}y)Zp@oE*OE`#v^LV*!%QHqLwe}v%`K$=^e2s4Yo@;=w-O1k<)tU`vY>&| z6E-Y}@+1Zy{>99g8?Ud1mE=Bbk}sFa8{Y1<@}tLfWQIN{(%MMg3cPj(&iY3;&X0>% zo$S7eSmE_rf9Gme)JxgPS7?4^*`=WDwjnMf{KV)@X~2|wC&O3eR!d(Z^uzBsZGXct zJM+Tw64qX?b)WOW<$+48?!?2aYnfvEkA3^!Zs{JzzG-aZ^s$Ityk3c?xm2p;ig>;u zzQsqcoMz=h?I6ep-)gEQ)vmhpXNQNTE!U<65%wQ`RgDYVw-ln_)lEwfcQ?2 z@rf}-;|y!*i;tE{tsF^vj`jWaeH{sU^!!(gpdZI zVz^OvQj7YAN^N6UV2{cookRez_jn7rb%&iER^87H zOVmt7MLU++tyVQIX{Ccu)9EkAy}@EueP==LI{HVv#^gUYP->o`7*7@f;p8Kf9}@%r zg@^A3C3#ImX;g6~iD-^~3Uu@4f1jCy6VfqzFt4%+;xPxEHe@K_hNZ|xsEPGuo3$;1 zvjA8SO8t)Na+Xu$|CAM-vjw4zmKcKGF|M&Jd}TH#^c?lTKtiSSByhlbkN21+L1o*R%pLopm!Mf`40yQP(UWh!WZyMae({r&qWSOn&2Sz05y<#k zM1TlhzhXCP@%t~G?>u#n(Ae7gu8^wi(M-HUBW1pWW{k0pHi$8{m%+o5zfffbev_=O zG(I^mnyR~1DLgjUhIIVR;5QkTR84r3(lQ}Dh~;3weRwp}aoA|b(nZr@U=UwWMbf^0 zDnjLgJJ?~*gR$K}koi?4G^(LI`nK}x?SUe6S3RsNojyH(!WUe>li{h`7!Pi|C@2nB?~KJ+Mu_0;Uq$o zp;bRQHq2_1x4on*$&4(f25S_Vc~-Tg(_Snn+k`FJ5cWzuub3a5-Fii0I0g0k=$@*c zS{-qt-6TC#4CI~3ERkWMD4V=(`X~9?bqd}seC+p)koJV9%{*kBEo@L?^Snx}Lx2S= zKhqTP67PDU&w@eoQwwt>ccF72;{4_)8iG3;pQVUmJ8)PT?!RQA+2l;?My#0Bo>}w67pY@H!y6H*jnrq#m4PO7 zlw!LFV1u`g^=Z$BKlzT#jgW-Jt>n!ufxrS{M-q+nCw%0-D`KJCN=NuYZ)h=^8_?7X z29Te9;<#}{R6e%T^$&L8yTcl1jdoXtM}FQ?-uU) z-oPjZWodl=ZWkcLf=P${f zNsGBj)1^tkS;&3UPD?_{*N{S0q652Z1u3MHn}wJ3;BuoW+k%Ld1>Bfyg6Iqa2(lEy z9~DdGv>(ZQQqXnYg?msMlFlrC=NB#We^S+8uz3!-&&`__y=F7}kivJh_{Ug}T~~Yf z4;?+gFOp>Rn`;-2TPWIpOahbWERTK!HobEv{Bk|6=)HPEv8u+2av<6I+j8@3fnF#5 z6I3yU$#M_>Z3nk*^9=2c(K9oKNi0?do>j&+LOG^WR|`o+OMlVqL$(HZMU9n_qQqiY;vW@k0~t4{nr?LbJ#87QJed zF-#>+%`j^l#9qn)#5Xb)4#?w#Mk942Rx*S5H}ZZ@DLr{vcU+BRS;iB-zCR&=DrbP< z9`fS!JrVb2IhEsaDm`S56WMwYkWP(jZ*cRM?g%H*4v@>t4QZ15E;MtDYJI{Y! zS`nsD(@OtxxAu3vk~n?|+Y*2{U+VW{$Wl(BMRLhvV9rJK{s6^equ=&N25OjLMN!pL zcVg=d7?P&HUyCAQUz8ixq}@Lm9TxQ_;hxH6r^qJGjHjeUecrXArS=$3mv`Qs18GY^;Z>5u-&0Y< z|D9lO4;qQx{8|5nvgWZ!bM0=gwcN?;>0XW*Yj(*T>DEi?a7gk%>QBZ?r*p*1L8r!e zNm9{chFCZC=Tx|EZ*aYL07SADFMEH1lQtDiOohf2c#BHB4niQr12!`c042<9UiTjw zd-(noJgNC+;+WaQ<*!gO{|nz#pu*2DMr6oOBT7PKSd08tV0~os$YFsk*H=e7N`dw7 zd}jQX`7%b${e4&PV0P0ztbg9?rPRwFA#RmSu*ybW{=>W^=PGXS+sa6wB(>OR_lDv; zrMnHS9S=9Z!T35uzlulBQ9bz^BZTUd$3jebSSuCR&=%1tqZyt7WJCmh}VR!8-wT$kBu9e-9? zlV`*9E#m zLgb&n5f6JRovr1IQkP9Hc7ndmd{e^>xga306lv>F-{m#YXIbIZ-2opyW>DD^9Y(ZC zZkTscV3Fd|iho?`iY^+&HwsDt zy=pTxBtuNjDPR<$iZ~f%{S5yyN=qg*iW!7j=h}6MXtUETH%YN06k*m|PYE25UuOE9 zz!DxCj?uW!3Musc^N69ujc9IfyG$<$9M@!grMOw|HuXfdNBo#TCjQ^l30=#IAxWyF z0U8R}OqwA@yp&1JUF;`VN4_%`=>er@z-_;<&;$v~mKvP`S`XmRT-2~i5<3tr$kIa4CEZsL*z;=mT?m?tlh^l}4z5-YXWF_& zU#&?w?LB;)TLV_DBRBm(x&ML{Q1vBQl`=rXC&9@^Rv%OMp_RQe^Z+vbSn-+&+M)Mf z4Ceh_7uop6Eq=sEYV}Y9BO5+e<)VB3#(>`K@}9^GHx$V!Dw&j@eg`>bT=~_!Jba=M**T{ zo?g~-Bx~`N876B5?B2O)wTRxNf+BnEHTtKD*CZv{|lC7@j*lr=be-z)1qN{3;#w!dr>_FtYELyFx=se~Y_o-wTV>IH)tN%4g9JVeQm z`*}`|B6J3t-@OeuAi}kJho~ne2L4|FMG(60qJts;%5cY6L{lK0{pA$*mY6_XhEhWb zAk59tQXudbot7d%xacPNYLPyd!6y;|c|_iFf?D(CgBN$q-dM41C9nH&sJ{z~6948Kf2Rd#0w7rsT$BSXV3~4EaRfKt;Y>&_|y4^s|o`WI9p6EA9ro zC1PVN!{p!>!bWS>6bSqUKBY)n=!nyr5_NoDK;+OH@A#JNo>N&x8rSBq%+A>?Yx&tags==%Yl6Osdi zj0J&^Hb`X-)<7`gt@oM|LD)GFWnm+%)NQmtAl#yEDG|gMBXf~1zhOl_*Ch8Ab=m9% zcuvR^I%Is51HUiQfYnH8Q-`~A^_sivVrVI=xnDuj3#DhIR?=!{q?>FM%F9FNu zIB)()@jOx(R@{@U=iyxVmC~^N_#!Qcm%cWax4%;!s>{o|hBRkBH{ct+>5H{cvijyA zlSR-FBZy0j9I1!_gjt_aX^aT79FwLuM7WzKY2Z`BcG>qmpsy|Razds=ioGbwg12{a z8=QNhJbb^r{xobe3*fihE%xc-k$8#qRX%b&1)+Bfgj{(4rZ_?@|BBN17lmP6EF8v% zwc=BsaBL!gc+Ho~xziHg6QBBlDFwk|jw{#XEL@gx7PkiSTk;kSz~%yS^`v*HR{K|r7P5P9)gR~mn4Kac6<5g1x z2p5rt5>kZ*K%%K4bGf0{Ed4R~EO9d-z&Ku+9S5+I?kY!gQyF2Gw+ z1PFgpOd4l^c#&l}cfpsA_zn3+d0Ua+qil(~Y@`8?tEQzLfSkCCWHcbHup4J#v*1iB ziUi>nVp2dJ@ww?OF#w+2_>pWAZ6Hrg`8f>KWg`pXT{ZTEsA&fvC+-F^e)kxw*@eyZ zh48Y(DTTm71)ifqW`}T+2R=F5=ZtUVF6m*ApKR4+Z<67M4Q02~C&*UIlbR&?gm_pB zQz5)QN%Zy+=i^NsVHzv2EinP!_*PAsK;BA}Q*5oQtInC<4TJ^5vZp{g{ggZbTM_{2 z)_X%tAZ&t#Ei40{hCaj?O^f})u}ge}V4yA=e&Cnpwd<5!kUjAv z=gds0=*OCekGPS{(Sc9I1w9)|1o0MhO-z8d_EWZ?>(KW-Mc#6l0BlhsS(osrt8u2j z7E4l=LoCC|%{@YO&j3E>-T6pLO zckQRFmM!wuly}iQc?s~=RnyXrK>h@4NeHCRcgyZJLL4w93qRR6!4NQBU!ZUH{qG3I09i3HM?Vu>6!MSqC|$wunrRyIJwO(rof z@+aB>@L2K&nhtpzKwjJpgdYRJ^BB&aB8GUO<49SIfvbdX>3) zypj>*5qHs(34BAl8xkO)Z^_3E@o-glOtms=Vg~%V@fNXi8y@=;YTUG>TtCPo?&9SY z&{x9apdx0Fmes?N%@XU9V0nB;`bgwlX{mpr`Teq={X)uh zgY+6LzBD8Z7$;gLA&@X=j2J+?1<#f)iuB)!ec%&Qzm_n-t_1?8F)90ZMap%9eBz$P z&j-HIi46&Wv@}7Mn-1cS5oab1M|xsQ0zf9e9dJgQs5HsheH)o4`=!j7fsUjwZY^oLQY7emQ@iNbY>7E-$UOF2GF-{9sYryjV$V@O*jwl`=3`K@IV6 z7JlNWDUCDj54UUKiPEvbb*U7@mgKIL4|@k)uaTSg!EX$H;NaIBe)j>+(WXx%OUeKz z%T>8(EQ6KIv0oyA%fXE#<*Kpl8gx~z81NP3a%m|O;7yCPR3r!r8J>^yY2GSg1Uxm_ zMFu`Oee57hO$rN|COI%z**1ldE2#LC(eG~I^u0exK6Rkann)3#pNu6QQrqEn({QBi z*<;&~9Ne=%O*uh>t}Lj)J2)c$L}{E!{v!Afmy6*u@wyA7>duVRl*XC(NEP^sc(Dcs z$Ia_A2_c*tZ>gxytl6!c!L^_QKfzjtmzNd!l#GGD=JLA_mJX-iQaZS5xd#9HNJWe! z&eiHt3A;L^ijj@jCnF8ICZYnrz~w@-q8VeQ{3e_`K815oljVuzNp)+SsT?ZeCv{Zo zh*rfUCv! zzk>!{gHeI^WEg~EMgBbV^=~=FeSq6~&sABEJFTz;B=+fv_z?AD4+hW^$zsL*8qXc2 zB=B}Xl*nAA8-3s&)CA<2VT#MQ%x1}f=U6_F0N|^`E;{gO=xYr*?5tlEDZ!`(ABdAH zf2JM$?!$>Lksa|w^3=ot^E|d^O6UKQ3|!+8qz67p79+Dy$A8-43Hd5efsbG~*=+rq z9Ft~Gu|cznczMip0o9bwnaZOk88}^L(!o|sOMC&>=NeI&M}uD-D)5mE1ES895%AV} zcaa{sI>J$2_e|v><^JFr)oQWnsWl10m>d81!D=rpn0`1^;E#obYvw#WUW+pLfNM%; z_o@13crW22W*pTzA~x`8WC+whQ5!LPH`LI%78c%FK6OG zD#L?oRI8;%oSi$(%iSker54mF=zEmW`Bk>~ciyvH< zz5!Re?6cLrLp~ zQt%DF`awc~e-M%Qz$d5g(;2fKpd|+sMXXxnNJE)Gdd0^7Kmvdd$!YSqMzxM)$y7mq z1GRlO(5jHxQ5Q|wNV#TmOzPpn<$o{zalRrsxW_8w82B{0YW_;vUS6}-+sKA$tWD&Dmx$k5CenmV;i3HndQIdmmRO^WN zz$ay2RNxB)y??wW8J;qiSl|@BGs(auoD~TnFsjv3t6%)OQGqWLNy{o%-I)a04K|tj)SP1Mrc&)hz@+ZB2?h7jLXH}UfzD87opERa&_cLE*fqs zl1E}xt3?MsT@fnqSI96}s-=F`oF~f6@R-jn5@(Ww+ub2uvcU34GT7fut@tZJ1^$ZJ z$u(nDay{M1+TC%_`XF4GEiAGZql|4~`S46l7o3-R$S@TW`WldqCe^kG+-M=#_%1)@j zE7@>F|LgeK;J3H0ntIWJ!dt*|rgm8f#E&-0R#6n?w#YC^hoUHoq9}@@D2k#eilQirq9}@@D2k#eilQir kq9}@@D2k#ein7B02ieUg4&s@ba{vGU07*qoM6N<$f`+VvDF6Tf literal 0 HcmV?d00001 From 1ebd241f51c4af513ca7d6576ca4f161bee2ee93 Mon Sep 17 00:00:00 2001 From: dunegxn Date: Thu, 10 Sep 2026 21:53:55 +0530 Subject: [PATCH 2/7] +/liveWiredIn --- app/live/layout.tsx | 4 ++ app/sitemap.ts | 8 +++- components/blog/app-download-cta.tsx | 31 +++++++------ components/blog/cta-content.ts | 2 +- components/global/announcement-banner.tsx | 53 ++++++++++++++--------- components/global/footer.tsx | 6 +-- components/global/nav-bar.tsx | 11 +++++ components/sign-up-button.tsx | 11 +++++ config/site.tsx | 8 ++-- lib/site-copy.ts | 10 +++++ lib/site-text.ts | 19 ++++++++ 11 files changed, 118 insertions(+), 45 deletions(-) diff --git a/app/live/layout.tsx b/app/live/layout.tsx index d99df57..b030c90 100644 --- a/app/live/layout.tsx +++ b/app/live/layout.tsx @@ -27,6 +27,10 @@ export const metadata: Metadata = { title: "Get Kokio — download & guide", description: "Download Kokio for iOS and Android, and see exactly how it works: sign in with a passkey, buy an eSIM, install it, and manage everything from a wallet only you control.", + alternates: { + canonical: "/live", + types: { "text/markdown": "/live.md" }, + }, }; export default function LiveLayout({ children }: { children: ReactNode }) { diff --git a/app/sitemap.ts b/app/sitemap.ts index 4de337d..0cd0322 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -5,7 +5,7 @@ import { legalConfig } from "@/config/legal"; import { GLOSSARY_UPDATED } from "@/content/glossary"; import { getAllPosts } from "@/lib/blog"; import { getManifesto } from "@/lib/manifesto"; -import { HOME_UPDATED } from "@/lib/site-copy"; +import { HOME_UPDATED, LIVE_UPDATED } from "@/lib/site-copy"; /** * Every lastModified here comes from the content itself, never from build time. @@ -30,6 +30,12 @@ export default function sitemap(): MetadataRoute.Sitemap { changeFrequency: "monthly", priority: 1, }, + { + url: new URL("/live", siteConfig.url).href, + lastModified: LIVE_UPDATED, + changeFrequency: "weekly", + priority: 0.9, + }, { // Posts are aimed at roughly every two weeks. The sitemap spec has no // value for that, and weekly is the closer of the two neighbours, so a diff --git a/components/blog/app-download-cta.tsx b/components/blog/app-download-cta.tsx index 2b3078f..75c36c9 100644 --- a/components/blog/app-download-cta.tsx +++ b/components/blog/app-download-cta.tsx @@ -1,24 +1,23 @@ import Image from "next/image"; +import Link from "next/link"; import { ArrowRight } from "lucide-react"; import Logomark from "@/assets/logomark.svg"; -const PLAY_STORE_URL = - "https://play.google.com/store/apps/details?id=app.kokio.mobile"; - -/** Download card for the Android app, dropped into blog posts that mention - * it. Mirrors ManifestoCTA's image + frosted-panel structure, but the image - * is the same beach-scene art behind the homepage hero (see components/hero.tsx) - * — clouds ~y0-600, ocean band ~y995-1120, sand + a bucket shape from ~y1120 +/** Download card dropped into blog posts that mention the app. Mirrors + * ManifestoCTA's image + frosted-panel structure, but the image is the same + * beach-scene art behind the homepage hero (see components/hero.tsx) — + * clouds ~y0-600, ocean band ~y995-1120, sand + a bucket shape from ~y1120 * down to ~y1582 (out of a 2068-tall viewBox). At the frosted panel's usual * height, object-top only shows plain sky, so the crop is pulled down to - * ~58% to land on the ocean-to-sand-and-bucket band instead. */ + * ~58% to land on the ocean-to-sand-and-bucket band instead. + * + * Points at /live (the interactive download + usage guide) rather than a + * store link directly, now that both iOS and Android are out — see D8. */ export function AppDownloadCTA() { return ( - - Koki'o on Android + Koki'o on iOS & Android

- Koki'o is live on Android — iOS lands shortly. + Koki'o is live.

Pick a plan that fits how you travel in over 200 countries. Pay @@ -47,13 +46,13 @@ export function AppDownloadCTA() { and secure.

- Get it on Google Play + Try out the App today!
- + ); } diff --git a/components/blog/cta-content.ts b/components/blog/cta-content.ts index 7f7a5b8..272514a 100644 --- a/components/blog/cta-content.ts +++ b/components/blog/cta-content.ts @@ -12,7 +12,7 @@ export const CTA_CONTENT = { live: { heading: "Try Kokio", body: "Tap. Authenticate. Connect.", - ctaLabel: "Get the app", + ctaLabel: "Try out the App today!", href: APP_URL, inlineText: "Ready to travel smarter?", inlineLabel: "Try Kokio", diff --git a/components/global/announcement-banner.tsx b/components/global/announcement-banner.tsx index 74882a9..cc42d15 100644 --- a/components/global/announcement-banner.tsx +++ b/components/global/announcement-banner.tsx @@ -2,11 +2,12 @@ import React from "react"; import Image from "next/image"; +import Link from "next/link"; import { usePathname } from "next/navigation"; import Twitter from "@/assets/icons/twitter-fill.svg"; -import { TWITTER_URL } from "@/config/site"; -import { LAUNCH_NOTICE } from "@/lib/site-copy"; +import { CTA_MODE, TWITTER_URL } from "@/config/site"; +import { LAUNCH_NOTICE, LIVE_NOTICE } from "@/lib/site-copy"; const AnnouncementBanner = () => { @@ -16,24 +17,36 @@ const AnnouncementBanner = () => { return (
-

- {LAUNCH_NOTICE} Follow us on{" "} - - X - {" "} - for updates. -

+ {CTA_MODE === "live" ? ( +

+ {LIVE_NOTICE}{" "} + + Try out the App today! + +

+ ) : ( +

+ {LAUNCH_NOTICE} Follow us on{" "} + + X + {" "} + for updates. +

+ )}
); diff --git a/components/global/footer.tsx b/components/global/footer.tsx index 0d8ef37..2725565 100644 --- a/components/global/footer.tsx +++ b/components/global/footer.tsx @@ -10,8 +10,8 @@ import Blog from "@/assets/icons/blog.svg"; import Twitter from "@/assets/icons/twitter.svg"; import GitHub from "@/assets/icons/github.svg"; import Docs from "@/assets/icons/docs.svg"; -import { TWITTER_URL } from "@/config/site"; -import { LAUNCH_NOTICE } from "@/lib/site-copy"; +import { CTA_MODE, TWITTER_URL } from "@/config/site"; +import { LAUNCH_NOTICE, LIVE_NOTICE } from "@/lib/site-copy"; import SignUpButton from "@/components/sign-up-button"; export const socialLinks = [ @@ -77,7 +77,7 @@ const Footer = () => {
diff --git a/components/sign-up-button.tsx b/components/sign-up-button.tsx index ecc9e30..9f9668f 100644 --- a/components/sign-up-button.tsx +++ b/components/sign-up-button.tsx @@ -1,8 +1,19 @@ "use client"; import { Button } from "@/components/ui/button"; import Link from "next/link"; +import { CTA_MODE } from "@/config/site"; const SignUpButton = () => { + if (CTA_MODE === "live") { + return ( + + + + ); + } + return (
- {/* eslint-disable-next-line @next/next/no-img-element */} - {/* eslint-disable-next-line @next/next/no-img-element */}
From cd5d1b3450f7606c0df4c1d0b2366f9c7ac943d2 Mon Sep 17 00:00:00 2001 From: dunegxn Date: Thu, 10 Sep 2026 22:17:30 +0530 Subject: [PATCH 4/7] all->Kokio --- components/blog/app-download-cta.tsx | 4 ++-- components/blog/manifesto-cta.tsx | 6 +++--- components/live/step-checkout.tsx | 2 +- lib/og-image.tsx | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/components/blog/app-download-cta.tsx b/components/blog/app-download-cta.tsx index 75c36c9..34e07f9 100644 --- a/components/blog/app-download-cta.tsx +++ b/components/blog/app-download-cta.tsx @@ -35,10 +35,10 @@ export function AppDownloadCTA() { aria-hidden="true" className="h-4 w-4" /> - Koki'o on iOS & Android + Kokio on iOS & Android

- Koki'o is live. + Kokio is live.

Pick a plan that fits how you travel in over 200 countries. Pay diff --git a/components/blog/manifesto-cta.tsx b/components/blog/manifesto-cta.tsx index f075a3d..c82f8cc 100644 --- a/components/blog/manifesto-cta.tsx +++ b/components/blog/manifesto-cta.tsx @@ -21,14 +21,14 @@ export function ManifestoCTA() { />

- The Koki'o Manifesto + The Kokio Manifesto

Connectivity is a human right. Privacy is its guardian.

- Koki'o is also a provider, and we say it upfront: don't - trust Koki'o, verify it. This is our position, written out in + Kokio is also a provider, and we say it upfront: don't + trust Kokio, verify it. This is our position, written out in full.

diff --git a/components/live/step-checkout.tsx b/components/live/step-checkout.tsx index 09033a7..52fd464 100644 --- a/components/live/step-checkout.tsx +++ b/components/live/step-checkout.tsx @@ -247,7 +247,7 @@ export function StepCheckoutVisual() { {couponState === "valid" ? ( <> - Balance: $10.00 KOKIO + Balance: $10.00 Kokio
From 6186a00ab4c90b9258d745ea9ea2b87facd5f3a1 Mon Sep 17 00:00:00 2001 From: dunegxn Date: Thu, 10 Sep 2026 22:31:56 +0530 Subject: [PATCH 5/7] +alignedIconIn/live --- app/live/layout.tsx | 2 +- app/live/live.css | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/live/layout.tsx b/app/live/layout.tsx index b030c90..0322e3c 100644 --- a/app/live/layout.tsx +++ b/app/live/layout.tsx @@ -24,7 +24,7 @@ const jetbrainsMono = JetBrains_Mono({ }); export const metadata: Metadata = { - title: "Get Kokio — download & guide", + title: "Get Kokio & Usage Guide", description: "Download Kokio for iOS and Android, and see exactly how it works: sign in with a passkey, buy an eSIM, install it, and manage everything from a wallet only you control.", alternates: { diff --git a/app/live/live.css b/app/live/live.css index 3501668..0de207a 100644 --- a/app/live/live.css +++ b/app/live/live.css @@ -804,7 +804,7 @@ * row is a link back to the site's homepage, same as a logo usually is. */ .kokio-live .hero-title-row { display: flex; - align-items: center; + align-items: flex-end; gap: 14px; text-decoration: none; transition: opacity 0.15s; @@ -816,11 +816,16 @@ width: 40px; height: 40px; flex: none; + /* align-items: flex-end on the row lines up bottoms, but the heading's + line-height leaves empty space below its actual glyphs — this pulls the + mark back up so it lands on the text's visual baseline, not its box edge. */ + margin-bottom: 23px; } @media (min-width: 640px) { .kokio-live .hero-title-mark { width: 52px; height: 52px; + margin-bottom: 29px; } } From 66596ca412b53f6354c6e369b8407653f6cb8cde Mon Sep 17 00:00:00 2001 From: dunegxn Date: Thu, 10 Sep 2026 22:57:57 +0530 Subject: [PATCH 6/7] +activationTrboubleShootingTips&tweaks --- app/live/live.css | 48 +++++++++++++++++++++++++++++++++++ app/live/page.tsx | 17 ++++++++++++- components/global/nav-bar.tsx | 11 -------- components/live/content.ts | 21 ++++++++++++--- content/faqs.ts | 18 +++++++++++++ lib/site-copy.ts | 2 +- 6 files changed, 100 insertions(+), 17 deletions(-) diff --git a/app/live/live.css b/app/live/live.css index 0de207a..5f20b97 100644 --- a/app/live/live.css +++ b/app/live/live.css @@ -405,6 +405,54 @@ flex: none; } +/* ---- closing tips, between the step guide and the Manifesto CTA ---- */ +.kokio-live .closing-tips { + padding: 40px 0 8px; + border-top: 1px solid var(--chrome-border); +} +.kokio-live .closing-tips h2 { + font-size: clamp(22px, 3.5vw, 30px); + font-weight: 800; + letter-spacing: -0.01em; + margin-bottom: 8px; +} +.kokio-live .closing-tips > p { + color: var(--chrome-muted); + font-size: 15px; + max-width: 64ch; + margin: 0 0 20px; +} +.kokio-live .closing-tips ul { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 12px; +} +@media (min-width: 700px) { + .kokio-live .closing-tips ul { + grid-template-columns: 1fr 1fr; + column-gap: 28px; + } +} +.kokio-live .closing-tips li { + display: flex; + gap: 10px; + align-items: flex-start; + font-size: 14.5px; + line-height: 1.55; + color: var(--chrome-ink); +} +.kokio-live .closing-tips li::before { + content: ""; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--chrome-accent); + flex: none; + margin-top: 8px; +} + /* ---- device stage ---- */ .kokio-live .stage { background: var(--app-bg); diff --git a/app/live/page.tsx b/app/live/page.tsx index 5ef8c08..36ccf92 100644 --- a/app/live/page.tsx +++ b/app/live/page.tsx @@ -3,7 +3,8 @@ import { LiveThemeProvider, ThemeToggle } from "@/components/live/theme-context" import { FlowProvider } from "@/components/live/flow-context"; import { LiveHero, IOS_URL, ANDROID_URL } from "@/components/live/hero"; import { StepSection } from "@/components/live/step-section"; -import { STEPS, SETTINGS_ABOUT, SETTINGS_CONTACT } from "@/components/live/content"; +import { STEPS, SETTINGS_ABOUT, SETTINGS_CONTACT, CLOSING_TIPS } from "@/components/live/content"; +import { RichText } from "@/components/live/rich-text"; import { ManifestoCTA } from "@/components/blog/manifesto-cta"; import { StepAuthVisual } from "@/components/live/step-auth"; import { StepShopVisual } from "@/components/live/step-shop"; @@ -72,6 +73,20 @@ export default function LivePage() { })} +
+

{CLOSING_TIPS.heading}

+

{CLOSING_TIPS.lead}

+
    + {CLOSING_TIPS.items.map((item, i) => ( +
  • + + + +
  • + ))} +
+
+
diff --git a/components/global/nav-bar.tsx b/components/global/nav-bar.tsx index 4680818..2c67364 100644 --- a/components/global/nav-bar.tsx +++ b/components/global/nav-bar.tsx @@ -16,7 +16,6 @@ import { import { Menu } from "lucide-react"; import { siteConfig } from "@/config/site"; import { DialogDescription } from "@radix-ui/react-dialog"; -import { Button } from "@/components/ui/button"; export const Links = () => { return siteConfig.socials.map((social) => { @@ -95,22 +94,12 @@ const NavBar = () => {
- - -
{/* Desktop Nav */}
- - -
diff --git a/components/live/content.ts b/components/live/content.ts index 97a95cb..394d356 100644 --- a/components/live/content.ts +++ b/components/live/content.ts @@ -38,7 +38,7 @@ export const STEPS: StepContent[] = [ alt: 0, title: "Onboarding", summary: - "Using biomterics(passkeys), Kokio never asks for your name, email, or any such personal information that can be used to indetify you irl.", + "Using biomterics (passkeys), Kokio never asks for your name, email, or any such personal information that can be used to indetify you irl.", items: [ "Open Kokio, Tap **New User** create and sign-in using passkeys.", "Only use **Existing User** if you've reinstalled Kokio on the same device without deleting that `kokio.app` passkey from device password manager?", @@ -46,7 +46,7 @@ export const STEPS: StepContent[] = [ ], callout: { strong: "Guard the passkey", - text: "A passkey synced through your platform's password manager can restore your account on a new or reinstalled device, but if the passkey itself is gone(if you delete it), or your manager doesn't sync, there's no way back in. Deleting your account (see in Step 07) is the one action that truly has no recovery, by anyone, ever.", + text: "A passkey synced through your platform's password manager can restore your account on a new or reinstalled device, but if the passkey itself is gone (if you delete it), or your manager doesn't sync, there's no way back in. Deleting your account (see in Step 07) is the one action that truly has no recovery, by anyone, ever.", }, tryHint: 'Switch to "Sign in" below, then tap a button.', }, @@ -55,9 +55,9 @@ export const STEPS: StepContent[] = [ index: "Step 02", sourceTag: "Explore", alt: 1, - title: "Pick a plan that suits your trip with well curated eSIM catalgoue with Local, Unlimited, Regional, Global and Special Plans", + title: "Explore plans", summary: - "Jump straight to the Shop tab.", + "Jump straight to the Shop tab. Pick a plan that suits your trip with well curated eSIM catalgoue with Local, Unlimited, Regional, Global and Special Plans", items: [ 'On Home, tap **Shop** or the cart icon in the tab bar, to open the shop.', "Tap **Countries**, **Regions**, **Global**, or **Special** to browse plans in that category.", @@ -201,3 +201,16 @@ export const DELETE_ACCOUNT = { ], confirmLabel: "Type DELETE to confirm.", }; + +export const CLOSING_TIPS = { + heading: "Activation & troubleshooting tips", + lead: "A few things worth knowing before and after you install, not covered in the steps above.", + items: [ + "Most eSIMs install once. Remove it from your device and it cannot be installed again, so only remove one you are finished with.", + "Install on a stable connection, at home on Wi-Fi or a private hotspot, before you travel rather than on the move.", + "Label the eSIM as you install it, something like **Japan** or **Kokio**, so your device's SIM list stays easy to read.", + "On iPhone, activation runs through **Settings → Mobile Service**: pick the new eSIM, keep your primary line set for calls and iMessage, set the new eSIM for cellular data, then turn on data roaming.", + "Connectivity trouble? Toggle **Airplane Mode** off and on first, that clears most issues.", + "Still stuck? Turn off automatic network selection, pick the strongest network manually, and check the eSIM's coverage from its details in the Kokio app, since it can sometimes latch onto a weaker one.", + ], +}; diff --git a/content/faqs.ts b/content/faqs.ts index 42c5a83..8be16cc 100644 --- a/content/faqs.ts +++ b/content/faqs.ts @@ -57,4 +57,22 @@ export const faqsData: Faq[] = [ answer: "Kokio goes into public launch in September 2026, on the App Store and Google Play, with data plans in over 200 destinations. Around the launch, Kokio is testing regionally across Southeast Asia and with partner communities, and starting the road to mainnet. Later in 2026 come the device wallet, so checkout happens inside the app, referral links, partner discount codes, sponsored eSIM flows for events, and connectivity partnerships. Early 2027 adds distribution through physical hubs and coworking partnerships, privacy rails driven by what people ask for, and plans that need verified identity, built with identity partners so that Kokio never holds the identity itself. These are targets rather than promises.", }, + { + id: "reinstall-esim", + question: "Can I reinstall my Kokio eSIM if I remove it?", + answer: + "No. Most eSIMs, including the ones Kokio issues, can only be installed once. Removing it from your device is final, so only remove an eSIM you are finished using.", + }, + { + id: "activate-esim", + question: "How do I activate my Kokio eSIM after installing it?", + answer: + "On iPhone, open Settings, tap Mobile Service, and select the new eSIM. Give it a label such as the destination or Kokio, keep your primary line set for calls and iMessage, and set the new eSIM as your line for cellular or mobile data. Turn on data roaming and the eSIM is ready to use.", + }, + { + id: "troubleshoot-connectivity", + question: "My Kokio eSIM won’t connect, what should I try?", + answer: + "Turn Airplane Mode off and on first, that resolves most connectivity issues. If it persists, turn off automatic network selection, manually choose the strongest available network, and check the eSIM’s coverage from its details inside the Kokio app, since it can sometimes stay connected to a weaker network than the one available.", + }, ]; diff --git a/lib/site-copy.ts b/lib/site-copy.ts index 3c5c650..ed753e8 100644 --- a/lib/site-copy.ts +++ b/lib/site-copy.ts @@ -50,7 +50,7 @@ export const CANONICAL_DESCRIPTION = * there is nothing to read a date from. Bump this when the copy changes, not * when the styling does. */ -export const HOME_UPDATED = new Date("2026-09-07"); +export const HOME_UPDATED = new Date("2026-09-10"); /** * Public launch target, and the notice the banner and the footer both render. From f1b381a4e5fbe5fb1daea4595504dea036b29eb7 Mon Sep 17 00:00:00 2001 From: dunegxn Date: Thu, 10 Sep 2026 23:49:45 +0530 Subject: [PATCH 7/7] +typoFixes --- components/live/content.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/live/content.ts b/components/live/content.ts index 394d356..969e3b6 100644 --- a/components/live/content.ts +++ b/components/live/content.ts @@ -38,7 +38,7 @@ export const STEPS: StepContent[] = [ alt: 0, title: "Onboarding", summary: - "Using biomterics (passkeys), Kokio never asks for your name, email, or any such personal information that can be used to indetify you irl.", + "Using biometrics (passkeys), Kokio never asks for your name, email, or any such personal information that can be used to indetify you irl.", items: [ "Open Kokio, Tap **New User** create and sign-in using passkeys.", "Only use **Existing User** if you've reinstalled Kokio on the same device without deleting that `kokio.app` passkey from device password manager?", @@ -120,7 +120,7 @@ export const STEPS: StepContent[] = [ "The Orders tab shows the install status too, any time you want to check.", ], callout: { - text: 'Kokio do not any personal info, so it cannot share this QR via email or messages to you. Share it to yourself through a channel you can open and scan, like messaging it to another device.', + text: 'Kokio **does not store** any personal info, so it cannot share this QR via email or messages to you. Share it to yourself through a channel you can open and scan, like messaging it to another device.', }, tryHint: "Switch tabs, or tap Copy.", }, @@ -134,8 +134,8 @@ export const STEPS: StepContent[] = [ "A Device Wallet lives on your Home screen, the passkey never leaves your phone's own hardware.", items: [ "Before you deploy one, Home shows a **Device Wallet** card with a simple prompt: tap to create it.", - "Once deployed, that same card shows your balance and a shortened wallet address, with quick links to copy it or open it on the block explorer.", - "It's running on the Base Sepolia testnet today, ahead of mainnet.", + "Once deployed, that same card shows your balance and a shortened wallet address, with quick links to copy it or open it in the block explorer.", + "It's live on Base Sepolia testnet today, ahead of mainnet.", ], callout: { text: "Wallet will be enabled soon for purchases and more.",