Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b76f5bd
Add invite and promo landing pages.
JonnyLuca Aug 29, 2026
f671f47
Add Playwright visuals for spent invite and promo landings.
JonnyLuca Aug 29, 2026
e895add
Add English Playwright visuals for invalid invite and promo landings.
JonnyLuca Aug 29, 2026
9253930
Add English Playwright visuals for spent invite and promo landings.
JonnyLuca Aug 29, 2026
9f2321d
Add English Playwright visuals for unavailable invite and promo landi…
JonnyLuca Aug 30, 2026
00421a1
Add Playwright visuals for invite and promo landings with no code.
JonnyLuca Aug 30, 2026
28d78af
Add English Playwright visuals for invite and promo with no code.
JonnyLuca Aug 30, 2026
8721892
Add loading mock and Playwright visuals for invite and promo landings.
JonnyLuca Aug 30, 2026
e10a9d4
Add English Playwright visuals for invite and promo loading states.
JonnyLuca Aug 30, 2026
f8669e6
Add Android Playwright visuals for invite and promo loading states.
JonnyLuca Aug 30, 2026
d32bfee
Add Android Playwright visuals for invalid invite and promo landings.
JonnyLuca Aug 30, 2026
16180fb
Add Android Playwright visuals for unavailable invite and promo landi…
JonnyLuca Aug 30, 2026
e2cf6bc
Add Android Playwright visuals for spent invite and promo landings.
JonnyLuca Aug 30, 2026
f01e5cb
Add Android Playwright visuals for invite and promo with no code.
JonnyLuca Aug 30, 2026
0be1bce
Add fallback mock and Playwright visuals for nameless invite and prom…
JonnyLuca Aug 30, 2026
4f1581d
Add English Playwright visuals for nameless invite and promo landings.
JonnyLuca Aug 30, 2026
38720a4
Add Android Playwright visuals for nameless invite and promo landings.
JonnyLuca Aug 30, 2026
d40508b
Add English Android Playwright visuals for invite and promo loading s…
JonnyLuca Aug 30, 2026
b9f06a2
Add English Android Playwright visuals for invalid invite and promo l…
JonnyLuca Aug 30, 2026
7ce5b70
Add English Android Playwright visuals for unavailable invite and pro…
JonnyLuca Aug 30, 2026
ef2ec32
Pin referral lookup name fields
JonnyLuca Aug 30, 2026
4d79bc0
test(web): refresh visual baselines
JonnyLuca Aug 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
COMMIT_MSG: ${{ github.event.head_commit.message }}
# wrangler looks for ./functions in the cwd (repo root), sibling to
# public/. That middleware injects Smart App Banner app-argument.
run: |
wrangler pages deploy public \
--project-name=realunit-web-dev \
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/prd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
COMMIT_MSG: ${{ github.event.head_commit.message }}
# wrangler looks for ./functions in the cwd (repo root), sibling to
# public/. That middleware injects Smart App Banner app-argument.
run: |
wrangler pages deploy public \
--project-name=realunit-web \
Expand Down
24 changes: 24 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,30 @@ This repo is the **realunit.app** website — public, static. See the
served. The dev dependencies exist **only** for the quality gates below
(formatting, HTML validation, unit tests, screenshots); nothing compiles or
bundles the site.
- **Invite/promo HTML rewrite is banner, canonical, and store handoff.** Safari,
Play, and share crawlers snapshot `apple-itunes-app`, `og:url`,
`rel=canonical`, `twitter:url`, `og:title`, `twitter:title`,
`og:description`, `twitter:description`, `og:image:alt`, `twitter:image:alt`,
`og:locale`, `og:site_name`, html `lang`, the Play
referrer, android-app / ios-app alternate links, Facebook App Links
(`al:ios:url` / `al:android:url` are `realunit-wallet://…`; `al:android:class`
is `swiss.realunit.app.MainActivity`; `al:web:url`
is the HTTPS landing), and Twitter App Card `twitter:app:url:iphone` /
`twitter:app:url:ipad` / `twitter:app:url:googleplay` (same custom scheme)
from the HTML bytes before
`/js/invite-banner.js` / `invite.js` run, so `functions/_middleware.js`
(repo-root `functions/`, picked up by `wrangler pages deploy public`) and
`scripts/dev-server.mjs` inject those from the request URL (www folded onto
the apex). The campaign code is in `og:title` / `twitter:title` /
`og:description` / `og:image:alt` / `twitter:image:alt`; `?lang=en` sets English title/description/alt and `og:locale`;
invitee names wait for lookup JS. The committed
`public/invite` and `public/promo` HTML stay generic (`app-id` only, og:url /
twitter:url `/invite/` or `/promo/`, `og:site_name` RealUnit, generic titles and descriptions, Play
href without referrer, no `al:*`, no `twitter:app:*`, `format-detection`
`telephone=no, date=no` so iOS does not turn the code or Aktionstext date
into a link, `x-apple-data-detectors="false"` on `#ok-code` / `#ok-body`
because Safari re-scans JS-inserted text, no inline
`<script>`). Do not add other Pages Functions or server-side rendering.
- **Keep the page self-contained.** `public/_headers` sets a strict CSP:
- No inline `<script>` and no external resources of any kind (scripts, styles,
images, fonts, fetch). Load JS from **same-origin** files instead.
Expand Down
52 changes: 48 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,59 @@ The website served at **realunit.app**. Public, static.

## v1 (current)

A minimal landing page — one hero image from the RealUnit app plus the three download buttons.
A minimal landing page — one hero image from the RealUnit app plus the store buttons —
plus invite/promo landings and the Aktionariat confirm / account-merge flows.
Deliberately **without a build toolchain**: plain HTML + assets in `public/`,
uploaded to Cloudflare Pages.

- `public/index.html` — landing, shows `assets/hero.jpg` centered on a light background
with the store/download buttons below it
- `public/invite/` and `public/promo/` — referral and campaign landings; look up
`GET /v1/realunit/referral/code/:code` (contract:
[JonnyLuca/dfx-referral-api](https://github.com/JonnyLuca/dfx-referral-api)),
open `realunit-wallet://invite|promo/{code}`,
and pass the code as a Play install referrer. Path and query codes
(including ads/Play `utm_content` / `referrer` and Facebook/Google/Outlook
`u=` / `q=` / `url=` / email `link=` wrapping `invite=` or a landing URL, with or without
`https://`; a campaign name in `utm_content` does not hide a later wrapper key;
an empty or foreign `code=` does not hide a later `invite=` / `promo=`;
short `ios-app://` alternate links; Proofpoint URL Defense and Outlook Safe Links wrapping a
RealUnit landing; a foreign `https://` URL is not a code) are
uppercased, stripped of messenger zero-width/fullwidth characters and
trailing sentence punct (`!`, `?`, `/`, …), and capped at 32 like the
API lookup — including in the crawler HTML bytes Safari snapshots.
The re-tap hint is iOS-only;
Android keeps the code via the Play referrer. An App Store / Play / CTA tap
also copies the code (user gesture) so an iOS badge install can still be
pasted at registration. iOS `format-detection` is `telephone=no, date=no`
so Safari does not turn the campaign code or Aktionstext date into a link.
`#ok-code` and `#ok-body` also set `x-apple-data-detectors="false"` because
those strings are written after load.
`www.realunit.app/invite|promo` is HTTP 200 (not a
301 to the apex) so Universal Links and the Smart App Banner keep the host.
The Smart App Banner `app-argument`, `og:url`, `rel=canonical`,
`twitter:url`, `og:title`, `twitter:title`, `og:description`,
`twitter:description`, `og:image:alt`, `twitter:image:alt`, `og:locale`,
`og:site_name`, Play install referrer,
android-app / ios-app alternate links, Facebook App Links
(`al:ios:url` / `al:android:url` are `realunit-wallet://…`; `al:android:class`
is `swiss.realunit.app.MainActivity`; `al:web:url`
is the HTTPS landing), and Twitter App Card `twitter:app:url:*` (same
custom scheme; `twitter:app:country` is CH) are injected
into the HTML bytes from the request URL (`functions/_middleware.js` on
Cloudflare Pages, and the local dev-server) so Safari, Play, WhatsApp, X,
and share crawlers can snapshot them before JS. `og:title`, `og:description`,
and image alt name the campaign code; `?lang=en` sets English copy and `og:locale=en_GB`;
invitee names wait for lookup JS. `/js/invite-banner.js` in `<head>`
is the CSP-safe JS fallback — Cloudflare Pages CSP blocks inline `<script>`.
- `public/.well-known/apple-app-site-association` and `assetlinks.json` — Universal
Links / App Links for `/invite/*` and `/promo/*` on apex and www (HTTP 200,
no 301). `sha256_cert_fingerprints`
lists (1) the v2 signing cert of GitHub release APK `realunit-1.2.17.apk`
(`O=DFX AG`, `CN=Konstantin Ullrich`) and (2) the Android debug keystore
used by the `pixel_jonny` AVD (`C=US, O=Android, CN=Android Debug`) so
debug App Links against `dev.realunit.app` verify. If Play App Signing uses
a different app-signing key, add that SHA-256 from Play Console beside them.
- `public/platform.js` — enlarges the store button matching the visitor's platform
(iOS → App Store, Android → Play Store); without JS all buttons stay equal-size
- `public/assets/hero.jpg` — hero (source: the app's splash background)
Expand Down Expand Up @@ -40,10 +87,7 @@ projects in the DNS/deployment configuration. The `handbook.` subdomain is unaff

## Roadmap (v2+)

- `/confirm-aktionariat` — guided Aktionariat address confirmation (calls `api.dfx.swiss`)
- `/account-merge` — confirms adding a wallet address to the existing account (calls the public DFX API)
- Legal pages — rendered from the app's `assets/legal/*.md` (build-time fetch, single source)
- Universal Links / App Links (`/.well-known/*`)

From v2 a build toolchain (Astro) is introduced; the plain-image landing stays the home page.

Expand Down
32 changes: 32 additions & 0 deletions functions/_middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Rewrite invite/promo HTML so apple-itunes-app already carries
* app-argument, og:url / canonical / twitter:url already name the
* landing URL, og:title / twitter:title / og:description already name
* the campaign code, ?lang=en already sets html lang / og:locale, and
* Facebook App Links are present. Safari and share
* crawlers snapshot those from the HTML bytes before
* /js/invite-banner.js and invite.js run.
* public/ stays generic; this is not a site-wide renderer.
*/
import { injectLandingFromRequestUrl, shouldRewriteItunesBanner } from './lib/itunes-banner.js';

export async function onRequest(context) {
const url = new URL(context.request.url);
if (!shouldRewriteItunesBanner(url.pathname)) {
return context.next();
}
const response = await context.next();
const type = response.headers.get('content-type') || '';
if (context.request.method !== 'GET' || !type.includes('text/html')) {
return response;
}
const html = await response.text();
const injected = injectLandingFromRequestUrl(html, context.request.url);
const headers = new Headers(response.headers);
headers.delete('content-length');
return new Response(injected, {
status: response.status,
statusText: response.statusText,
headers,
});
}
Loading