From 26557e33761ca7713ad0f1b07eb4b1fdb3f0af1e Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Fri, 10 Apr 2026 13:24:26 -0500 Subject: [PATCH] Add consent confirmation before lead retrieval scanning Shows a Code of Conduct consent dialog on first use of the lead scanner. User must agree to only scan badges with explicit verbal permission. Consent stored in local storage, resets on logout. Cancel hides the scan button. Dialog doesn't reappear after agreeing. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/pages/map/map.ts | 33 +++++++++++++++++++++++++++++++++ src/app/providers/user-data.ts | 12 ++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/app/pages/map/map.ts b/src/app/pages/map/map.ts index 6e074681..ff12f861 100644 --- a/src/app/pages/map/map.ts +++ b/src/app/pages/map/map.ts @@ -243,6 +243,9 @@ export class MapPage implements OnInit, OnDestroy { this.refresh_presentation(); setTimeout(this.syncAllPending, 60000); + // Show consent dialog on first use + await this.showConsentDialog(); + // Check if this is a staff user (has door_check but not lead_retrieval) const hasLeadRetrieval = await this.userData.checkHasLeadRetrieval(); const hasDoorCheck = await this.userData.checkHasDoorCheck(); @@ -252,6 +255,36 @@ export class MapPage implements OnInit, OnDestroy { } } + async showConsentDialog() { + const hasConsent = await this.userData.checkScannerConsent(); + if (hasConsent) return; + + const alert = await this.alertCtrl.create({ + header: 'Lead Scanner Consent', + message: 'By using the lead scanner, you confirm that you will only scan badges of attendees who have given you explicit verbal permission. Scanning without consent violates the PyCon US Code of Conduct.', + backdropDismiss: false, + buttons: [ + { + text: 'Cancel', + role: 'cancel', + handler: () => { + // Disable scanning — user didn't consent + this.scan_start_button_visibility = 'hidden'; + }, + }, + { + text: 'I Agree', + handler: () => { + this.userData.setScannerConsent(true); + }, + }, + ], + }); + await alert.present(); + // Wait for dismissal before continuing + await alert.onDidDismiss(); + } + async showSponsorSelector() { try { const result: any = await this.pycon.fetchLeadRetrievalSponsors(); diff --git a/src/app/providers/user-data.ts b/src/app/providers/user-data.ts index 48fd3ab8..b9eba608 100644 --- a/src/app/providers/user-data.ts +++ b/src/app/providers/user-data.ts @@ -20,6 +20,7 @@ export class UserData { HAS_DOOR_CHECK = 'hasDoorCheck'; HAS_MASK_VIOLATION = 'hasMaskViolation'; IS_SPEAKER = 'isSpeaker'; + HAS_SCANNER_CONSENT = 'hasScannerConsent'; constructor( public storage: Storage, @@ -168,6 +169,7 @@ export class UserData { this.storage.remove(this.HAS_DOOR_CHECK); this.storage.remove(this.HAS_MASK_VIOLATION); this.storage.remove(this.IS_SPEAKER); + this.storage.remove(this.HAS_SCANNER_CONSENT); }).then(() => { window.dispatchEvent(new CustomEvent('user:logout')); }); @@ -255,6 +257,16 @@ export class UserData { }); } + setScannerConsent(value: boolean): Promise { + return this.storage.set(this.HAS_SCANNER_CONSENT, value); + } + + checkScannerConsent(): Promise { + return this.storage.get(this.HAS_SCANNER_CONSENT).then((value) => { + return value === true; + }); + } + setIsSpeaker(value: boolean): Promise { return this.storage.set(this.IS_SPEAKER, value); }