Flussi Utente
Il Contributor Portal ha due percorsi di ingresso:
- candidatura pubblica all'URL root
- onboarding diretto su invito o referral attraverso
?invite=CODEo?ref=CODE - identity link dell'app Playroll attraverso
?link_intent=<signed-token>
Entrambi i percorsi si basano sulla verifica email Supabase. L'email della sessione Supabase verificata è l'identità del contributor usata dal portale e, successivamente, dai controlli di accesso al recorder.
Candidatura Pubblica
Open public page
Enter email
Receive Supabase email link
Open latest link
Submit application
Application row is written to public.applications
Return later with same email to read latest application state
Campi raccolti dalla candidatura:
legal_name
verified email
city
university or group
contact handle
weekly availability
notes
privacy notice acknowledgement
Validazione e normalizzazione:
citydeve corrispondere al catalogo locale dei comuni ISTAT.universityviene normalizzata sul catalogo locale MUR/USTAT quando possibile.- Gruppi in formato libero o istituzioni non corrispondenti sono accettati come metadata non normalizzati.
- L'insert della application richiede un utente Supabase authenticated la cui email verificata corrisponda all'email inviata.
Onboarding su Invito o Referral
Gli URL di invito e referral aprono direttamente il workspace di onboarding:
/?invite=CODE
/?ref=CODE
Se il contributor non è autenticato, può compilare prima il form. Al submit, il browser salva la bozza in locale, invia l'email di verifica e scrive su Supabase soltanto dopo che il contributor torna con una sessione verificata.
Onboarding
Il workspace di onboarding è raggruppato in:
1. Profile and contact
2. Documents and declarations
3. Recorder access identity
Campi pre-recorder raccolti:
legal_name
verified email
city
contact_handle
tax_status
accepted_documents
document_acceptance_evidence
Il flusso visibile non raccoglie a questo stadio dati di pagamento né dati di identità ad alta sensibilità.
Accettazione dei Documenti
L'app genera documenti testuali a partire dai dati attuali del form:
- lettera di incarico
- privacy acknowledgement
- dichiarazione fiscale e ISEE notice
L'accettazione crea evidenza in public.onboarding_submissions.document_acceptance_evidence:
package_version
accepted_at
auth_user_id
verified_email
accepted_items.incarico.document_version
accepted_items.incarico.document_hash_sha256
accepted_items.privacy.document_version
accepted_items.privacy.document_hash_sha256
accepted_items.isee_notice.document_version
accepted_items.isee_notice.document_hash_sha256
accepted_items.compensation.document_version
accepted_items.compensation.document_hash_sha256
Una volta accettato un checkbox, i campi che modificherebbero il testo renderizzato sono bloccati finché la relativa accettazione non viene deselezionata. Per esempio, il tax status non può essere modificato finché la dichiarazione fiscale e l'ISEE acknowledgement sono accettati.
Rewards e Receiving Method
Dopo l'invio dell'onboarding, il contributor atterra nel workspace rewards. Le tre metriche vengono lette per contributor dalla vista public.contributor_dashboard; nulla è hard-coded.
Tempo validato := dashboard.validated_seconds
Ricompense sbloccate := dashboard.unlocked_eur
Cap rimanente := dashboard.remaining_cap_eur (== cap_limit_eur − fulfilled_eur)
contributor_dashboard aggrega:
playroll_time_ledger.seconds_deltadovestatus = 'validated'(somma)contributor_milestone_unlocksin join conreward_milestones, sommandoreward_value_eurperstatus in ('unlocked','fulfilled')(sbloccate) e solo'fulfilled'(pagate)- la riga più recente di
cap_snapshots(totale rolling 12 mesi, status) contributors.cap_limit_eur/cap_warning_eurper la riga del contributor
La vista è security_invoker, quindi la RLS sulle tabelle sottostanti si applica — un contributor vede solo la propria riga.
La pagina rewards raccoglie inoltre un receiving method così che il contributor sia pronto prima del primo trasferimento:
- percorso automatico: SEPA IBAN intestato al contributor
- percorso di fallback: richiesta di manual review
Per l'IBAN SEPA il browser valida formato, lunghezza paese e checksum prima dell'insert. Supabase normalizza e valida l'IBAN anche con funzioni di database e memorizza iban_last4.
Il metodo non è operativo finché la conferma email non va a buon fine:
status = confirmed
email_confirmation_status = confirmed
Le righe di manual review tengono il prelievo bloccato finché un sistema ops/pagamento non approva un'alternativa supportata al di fuori di questo repo.
Sequenza
1. User clicks "Salva metodo" on the IBAN form
→ recordEvent payout.receiving_method.submitted (web)
2. Client-side validate IBAN (length, country, checksum)
→ on failure: recordEvent payout.receiving_method.rejected
3. RPC payout_iban_usage to detect duplicate IBAN across profiles
4. INSERT into payout_receiving_methods with status='pending_email_confirmation'
→ recordEvent payout.receiving_method.persisted
5. RPC mint_payout_confirmation_token(p_method_id) → returns a single-use
64-char token bound to (auth.uid(), row id, pending status), valid 24h
6. signInWithOtp(verifiedEmail, intent=payout, redirect=?payout_confirm_token=<token>)
→ recordEvent auth.otp.requested, then auth.otp.sent or auth.otp.failed
7. User opens email link → verifyOtp callback
→ RPC confirm_payout_receiving_method_with_token(p_token) flips status to
'confirmed' in one SECURITY DEFINER transaction (status, email_confirmation_status,
email_confirmed_at) and clears the token
→ recordEvent payout.receiving_method.confirmed
8. If a link_intent is still on the URL, the dedup-guarded
syncContributorAppLink fires exactly once.
Revisione Documenti in Sola Lettura
Dal workspace rewards, un contributor può riaprire i documenti accettati in modalità di sola lettura. Può copiare o scaricare il testo renderizzato, ma non può modificare i campi profilo, cambiare il tax status, deselezionare accettazioni o reinviare il form di firma.
App Link Playroll
L'app link è opzionale al momento dell'ingresso ma è obbligatorio prima che l'app Playroll possa mostrare il contributor come verificato.
L'App Avvia il Flusso
Contributor is logged into Playroll app
App creates signed link intent (HS256 JWT, shared secret)
App opens portal with ?link_intent=<signed-token>
Contributor logs into portal
Portal captures pending link if activation gate is incomplete
Contributor completes documents and confirms SEPA IBAN
Portal activates link and syncs app projection
App refreshes on focus and shows Verified
L'app non può marcare il contributor come verificato localmente. Deve attendere contributor_status_projection sul progetto dell'app Playroll (vtyolotvuvlbvqbgbzde).
Chiamata di attivazione: una per sessione
Il portale chiama activate-contributor-app-link esattamente una volta per intent nonce. L'hook traccia i nonce processati in memoria e una singola promise in-flight per coalescere le chiamate concorrenti durante boot, conferma payout e completamento dell'onboarding. La telemetria mostrerà:
app_link.activate.requested (web) → 1
app_link.activate.received (edge) → 1
app_link.activate.verified (web + edge) → 1
Una query che restituisce più di un app_link.activate.requested per session_id è una regressione — vedi Telemetria.
Il Portale Avvia il Flusso (activation code)
Quando il contributor si è iscritto prima sul portale (es. landing, Discord, marketing) e installa l'app solo in seguito, non c'è una sessione app verso cui il portale possa pushare. Il portale emette un codice di attivazione a vita breve e il contributor lo incolla nell'app.
Contributor signs up on portal, completes documents, confirms SEPA IBAN
Portal Rewards section shows "Hai installato l'app PlayRoll? Genera un codice"
Contributor clicks "Genera codice"
Portal mint-activation-code returns an 8-char code (e.g. ZJ929AEY) with 15 min TTL
Portal UI shows the code formatted as "ZJ92 9AEY" plus a countdown
Contributor opens Playroll app, signs in with Steam
Contributor goes to Settings -> Account contributor
Contributor pastes the code and clicks Riscatta
App calls redeem-activation-code on the app project
Edge marks the code redeemed, flips contributor_app_links to active, upserts contributor_status_projection
App refreshes within ~1s, badge shows VERIFIED, the section in Settings disappears
Spec del codice: 8 caratteri, alfabeto A-H J K M N P-Z 2-9 (esclude 0/1/I/L/O per leggibilità), monouso, hashato SHA-256 a riposo, TTL 15 minuti. Il codice in chiaro viene mostrato al contributor esattamente una volta.
Telemetria per verificare che una sessione portal-first sia andata bene (una riga per stage):
select occurred_at, event_name, attrs, error_code
from public.telemetry_events
where session_id = '<the portal session_id>'
and event_name like 'recorder_code.%'
order by occurred_at;
Sequenza attesa: recorder_code.requested -> recorder_code.minted -> recorder_code.displayed -> recorder_code.copy_clicked -> recorder_code.redeemed. Qualsiasi *_failed porta un error_code che corrisponde alla typed error map della funzione di redeem (vedi Identity Link).
Il bridge non è il matching via email. Richiede comunque sia un utente app authenticated (Steam) sia un utente portale authenticated (magic link email); il codice di attivazione è la prova che entrambe le cose sono avvenute — semplicemente disaccoppiate nel tempo.