initial commit
This commit is contained in:
+92
@@ -0,0 +1,92 @@
|
||||
# Phone-Verification Gate for Public Share/Approval Pages
|
||||
|
||||
## Problem
|
||||
|
||||
You have a public share link (UUID-based token in URL) that lets customers approve or decline services. Anyone with the URL can access it — forwarding the link gives full control. You need a low-friction identity check without adding SMS gateways, email infrastructure, or database changes.
|
||||
|
||||
## Solution
|
||||
|
||||
Gate the page behind a **last-4-digits phone verification** screen. The customer's phone number is already stored on the record (`customerPhone`). Show a simple 4-digit input before revealing the decision UI.
|
||||
|
||||
## Pattern
|
||||
|
||||
### 1. Add a new page status
|
||||
|
||||
```ts
|
||||
type PageStatus = 'loading' | 'invalid' | 'expired' | 'done' | 'error' | 'phone_verify';
|
||||
const MAX_PHONE_ATTEMPTS = 3;
|
||||
```
|
||||
|
||||
### 2. Add state variables
|
||||
|
||||
```ts
|
||||
const [phoneInput, setPhoneInput] = useState('');
|
||||
const [phoneAttempts, setPhoneAttempts] = useState(0);
|
||||
```
|
||||
|
||||
### 3. Extract phone from record and gate
|
||||
|
||||
After loading the record, check if a phone exists (≥4 digits). If yes → `'phone_verify'`, else skip to `'done'`:
|
||||
|
||||
```ts
|
||||
const rawPhone = (record.customerPhone as string) || '';
|
||||
const hasPhone = rawPhone.replace(/\D/g, '').length >= 4;
|
||||
setPageStatus(hasPhone ? 'phone_verify' : 'done');
|
||||
```
|
||||
|
||||
**Important:** Do NOT call `setPageStatus('done')` unconditionally after loading — the phone-verify check must be the one that sets it.
|
||||
|
||||
### 4. Verification handler
|
||||
|
||||
```ts
|
||||
function handlePhoneVerify() {
|
||||
if (!record) return;
|
||||
const digitsOnly = record.customerPhone.replace(/\D/g, '');
|
||||
const last4 = digitsOnly.slice(-4);
|
||||
const inputDigits = phoneInput.replace(/\D/g, '');
|
||||
if (inputDigits === last4) {
|
||||
setPhoneAttempts(0);
|
||||
setPageStatus('done');
|
||||
} else {
|
||||
const newAttempts = phoneAttempts + 1;
|
||||
setPhoneAttempts(newAttempts);
|
||||
setPhoneInput('');
|
||||
if (newAttempts >= MAX_PHONE_ATTEMPTS) {
|
||||
setError('Too many incorrect attempts. Please contact the shop directly.');
|
||||
} else {
|
||||
setError(`Incorrect. ${MAX_PHONE_ATTEMPTS - newAttempts} attempt(s) remaining.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Verification UI (screen component)
|
||||
|
||||
Render this when `pageStatus === 'phone_verify'`:
|
||||
|
||||
- Logo/branding header (reuse same accent color)
|
||||
- Customer name display ("This quote is for John")
|
||||
- Instruction: "Enter last 4 digits of phone on file"
|
||||
- Large centered input (24px font, letter-spacing, `inputMode="numeric"`)
|
||||
- Submit button (disabled until 4 digits entered)
|
||||
- Enter key support (`onKeyDown`)
|
||||
- Locked state after max attempts (show shop phone number, hide input)
|
||||
|
||||
### 6. Skip when no phone
|
||||
|
||||
If the record has no phone or <4 digits, the verification screen never shows — backward compatible with existing records.
|
||||
|
||||
## Key Details
|
||||
|
||||
- **Stripping non-digits:** Always normalize both sides with `.replace(/\D/g, '')` before comparing. Phone formats vary (555-0100, +1 555 0100, 555.0100).
|
||||
- **Last 4 only:** Slice with `.slice(-4)` — handles any phone length globally.
|
||||
- **3 attempts max:** Prevents brute force. After lockout, show a "call the shop" message with the shop's business phone from settings.
|
||||
- **No backend changes:** The phone is already on the record the token gives access to. The verification is client-side — anyone who already has the token can see the record, but can't submit decisions without the verification step.
|
||||
- **Field name:** `customerPhone` matches the PocketBase collection field name used in SPQ-v2's `quoteWriteSchema`.
|
||||
|
||||
## Design Notes
|
||||
|
||||
- The input uses `tracking-[0.5em]` for wide spacing between digits (easy to read)
|
||||
- `autoFocus` and `maxLength={4}` make it feel like a PIN entry
|
||||
- On Enter, submit the verify, not the whole form
|
||||
- The shop's business phone number (`settings.businessPhone`) is shown on the lockout screen
|
||||
Reference in New Issue
Block a user