Claim a Guest into an Account, new or existing #365

Closed
opened 2026-10-11 16:09:20 +00:00 by root · 11 comments
root commented 2026-10-11 16:09:20 +00:00 (Migrated from gitlab.core-project.space)

Imported from GitHub amaurycoudr/chess-project#365, opened by @amaurycoudr on 2026-10-11 16:09 UTC.


Problem Statement

A Player who has been playing without an Account has decided Games owned by their browser's Guest. Once they sign up, or sign in to an Account they already have, those Games still belong to the Guest: they show only in this browser's "My games", not on the Player's other devices, and a cleared browser loses them for good. A Player who signs in halfway through a Game keeps their Seat as the Guest (ADR 0034), so that Game would land on the Guest too. And on a shared browser, the Guest's Games may not be theirs at all, so nothing may move without them saying so.

Solution

An Account claims a Guest, and with it the Guest's Games, only when the Player confirms (ADR 0035). The offer appears wherever the browser holds an unclaimed Guest that owns Games:

  • on the sign-up form, as a choice ticked by default;
  • right after sign-in, as a dialog;
  • on "My games", as a banner.

A claim is final. Every Game the Guest owns moves to the Account, and so does any Game decided later for a Seat the Guest still holds. The Games then show in "My games" on every signed-in browser of the Account. A Guest can be claimed by one Account only, and the claim cannot be undone. After the claim, the Guest keeps the Seats it already holds but takes no new one. The browser forgets the Guest on sign-out, so the next Player on that browser starts with a fresh Guest. Deleting the Account deletes the Guests it claimed.

User Stories

  1. As a Player signing up in a browser whose Guest owns Games, I want the sign-up form to offer adding them to my new Account, ticked by default and showing how many there are, so that my history comes with me in one step.
  2. As a Player signing up on a shared browser, I want to untick that choice, so that someone else's Games don't become mine.
  3. As a Player signing in to an existing Account in a browser whose Guest owns Games, I want a dialog that asks whether to add them, with how many there are, so that my history joins my Account.
  4. As a Player who answers "Not now" after sign-in, I want nothing to move, and the offer to stay on "My games", so that I can decide later.
  5. As a signed-in Player whose browser holds an unclaimed Guest that owns Games, I want "My games" to show a banner offering to add them, so that I can claim them whenever I choose.
  6. As a signed-out Player whose Guest owns Games, I want "My games" to invite me to sign up or sign in to keep them, so that I learn I can take them to another device.
  7. As a Player whose browser has no Guest, or a Guest with no Games, I want no offer anywhere, so that I am never asked about nothing.
  8. As a Player who confirms a claim, I want every Game the Guest owns to show in my Account's "My games", with my side and result as I played them, and to hear how many were added, so that I know it worked.
  9. As a Player with an Account, I want the claimed Games in "My games" on every browser I sign in to, so that I can find them on another device.
  10. As a Player who signed in in the middle of a Game and then claimed my Guest, I want that Game to land in my Account when it is decided, so that I don't have to claim it again.
  11. As a Player, I want the claim to be final and one Account per Guest, so that another Account on the same browser cannot take my Games afterwards.
  12. As a Player whose browser's Guest was already claimed by another Account, I want no offer and that Guest forgotten, so that I start fresh instead of adding to someone else's Account.
  13. As a Player who signs out after a claim, I want this browser to forget the claimed Guest, so that whoever plays here next doesn't add Games to my Account.
  14. As a Player on a browser that still holds a claimed Guest but has no Session, I want creating a Room or taking a Seat to give me a fresh Guest, so that my new Games never land on someone else's Account.
  15. As a Player who claims two Guests that once played each other, or a Guest that played my own Account, I want that self-game kept, shown once in "My games" with both sides as me, so that my history stays complete without counting it as a win or a loss.
  16. As a Player who deletes my Account, I want the Guests it claimed deleted too, and this browser to forget its claimed Guest, so that nothing of mine is left behind; the Games stay for my opponents, ownerless on my side.
  17. As a Player exporting my Account's data, I want the export to say which Guest claims I made and when, so that the export is complete.
  18. As a Player whose claim fails (network, server), I want to be told, with the offer still available, so that a failure never silently loses the choice.

Seams

  • The RPC wire, through the real app in process with rpc.fixtures.ts on PGlite, signing in through the Session cookies the Accounts v1 spec adds to that fixture. Covers 8, 10, 11, 14, 16, 17. The server halves of 7, 12 and 15 (the guest.get answers, one listing row for a self-game) are tested here too, as edge paths of those stories.
  • The front views, beside each view with its harness and MSW: the Accounts v1 sign-up and sign-in views, the claim banner view, the "My games" view and the Game page, and sign-out. Covers 1, 2, 3, 4, 5, 6, 7, 12, 13, 15, 18. The front half of 16 (forgetting the Guest on Account deletion) is tested here as an edge path.
  • e2e (apps/e2e), one new spec:
    • two Players play a Game and one resigns; the Guest who resigned signs up with the choice left ticked, and sees the Game in "My games"; a fresh browser context signs in to that Account and sees it too;
    • a Player with an Account made through the UI in another context plays as a Guest, signs in, confirms the dialog, and sees the Game in "My games".
      Covers 9, and walks 1, 3 and 8 end to end.

No Room seat: the Room keeps reporting the Guest holding each Seat, and claims are resolved when the Game is written.

Implementation Decisions

Applies: ADR 0035 (the claim), ADR 0034 (occupants and Game ownership), ADR 0030 (Sessions), ADR 0027 (resource-oriented contract), ADR 0028 (origin guard), ADR 0011 and ADR 0019 (front layout and Query), ADR 0025 (copy). No new ADR. If review moves where the claim lives in the front (below), that placement may need a line in ADR 0011's exceptions instead.

Schema (one migration, on top of the Guest history and Accounts v1 schemas; nothing they own is redefined)

  • The claim link is two nullable columns this spec adds to the Guest history spec's guests row: the claiming Account, and when it was claimed. Both are set together or not at all.
  • That spec writes a guests row only the first time a Guest owns a decided Game. So a Guest header with no row is a normal, valid, unclaimed Guest with zero Games, such as a Player seated in their first Game. The claim upserts the row by hash, as a Game write does.
  • The Account reference cascades on delete, so deleting an Account deletes the Guests it claimed with no change to the Accounts v1 deletion flow. The Games no longer reference those Guests (the claim moved them to the Account), and the Accounts v1 rule leaves the Account's side with no owner.
  • No column is ever cleared once set: there is no release.

The claim (games feature module in the API, beside the repository the Guest history spec builds)

  • One transaction:
    1. upsert the caller's Guest row by hash and lock it;
    2. refuse if it is claimed by another Account;
    3. if it is claimed by the caller's Account, change nothing and answer with zero Games moved;
    4. otherwise set the link, then rewrite every Game where the Guest is the white Guest, setting the white Account owner and emptying the white Guest, and the same for Black. This keeps Accounts v1's check of at most one owner per Color.
  • A self-game is rewritten like any other: both sides end up owned by the same Account.
  • A claim of a Guest that owns no Games yet is allowed and links it: a Game it then decides for a Seat it holds goes to the Account (ADR 0035). The front offers a claim only when the count is above zero.
  • Answering a repeat claim by the same Account with success keeps a double submit or a retry harmless. A claim by any other Account is the "second claim" ADR 0035 refuses.

A Game decided later

  • The Room is unchanged: when a Game is decided, it still reports the occupant of each Seat, which may be a claimed Guest.
  • When writing the Game, the repository resolves each Color's Guest after the Guest history spec's upsert of its row: a claimed Guest is written as that Color's Account owner with the Guest reference empty, and an unclaimed one as the Guest.
  • The write reads the claim under a lock on the Guest row it upserted, and the claim holds an exclusive lock on the same row. So a claim and a Game write that race cannot leave a Game on a claimed Guest.

Taking Seats

  • With a Session, room.create and seat.take act as the Account (Accounts v1). Nothing changes.
  • Without a Session, a Guest header naming a claimed Guest counts as no header on create and take, so the Guest history spec's minting gives a fresh Guest. One exception: a take in a Room where that Guest already holds a Seat hands it back, since a claimed Guest keeps the Seats it holds.
  • Only a claimed Guest is treated so. A header with no guests row is an ordinary Guest and is never re-minted.
  • Finding out whether a Guest is claimed takes one lookup by hash. It happens only in create and take without a Session, in the service and never in the RPC context, which stays synchronous as the Guest history spec requires.
  • Every other procedure still accepts a claimed Guest's header as proof of the Seats it holds. That is how a Player who signed in mid-game keeps playing.

Contract: a new guest router in @chess/contracts. The Guest arrives in the RPC context from its header, never in an input (ADR 0034).

  • guest.get, GET /guest: the caller's Guest as one of

    • none: no header;
    • unclaimed, with how many Games it owns, zero for a Guest with no row;
    • claimed-by-you: claimed by the Account of the caller's Session;
    • claimed-by-other: claimed, and the caller has no Session or a different Account.

    It is a read, so it needs no Origin (ADR 0028) and changes nothing.

  • guest.claim, POST /guest/claim: needs a Session, refused the way Accounts v1 refuses an unauthenticated call. It answers how many Games moved. New refusals:

    • GUEST_NOT_FOUND (404): no header;
    • GUEST_ALREADY_CLAIMED (409): claimed by another Account.

    Both go in the contract's refusal status table.

  • Self-games: the caller's Color in the "My games" listing, the viewer on the Game read (the owner of a Color, or a reader) and the Color in the Accounts v1 export gain one more value, the owner of both Colors. The listing returns such a Game once. This widens the earlier specs' shapes in place, and only this spec can produce the value.

Account export (Accounts v1 owns the flow): it gains one list, the Guests the Account claimed, each with its claim date and nothing else, never the hash. The claimed Games are already in the export as the Account's Games.

Front

  • The claim lives in the vertical Accounts v1 builds for Accounts (ADR 0030 names it auth/), not in games/: a vertical never imports a sibling, and the sign-up and sign-in views that offer the claim are there. Its services/ owns:
    • the query options for guest.get;
    • the claim mutation, which invalidates the Guest status and "My games" in onSettled (front rule 7).
  • Sign-up view: when the status is unclaimed, the form shows a checkbox, ticked by default, naming the count. After sign-up succeeds with it ticked, the view calls the claim. A failed claim leaves the Account signed in, shows a toast and leaves the banner on "My games". The sign-up procedure itself is not changed.
  • Sign-in view: after sign-in succeeds with status unclaimed, a dialog asks "Add N games to your account?" with "Add them" and "Not now". "Not now" changes nothing, and the dialog shows again at the next sign-in.
  • Claim banner view, composed by the "My games" route above the games/ view:
    • signed in and unclaimed: the offer, with the count;
    • signed out and unclaimed: links to sign up and sign in;
    • anything else: nothing.
  • The browser forgets its Guest, through the store the Guest history spec placed outside every vertical:
    • on sign-out and on Account deletion, when the status is claimed-by-you;
    • whenever a status read answers claimed-by-other.
  • When create or take returns a minted Guest, the browser stores it in place of the one it held, as the Guest history spec already does.
  • A copy of a deleted Guest's header left in some browser reads as a fresh unclaimed Guest with zero Games. This is accepted: that Guest owns nothing, and the Account it was claimed by is gone.
  • "My games" and the Game page show a self-game with both sides as "You", the result as the winning Color or a draw rather than won or lost, and White at the bottom.
  • Every new string goes in the Paraglide catalogue, in English and French (ADR 0025).

Testing Decisions

  • Prior art:
    • rpc.integration.test.ts with rpc.fixtures.ts, and the Guest history and Accounts v1 wire tests built on them;
    • the front's view tests with their harness and MSW handlers (the Room vertical's room.harness.tsx and room.handlers.ts show the pattern), and the Accounts v1 sign-up and sign-in view tests;
    • resign.spec.ts and room.fixtures.ts (seatedPair) in apps/e2e, with the Accounts v1 e2e helpers for signing up and in.
  • Edge and unhappy paths:
    • claim without a Session, and without a Guest header;
    • claim of a Guest with no row yet: the row is created and linked, and a Game it decides afterwards goes to the Account;
    • claim of a Guest claimed by another Account (refused, nothing moves) and by the same Account (success, zero moved);
    • two Accounts claiming one Guest at the same time: exactly one wins;
    • a claim racing a Game write for that Guest: the Game ends up on the Account;
    • a Guest owning White in one Game and Black in another: both move; the opponent's side is untouched;
    • a self-game, from two claimed Guests and from a Guest against its own Account: one listing row, both Colors as you;
    • a Game decided after the claim for a Seat the claimed Guest holds, with and without a Session on the move that decides it;
    • create and take without a Session but with a claimed Guest header mint a fresh Guest, except a take that hands back a Seat the claimed Guest holds; with a header that has no row they mint nothing; with a Session they act as the Account;
    • deleting the Account deletes its claimed Guests; their header afterwards reads as unclaimed with zero Games; the Games stay readable, ownerless on that side;
    • guest.get in each of its four answers, including unclaimed with zero for a header with no row, and a claimed Guest read with a different Account's Session;
    • sign-up with the box unticked claims nothing; a claim that fails after sign-up leaves the banner;
    • sign-out and Account deletion forget only a Guest claimed by this Account; an unclaimed Guest survives sign-out;
    • no offer for a Guest with no Games, or with no Guest at all, in every one of the three places.

Out of Scope

  • Everything Accounts v1 owns: sign-up, sign-in, sign-out, Sessions, account deletion and export flows, rate limiting.
  • The Game and Guest records and "My games" itself, owned by the Guest history spec; this spec adds only the claim link, the rewrite and the self-game value.
  • Undoing a claim, moving Games between Accounts, or claiming a Guest from another browser: a Guest lives in one browser, and a claim is final.
  • Claiming a Guest whose secret is lost; its Games stay unlisted for good (accepted in What a finished Game keeps, and which guest identity owns it).
  • Letting a Session prove a Seat held by a Guest it claimed. A Player who signs out mid-game after a claim loses that Seat in this browser, as ADR 0035's sign-out rule implies.
  • Excluding self-games from a rating: that is for the Elo map.
  • Knowing which Guest played a claimed Game (ADR 0035, "Revisit when").
  • The public profile, which lists the Account's Games, claimed ones included.

Further Notes

> _Imported from GitHub [amaurycoudr/chess-project#365](https://github.com/amaurycoudr/chess-project/issues/365), opened by [@amaurycoudr](https://github.com/amaurycoudr) on 2026-10-11 16:09 UTC._ <!-- github-issue:365 --> --- ## Problem Statement A **Player** who has been playing without an **Account** has decided **Games** owned by their browser's **Guest**. Once they sign up, or sign in to an **Account** they already have, those **Games** still belong to the **Guest**: they show only in this browser's "My games", not on the **Player**'s other devices, and a cleared browser loses them for good. A **Player** who signs in halfway through a **Game** keeps their **Seat** as the **Guest** (ADR 0034), so that **Game** would land on the **Guest** too. And on a shared browser, the **Guest**'s **Games** may not be theirs at all, so nothing may move without them saying so. ## Solution An **Account** claims a **Guest**, and with it the **Guest**'s **Games**, only when the **Player** confirms (ADR 0035). The offer appears wherever the browser holds an unclaimed **Guest** that owns **Games**: - on the sign-up form, as a choice ticked by default; - right after sign-in, as a dialog; - on "My games", as a banner. A claim is final. Every **Game** the **Guest** owns moves to the **Account**, and so does any **Game** decided later for a **Seat** the **Guest** still holds. The **Games** then show in "My games" on every signed-in browser of the **Account**. A **Guest** can be claimed by one **Account** only, and the claim cannot be undone. After the claim, the **Guest** keeps the **Seats** it already holds but takes no new one. The browser forgets the **Guest** on sign-out, so the next **Player** on that browser starts with a fresh **Guest**. Deleting the **Account** deletes the **Guests** it claimed. ## User Stories 1. As a **Player** signing up in a browser whose **Guest** owns **Games**, I want the sign-up form to offer adding them to my new **Account**, ticked by default and showing how many there are, so that my history comes with me in one step. 2. As a **Player** signing up on a shared browser, I want to untick that choice, so that someone else's **Games** don't become mine. 3. As a **Player** signing in to an existing **Account** in a browser whose **Guest** owns **Games**, I want a dialog that asks whether to add them, with how many there are, so that my history joins my **Account**. 4. As a **Player** who answers "Not now" after sign-in, I want nothing to move, and the offer to stay on "My games", so that I can decide later. 5. As a signed-in **Player** whose browser holds an unclaimed **Guest** that owns **Games**, I want "My games" to show a banner offering to add them, so that I can claim them whenever I choose. 6. As a signed-out **Player** whose **Guest** owns **Games**, I want "My games" to invite me to sign up or sign in to keep them, so that I learn I can take them to another device. 7. As a **Player** whose browser has no **Guest**, or a **Guest** with no **Games**, I want no offer anywhere, so that I am never asked about nothing. 8. As a **Player** who confirms a claim, I want every **Game** the **Guest** owns to show in my **Account**'s "My games", with my side and result as I played them, and to hear how many were added, so that I know it worked. 9. As a **Player** with an **Account**, I want the claimed **Games** in "My games" on every browser I sign in to, so that I can find them on another device. 10. As a **Player** who signed in in the middle of a **Game** and then claimed my **Guest**, I want that **Game** to land in my **Account** when it is decided, so that I don't have to claim it again. 11. As a **Player**, I want the claim to be final and one **Account** per **Guest**, so that another **Account** on the same browser cannot take my **Games** afterwards. 12. As a **Player** whose browser's **Guest** was already claimed by another **Account**, I want no offer and that **Guest** forgotten, so that I start fresh instead of adding to someone else's **Account**. 13. As a **Player** who signs out after a claim, I want this browser to forget the claimed **Guest**, so that whoever plays here next doesn't add **Games** to my **Account**. 14. As a **Player** on a browser that still holds a claimed **Guest** but has no **Session**, I want creating a **Room** or taking a **Seat** to give me a fresh **Guest**, so that my new **Games** never land on someone else's **Account**. 15. As a **Player** who claims two **Guests** that once played each other, or a **Guest** that played my own **Account**, I want that self-game kept, shown once in "My games" with both sides as me, so that my history stays complete without counting it as a win or a loss. 16. As a **Player** who deletes my **Account**, I want the **Guests** it claimed deleted too, and this browser to forget its claimed **Guest**, so that nothing of mine is left behind; the **Games** stay for my opponents, ownerless on my side. 17. As a **Player** exporting my **Account**'s data, I want the export to say which **Guest** claims I made and when, so that the export is complete. 18. As a **Player** whose claim fails (network, server), I want to be told, with the offer still available, so that a failure never silently loses the choice. ## Seams - **The RPC wire**, through the real app in process with `rpc.fixtures.ts` on PGlite, signing in through the **Session** cookies the Accounts v1 spec adds to that fixture. Covers 8, 10, 11, 14, 16, 17. The server halves of 7, 12 and 15 (the `guest.get` answers, one listing row for a self-game) are tested here too, as edge paths of those stories. - **The front views**, beside each view with its harness and MSW: the Accounts v1 sign-up and sign-in views, the claim banner view, the "My games" view and the **Game** page, and sign-out. Covers 1, 2, 3, 4, 5, 6, 7, 12, 13, 15, 18. The front half of 16 (forgetting the **Guest** on **Account** deletion) is tested here as an edge path. - **e2e** (`apps/e2e`), one new spec: - two **Players** play a **Game** and one resigns; the **Guest** who resigned signs up with the choice left ticked, and sees the **Game** in "My games"; a fresh browser context signs in to that **Account** and sees it too; - a **Player** with an **Account** made through the UI in another context plays as a **Guest**, signs in, confirms the dialog, and sees the **Game** in "My games". Covers 9, and walks 1, 3 and 8 end to end. No **Room** seat: the **Room** keeps reporting the **Guest** holding each **Seat**, and claims are resolved when the **Game** is written. ## Implementation Decisions **Applies:** ADR 0035 (the claim), ADR 0034 (occupants and **Game** ownership), ADR 0030 (**Sessions**), ADR 0027 (resource-oriented contract), ADR 0028 (origin guard), ADR 0011 and ADR 0019 (front layout and Query), ADR 0025 (copy). No new ADR. If review moves where the claim lives in the front (below), that placement may need a line in ADR 0011's exceptions instead. **Schema** (one migration, on top of the Guest history and Accounts v1 schemas; nothing they own is redefined) - The claim link is two nullable columns this spec adds to the Guest history spec's `guests` row: the claiming **Account**, and when it was claimed. Both are set together or not at all. - That spec writes a `guests` row only the first time a **Guest** owns a decided **Game**. So a **Guest** header with no row is a normal, valid, unclaimed **Guest** with zero **Games**, such as a **Player** seated in their first **Game**. The claim upserts the row by hash, as a **Game** write does. - The **Account** reference cascades on delete, so deleting an **Account** deletes the **Guests** it claimed with no change to the Accounts v1 deletion flow. The **Games** no longer reference those **Guests** (the claim moved them to the **Account**), and the Accounts v1 rule leaves the **Account**'s side with no owner. - No column is ever cleared once set: there is no release. **The claim** (games feature module in the API, beside the repository the Guest history spec builds) - One transaction: 1. upsert the caller's **Guest** row by hash and lock it; 2. refuse if it is claimed by another **Account**; 3. if it is claimed by the caller's **Account**, change nothing and answer with zero **Games** moved; 4. otherwise set the link, then rewrite every **Game** where the **Guest** is the white **Guest**, setting the white **Account** owner and emptying the white **Guest**, and the same for Black. This keeps Accounts v1's check of at most one owner per **Color**. - A self-game is rewritten like any other: both sides end up owned by the same **Account**. - A claim of a **Guest** that owns no **Games** yet is allowed and links it: a **Game** it then decides for a **Seat** it holds goes to the **Account** (ADR 0035). The front offers a claim only when the count is above zero. - Answering a repeat claim by the same **Account** with success keeps a double submit or a retry harmless. A claim by any other **Account** is the "second claim" ADR 0035 refuses. **A Game decided later** - The **Room** is unchanged: when a **Game** is decided, it still reports the occupant of each **Seat**, which may be a claimed **Guest**. - When writing the **Game**, the repository resolves each **Color**'s **Guest** after the Guest history spec's upsert of its row: a claimed **Guest** is written as that **Color**'s **Account** owner with the **Guest** reference empty, and an unclaimed one as the **Guest**. - The write reads the claim under a lock on the **Guest** row it upserted, and the claim holds an exclusive lock on the same row. So a claim and a **Game** write that race cannot leave a **Game** on a claimed **Guest**. **Taking Seats** - With a **Session**, `room.create` and `seat.take` act as the **Account** (Accounts v1). Nothing changes. - Without a **Session**, a **Guest** header naming a claimed **Guest** counts as no header on create and take, so the Guest history spec's minting gives a fresh **Guest**. One exception: a take in a **Room** where that **Guest** already holds a **Seat** hands it back, since a claimed **Guest** keeps the **Seats** it holds. - Only a claimed **Guest** is treated so. A header with no `guests` row is an ordinary **Guest** and is never re-minted. - Finding out whether a **Guest** is claimed takes one lookup by hash. It happens only in create and take without a **Session**, in the service and never in the RPC context, which stays synchronous as the Guest history spec requires. - Every other procedure still accepts a claimed **Guest**'s header as proof of the **Seats** it holds. That is how a **Player** who signed in mid-game keeps playing. **Contract**: a new `guest` router in `@chess/contracts`. The **Guest** arrives in the RPC context from its header, never in an input (ADR 0034). - `guest.get`, `GET /guest`: the caller's **Guest** as one of - `none`: no header; - `unclaimed`, with how many **Games** it owns, zero for a **Guest** with no row; - `claimed-by-you`: claimed by the **Account** of the caller's **Session**; - `claimed-by-other`: claimed, and the caller has no **Session** or a different **Account**. It is a read, so it needs no Origin (ADR 0028) and changes nothing. - `guest.claim`, `POST /guest/claim`: needs a **Session**, refused the way Accounts v1 refuses an unauthenticated call. It answers how many **Games** moved. New refusals: - `GUEST_NOT_FOUND` (404): no header; - `GUEST_ALREADY_CLAIMED` (409): claimed by another **Account**. Both go in the contract's refusal status table. - Self-games: the caller's **Color** in the "My games" listing, the viewer on the **Game** read (the owner of a **Color**, or a reader) and the **Color** in the Accounts v1 export gain one more value, the owner of both **Colors**. The listing returns such a **Game** once. This widens the earlier specs' shapes in place, and only this spec can produce the value. **Account export** (Accounts v1 owns the flow): it gains one list, the **Guests** the **Account** claimed, each with its claim date and nothing else, never the hash. The claimed **Games** are already in the export as the **Account**'s **Games**. **Front** - The claim lives in the vertical Accounts v1 builds for **Accounts** (ADR 0030 names it `auth/`), not in `games/`: a vertical never imports a sibling, and the sign-up and sign-in views that offer the claim are there. Its `services/` owns: - the query options for `guest.get`; - the claim mutation, which invalidates the **Guest** status and "My games" in `onSettled` (front rule 7). - Sign-up view: when the status is `unclaimed`, the form shows a checkbox, ticked by default, naming the count. After sign-up succeeds with it ticked, the view calls the claim. A failed claim leaves the **Account** signed in, shows a toast and leaves the banner on "My games". The sign-up procedure itself is not changed. - Sign-in view: after sign-in succeeds with status `unclaimed`, a dialog asks "Add N games to your account?" with "Add them" and "Not now". "Not now" changes nothing, and the dialog shows again at the next sign-in. - Claim banner view, composed by the "My games" route above the `games/` view: - signed in and `unclaimed`: the offer, with the count; - signed out and `unclaimed`: links to sign up and sign in; - anything else: nothing. - The browser forgets its **Guest**, through the store the Guest history spec placed outside every vertical: - on sign-out and on **Account** deletion, when the status is `claimed-by-you`; - whenever a status read answers `claimed-by-other`. - When create or take returns a minted **Guest**, the browser stores it in place of the one it held, as the Guest history spec already does. - A copy of a deleted **Guest**'s header left in some browser reads as a fresh unclaimed **Guest** with zero **Games**. This is accepted: that **Guest** owns nothing, and the **Account** it was claimed by is gone. - "My games" and the **Game** page show a self-game with both sides as "You", the result as the winning **Color** or a draw rather than won or lost, and White at the bottom. - Every new string goes in the Paraglide catalogue, in English and French (ADR 0025). ## Testing Decisions - Prior art: - `rpc.integration.test.ts` with `rpc.fixtures.ts`, and the Guest history and Accounts v1 wire tests built on them; - the front's view tests with their harness and MSW handlers (the **Room** vertical's `room.harness.tsx` and `room.handlers.ts` show the pattern), and the Accounts v1 sign-up and sign-in view tests; - `resign.spec.ts` and `room.fixtures.ts` (`seatedPair`) in `apps/e2e`, with the Accounts v1 e2e helpers for signing up and in. - Edge and unhappy paths: - claim without a **Session**, and without a **Guest** header; - claim of a **Guest** with no row yet: the row is created and linked, and a **Game** it decides afterwards goes to the **Account**; - claim of a **Guest** claimed by another **Account** (refused, nothing moves) and by the same **Account** (success, zero moved); - two **Accounts** claiming one **Guest** at the same time: exactly one wins; - a claim racing a **Game** write for that **Guest**: the **Game** ends up on the **Account**; - a **Guest** owning White in one **Game** and Black in another: both move; the opponent's side is untouched; - a self-game, from two claimed **Guests** and from a **Guest** against its own **Account**: one listing row, both **Colors** as `you`; - a **Game** decided after the claim for a **Seat** the claimed **Guest** holds, with and without a **Session** on the move that decides it; - create and take without a **Session** but with a claimed **Guest** header mint a fresh **Guest**, except a take that hands back a **Seat** the claimed **Guest** holds; with a header that has no row they mint nothing; with a **Session** they act as the **Account**; - deleting the **Account** deletes its claimed **Guests**; their header afterwards reads as `unclaimed` with zero **Games**; the **Games** stay readable, ownerless on that side; - `guest.get` in each of its four answers, including `unclaimed` with zero for a header with no row, and a claimed **Guest** read with a different **Account**'s **Session**; - sign-up with the box unticked claims nothing; a claim that fails after sign-up leaves the banner; - sign-out and **Account** deletion forget only a **Guest** claimed by this **Account**; an unclaimed **Guest** survives sign-out; - no offer for a **Guest** with no **Games**, or with no **Guest** at all, in every one of the three places. ## Out of Scope - Everything Accounts v1 owns: sign-up, sign-in, sign-out, **Sessions**, account deletion and export flows, rate limiting. - The **Game** and **Guest** records and "My games" itself, owned by the Guest history spec; this spec adds only the claim link, the rewrite and the self-game value. - Undoing a claim, moving **Games** between **Accounts**, or claiming a **Guest** from another browser: a **Guest** lives in one browser, and a claim is final. - Claiming a **Guest** whose secret is lost; its **Games** stay unlisted for good (accepted in [What a finished Game keeps, and which guest identity owns it](https://github.com/amaurycoudr/chess-project/issues/348)). - Letting a **Session** prove a **Seat** held by a **Guest** it claimed. A **Player** who signs out mid-game after a claim loses that **Seat** in this browser, as ADR 0035's sign-out rule implies. - Excluding self-games from a rating: that is for the Elo map. - Knowing which **Guest** played a claimed **Game** (ADR 0035, "Revisit when"). - The public profile, which lists the **Account**'s **Games**, claimed ones included. ## Further Notes - Map: [Wayfinder: accounts](https://github.com/amaurycoudr/chess-project/issues/346). Decisions: [How are guest games merged into an account?](https://github.com/amaurycoudr/chess-project/issues/352) (resolution comment, ADR 0035), [How does an account relate to the Seat token?](https://github.com/amaurycoudr/chess-project/issues/351) (ADR 0034), [Last product decisions before the account specs](https://github.com/amaurycoudr/chess-project/issues/362). - Blocked by: [Guest history: a Guest holds the Seat and owns its decided Games, listed on this browser, read and exported by link](https://github.com/amaurycoudr/chess-project/issues/355), and [Accounts v1: sign up with a username, sign in on any device, reset a password by email](https://github.com/amaurycoudr/chess-project/issues/364). - Likely two PRs (the API claim and wire; then the front and e2e). Split with `to-tickets` if it does not fit one.
root commented 2026-10-11 16:09:20 +00:00 (Migrated from gitlab.core-project.space)

GitHub issue relations, imported as "related" links (GitLab CE has no sub-issues or blocking links).

> _GitHub issue relations, imported as "related" links (GitLab CE has no sub-issues or blocking links)._ <!-- github-relations --> - Sub-issues: #385, #386, #387, #388
root commented 2026-10-13 08:39:50 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #385

mentioned in issue #385
root commented 2026-10-13 08:39:50 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #386

mentioned in issue #386
root commented 2026-10-13 08:39:51 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #387

mentioned in issue #387
root commented 2026-10-13 08:39:51 +00:00 (Migrated from gitlab.core-project.space)

mentioned in issue #388

mentioned in issue #388
root commented 2026-10-13 08:42:30 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #385

marked this issue as related to #385
root commented 2026-10-13 08:42:31 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #386

marked this issue as related to #386
root commented 2026-10-13 08:42:31 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #387

marked this issue as related to #387
root commented 2026-10-13 08:42:31 +00:00 (Migrated from gitlab.core-project.space)

marked this issue as related to #388

marked this issue as related to #388
root commented 2026-10-14 15:46:23 +00:00 (Migrated from gitlab.core-project.space)

mentioned in merge request !527

mentioned in merge request !527
root commented 2026-10-14 16:34:11 +00:00 (Migrated from gitlab.core-project.space)

mentioned in merge request !529

mentioned in merge request !529
root (Migrated from gitlab.core-project.space) closed this issue 2026-10-15 08:20:23 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
amaurycoudr/chess-project#365
No description provided.