Add OTP-based signup page for website onboarding
- New portal/signup.html with phone → OTP → profile flow - Handle both new users (signup) and existing users (login) - Auto-detect user type and use appropriate API endpoints - Show DEV_OTP on page for local testing - Updated sendOTP.cfm to gracefully handle missing Twilio Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ed3f9192d5
commit
8e3bb681e7
2 changed files with 851 additions and 8 deletions
|
|
@ -130,19 +130,28 @@ try {
|
||||||
}, { datasource: "payfrit" });
|
}, { datasource: "payfrit" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send OTP via Twilio
|
// Send OTP via Twilio (if available)
|
||||||
smsResult = application.twilioObj.sendSMS(
|
smsMessage = "Code saved (SMS skipped in dev)";
|
||||||
recipientNumber: "+1" & phone,
|
devOTP = otp; // Return OTP in dev mode for testing
|
||||||
messageBody: "Your Payfrit verification code is: " & otp
|
|
||||||
);
|
|
||||||
|
|
||||||
smsStatus = smsResult.success ? "sent" : "failed: " & smsResult.message;
|
if (structKeyExists(application, "twilioObj")) {
|
||||||
|
try {
|
||||||
|
smsResult = application.twilioObj.sendSMS(
|
||||||
|
recipientNumber: "+1" & phone,
|
||||||
|
messageBody: "Your Payfrit verification code is: " & otp
|
||||||
|
);
|
||||||
|
smsMessage = smsResult.success ? "Verification code sent" : "SMS failed - please try again";
|
||||||
|
if (smsResult.success) devOTP = ""; // Don't leak OTP in production
|
||||||
|
} catch (any smsErr) {
|
||||||
|
smsMessage = "SMS error: " & smsErr.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
writeOutput(serializeJSON({
|
writeOutput(serializeJSON({
|
||||||
"OK": true,
|
"OK": true,
|
||||||
"UUID": userUUID,
|
"UUID": userUUID,
|
||||||
"MESSAGE": smsResult.success ? "Verification code sent" : "SMS failed but code created - contact support",
|
"MESSAGE": smsMessage,
|
||||||
"SMS_STATUS": smsStatus
|
"DEV_OTP": devOTP
|
||||||
}));
|
}));
|
||||||
|
|
||||||
} catch (any e) {
|
} catch (any e) {
|
||||||
|
|
|
||||||
834
portal/signup.html
Normal file
834
portal/signup.html
Normal file
|
|
@ -0,0 +1,834 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Get Started - Payfrit</title>
|
||||||
|
<link rel="stylesheet" href="portal.css">
|
||||||
|
<style>
|
||||||
|
/* Dark theme variables for signup/login pages */
|
||||||
|
:root {
|
||||||
|
--bg-primary: #0a0a0a;
|
||||||
|
--bg-card: #141414;
|
||||||
|
--bg-secondary: #1a1a1a;
|
||||||
|
--text-primary: #ffffff;
|
||||||
|
--text-secondary: #a0a0a0;
|
||||||
|
--text-muted: #666666;
|
||||||
|
--border-color: #2a2a2a;
|
||||||
|
--primary: #00ff88;
|
||||||
|
--primary-hover: #00cc6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-logo {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
background: var(--primary);
|
||||||
|
color: #000;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-header h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-header p {
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 15px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 255, 136, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input::placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-btn {
|
||||||
|
padding: 14px 24px;
|
||||||
|
background: var(--primary);
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-btn:hover {
|
||||||
|
background: var(--primary-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-error {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
color: #ef4444;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-error.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-success {
|
||||||
|
background: rgba(34, 197, 94, 0.1);
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||||
|
color: #22c55e;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-success.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
padding-top: 24px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-footer a {
|
||||||
|
color: var(--primary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* OTP Input Styling */
|
||||||
|
.otp-inputs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.otp-inputs input {
|
||||||
|
width: 48px;
|
||||||
|
height: 56px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.otp-inputs input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 255, 136, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resend-link {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resend-link button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--primary);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resend-link button:disabled {
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: not-allowed;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Phone input with flag */
|
||||||
|
.phone-input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-prefix {
|
||||||
|
padding: 14px 12px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 15px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-input-group input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Back button */
|
||||||
|
.back-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Welcome message for existing users */
|
||||||
|
.welcome-back {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-back h2 {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 20px;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-back p {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="signup-container">
|
||||||
|
<div class="signup-card">
|
||||||
|
<div class="signup-header">
|
||||||
|
<div class="signup-logo">P</div>
|
||||||
|
<h1>Get Started with Payfrit</h1>
|
||||||
|
<p>Create your account to start accepting orders</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 1: Phone Number -->
|
||||||
|
<div class="step active" id="step-phone">
|
||||||
|
<form class="signup-form" id="phoneForm">
|
||||||
|
<div class="signup-error" id="phoneError"></div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="phone">Mobile Phone Number</label>
|
||||||
|
<div class="phone-input-group">
|
||||||
|
<span class="phone-prefix">+1</span>
|
||||||
|
<input type="tel" id="phone" name="phone" placeholder="(555) 123-4567" required
|
||||||
|
pattern="[0-9()\- ]{10,14}" inputmode="tel" autocomplete="tel">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="signup-btn" id="phoneBtn">Send Verification Code</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="signup-footer">
|
||||||
|
<span style="color: var(--text-muted); font-size: 14px;">Already have an account? </span>
|
||||||
|
<a href="login.html">Sign in with password</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: OTP Verification -->
|
||||||
|
<div class="step" id="step-otp">
|
||||||
|
<a href="#" class="back-link" onclick="PayfritSignup.showStep('phone'); return false;">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
||||||
|
</svg>
|
||||||
|
Back
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div id="welcomeBack" class="welcome-back" style="display: none;">
|
||||||
|
<h2>Welcome back!</h2>
|
||||||
|
<p>Enter the code we sent to continue</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="signup-form" id="otpForm">
|
||||||
|
<div class="signup-error" id="otpError"></div>
|
||||||
|
<div class="signup-success" id="otpSuccess"></div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label style="text-align: center;">Enter 6-digit code sent to <span id="maskedPhone"></span></label>
|
||||||
|
<div class="otp-inputs">
|
||||||
|
<input type="text" maxlength="1" inputmode="numeric" pattern="[0-9]" class="otp-digit" data-index="0">
|
||||||
|
<input type="text" maxlength="1" inputmode="numeric" pattern="[0-9]" class="otp-digit" data-index="1">
|
||||||
|
<input type="text" maxlength="1" inputmode="numeric" pattern="[0-9]" class="otp-digit" data-index="2">
|
||||||
|
<input type="text" maxlength="1" inputmode="numeric" pattern="[0-9]" class="otp-digit" data-index="3">
|
||||||
|
<input type="text" maxlength="1" inputmode="numeric" pattern="[0-9]" class="otp-digit" data-index="4">
|
||||||
|
<input type="text" maxlength="1" inputmode="numeric" pattern="[0-9]" class="otp-digit" data-index="5">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="signup-btn" id="otpBtn">Verify Code</button>
|
||||||
|
|
||||||
|
<div class="resend-link">
|
||||||
|
<button type="button" id="resendBtn" onclick="PayfritSignup.resendOTP()" disabled>
|
||||||
|
Resend code in <span id="resendTimer">60</span>s
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 3: Profile (for new users only) -->
|
||||||
|
<div class="step" id="step-profile">
|
||||||
|
<a href="#" class="back-link" onclick="PayfritSignup.showStep('otp'); return false;">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
||||||
|
</svg>
|
||||||
|
Back
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div style="text-align: center; margin-bottom: 24px;">
|
||||||
|
<h2 style="color: var(--text-primary); font-size: 20px; margin: 0 0 8px;">Almost there!</h2>
|
||||||
|
<p style="color: var(--text-muted); font-size: 14px; margin: 0;">Tell us a bit about yourself</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="signup-form" id="profileForm">
|
||||||
|
<div class="signup-error" id="profileError"></div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="firstName">First Name</label>
|
||||||
|
<input type="text" id="firstName" name="firstName" placeholder="John" required autocomplete="given-name">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="lastName">Last Name</label>
|
||||||
|
<input type="text" id="lastName" name="lastName" placeholder="Smith" required autocomplete="family-name">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email Address</label>
|
||||||
|
<input type="email" id="email" name="email" placeholder="john@example.com" required autocomplete="email">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="signup-btn" id="profileBtn">Complete Setup</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Detect base path for API calls
|
||||||
|
const BASE_PATH = (() => {
|
||||||
|
const path = window.location.pathname;
|
||||||
|
const portalIndex = path.indexOf('/portal/');
|
||||||
|
if (portalIndex > 0) {
|
||||||
|
return path.substring(0, portalIndex);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
})();
|
||||||
|
|
||||||
|
const PayfritSignup = {
|
||||||
|
userUUID: null,
|
||||||
|
userToken: null,
|
||||||
|
userId: null,
|
||||||
|
phone: null,
|
||||||
|
isExistingUser: false,
|
||||||
|
resendCountdown: null,
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Setup form handlers
|
||||||
|
document.getElementById('phoneForm').addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.submitPhone();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('otpForm').addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.submitOTP();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('profileForm').addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.submitProfile();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup OTP input auto-focus
|
||||||
|
this.setupOTPInputs();
|
||||||
|
|
||||||
|
// Format phone number as user types
|
||||||
|
document.getElementById('phone').addEventListener('input', (e) => {
|
||||||
|
this.formatPhoneInput(e.target);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
formatPhoneInput(input) {
|
||||||
|
let value = input.value.replace(/\D/g, '');
|
||||||
|
if (value.length > 10) value = value.substring(0, 10);
|
||||||
|
|
||||||
|
if (value.length >= 6) {
|
||||||
|
input.value = `(${value.substring(0, 3)}) ${value.substring(3, 6)}-${value.substring(6)}`;
|
||||||
|
} else if (value.length >= 3) {
|
||||||
|
input.value = `(${value.substring(0, 3)}) ${value.substring(3)}`;
|
||||||
|
} else if (value.length > 0) {
|
||||||
|
input.value = `(${value}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setupOTPInputs() {
|
||||||
|
const inputs = document.querySelectorAll('.otp-digit');
|
||||||
|
inputs.forEach((input, index) => {
|
||||||
|
input.addEventListener('input', (e) => {
|
||||||
|
const value = e.target.value.replace(/\D/g, '');
|
||||||
|
e.target.value = value.substring(0, 1);
|
||||||
|
|
||||||
|
if (value && index < 5) {
|
||||||
|
inputs[index + 1].focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-submit when all digits entered
|
||||||
|
if (this.getOTPValue().length === 6) {
|
||||||
|
this.submitOTP();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Backspace' && !e.target.value && index > 0) {
|
||||||
|
inputs[index - 1].focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle paste
|
||||||
|
input.addEventListener('paste', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const paste = (e.clipboardData || window.clipboardData).getData('text');
|
||||||
|
const digits = paste.replace(/\D/g, '').substring(0, 6);
|
||||||
|
|
||||||
|
digits.split('').forEach((digit, i) => {
|
||||||
|
if (inputs[i]) {
|
||||||
|
inputs[i].value = digit;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (digits.length === 6) {
|
||||||
|
this.submitOTP();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getOTPValue() {
|
||||||
|
return Array.from(document.querySelectorAll('.otp-digit'))
|
||||||
|
.map(input => input.value)
|
||||||
|
.join('');
|
||||||
|
},
|
||||||
|
|
||||||
|
clearOTPInputs() {
|
||||||
|
document.querySelectorAll('.otp-digit').forEach(input => {
|
||||||
|
input.value = '';
|
||||||
|
});
|
||||||
|
document.querySelector('.otp-digit').focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
showStep(step) {
|
||||||
|
document.querySelectorAll('.step').forEach(s => s.classList.remove('active'));
|
||||||
|
document.getElementById(`step-${step}`).classList.add('active');
|
||||||
|
|
||||||
|
// Focus first input in step
|
||||||
|
setTimeout(() => {
|
||||||
|
const firstInput = document.querySelector(`#step-${step} input:not([type="hidden"])`);
|
||||||
|
if (firstInput) firstInput.focus();
|
||||||
|
}, 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
showError(elementId, message) {
|
||||||
|
const el = document.getElementById(elementId);
|
||||||
|
el.textContent = message;
|
||||||
|
el.classList.add('show');
|
||||||
|
},
|
||||||
|
|
||||||
|
hideError(elementId) {
|
||||||
|
const el = document.getElementById(elementId);
|
||||||
|
el.classList.remove('show');
|
||||||
|
},
|
||||||
|
|
||||||
|
showSuccess(elementId, message) {
|
||||||
|
const el = document.getElementById(elementId);
|
||||||
|
el.textContent = message;
|
||||||
|
el.classList.add('show');
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitPhone() {
|
||||||
|
const phoneRaw = document.getElementById('phone').value;
|
||||||
|
this.phone = phoneRaw.replace(/\D/g, '');
|
||||||
|
|
||||||
|
if (this.phone.length !== 10) {
|
||||||
|
this.showError('phoneError', 'Please enter a valid 10-digit phone number');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hideError('phoneError');
|
||||||
|
const btn = document.getElementById('phoneBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Sending...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First try login (existing user)
|
||||||
|
const loginResponse = await fetch(BASE_PATH + '/api/auth/loginOTP.cfm', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ phone: this.phone })
|
||||||
|
});
|
||||||
|
|
||||||
|
const loginData = await loginResponse.json();
|
||||||
|
console.log('[Signup] loginOTP response:', loginData);
|
||||||
|
|
||||||
|
if (loginData.OK) {
|
||||||
|
// Existing user - login flow
|
||||||
|
this.isExistingUser = true;
|
||||||
|
this.userUUID = loginData.UUID;
|
||||||
|
this.devOTP = loginData.DEV_OTP || loginData.dev_otp || '';
|
||||||
|
document.getElementById('welcomeBack').style.display = 'block';
|
||||||
|
this.proceedToOTP();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginData.ERROR === 'no_account') {
|
||||||
|
// New user - signup flow
|
||||||
|
const signupResponse = await fetch(BASE_PATH + '/api/auth/sendOTP.cfm', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ phone: this.phone })
|
||||||
|
});
|
||||||
|
|
||||||
|
const signupData = await signupResponse.json();
|
||||||
|
console.log('[Signup] sendOTP response:', signupData);
|
||||||
|
|
||||||
|
if (signupData.OK) {
|
||||||
|
this.isExistingUser = false;
|
||||||
|
this.userUUID = signupData.UUID;
|
||||||
|
this.devOTP = signupData.DEV_OTP || signupData.dev_otp || '';
|
||||||
|
document.getElementById('welcomeBack').style.display = 'none';
|
||||||
|
this.proceedToOTP();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle signup errors
|
||||||
|
if (signupData.ERROR === 'phone_exists') {
|
||||||
|
this.showError('phoneError', 'This phone already has an account. Try signing in.');
|
||||||
|
} else {
|
||||||
|
this.showError('phoneError', signupData.MESSAGE || 'Failed to send code. Please try again.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.showError('phoneError', loginData.MESSAGE || 'Something went wrong. Please try again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Signup] Error:', err);
|
||||||
|
this.showError('phoneError', 'Connection error. Please try again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Send Verification Code';
|
||||||
|
},
|
||||||
|
|
||||||
|
proceedToOTP() {
|
||||||
|
// Show masked phone
|
||||||
|
const masked = `(${this.phone.substring(0, 3)}) ***-${this.phone.substring(6)}`;
|
||||||
|
document.getElementById('maskedPhone').textContent = masked;
|
||||||
|
|
||||||
|
// Show DEV_OTP if available (local dev only)
|
||||||
|
if (this.devOTP) {
|
||||||
|
this.showSuccess('otpSuccess', `DEV CODE: ${this.devOTP}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset and show OTP step
|
||||||
|
this.clearOTPInputs();
|
||||||
|
this.hideError('otpError');
|
||||||
|
this.showStep('otp');
|
||||||
|
|
||||||
|
// Start resend countdown
|
||||||
|
this.startResendCountdown();
|
||||||
|
|
||||||
|
// Reset button
|
||||||
|
document.getElementById('phoneBtn').disabled = false;
|
||||||
|
document.getElementById('phoneBtn').textContent = 'Send Verification Code';
|
||||||
|
},
|
||||||
|
|
||||||
|
startResendCountdown() {
|
||||||
|
let seconds = 60;
|
||||||
|
const btn = document.getElementById('resendBtn');
|
||||||
|
const timer = document.getElementById('resendTimer');
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
if (this.resendCountdown) clearInterval(this.resendCountdown);
|
||||||
|
|
||||||
|
this.resendCountdown = setInterval(() => {
|
||||||
|
seconds--;
|
||||||
|
timer.textContent = seconds;
|
||||||
|
|
||||||
|
if (seconds <= 0) {
|
||||||
|
clearInterval(this.resendCountdown);
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = 'Resend code';
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
|
||||||
|
async resendOTP() {
|
||||||
|
const endpoint = this.isExistingUser ? '/api/auth/loginOTP.cfm' : '/api/auth/sendOTP.cfm';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(BASE_PATH + endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ phone: this.phone })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.OK) {
|
||||||
|
this.userUUID = data.UUID;
|
||||||
|
this.showSuccess('otpSuccess', 'New code sent!');
|
||||||
|
this.startResendCountdown();
|
||||||
|
this.clearOTPInputs();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('otpSuccess').classList.remove('show');
|
||||||
|
}, 3000);
|
||||||
|
} else {
|
||||||
|
this.showError('otpError', data.MESSAGE || 'Failed to resend code');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.showError('otpError', 'Connection error. Please try again.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitOTP() {
|
||||||
|
const otp = this.getOTPValue();
|
||||||
|
|
||||||
|
if (otp.length !== 6) {
|
||||||
|
this.showError('otpError', 'Please enter the complete 6-digit code');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hideError('otpError');
|
||||||
|
const btn = document.getElementById('otpBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Verifying...';
|
||||||
|
|
||||||
|
const endpoint = this.isExistingUser ? '/api/auth/verifyLoginOTP.cfm' : '/api/auth/verifyOTP.cfm';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(BASE_PATH + endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ uuid: this.userUUID, otp: otp })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('[Signup] verify response:', data);
|
||||||
|
|
||||||
|
if (data.OK) {
|
||||||
|
this.userToken = data.Token || data.TOKEN;
|
||||||
|
this.userId = data.UserID || data.USERID;
|
||||||
|
|
||||||
|
// Save auth
|
||||||
|
localStorage.setItem('payfrit_portal_token', this.userToken);
|
||||||
|
localStorage.setItem('payfrit_portal_userid', this.userId);
|
||||||
|
|
||||||
|
if (this.isExistingUser) {
|
||||||
|
// Existing user - check if they have businesses and redirect
|
||||||
|
this.checkBusinessesAndRedirect();
|
||||||
|
} else if (data.NeedsProfile || data.NEEDSPROFILE) {
|
||||||
|
// New user needs profile
|
||||||
|
this.showStep('profile');
|
||||||
|
} else {
|
||||||
|
// Profile already complete (shouldn't happen for new users)
|
||||||
|
this.checkBusinessesAndRedirect();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.showError('otpError', data.MESSAGE || 'Invalid code. Please try again.');
|
||||||
|
this.clearOTPInputs();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Signup] Verify error:', err);
|
||||||
|
this.showError('otpError', 'Connection error. Please try again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Verify Code';
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitProfile() {
|
||||||
|
const firstName = document.getElementById('firstName').value.trim();
|
||||||
|
const lastName = document.getElementById('lastName').value.trim();
|
||||||
|
const email = document.getElementById('email').value.trim();
|
||||||
|
|
||||||
|
if (!firstName || !lastName || !email) {
|
||||||
|
this.showError('profileError', 'Please fill in all fields');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
|
||||||
|
this.showError('profileError', 'Please enter a valid email address');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hideError('profileError');
|
||||||
|
const btn = document.getElementById('profileBtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Completing...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(BASE_PATH + '/api/auth/completeProfile.cfm', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-User-Token': this.userToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ firstName, lastName, email })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('[Signup] completeProfile response:', data);
|
||||||
|
|
||||||
|
if (data.OK) {
|
||||||
|
// Profile complete - redirect to setup wizard
|
||||||
|
this.redirectToWizard();
|
||||||
|
} else {
|
||||||
|
this.showError('profileError', data.MESSAGE || 'Failed to save profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Signup] Profile error:', err);
|
||||||
|
this.showError('profileError', 'Connection error. Please try again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Complete Setup';
|
||||||
|
},
|
||||||
|
|
||||||
|
async checkBusinessesAndRedirect() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(BASE_PATH + '/api/portal/myBusinesses.cfm', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-User-Token': this.userToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ UserID: this.userId })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('[Signup] myBusinesses response:', data);
|
||||||
|
|
||||||
|
if (data.OK && data.BUSINESSES && data.BUSINESSES.length > 0) {
|
||||||
|
// Has businesses - go to portal
|
||||||
|
localStorage.setItem('payfrit_portal_business', data.BUSINESSES[0].BusinessID || data.BUSINESSES[0].BUSINESSID);
|
||||||
|
window.location.href = BASE_PATH + '/portal/index.html';
|
||||||
|
} else {
|
||||||
|
// No businesses - go to wizard
|
||||||
|
this.redirectToWizard();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Signup] Check businesses error:', err);
|
||||||
|
// Default to wizard
|
||||||
|
this.redirectToWizard();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
redirectToWizard() {
|
||||||
|
// Clear any existing business selection (we're creating new)
|
||||||
|
localStorage.removeItem('payfrit_portal_business');
|
||||||
|
window.location.href = BASE_PATH + '/portal/setup-wizard.html';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => PayfritSignup.init());
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Add table
Reference in a new issue