Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0380208
Add invite and promo landing pages.
JonnyLuca Aug 29, 2026
68270a9
Add Playwright visuals for spent invite and promo landings.
JonnyLuca Aug 29, 2026
3159502
Add English Playwright visuals for invalid invite and promo landings.
JonnyLuca Aug 29, 2026
6692ebd
Add English Playwright visuals for spent invite and promo landings.
JonnyLuca Aug 29, 2026
f649eca
Add English Playwright visuals for unavailable invite and promo landi…
JonnyLuca Aug 30, 2026
d9d5397
Add Playwright visuals for invite and promo landings with no code.
JonnyLuca Aug 30, 2026
e97e3e4
Add English Playwright visuals for invite and promo with no code.
JonnyLuca Aug 30, 2026
cd9a3aa
Add loading mock and Playwright visuals for invite and promo landings.
JonnyLuca Aug 30, 2026
a1da83e
Add English Playwright visuals for invite and promo loading states.
JonnyLuca Aug 30, 2026
b0d337a
Add Android Playwright visuals for invite and promo loading states.
JonnyLuca Aug 30, 2026
8b89616
Add Android Playwright visuals for invalid invite and promo landings.
JonnyLuca Aug 30, 2026
48f28d8
Add Android Playwright visuals for unavailable invite and promo landi…
JonnyLuca Aug 30, 2026
683970c
Add Android Playwright visuals for spent invite and promo landings.
JonnyLuca Aug 30, 2026
16fbda1
Add Android Playwright visuals for invite and promo with no code.
JonnyLuca Aug 30, 2026
bac05eb
Add fallback mock and Playwright visuals for nameless invite and prom…
JonnyLuca Aug 30, 2026
d18a03f
Add English Playwright visuals for nameless invite and promo landings.
JonnyLuca Aug 30, 2026
1ddabb2
Add Android Playwright visuals for nameless invite and promo landings.
JonnyLuca Aug 30, 2026
0da8d38
Add English Android Playwright visuals for invite and promo loading s…
JonnyLuca Aug 30, 2026
2293e09
Add English Android Playwright visuals for invalid invite and promo l…
JonnyLuca Aug 30, 2026
4452dfb
Add English Android Playwright visuals for unavailable invite and pro…
JonnyLuca Aug 30, 2026
399db91
Pin referral lookup name fields
JonnyLuca Aug 30, 2026
7b307aa
test(web): refresh visual baselines
JonnyLuca Aug 30, 2026
b12e727
fix(referral): harden invite/promo landing per review
JonnyLuca Sep 4, 2026
3cb4e24
test(web): widen remaining English visual views to tablet and mobile
JonnyLuca Sep 4, 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
Loading