aboutsummaryrefslogtreecommitdiffstats
Commit message (Collapse)AuthorAgeFilesLines
...
* Fix "Cannot mix BigInt and other types" when creating an on-chain gift.Adam Soltys2026-09-072-3/+93
| | | | | | | | | _buildGiftPsbt (the v2 ephemeral-key gift builder) subtracted the funding fee — _giftFundFee returns a Number — directly from BigInt coin sums (sum - _giftFundFee(...), sum - gift - _giftFundFee(...)), which throws in JS. Any on-chain (Savings) gift creation hit it: "Split a coin first", "Lock the whole coin", and gift-all. Wrap the fee in BigInt at those sites (new fundFee() helper). Adds tools/gift-build-test.js — the on-chain gift BUILDER had no coverage, which is how this shipped. It builds a specific-amount gift, a whole-balance gift, and the split carve-out shape (all three fee branches) against a mock wallet and asserts they produce claimable v2 codes without throwing. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Tap payments: no recipient-form flash before the pay animation.Adam Soltys2026-09-071-8/+22
| | | | | | | | | A scanned/tapped/pasted payment (NFC LNURL, a pasted lnurl/lnaddr) briefly showed the recipient form — heading, recipient chip, "finding…" spinner and a Back button — while the LNURL was fetched over the network, before flowing into the pay animation + success. That resolving skeleton read as a form the user had to look at on every tap. Now a tap carries a `tap` flag (set for the scan/paste/NFC entry points: matchSendText with !typed, lnAddressFallback, offers), and during resolve/invoice it renders a minimal centered paying shell — the same autopay-card the ark pay animation uses (spinner-ring ⚡ badge, no Back button) — so a tap goes straight spinner → animation → success with no form in between. Typed lightning addresses keep the normal form (they still resolve through the send form, not a tap). The resolved recipient name/amount arrive on the next screen; the shared card class makes the handoff into the ark autopay animation seamless. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Stop tracking the local runtime session store (db/).Adam Soltys2026-09-072-0/+3
| | | | | | | db/sessions/ is a local server-side session store (398 binary files) that isn't part of the wallet build and was cluttering the working tree; one file (2029240f…) had been committed by accident in 3a94783. Untrack that file (kept on disk) and gitignore /db/ so the store stays local. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Durable sync outbox: fix the fresh-device phantom "Sent" that showed a zero ↵Adam Soltys2026-09-069-55/+503
| | | | | | | | | | | | | | | | | | balance (authored by codex). Root cause of the "signed in and my balance is 0 with a huge Sent I never made": a device's snapshot carrying its change/new coins never reached the relay. The publish was a setTimeout debounce that got canceled on page-hide / logout / feature stop, so coins created by another session were never synced; a fresh device then loaded only the old (now-spent) coins, and reconcile dressed the spent inputs as a definitive "Sent" row and zeroed the balance. No funds ever moved — verified on the ASP that no such payment existed and the change coin was still spendable. Fix (codex): - src/sync-outbox.js (new): a durable outbox. Snapshots are signed synchronously at save time under the current identity (no cross-account mixing) and persisted already-encrypted to localStorage (no keys/plaintext in storage). Delivery retries with backoff and wakes on online/pageshow/visibilitychange/storage, surviving suspension, logout and relay outages. The last copy doubles as a local recovery source if an account cache is cleared. - sync.js: replace the cancel-on-stop debounced publish with the outbox; fetchAllStates also merges the outbox's own (possibly-unpublished) events so a rejected publish is still recoverable locally; forgetAll clears it. - nostr.js: factor out stateEvent() (synchronous signing) and decodeStateEvents(), shared by fetch and the outbox. - ark.js: adoptArkSnapshot() applies a synced snapshot to a LIVE connected manager without restarting payment ceremonies (a snapshot arriving while connected previously stayed invisible until reload); the pre-handshake hold-key merges instead of overwrites; and the reconcile row is reworded from a red "Sent -N" to a neutral "History incomplete / Payment details not synced" (↻) with an explainer, since a spent input proves neither an amount nor a date. - app.js: clearAll also clears the outbox (forgetAll). Tests (codex): sync-outbox-test, sync-browser-test (real Chrome: relay rejects a snapshot, browser closes, reopens and retries; a second browser recovers the balance + history without reload), sync-split-test, plus build and morph smoke — all green. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Android APK: fix the white status bar (TWA had no system-bar color set).Adam Soltys2026-09-064-2/+21
| | | | | | | androidbrowserhelper defaults the TWA status/navigation bars to white when no color is declared — the PWA doesn't hit this because it uses the manifest theme_color. Added STATUS_BAR_COLOR / NAVIGATION_BAR_COLOR meta-data (light + dark variants) pointing at new color resources set to the PWA's theme_color #15171a, so the top bar matches the dark app chrome. Bumped to versionCode 6 / versionName 1.5 and rebuilt static/coinos.apk (meta-data verified present via aapt). Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Auto-pay: no "Paying" screen — the badge animates spinner → green check ↵Adam Soltys2026-09-062-11/+36
| | | | | | | | | into Paid. The armed auto-pay flow flashed a "Paying" title/spinner card that then swapped title, badge and footer for the "Paid" card — too much text change and layout shift for something that completes in a moment. Now the in-flight and success states are ONE structurally identical card (autopayCard): recipient name and amount sit in their final positions the whole time, and the step to Paid is just a class flip. The badge starts as a spinner ring (the ⚡ hidden) and, on success, settles into the green success check with a pop while the ⚡ fades in; the "tap to proceed" footer occupies reserved height so nothing jumps. No "Paying" wording at all — the badge carries the state. Respects prefers-reduced-motion. Manual (non-auto) pays keep their normal Paid screen. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Chat reactions render inside the bubble (Telegram-style), not floating below it.Adam Soltys2026-09-063-21/+25
| | | | | | | Reactions were a sibling of the bubble (a separate row under it), so they hung outside the message. Moved the reaction pills inside .chat-bubble, tucked under the text, across all three renderers: community channel messages, DMs, and the read-only public /chat page. The pill background and the "your reaction" highlight border now derive from the bubble's own text colour (color-mix on currentColor), so they read correctly on both the plain bubble and the accent "me" bubble. Tapping a pill stops propagation so it toggles the reaction without also opening the message action sheet. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* LNURL auto-pay: budget presets + skip the review-screen flash on an armed pay.Adam Soltys2026-09-062-12/+31
| | | | | | | | | (1) The auto-pay opt-in on a fixed-amount merchant pay was a single checkbox that stored "up to the amount you just paid". Replaced with preset budget buttons — 1000 / 5000 / 10000 / 100000 sats — so you set the ceiling deliberately; the chosen amount is what's stored on a successful pay. (2) An NFC tap into an already-armed auto-pay briefly flashed the review/confirm card (the "Getting a quote…" spinner) before advancing to pay. startArkLnPay now decides up front — from the invoice amount and the stored budget — whether it will auto-pay, and the view renders a centered paying screen straight away (matching the paid screen's layout so it morphs cleanly into it), never showing the review. You just see paying → paid. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Fix the on-chain gift front-running vulnerability architecturally: ↵Adam Soltys2026-09-063-54/+170
| | | | | | | | | | | | | | | | | ephemeral-key (v2) gifts. The old on-chain gift presigned a SIGHASH_NONE/SINGLE spend of the sender's own coin — a blank cheque whose claimer output isn't committed, so anyone (from the link, or a mempool watcher once a claim broadcast) could redirect the presigned input to their own address via a higher-fee tx. A sweeper bot had been front-running coinos gift links for ~a month; it drained a real 5000-sat gift ("claimed! → empty wallet"). Root-caused via chain + code forensics: theft tx input0 sighash = SINGLE. New design (v2, lazy ephemeral key): the sender presigns a FULLY COMMITTED (SIGHASH_ALL) funding tx paying the gift to a throwaway key E, and puts E's private key in the link. The claimer broadcasts the funding tx, then a fresh SIGHASH_ALL sweep of E into their own wallet. Both txs commit to every output, so neither is redirectable in the mempool — redirecting needs E's key, which lives only in the link (the intended bearer property: holding the link is the only way to claim). Lazy: the funding tx isn't broadcast until claim, so an unclaimed gift moves no coins and is revoked for free by spending the still-reserved funding coin. Sender pays the funding fee (from change / the swept total); claimer pays the sweep fee (from the gift). - gifts-wallet.js: _buildGiftPsbt now builds+signs the ephemeral funding tx (coin -> E + change, SIGHASH_ALL); new GIFT_MAGIC_V2 codec (parseLazyGift/_encodeLazyGift: [magic][E priv:32][le32 len][funding tx]); giftClaimTxs returns the {funding, sweep} pair; giftOutpoints/previewGift read E's output; isRetiredGift flags the old v1 (compact + legacy PSBT) links. Fee threaded through _selectGiftCoins/createGift/createGiftAll/createGiftFromSplit. - gifts.js doClaim: broadcast funding (tolerating "already in mempool" from a co-holder/retry) then sweep; refuse retired v1 links with a clear message; verifyClaimWon now watches E's output. - i18n: giftChainWarn reworded (safe bearer link funding a fresh on-chain wallet); giftRetired added. Verified: focused crypto test confirms codec round-trip + both inputs signed SIGHASH_ALL (committed). Full regtest end-to-end: funding + sweep both relayed (child accepted spending an unconfirmed parent), mined, claimer address credited the gift. UI morph smoke passes. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Interim safety for on-chain gifts ahead of the crypto redesign. (1) On-chain ↵Adam Soltys2026-09-062-0/+37
| | | | | | | (Savings) gift creation now shows a hard bearer-risk warning: the presigned input is redirectable, a sweeper bot is actively front-running shared links, so share privately and claim immediately (Spending/ark gifts are unaffected). (2) The false "success" is fixed: a broadcast returning OK was never proof WE won — a higher-fee sweep can evict our claim (the "claimed! → empty wallet" the bot produced). verifyClaimWon now polls the funding coin after broadcast and, if it settles spent by a txid that isn't ours, retracts the celebration and shows the already-taken screen. Full architectural fix (ephemeral-key gifts, non-malleable SIGHASH_ALL claim) is next Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* On-chain gift claims can no longer strand funds at an address the claimer's ↵Adam Soltys2026-09-061-1/+19
| | | | | | | backed-up seed doesn't control. A claim into a fresh wallet reportedly paid a DIFFERENT wallet's address than the seed shown for backup (5000-sat gift claimed in incognito → funds landed at an accumulator address the given seed couldn't derive), because the destination was wallet.receive[0] — which can be a stale address a previous account in the session left behind. Now the claim derives THIS wallet's own address fresh from the active seed (never receive[0]), and a hard guard refuses to broadcast unless that address is provably derivable from the wallet whose recovery phrase is about to be handed over — any future seed/wallet divergence becomes a safe error, not lost money Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* LNURL/merchant payments read as PAYMENTS, not zaps, and skip redundant ↵Adam Soltys2026-09-064-10/+96
| | | | | | | steps. (1) Terminology: a payment is a "zap" only when it targets a nostr person or note (npub / note-zap with an eventId, captured as z.social at begin); paying a lightning address, LNURL or merchant now says "Pay over Lightning" / "Pay" everywhere — amount screen, review title, success screen — even when the endpoint supports zap receipts. (2) A fixed-amount payment (min==max — a POS tag, paywall, bolt card) has nothing to type, so the amount screen is skipped and the flow goes straight to the quote/review; the two perceived confirmations collapse to one. (3) Per-recipient auto-pay budget: the review offers "Auto-pay <merchant> up to N next time" (ctx.lnBudget/setLnBudget in prefs, synced), and a fixed charge within a set budget pays as soon as the quote lands — no tap. Guarded: only fixed amounts, only within the budget, only when funds cover it; over budget / no funds / quote error all fall back to the normal review, and social zaps are untouched. Verified with a driven-feature harness (fake ctx + local LNURL server): fixed skips to startLnPay with social=false/fixedSat set, a covering budget sets autopay, variable still shows the amount screen titled/buttoned "Pay" Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Spending wears just the ⚡ now, not ⚡ + the ark mark — the ark symbol ↵Adam Soltys2026-09-051-2/+1
| | | | | | | was noise beside lightning for a rail users think of as "instant lightning-like." Savings keeps its bitcoin disc. Dropped the now-unused ARK_MARK import Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Heal dead push subscriptions from a VAPID key rotation — the reason ↵Adam Soltys2026-09-052-4/+20
| | | | | | | notifications went silent. registerPush reused whatever getSubscription() returned and only ever subscribed fresh when there was none; a subscription made against an OLD VAPID key can never receive a push (the notifier's send fails with a permanent 403, which was never pruned, and the client kept re-registering the same dead endpoint). Now the client fetches the current VAPID key first and, when an existing subscription's applicationServerKey doesn't match, unsubscribes and re-subscribes against the current key — so a device self-heals on next open. And the notifier prunes 403 endpoints alongside 404/410. Deployed nwcpush on cs Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Payment push notifications are scoped to their network, so a mainnet payment ↵Adam Soltys2026-09-053-3/+23
| | | | | | | stops opening the staging PWA. The same seed derives the same nostr pubkey on mainnet and staging, so both PWAs registered push under one pubkey and the notifier — matching only by pubkey — fanned every mainnet payment out to the staging subscription too; tapping it opened staging.coinos.io. Now the client stamps its network on the push registration (pushWatch net: getNetwork()), the registrar declares its own network on the /notify call (CFG.forwarder.network), and nwcpush delivers a payment only to registrations whose net matches — with a rollout-safe fallback: a device that hasn't re-registered yet (net=null) still gets the push, and a caller that omits net still reaches everyone, so nobody goes dark mid-deploy. Deployed names + nwcpush on cs Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* POS tools are merchant-only now: the tap-to-pay link and terminal-token ↵Adam Soltys2026-09-052-3/+13
| | | | | | | cards hide behind a merchant designation — a one-line "I'm a merchant — show point-of-sale tools" toggle at the foot of the payment-address card, swapping to "Hide point-of-sale tools" once on. The flag lives in the names feature state, so it follows the account to every device via wallet sync; regular users just see one quiet line instead of two cards of terminal plumbing Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* The POS-link QR is itself a lightning: link — tapping it on the device ↵Adam Soltys2026-09-051-1/+7
| | | | | | | showing it hands the LNURL straight to whatever wallet owns the scheme, no scanning your own screen Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* The POS-link card's LNURL copy button copies a lightning: URI (lowercase ↵Adam Soltys2026-09-051-1/+3
| | | | | | | bech32 with the prefix) — tappable and pasteable anywhere that registers the scheme, including our own Android app — while the QR keeps the bare uppercase form for alphanumeric-mode density Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Settings: lay the payment-address sections out as cards; zap settings under ↵Adam Soltys2026-09-052-59/+65
| | | | | | | | | | | | Nostr Custom name, point of sale, the terminal's tap-to-pay link, own node and own domain each get a card on Settings → Payments instead of hiding in collapsed sections. The one-tap zap amount and the CLINK zap code move to Settings → Nostr, where the other nostr settings are. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMi2Y1oKPZ3TeeFmLej47y
* morph: leave <details open> to the user; key the names settings sectionsAdam Soltys2026-09-052-7/+19
| | | | | | | | | | | | Every background repaint stripped the browser-set `open` attribute (the fresh render never carries it), collapsing expanded sections, and the positional child match handed one section's open state to its neighbour whenever the conditional zap-code section came or went. `open` on a details is now user-owned unless a render sets it, and nodes carrying data-key only patch into the same key. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMi2Y1oKPZ3TeeFmLej47y
* Point of sale: show the terminal's tap-to-pay LNURL, link and QRAdam Soltys2026-09-052-2/+22
| | | | | Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMi2Y1oKPZ3TeeFmLej47y
* Point of sale: a coinos-pos terminal works against a nameAdam Soltys2026-09-053-2/+240
| | | | | | | | | | | | | | | | | | Registrar: the owner mints a bearer POS token from Settings (NIP-98 POST /pos/token, DELETE revokes; /admin/pos/token for operators). With it the terminal gets /pos/me, rings up sales at /pos/invoice (fiat via coinos.io rates, or sats), reads /pos/payments, and holds a websocket at /pos/ws that gets a payment message when the sale settles. Sales are always minted on our node so the settle loop sees them and the ordinary forward path delivers the sats to the name's ark address. The name's lnurlp pins the newest unpaid sale under five minutes old — coinos.io's /p/<name> rule — so an NFC tap or a scan pays exactly that invoice. Wallet: a "Point of sale" section on the names card creates, copies and revokes tokens. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01VMi2Y1oKPZ3TeeFmLej47y
* Account cards carry identity at a glance (user feedback): the Spending card ↵Adam Soltys2026-09-052-5/+25
| | | | | | | wears ⚡ + the ark mark in violet beside its label with a violet hairline across the card's crown; Savings wears the bitcoin disc in its orange with an orange crown. The tint is an inset shadow stacked with the card's own — clips to the radius, zero layout shift, so the carousel's equal-height cards stay equal. Kind-locked and single-balance wallets keep their plain label. (Same feedback proposed vertical-only navigation and one global send/receive button — parked, not obviously better than the carousel + per-account model.) Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* The chat inbox rows (DM list + community list) read at message size: peer ↵Adam Soltys2026-09-051-0/+4
| | | | | | | names 15px, last-message previews and member counts 14px — the generic .small (12px) made the inbox feel like fine print next to the freshly enlarged bubbles Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* LNURL payments name the merchant: after the pay-params fetch, the metadata's ↵Adam Soltys2026-09-051-0/+24
| | | | | | | text/identifier (the lightning address) replaces the bech32 blob as the payee's name and address on the amount screen, with the text/plain description as fallback. And when the address is a coinos name — whether it arrived as an lnurl's metadata, a typed [email protected], or an intent — the registrar is asked who owns it and the screen wears their AVATAR via the same profileChip the npub zap flow already uses; the pubkey also lets a NIP-57 zap request attribute to the right person. Lookup is fire-and-forget so the amount screen never waits on it Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Chat goes full-bleed on phones like the notes feed — the home list, ↵Adam Soltys2026-09-053-6/+21
| | | | | | | community rooms and DM threads (now sharing a chat-page class) drop their card walls and run edge to edge under 520px — and the type steps up a notch: bubbles 14→15px, names 13→14px, timestamps 11→12px. Plus: a fixed-price LNURL (minSendable == maxSendable — bolt-card top-ups, paywalls, static charges) no longer prompts for an amount — resolve() fills it and locks the field via the same fixedSat machinery fixed-amount BOLT 12 offers already use (disabled input, adjusted hints, confirm bypass) Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Handle lightning:lnurl1… links properly. Two faults: (1) an intent often ↵Adam Soltys2026-09-052-3/+29
| | | | | | | arrives BEFORE the paying feature is ready — ark connects lazily, so zaps' matchSendText declined and the raw URI fell through into the recipient field as dead text; the intent dispatcher now keeps re-offering the text to the feature chain for up to 20s and sweeps the leftover raw text out of the recipient when a feature claims it (the zap amount screen takes over). (2) When the fallback button did render, it read "⚡ Zap lightning:lnurl1dp68gurn8ghj7…<overflow>" — zapDisplay now names targets like a human: the address when there is one, a shortened bech32 otherwise, on both the send-form button and the amount screen Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Android wrapper v1.4: the launcher icon now truly matches the PWA. Two ↵Adam Soltys2026-09-059-2/+6
| | | | | | | discoveries via on-device screenshot comparison: (1) Chrome's maskable handling ZOOMS the PWA icon so far that the logo's outer ring is cropped away entirely — the PWA tile is actually the middle ring + half-moon only, so the adaptive foreground now renders from icon-inner.svg (the icon.svg minus its white disc and outer ring); (2) this launcher displays ~91% of the adaptive layer rather than the spec's 66% viewport, so the size was tuned empirically against side-by-side screenshots (three iterations: 61% too small, 84% too big, 77% matches). Published as 1.4 on zapstore; versionCode 5 Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Scrub silent payments from everything user-facing — the receive feature is ↵Adam Soltys2026-09-055-14/+6
| | | | | | | gone (no sp in the feature registry) but its mentions lingered: the zapstore description now says plain "full BIP84 bitcoin wallet" (fresh kind 32267 republished, verified clean on the relay), the README loses the sp feature section and its mentions in the E2E-suite and dev-proxy lists, the dead receiveSpTab i18n key is removed, and two stale comments (app.js seams, build.js HAL_FEATURES list) stop naming it. Deliberately KEPT: silentPaymentNote — the send review still labels an output paying someone else's sp1 address, which remains supported Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Android wrapper v1.2: launcher label is "Coinos" (capital C) and the icon ↵Adam Soltys2026-09-0511-3/+15
| | | | | | | matches the PWA exactly — the PWA's "maskable" icon turned out to be the plain transparent-background logo that Chrome plates on white, so the APK now ships a proper adaptive icon doing the same (white background layer, logo scaled into the 66% safe zone, launcher applies its mask); legacy mipmaps regenerated to match. Published to zapstore as release 1.2 and installed END-TO-END from the store on a real phone: the phone's Zapstore 0.2.7 couldn't read zsp's event schema ("app no longer exists") — updated to 1.1.2 and the flow ran clean: listing under the recognized coinos profile → Trust dialog → Play Protect first-seen scan → installed → launches mainnet v3 beta fullscreen Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Zapstore publishing config: coinos v3 is its own zapstore app (identifier ↵Adam Soltys2026-09-041-0/+29
| | | | | | | io.coinos.app from the APK package — separate listing from the legacy coinos app), published under the coinos npub with zsp (the current CLI, superseding zapstore-cli). Publish recipe in the file header; release 1.1 pushed to relay.zapstore.dev (kind 32267 app + 30063 release, APK on cdn.zapstore.dev) Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Android wrapper v1.1: scheme + NFC intents move onto a per-intent trampoline ↵Adam Soltys2026-09-045-20/+53
| | | | | | | (IntentActivity, noHistory) — a TWA already on screen ignores a new intent delivered to its still-alive launcher, so a second lightning: tap did nothing; the trampoline maps the URI to ?u= form and relaunches the TWA with CLEAR_TOP, which navigates the open app. Verified on a real phone over adb: URL-bar-less fullscreen (assetlinks verified after the site began serving /.well-known/assetlinks.json — the deploy hook's dist/* glob was blind to dot-directories, fixed on cs), lightning: intent lands on Send with the invoice filled, and a second bitcoin: intent fired while the app was open navigated it (BIP-21 parsed, address + amount). NFC NDEF filters ride the trampoline for bolt cards / NDEF tags Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Android wrapper app (android/): a minimal TWA around v3.coinos.io whose ↵Adam Soltys2026-09-0417-0/+212
| | | | | | | whole reason to exist is intent registration — lightning:/bitcoin:/nostr:/lnurl:/lnurlw:/lnurlp: VIEW intents and the same schemes via NFC NDEF dispatch (bolt cards, address stickers; NFC declared required=false), plus verified https links to the site. LauncherActivity forwards a scheme URI into the web app as ?u=<encoded>; the web app reads it BEFORE features construct — a nostr npub/nprofile rewrites itself onto the existing /npub profile deep link, payment URIs stash and dispatch through handleScanned (the QR path) once a wallet is open, and the param is stripped from the URL either way. assetlinks.json (static/.well-known/, copied by build.js) vouches for the signing cert so the TWA runs URL-bar-less; the built APK is served at /coinos.apk for sideloading. Signing keystore lives in android/ GITIGNORED (keystore.properties beside it) — the assetlinks fingerprint pins it, so losing it means re-shipping assetlinks too. Verified: headless ?u= routing (lightning → Send with invoice filled + URL cleaned; nostr → profile page), APK installed on a real phone via adb, schemes registered (chooser appears where other wallets also claim lightning:) Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Public kind-1 publishes (profile posts and thread replies) carry a NIP-89 ↵Adam Soltys2026-09-041-1/+8
| | | | | | | client tag — ['client', 'coinos'], name-only since no kind-31990 handler coordinate is published yet — so other clients show "via coinos". Deliberately absent from everything encrypted (DMs, community wraps): private traffic needs no billboard Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Spending-coins rows say what they mean: the exit chip reads "exit cost 3,688 ↵Adam Soltys2026-09-042-16/+12
| | | | | | | sats" (unit included) instead of "exit 3,688", the renewal price moved off the row's right edge into a chip like its siblings — "renewal cost 117 sats" (or "renewal cost free") — and the expiry chip carries a word beside its hourglass ("⏳ expiry 21d"). The depth chip is gone: hop count is implementation detail nobody needed to weigh, and the exit cost already prices its consequence. depthOf and the depth strings retired with it Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Hats no longer pop in a beat after page load. The hats feature is a deferred ↵Adam Soltys2026-09-042-1/+55
| | | | | | | chunk, so every boot painted bare heads until it arrived — but the core bundle can't carry the hat art. Instead, wrapAvatar now leaves a STAMP per head in localStorage (art + percent geometry, size-independent, capped at 40, cleared when a head goes bare so removals can't ghost), and the core avatar factory replays stamps until hatsReady answers — from then on a null from wrapAvatar genuinely means bare. Verified headless: a stamped hat is on the header avatar 15ms after DOM-ready, and once the chunk lands with a bare-head answer the stamp is cleared with no double-wrap Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Three profile/thread refinements from field use. (1) The thread reply box ↵Adam Soltys2026-09-041-62/+114
| | | | | | | sits INLINE, directly under the note it answers, instead of at the bottom of the thread where nobody scrolled to find it — slotted after the focused note (the root by default), appended at the end only until the focused note loads, with a refocus handshake so the cursor survives the box relocating when the thread finishes loading. (2) Posting is OPTIMISTIC: a provisional dimmed copy tops the feed before any signing or relay round-trip (a remote signer alone can take seconds), swapped for the signed event on success and withdrawn on failure — the composer closes immediately and reopens with the text intact if publishing fails; pending rows can't be opened/replied/zapped until real. (3) The full header nav (search, chat, settings, lock, avatar) now shows on the profile, thread, and search screens whenever a wallet is open — losing the search button on a profile made finding the next person a trek back home; the public no-wallet surface keeps the plain brand. All verified headless: box renders mid-card under the focused note with focus landing after load, a post appears 250ms after tapping Post (pre-publish) and survives settle, and the search button works from a profile Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Every nostr post row carries a ↩ Reply button beside the ⚡ zap: it opens ↵Adam Soltys2026-09-041-1/+14
| | | | | | | the post's thread focused on that note and puts the cursor straight in the reply box (inside an already-open thread it just re-targets the reply to that message). Verified headless on /adam: 51 rows all carry the button, tapping one lands with the thread reply input focused Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Links inside your own DM/chat bubble now inherit the bubble's foreground ↵Adam Soltys2026-09-041-0/+5
| | | | | | | with an underline, instead of the page link color — that color is calibrated against the page background, so in dark mode a near-white link sat on the accent-white own-bubble, unreadable. Verified in dark mode: link color equals the bubble text color (rgb 15,18,22), underlined Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Profile notes become a real feed: INFINITE SCROLL (nearing the page bottom ↵Adam Soltys2026-09-042-6/+63
| | | | | | | pulls the next page of older kind-1s — until = oldest seen, one fetch in flight, a short answer marks the end; the listener registers at feature construction so the public no-wallet /username pages scroll too), VIDEO rendering in noteBody (mp4/webm/mov/m4v as <video controls preload=metadata playsinline> beside the existing inline images), and FULL-BLEED on phones — the notes card drops its walls and runs edge to edge under 520px; the inner 440px scrollbox is gone everywhere, the page itself scrolls. Verified headless at phone width on /adam: feed spans 0px-to-edge both sides, images render inline, and scrolling grew the list 51 → 82 posts Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* The tap-a-bubble action sheet works in DMs too: reactions + Reply + Copy (no ↵Adam Soltys2026-09-042-8/+143
| | | | | | | Delete — a gift-wrapped DM can't be retracted from the peer's relays). DM reactions ride the kind-7-in-a-gift-wrap shape 0xchat uses (makeDMReaction in dm.js), sent to the peer and echoed to our own inbox so other devices fold them in; inbound kind-7 rumors land in dmReacts keyed by the reacted message's id — no thread lookup, so a reaction arriving before its message still sticks. Replies e-tag the quoted kind-14 (makeDMRumor grew extraTags) and render the same accent-bar quote as community chat; the composer gets the same replying-to bar. Chips are joinable and your own reaction is highlighted/replaceable, all optimistic with rollback. Round-trip verified: reply and reaction rumors wrap/unwrap with tags intact Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* The pager's Prev/Next buttons loop within the pages instead of disabling at ↵Adam Soltys2026-09-041-2/+5
| | | | | | | the ends: Next on the last page returns to page 1, Prev on page 1 jumps to the end. Unlike the swipe ring they never leave the list for the tabs — buttons paginate, swipes navigate. Applies to the shared pager, so the address list wraps the same way Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Flip the swipe ring's tab order per feedback: … page0 ⇄ page1 … ↵Adam Soltys2026-09-041-8/+8
| | | | | | | lastPage ⇄ Receive ⇄ Send ⇄ page0 … Left from the last history page now wraps into Receive then Send (matching the tab bar's visual order and the pre-ring muscle memory of left-from-history landing on Receive); right from page 1 gives Send, then Receive, then the last page. Verified headless: right×3 walks history → Send → Receive → history(last), left×3 walks it back Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* The wallet swipe surfaces now form ONE RING with no dead ends: … page0 ⇄ ↵Adam Soltys2026-09-041-25/+33
| | | | | | | page1 … lastPage ⇄ Send ⇄ Receive ⇄ page0 … Swiping left over history turns pages older and wraps into Send, then Receive, then back to page 0; swiping right from page 1 goes the other way — Receive, then Send, then the LAST page (entering history from a wrap lands on the chosen page; an oversized index clamps to the last). A detail view / spinner / empty list publishes no pager and is treated as a single page, so the swipe still reaches the tabs from anywhere. Verified headless with synthetic touches: right×3 walks history → Receive → Send → history(last), left×3 walks it back Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* An uploaded (or pasted) profile picture or cover previews at the top of the ↵Adam Soltys2026-09-041-6/+17
| | | | | | | profile page immediately, not after Save: while the editor is open the DRAFT is the truth for the banner, the avatar, and the display name shown up top — clearing a field previews the removal too. The editor's text fields render per keystroke so pasted URLs track live (the morph never rewrites a focused field, so it can't fight the typing); uploads already rendered after filling the field. Verified headless: typing picture/cover URLs into the edit form updates the page-top avatar and banner backgrounds before any save Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Your own profile page opens as a PROFILE now, not a form: npub (tap to copy ↵Adam Soltys2026-09-043-14/+64
| | | | | | | — it was hidden on your own page), bio, and latest posts, exactly like anyone else's, with the editor behind an explicit Edit profile button (Save or the new Cancel returns to the view) and a New post button that unfolds a composer — publishPost signs a plain kind-1 with the login/wallet identity, publishes to the identity's own relays, and prepends it to the notes list so it shows immediately. The header avatar's tooltip says "Your profile" instead of "Edit profile" to match. Compose/edit state clears on open/back so drafts never leak across profiles. Verified headless end-to-end: onboard → avatar → view mode (buttons + npub, no form) → Edit opens form → Cancel → New post opens composer Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Telegram-style message action sheet + replies in community chat. Tapping any ↵Adam Soltys2026-09-044-28/+129
| | | | | | | bubble opens a centered sheet: a quick row of the six reaction emojis (your current one highlighted; picking fires immediately) above Reply / Copy / Delete (delete on own messages only) — replacing the hover-only 🙂 picker, which was invisible on the phones that need it most. Links, images and the hover × keep their own clicks, and a desktop text-selection drag never pops the sheet over the copy. REPLIES are new: Reply arms a composer bar showing who/what you're answering (× to cancel, cleared on channel/room switch), the sent kind-9 e-tags the quoted message, and rows carrying an e-tag render the quoted context above their text — author + one-line snippet, silent when the original is deleted or not held. The public /chat page renders the same quotes. Verified against the built stylesheet: 300px sheet, six even emoji buttons, quote ellipsizes, circular cancel button Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* The delete × on chat bubbles is a true 18px circle again and no longer ↵Adam Soltys2026-09-041-1/+5
| | | | | | | overlaps the react opener: the global button padding (11px 16px) was flooring the ×'s border-box past its fixed 18px width — an oval — and the react button's step-aside was calibrated against that phantom width. padding:0 on .chat-del restores the circle; the opener steps to right:16px, leaving a measured 5px of air (verified headless with getBoundingClientRect: both 18×18, gap 5.0) Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Chat renders links and sends emoji reactions. Links: community bubbles and ↵Adam Soltys2026-09-044-5/+95
| | | | | | | DM bubbles now run through noteBody (URLs → anchors, image URLs inline, npub mentions clickable, all content kept as text nodes — nothing from a relay reaches innerHTML); the public /chat page gets its own linkedBody (same splitter, nostr refs stay dim stubs since there are no profile screens there). Reactions: a 🙂 opener on every bubble (rides the edge like the delete ×, steps left on own rows) unfolds a six-emoji picker; tapping an existing chip joins that vote, your own reaction is highlighted, and picking a different emoji replaces it (kind-7 rumor on the channel stream, one per author, fold keeps the latest — inbound rendering already existed, this adds the sending half). Optimistic like sendMessage: the chip lands instantly and is rolled back to whatever stood before if signing/wrapping fails Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz
* Public read-only coinos community chat at /chat — a share-anywhere link ↵Adam Soltys2026-09-045-15/+292
| | | | | | | that shows the live chat history to anyone, logged in or not, with a "Sign in to chat" button ("Open coinos" when the device already has an account). No server-side decoder needed: the community's join material was already public by construction (baked into the client bundle — every coinos user is meant to be a member), so the page derives the stream keys and decrypts wraps client-side exactly like the app's Chat tab, worker-offloaded with the same sliced fallback. Renders messages + edits + deletes + reactions and honors the folded control state's ban list; presence/typing/guestbook deliberately omitted. Profiles come from ONE batched kind-0 query per burst (a REQ per author tripped relay concurrency limits). Plumbing: the join material moved to src/community.js (shared with messages.js so they can never drift), /chat is excluded from the single-segment profile deep-link (which was eating the path and rewriting the URL before the route ran), and app renders stand down in public-chat mode so a feature-init render can't paint onboarding over the page. Verified headless against the live relays: 59 messages decrypt, ban-fold applies, names resolve Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MGzU12QrAA4G6BYGzGVdXz