Skip to content

feat(mobile): a native iOS + Android app, as a shell over the existing web app - #616

Draft
an1va wants to merge 17 commits into
mainfrom
feat/mobile-pwa
Draft

feat(mobile): a native iOS + Android app, as a shell over the existing web app#616
an1va wants to merge 17 commits into
mainfrom
feat/mobile-pwa

Conversation

@an1va

@an1va an1va commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Draft. A native iOS + Android app for Derive, built as a native shell hosting the existing web app rather than a second front end.

Why this shape

apps/web is not merely responsive, it is deliberately designed for phones — nine files carry mobile branches, and the artifact page alone has twenty-one, including a 0.28 anchor-scroll bias so a quoted highlight clears the comments sheet. Rebuilding that in React Native would mean re-deriving every one of those judgements and maintaining two copies forever.

So the shell owns only what a browser tab cannot do. A web deploy reaches phones with no app release.

A security consequence worth stating: a native web view has no equivalent of the iframe sandbox attribute. Hosting the SPA preserves the containment the web already has, because an artifact still renders inside the web app's own sandboxed iframe. The cheaper architecture is also the safer one.

What's here

apps/mobile — the Expo shell (SDK 54). Hosts the web app, native tab bar, deep links from cold start, off-origin taps to the system browser, real failure state with retry. Outside the pnpm workspace with its own lockfile; RN wants hoisting and the repo-wide fix would change resolution for the API and web app to suit Metro.

apps/web — PWA (manifest, icons, installed-app metas) and the "Open in Derive" bar, plus three fixes found by running it (below).

apps/api — the session hand-off and the app-association files.

Native sign-in, the one real blocker

Google refuses OAuth from an embedded web view (disallowed_useragent), and Derive has Google enabled in production. Not fixed by spoofing the user agent — that policy protects the person signing in.

Sign-in runs in a real browser, then Better Auth's oneTimeToken plugin hands the session into the web view's cookie jar, where the cookie actually belongs. Works for every method, not just the one that forced it. No new endpoint, no new table.

Two properties carry the safety, both tested:

  • Single-use — verify goes through consumeVerificationValue; storeToken: "hashed" means a leaked row is not a credential.
  • Nonce-bound — any page can fire a deep link, so a crafted derive://auth-callback would otherwise sign the app into someone else's account.

Residual risk documented rather than hidden: a custom scheme is not exclusively ours. Single-use plus a two-minute expiry bounds it; the real fix is an https callback on a verified domain, which the association files now make possible.

Three bugs in apps/web that only a phone found

Each was invisible to types, tests and the bundler.

  1. Comments silently failed on any non-secure origin. crypto.randomUUID is secure-context only; addComment used it unguarded after closing the composer, so the throw took the send with it — no request, no error. Real for a self-host over http on an internal network. lib/guest-id already guarded this; addComment missed it.
  2. Copy did nothing. navigator.clipboard?.writeText(url).then().catch(fallback) reads as graceful degradation, but optional chaining short-circuits the whole chain — the fallback was unreachable. Now succeeds via execCommand instead of only reporting failure.
  3. Every mobile page load threw away the server-rendered shell. Hydration mismatch on every route at 390px, none at 1280px. useIsMobile's lazy initializer runs on the client's first render, so the prerender said desktop and a phone said mobile. useSyncExternalStore's getServerSnapshot fixes it. 3 → 0.

App Review status

Every row exercised end to end, not confirmed by finding a button — a distinction that earned its keep, since completing deletion needs a password and the word "delete", and a new account is held on /welcome until onboarding is skipped.

Requirement Verified
5.1.1(v) account deletion Deleted an account; user row gone, re-sign-in refused, zero orphaned sessions/accounts/passkeys/memberships
1.2 report user content Submitted a real report; row lands with its reason
2.1 comments Post, react, resolve/edit/delete menu on the mobile sheet
2.1 no dead ends Missing artifact and unknown route render real states
4.2 native tab bar All four tabs drive the SPA router with zero full page loads
Export compliance Declared, so submission stops asking
4.2 push Blocked on an Apple Developer account
4.8 Sign in with Apple Blocked — required because Google is offered

Reviewing this

~11.7k of the ~14k insertions are apps/mobile/package-lock.json. Actual code is ~2,600 lines.

Worth a close look:

  • apps/mobile/src/links.ts, src/auth.ts — the security-relevant logic
  • apps/api/src/auth-config.ts — the oneTimeToken wiring
  • apps/web/src/lib/use-is-mobile.ts — touches every mobile branch in the app

Not done, and honestly

Nothing has run on a device by me — no Xcode, simulator or Android SDK on the machine this was built on. Logic is covered by 44 shell tests in CI; scroll feel, cookie jars, the keyboard, safe areas and the tab bar's feel are not. Tests are not a phone.

Push, the share extension, Sign in with Apple and turning on universal links are all gated on the Apple Developer account, not on code.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

an1va and others added 17 commits July 31, 2026 14:26
Phase 1 of the mobile plan, shipping on its own ahead of any native shell.
Both halves live in apps/web, so they reach the app, an installed Home Screen
PWA, and plain mobile Safari at once, and they ship on deploy.

PWA. A web app manifest (standalone, two shortcuts), the icon set generated
from the opaque brand tile, and the installed-app metas. apple-touch-icon
moves off favicon.png, which is a transparent white glyph and disappears
against the light tile iOS composites it onto.

theme-color is rendered as raw tags in RootDocument rather than through the
route's head(): head management dedupes meta by name, so the light/dark pair
collapsed to the dark one and the light variant never reached the served HTML.
Caught by reading the response, not the source.

Open-in-app bar. A Derive link tapped in Slack on a phone opens in Slack's own
web view, where a universal link CANNOT reach a native app (Apple opens the
site instead), so the page offers the hop itself on a custom scheme, which does
escape a web view. Detection-gated and dismissible: nothing can tell whether the
app is installed, so an automatic redirect would strand everyone without it.

Renders in flow above the mobile top bar, never over it, so it cannot suppress
navigation. No-ops on desktop, in a real mobile browser, and once installed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phase 2, partial. The shell boots, hosts apps/web in a web view, routes deep
links (cold start included), keeps off-origin navigations out of the frame, and
makes failure legible instead of white. Runs on a real phone through Expo Go, so
it needs no Xcode to try.

Not a workspace member. React Native wants a hoisted node_modules and the usual
fix is repo-wide, so apps/mobile is excluded from pnpm-workspace.yaml and keeps
its own lockfile. It needs nothing from the workspace: HTTP to the API, HTTPS to
the web app. Biome and the repo gate still cover it.

SDK 57, not the 55 the plan said. 55 was two releases stale; the versions here
come from the official template rather than from guessing.

Deep-link resolution is pure and Expo-free (src/links.ts) because it is the one
security-relevant thing in the shell: a deep link is untrusted input that decides
what the frame shows. Off-origin targets, javascript: payloads and suffix
lookalikes (derive.to.evil.example) are all refused; verified against 11 cases.

ALLOWED_ORIGINS is likewise a boundary, not a convenience. A native web view has
no iframe sandbox attribute, so hosting the SPA is what preserves the containment
the web already has: an artifact still renders inside the web app's own sandboxed
iframe. Raw-bytes origins must never join that list.

Deliberately unbuilt, and why, in apps/mobile/README.md: the native tab bar
(needs a device to get the feel right) and the auth handoff (Google blocks OAuth
in embedded web views, so it needs a real browser plus a cookie transfer).

Verified: tsc clean, Metro bundles for iOS, the repo's 20 guardrails still pass.
NOT verified: anything on a device. No Xcode or Android SDK on this machine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Google refuses OAuth from an embedded web view (disallowed_useragent), and
Derive has Google enabled in production, so "Continue with Google" inside the
shell was a dead end. A sign-in navigation is now recognised and handed to a real
browser via openAuthSessionAsync, which Google accepts and which closes itself on
derive://auth-callback.

NOT fixed by spoofing the user agent. That policy protects the person signing in
and defeating it violates Google's terms.

Detection is pure and separately exercised: 15 cases covering the flow starts,
the provider hosts, and the things it must NOT hijack. Ordinary pages, including
/login itself, stay in the web view; accounts.google.com.evil.example, an
off-origin /api/auth/... path, and a /api/authx prefix are all refused.

HALF A FIX, on purpose. The browser that completes the flow has its own cookie
jar, so the session lands there and not in the web view's; on iOS those are
genuinely separate stores and no client-side trick bridges them. The remaining
piece is a server endpoint that exchanges a single-use token for a Set-Cookie on
a request the WEB VIEW makes. It is security-sensitive, belongs in apps/api with
tests, and is documented rather than guessed at. See apps/mobile/README.md.

Also adds the web target, which is how the shell was tested locally: it boots,
routes, themes and renders its own loading state. react-native-webview has no web
implementation, and derive.to sends frame-ancestors 'none', so the hosted content
itself cannot be exercised without a device.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…'s web view

Closes the half of native sign-in that was missing. Google refuses OAuth from an
embedded web view, so the shell runs sign-in in a REAL browser — which has its own
cookie jar, so the session landed there and never reached the web view. On iOS
those are genuinely separate stores, so no client-side trick bridges them.

Better Auth ships the right primitive: the one-time-token plugin. The browser
mints a token for the session it just created, the app carries it across, and the
WEB VIEW spends it — so the Set-Cookie lands in the jar that needs it. Works for
every sign-in method (Google, password, passkey, enterprise OIDC), not just the
one that forced the design. No new endpoint, no new table.

Two properties carry the safety, both tested rather than asserted in a comment:

SINGLE-USE. Verify goes through consumeVerificationValue, so a replay finds
nothing; storeToken:"hashed" means a leaked verification row is not a credential.
Six API tests cover the hand-off, replay, forgery, anonymous minting, minting for
another user, and that the plaintext is not in the table.

NONCE-BOUND. Any web page can fire a deep link, so without binding the round trip
to a value the app generated, a crafted derive://auth-callback could sign the app
into someone else's account. The app refuses a callback that does not echo its
nonce; 13 cases cover that and the injected script's escaping.

Residual risk documented, not hidden: a custom scheme is not exclusively ours, so
another app registering derive:// could intercept the callback. Single-use plus a
two-minute expiry bounds it; the real fix is an https callback on a verified
associated domain, which needs the app-association files Phase 4 will serve.

auth-config.test.ts asserted the plugin set by length; it now names the set, so a
future break says which plugin moved rather than that the number did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the app

The two documents iOS and Android fetch at install time to decide whether a
derive.to link opens the native app or a browser tab. Without them a universal
link is just a web link, so this is what "tap a Derive link in Slack and land in
the app" actually needs.

OFF BY DEFAULT, and that is the important part. An instance with no app of its own
must not publish an association: naming a bundle id hands that app the right to
claim this domain's links. Each file appears only once its identifiers are
configured and 404s otherwise, which is the honest answer for a self-host.

Content type is set explicitly because iOS rejects an AASA served as anything but
application/json and says nothing about it — the only symptom is that links
quietly stay web links. /raw/* and /api/* are excluded: they are fetched, not
navigated to, and claiming them would route background asset requests into the app.

Six tests cover both files, the single-sided cases, that blank or comma-only
config counts as unset rather than as an association that verifies nothing, and
that no session is needed.

Does NOT fix the Slack in-app browser: iOS ignores universal links from inside a
web view, whatever the association says. That path stays covered by the web-side
"Open in Derive" bar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… edit

EXPO_PUBLIC_DERIVE_WEB_ORIGIN now wins over app.json's extra.webOrigin. The value
is a LAN address, so it must never reach a commit — telling people to edit a
tracked file to run locally was an invitation to do exactly that.

Verified over the LAN before handing it to a phone: the dev bundle compiles and
carries the override, and the full session hand-off works against a LAN origin
(mint, spend from a clean cookie jar, that jar comes back signed in, replay
refused). The README now also names the DERIVE_WEB_ORIGIN the API needs, since
without it Better Auth's CSRF check rejects sign-in with INVALID_ORIGIN — which
is the first wall anyone hits pointing a phone at their laptop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The app failed on a real phone with "Project is incompatible with this version of
Expo Go." The project was on SDK 57 because npm's `latest` tag says 57.0.9 — but
npm's latest is the wrong source of truth when Expo Go is the delivery mechanism.
The App Store ships Expo Go 54.0.2, released September 2025, so 57 cannot run
there and no amount of updating the app fixes it.

Caught only by putting it on a handset, which is exactly the class of thing the
bundle compiling and the types checking cannot tell you.

Every version here now comes from `expo install` against SDK 54 rather than from a
template or a guess. Manifest reports exposdk:54.0.0, tsc is clean, and Metro
serves a 6.4MB dev bundle carrying the configured origin.

README documents the constraint and the one-line check against the App Store, so
the next SDK bump starts from what the store ships instead of what npm tags.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Expo stopped shipping new Expo Go builds to the App Store: SDK 55 stalled in
Apple review and Expo pivoted to eas go, simulators and dev builds. SDK 54 is the
last Expo Go on the store, so on a physical phone with no Apple Developer account
it is the only SDK that can run, and waiting for an update is waiting for
something that will not ship.

The previous note called this a lag, which would have sent the next person
looking for a store update that does not exist. It also misses the useful part:
the SDK bump and the Apple enrolment are the same unlock, and dev builds are
where this app is headed regardless, since push and the share extension cannot
run in Expo Go at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…back

Two things a phone said that the bundle compiling could not.

The top strip had no background, so it rendered as the platform default grey: a
dead band sitting above chrome that is trying to look continuous with it, visible
before any content loads. The web app pads safe-area-inset-BOTTOM itself (comments
sheet, selection bar) but has no top handling, so this strip really is the shell's
to paint, and it has to follow the colour scheme.

Pull to refresh was off because I guessed a bounce would fight the artifact
viewer's locked scroll regions. On a device the absence just reads as a page that
ignores you, so the guess loses to the handset: bounces and pullToRefreshEnabled
are on, plus overScrollMode/nestedScrollEnabled for Android. WebKit only fires the
refresh at the top of the document, so a scrolled list is unaffected.

Also keyboardDisplayRequiresUserAction=false so the comment composer can raise the
keyboard itself, and webviewDebuggingEnabled in dev only, which is the only way to
inspect the hosted app from a device.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reported from a phone as "nothing happens on post", and it was real: the comment
vanished, no request went out, no error appeared.

crypto.randomUUID is SECURE-CONTEXT ONLY. On https and on localhost it exists; on
plain http to a hostname or a LAN IP it is undefined and calling it throws.
addComment built its optimistic row's temp id with it, unguarded, AFTER the
composer had already closed — so the throw landed mid-handler and took the whole
send with it, leaving nothing on screen to explain the loss.

Not hypothetical. It is how Derive is reached when a self-host runs over http on
an internal network, and when a phone is pointed at a laptop's dev server.

lib/guest-id already guarded this exact hazard; addComment simply missed it. Both
now share lib/random-id, so there is one place to be right. The ids label
optimistic rows and anonymous presence, which the server re-derives or replaces,
so the Math.random fallback weakens nothing.

Proven rather than argued. Before: no POST, comment not visible. After: POST
fires, comment renders. Same Playwright script, same LAN origin, and a unit test
pins the non-secure branch that has no coverage from a normal test run, since
vitest and localhost are both secure contexts.

Worth noting the diagnosis path: this looked like a WebView bug, then a mobile
bug, then an auth bug. It was none of those, and it reproduced identically at
desktop width in plain Chrome.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The strip came back wrong a second time, and the second reason was more
interesting than the first. Painting it from the device colour scheme assumes the
app and the OS agree about the theme. They do not: the web app resolves its own
(a stored choice first, the OS only as a fallback), so a phone in light mode
showing a dark-themed app got a white band sitting above black chrome.

The page is the only honest source, so ask it. An injected probe reports the
computed background and re-reports on change, which also covers the in-app theme
toggle — that swaps a class on <html> with no navigation for the shell to hook.
Device tokens remain the fallback until the first message, so the strip is never
unpainted.

Reads the computed colour rather than a token name, so it cannot drift when the
theme system changes. Verified against the running app: stored dark reports
rgb(10,11,13), stored light rgb(247,248,250) — both exactly the --background
tokens, and both following the STORED theme rather than the OS, which is the
disagreement that caused this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every check I ran on this shell during development was a throwaway script in
/tmp. The README even admitted it: "There is no test runner in this package yet."
So the deep-link refusals and the auth nonce binding — the security-relevant
parts — were verified once and then guarded by nothing.

32 tests now cover what can be checked without a device: deep-link resolution and
each refusal (off-origin, javascript:, the suffix lookalike derive.to.evil.example,
the auth callback as a navigation), the nonce binding that stops a crafted deep
link signing the app into someone else's account, the claim script's escaping,
and the background probe.

The probe runs in jsdom rather than being asserted as a string, because it is a
script that executes in a document. That is the code that broke twice, both times
visibly on launch, and both times invisible to every other gate.

CI matters more than the tests here. apps/mobile is outside the pnpm workspace, so
`pnpm -r` never reaches it and these would have passed locally and run nowhere.
The workflow now installs and tests it on a lane that was already up.

README states the ceiling plainly: no web view is exercised, so scroll, cookies,
the auth hand-off, the keyboard and safe areas stay invisible here. Every bug this
shell actually shipped lived in that set. A green suite means the logic holds, not
that the app works.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by dogfooding the app at phone size against a plain-http origin, which is
where a phone pointed at a laptop actually lives — and where a self-host on an
internal network lives too.

navigator.clipboard is secure-context only, so on http every copy in the app
either showed "Couldn't copy to clipboard" or did nothing. Doing nothing was the
worse one, and it hid in plain sight:

    navigator.clipboard?.writeText(url).then(ok).catch(fallback)

reads as graceful degradation, but optional chaining short-circuits the WHOLE
chain: with no clipboard the .catch never runs. The fallback written for exactly
this case was unreachable, so copying a comment link was a tap that did nothing.

copyText() does not just report failure better, it succeeds. execCommand("copy")
is deprecated but is not secure-context gated, so the artifact's copy link, share
link and embed snippet now work on http instead of erroring. Verified against the
running app: navigator.clipboard undefined, and the copy reports "Link copied"
rather than the error it used to.

Six tests pin both branches, which no ordinary run can reach: vitest and localhost
are both secure contexts, so the fallback would otherwise be dead code that only
executes for users.

Settings and context surfaces still use the direct API. They fail VISIBLY with an
error toast rather than silently, so they are honest today; converting them is
mechanical and belongs with someone who can click through those screens.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e load

Measured while dogfooding at phone size: a hydration mismatch on EVERY route at
390px, and none at 1280px. React could not reconcile the tree it was hydrating,
so it discarded the prerendered shell and rebuilt it client-side — a full
re-render and a visible pop, on the device least able to afford either.

useIsMobile's initializer was the cause, and its own comment said the opposite:

    useState(() => typeof window !== "undefined" && matchMedia(q).matches)

That is SSR-safe but not hydration-safe. A lazy initializer runs on the client's
FIRST render, not after mount, so the prerender said desktop and a phone said
mobile before hydration had a chance to agree. The useEffect underneath was
already doing the right thing; the initial value undercut it.

useSyncExternalStore is the API for this: getServerSnapshot is what hydration
compares against, so returning a viewport-independent value there makes the first
paint match by construction. The real answer still lands on the next tick, as an
ordinary update instead of a failed hydration.

Verified rather than assumed, since this drives every mobile branch in the app:
mismatches 3 -> 0 at 390px, still 0 at 1280px; the mobile top bar, nav drawer and
comments sheet all still render and post; and a fresh desktop context still gets
the rail and not the sheet.

useCoarsePointer had the identical bug and is fixed with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
THE TAB BAR is the shell's main answer to Guideline 4.2, which names a native tab
bar and push as what separates an app from a repackaged website. It was deferred
as "needs a device to get the feel right", but the risky part was never the feel
— it was whether a tab could move the hosted app WITHOUT reloading it, since
setting the web view's source re-boots the SPA and reads as a page load.

That is now proven rather than assumed: history.pushState plus a popstate event
drives the client-side router, verified against the running app across all four
tabs with ZERO full page loads. The command goes through an injection handle
rather than a prop, so a tap never re-renders the frame.

Two mapping rules are pinned by tests because both fail silently. /settings
redirects to /settings/profile, so tabs own their sub-routes or Settings goes dark
the instant you tap it — caught by integration-checking against the real app, not
by reading routes. And an artifact selects NOTHING, because lighting Library there
would say you are somewhere you are not.

READINESS PASS, checked live at phone size rather than assumed:
  - 5.1.1(v) account deletion: present (Settings > Security)
  - 1.2 report user content: present (artifact menu)
  - 2.1 dead ends: missing artifact and unknown route both show real states
  - 2.1 sign out: present (drawer > user menu)
Two earlier "needs confirming" items are now confirmed, not carried.

usesNonExemptEncryption is declared so every submission stops asking, and
answering it wrong blocks a build. NSAllowsLocalNetworking lets a dev build reach
a plain-http laptop without opening arbitrary http loads, which is what review
looks at.

Tokens widened from the `as const` literals: the background is overridden at
runtime with the colour the PAGE reports, and a literal type rejected the very
value that makes the strip correct.

Still blocked on an Apple Developer account, not on code: push (the other half of
4.2) and Sign in with Apple (required because Google is offered).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous pass confirmed the delete-account button existed and that "Report"
appeared in a menu. Neither had been clicked. App Review clicks them, so that gap
mattered more than the checkmarks suggested.

Both now run end to end:

ACCOUNT DELETION (5.1.1(v)). Created an account, deleted it through the UI,
confirmed the user row is gone, re-sign-in is refused, and there are ZERO orphaned
sessions, accounts, passkeys or memberships left behind. Deleting the account and
its associated data is the actual requirement, not showing a button.

REPORT (1.2). Submitted a real one. The dialog confirms, and the row lands in the
database carrying its reason.

The distinction earned its keep immediately. Completing deletion needs a password
AND the word "delete" typed, and a brand-new account is held on /welcome until
onboarding is skipped — so the control is unreachable on a fresh sign-up until
that is cleared. A presence check reported all of that as fine.

Also exercised: the comment lifecycle on the mobile sheet, sign-out, and the
missing-artifact and unknown-route states.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Main moved 8 commits ahead (chat, the boot-fetch head start, derived facts). One
real conflict, in __root.tsx: main added BOOT_START_URLS + DATA_BOOT exactly where
the mobile branch added THEME_COLOR. Both are additions, so both stay, and both
still render — the two theme-color metas and the boot-fetch script.

Merging before opening the PR rather than after: a conflicting PR gets ZERO CI
runs on this repo, so it would have looked untested rather than failing.

Verified after merging, not before: 21 guardrails, tsgo clean across the
workspace, and the full suite green including main's new tests.
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

Preview

https://derive-pr-616.derive-to.workers.dev

Deployed from 617ad29 as derive-pr-616.

It shares production's database — sign in with your real account, and treat anything you change here as changed for real. It has no routes, no cron, no queue consumer and no OG renderer, so it cannot serve derive.to, run scheduled work, or write images onto real artifacts.

Unlike production it serves artifact HTML on its own origin (that is what makes frame-side changes visible here). Storage is still sandboxed away, but untrusted HTML and the sign-in form share a hostname — treat this URL as you would any link: don't type a password into it because a page asked you to.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant