Issue 11 · DApp #28

Purchase workflow, visible state by state.

XState coordinates round loading, pricing, identity, KYC, wallet intake, recovery, and backend confirmation. Regions run in parallel; context carries data and evidence.

Source: vscPurchaseWorkflow.ts

Top-level machine

One machine. Five independent regions. UI reads one projected selector.

purchase · type: parallel
round
loading
loaded
failed
pricing
idle
working
ESTIMATE
intake
idle
working
CONTRIBUTE
verification
idle
starting
redirecting
identity
checking
idle
refreshing

Implemented state-machine workflow

Exact XState regions and event-driven transitions. Horizontal scroll available on small screens.

createPurchaseWorkflow()
Implemented transitionFailure, guard, or safety branch

Normal purchase path

1

Load round.
Fetch snapshot, inspect recovery storage, trigger first estimate.

2

Choose amount.
Tokens or EUR input starts pricing actor. Chain returns quote.

3

Verify identity.
KYC status must become verified before contribution gate opens.

4

Review and contribute.
Machine enters intake.working only after every safety gate passes.

5

Confirm.
Wallet receipt and backend record produce final chain, record, and email states.

UI projection

selectPurchase(snapshot)

// derived UI flags
checkoutDisabled =
isSubmitting
|| persistedIntake
|| recoveryUnavailable
|| !kycReady
|| !realDepositsEnabled
|| !estimate
|| estimateBusy
|| belowMinimum

// UI receives one coherent view
estimateBusy, isSubmitting,
kycReady, recoveryPresentation,
chainState, recordState, emailState

Contribution path

CONTRIBUTE
→
Validate gates
→
Persist intent
→
Enroll
→
Approve
→
Re-read quote
→
Simulate
→
Wallet contribute
→
Backend confirm

Gate rejects wallet request when:

  • Wallet not authenticated or wrong chain.
  • KYC not verified.
  • Amount below round minimum.
  • Recovery unavailable or scope mismatched.
  • Quote missing or deposits disabled.

Quote changed after review:

agreement-requiredPayment maximum increased. Machine returns to safe idle state. User must review and explicitly accept new amount.

Recovery journal

Create contributionId
→
Persist intent
→
Save tx hashes
→
Confirm record
→
Clear intent
  • Unresolved payment keeps same contributionId.
  • Recovery retries confirmation, not payment submission.
  • Wallet, chain, deployment, manager, and round must match.
  • Confirmed payment gets new ID for next purchase.

Final result

chainState: idle | pending | confirmed | reverted | suspended-mismatch | unknown
recordState: not-started | pending | confirmed | ...
emailState: unknown | sent | failed

suspended-mismatch
Recovery evidence does not match.
Stored details remain. Retry disabled.

Ownership boundaries

State machine owns

  • Transitions and guards
  • Parallel operation status
  • UI-facing context
  • Errors and recovery presentation

Browser dependencies own

  • Wallet client and chain reads
  • localStorage journal
  • Enrollment and allowance
  • On-chain contribution and API confirmation
Key limitationThis is not one atomic transaction. Multiple wallet transactions run under a persisted recovery journal. Automatic resend stays disabled after unresolved outcomes.