This commit is contained in:
parent
6e673ed514
commit
219493e1b0
@ -1 +1 @@
|
|||||||
33fb91d4b60b7f14d236f83e44c9db42fa1d440f-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b
|
767a2e51259e707655c80d6449afa93abf982fec-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b
|
||||||
|
@ -1024,7 +1024,8 @@ app.post('/api/user-profile', requireAuth, async (req, res) => {
|
|||||||
career_priorities,
|
career_priorities,
|
||||||
career_list,
|
career_list,
|
||||||
phone_e164,
|
phone_e164,
|
||||||
sms_opt_in
|
sms_opt_in,
|
||||||
|
sms_reminders_opt_in
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -1069,6 +1070,10 @@ app.post('/api/user-profile', requireAuth, async (req, res) => {
|
|||||||
? (sms_opt_in ? 1 : 0)
|
? (sms_opt_in ? 1 : 0)
|
||||||
: (existing?.sms_opt_in ?? 0);
|
: (existing?.sms_opt_in ?? 0);
|
||||||
|
|
||||||
|
const smsRemindersFinal = (typeof sms_reminders_opt_in === 'boolean')
|
||||||
|
? (sms_reminders_opt_in ? 1 : 0)
|
||||||
|
: (existing?.sms_reminders_opt_in ?? 0);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
const updateQuery = `
|
const updateQuery = `
|
||||||
UPDATE user_profile
|
UPDATE user_profile
|
||||||
@ -1086,7 +1091,14 @@ app.post('/api/user-profile', requireAuth, async (req, res) => {
|
|||||||
career_priorities = ?,
|
career_priorities = ?,
|
||||||
career_list = ?,
|
career_list = ?,
|
||||||
phone_e164 = ?,
|
phone_e164 = ?,
|
||||||
sms_opt_in = ?
|
sms_opt_in = ?,
|
||||||
|
sms_reminders_opt_in = ?
|
||||||
|
sms_reminders_opt_in_at =
|
||||||
|
CASE
|
||||||
|
WHEN ? = 1 AND (sms_reminders_opt_in IS NULL OR sms_reminders_opt_in = 0)
|
||||||
|
THEN UTC_TIMESTAMP()
|
||||||
|
ELSE sms_reminders_opt_in_at
|
||||||
|
END
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`;
|
`;
|
||||||
const params = [
|
const params = [
|
||||||
@ -1105,6 +1117,7 @@ app.post('/api/user-profile', requireAuth, async (req, res) => {
|
|||||||
finalCareerList,
|
finalCareerList,
|
||||||
phoneFinal,
|
phoneFinal,
|
||||||
smsOptFinal,
|
smsOptFinal,
|
||||||
|
smsRemindersFinal,
|
||||||
profileId
|
profileId
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -1116,17 +1129,17 @@ app.post('/api/user-profile', requireAuth, async (req, res) => {
|
|||||||
INSERT INTO user_profile
|
INSERT INTO user_profile
|
||||||
(id, username, firstname, lastname, email, email_lookup, zipcode, state, area,
|
(id, username, firstname, lastname, email, email_lookup, zipcode, state, area,
|
||||||
career_situation, interest_inventory_answers, riasec_scores,
|
career_situation, interest_inventory_answers, riasec_scores,
|
||||||
career_priorities, career_list, phone_e164, sms_opt_in)
|
career_priorities, career_list, phone_e164, sms_opt_in, sms_reminders_opt_in)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||||
?, ?, ?,
|
?, ?, ?,
|
||||||
?, ?, ?, ?)
|
?, ?, ?, ?, ?)
|
||||||
`;
|
`;
|
||||||
const params = [
|
const params = [
|
||||||
profileId,
|
profileId,
|
||||||
finalUserName,
|
finalUserName,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
encEmail, // <-- was emailNorm
|
encEmail,
|
||||||
emailLookupVal,
|
emailLookupVal,
|
||||||
zipCode,
|
zipCode,
|
||||||
state,
|
state,
|
||||||
@ -1137,7 +1150,8 @@ app.post('/api/user-profile', requireAuth, async (req, res) => {
|
|||||||
finalCareerPriorities,
|
finalCareerPriorities,
|
||||||
finalCareerList,
|
finalCareerList,
|
||||||
phoneFinal,
|
phoneFinal,
|
||||||
smsOptFinal
|
smsOptFinal,
|
||||||
|
smsRemindersFinal
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ import pkg from 'pdfjs-dist';
|
|||||||
import pool from './config/mysqlPool.js';
|
import pool from './config/mysqlPool.js';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { decrypt } from './shared/crypto/encryption.js'
|
import { decrypt } from './shared/crypto/encryption.js'
|
||||||
|
import crypto from 'crypto';
|
||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
@ -690,7 +690,7 @@ const twilioForm = express.urlencoded({ extended: false });
|
|||||||
app.post('/api/auth/sms/inbound', twilioForm, async (req, res) => {
|
app.post('/api/auth/sms/inbound', twilioForm, async (req, res) => {
|
||||||
const body = String(req.body?.Body || '').trim().toUpperCase();
|
const body = String(req.body?.Body || '').trim().toUpperCase();
|
||||||
if (body === 'HELP') {
|
if (body === 'HELP') {
|
||||||
const twiml = `<?xml version="1.0" encoding="UTF-8"?><Response><Message>AptivaAI: Help with SMS. Email admin@aptivaai.com. Msg&Data rates may apply. Reply STOP to cancel.</Message></Response>`;
|
const twiml = `<?xml version="1.0" encoding="UTF-8"?><Response><Message>AptivaAI: Help with SMS. Email support@aptivaai.com. Msg&Data rates may apply. Reply STOP to cancel.</Message></Response>`;
|
||||||
return res.type('text/xml').send(twiml);
|
return res.type('text/xml').send(twiml);
|
||||||
}
|
}
|
||||||
return res.type('text/xml').send(`<?xml version="1.0" encoding="UTF-8"?><Response/>`);
|
return res.type('text/xml').send(`<?xml version="1.0" encoding="UTF-8"?><Response/>`);
|
||||||
@ -4024,11 +4024,11 @@ app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => {
|
|||||||
|
|
||||||
/* ───────────────── SMS reminder ───────────────── */
|
/* ───────────────── SMS reminder ───────────────── */
|
||||||
if (due_date) { // only if task has a due date
|
if (due_date) { // only if task has a due date
|
||||||
const [[profile]] = await pool.query(
|
const [[profile]] = await pool.query(
|
||||||
'SELECT phone_e164, sms_opt_in FROM user_profile WHERE id = ?',
|
'SELECT phone_e164, phone_verified_at, sms_reminders_opt_in FROM user_profile WHERE id = ?',
|
||||||
[req.id]
|
[req.id]
|
||||||
);
|
);
|
||||||
if (profile?.sms_opt_in && profile.phone_e164) {
|
if (profile?.sms_reminders_opt_in && profile.phone_verified_at && profile.phone_e164) {
|
||||||
await createReminder({
|
await createReminder({
|
||||||
userId : req.id,
|
userId : req.id,
|
||||||
phone : profile.phone_e164,
|
phone : profile.phone_e164,
|
||||||
|
@ -252,3 +252,8 @@ mysqldump \
|
|||||||
ALTER TABLE user_profile
|
ALTER TABLE user_profile
|
||||||
ADD COLUMN email_verified_at DATETIME NULL AFTER email_lookup,
|
ADD COLUMN email_verified_at DATETIME NULL AFTER email_lookup,
|
||||||
ADD COLUMN phone_verified_at DATETIME NULL AFTER phone_e164;
|
ADD COLUMN phone_verified_at DATETIME NULL AFTER phone_e164;
|
||||||
|
|
||||||
|
ALTER TABLE user_profile
|
||||||
|
ADD COLUMN sms_reminders_opt_in TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN sms_reminders_opt_in_at DATETIME NULL;
|
||||||
|
|
||||||
|
21
nginx.conf
21
nginx.conf
@ -30,6 +30,13 @@ http {
|
|||||||
listen [::]:80;
|
listen [::]:80;
|
||||||
server_name dev1.aptivaai.com;
|
server_name dev1.aptivaai.com;
|
||||||
return 301 https://$host$request_uri;
|
return 301 https://$host$request_uri;
|
||||||
|
|
||||||
|
location ^~ /api/auth/sms/ {
|
||||||
|
proxy_pass http://backend5002; # server3
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
########################################################################
|
########################################################################
|
||||||
@ -171,6 +178,13 @@ http {
|
|||||||
location ^~ /api/auth/ { limit_conn perip 5;
|
location ^~ /api/auth/ { limit_conn perip 5;
|
||||||
limit_req zone=reqperip burst=10 nodelay;
|
limit_req zone=reqperip burst=10 nodelay;
|
||||||
proxy_pass http://backend5000; }
|
proxy_pass http://backend5000; }
|
||||||
|
|
||||||
|
location ^~ /api/auth/sms/ {
|
||||||
|
proxy_pass http://backend5002; # server3
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
}
|
||||||
|
|
||||||
location = /api/user-profile { limit_conn perip 5;
|
location = /api/user-profile { limit_conn perip 5;
|
||||||
limit_req zone=reqperip burst=10 nodelay;
|
limit_req zone=reqperip burst=10 nodelay;
|
||||||
proxy_pass http://backend5000; }
|
proxy_pass http://backend5000; }
|
||||||
@ -208,6 +222,13 @@ http {
|
|||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto https;
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Twilio webhooks live in server3 (backend5002)
|
||||||
|
location ^~ /api/auth/sms/ {
|
||||||
|
proxy_pass http://backend5002;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
########################################################################
|
########################################################################
|
||||||
|
BIN
public/a2p/assets/opt-in-checkbox.png
Normal file
BIN
public/a2p/assets/opt-in-checkbox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
BIN
public/a2p/assets/reminders-toggle.png
Normal file
BIN
public/a2p/assets/reminders-toggle.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
public/a2p/assets/sms-terms.png
Normal file
BIN
public/a2p/assets/sms-terms.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
BIN
public/a2p/assets/verify-screen.png
Normal file
BIN
public/a2p/assets/verify-screen.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<meta name="robots" content="noindex" />
|
<meta name="robots" content="noindex" />
|
||||||
<title>AptivaAI – A2P 10DLC Campaign Verification</title>
|
<title>AptivaAI - A2P 10DLC Campaign Verification</title>
|
||||||
<style>
|
<style>
|
||||||
:root { --fg:#111; --muted:#555; --link:#1d4ed8; }
|
:root { --fg:#111; --muted:#555; --link:#1d4ed8; }
|
||||||
body { font:16px/1.6 system-ui,Segoe UI,Roboto,Arial,sans-serif; color:var(--fg);
|
body { font:16px/1.6 system-ui,Segoe UI,Roboto,Arial,sans-serif; color:var(--fg);
|
||||||
@ -23,7 +23,7 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>AptivaAI – A2P 10DLC Campaign Verification</h1>
|
<h1>AptivaAI - A2P 10DLC Campaign Verification</h1>
|
||||||
<div class="muted">Last updated: Sep 13, 2025</div>
|
<div class="muted">Last updated: Sep 13, 2025</div>
|
||||||
|
|
||||||
<h2>Program & Use Case</h2>
|
<h2>Program & Use Case</h2>
|
||||||
@ -38,6 +38,8 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p><strong>Originating number(s):</strong> +1 478-500-3955</p>
|
||||||
|
|
||||||
<h2>Call to Action (CTA) & Consent</h2>
|
<h2>Call to Action (CTA) & Consent</h2>
|
||||||
<p>Users opt in <em>inside their account</em> by entering a mobile number, checking a non-prechecked consent box linking to our
|
<p>Users opt in <em>inside their account</em> by entering a mobile number, checking a non-prechecked consent box linking to our
|
||||||
<a href="/sms" target="_blank" rel="noreferrer">SMS Terms</a>, <a href="/legal/privacy" target="_blank" rel="noreferrer">Privacy Policy</a>, and
|
<a href="/sms" target="_blank" rel="noreferrer">SMS Terms</a>, <a href="/legal/privacy" target="_blank" rel="noreferrer">Privacy Policy</a>, and
|
||||||
@ -66,8 +68,10 @@
|
|||||||
<p>All samples include brand and opt-out/help language; no shortened links or phone numbers in 2FA content.</p>
|
<p>All samples include brand and opt-out/help language; no shortened links or phone numbers in 2FA content.</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><code>AptivaAI code: 123456. Expires in 10 minutes. Reply STOP to cancel, HELP for help.</code></li>
|
<li><code>AptivaAI code: 123456. Expires in 10 minutes. Reply STOP to cancel, HELP for help.</code></li>
|
||||||
<li><code>AptivaAI sign-in code: 654321. If you didn’t request this, change your password.</code></li>
|
<li><code>AptivaAI sign-in code: 654321. If you didn't request this, change your password.</code></li>
|
||||||
<li><code>AptivaAI password reset code: 112233. Expires in 10 minutes. Reply STOP to cancel.</code></li>
|
<li><code>AptivaAI password reset code: 112233. Expires in 10 minutes. Reply STOP to cancel.</code></li>
|
||||||
|
<li><code>AptivaAI: SMS for verification & security alerts enabled. Message & Data rates may apply. Reply HELP for help, STOP to cancel. Frequency varies.</code></li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -91,12 +95,46 @@
|
|||||||
<img src="/a2p/assets/opt-in-checkbox.png" alt="Consent checkbox and copy" />
|
<img src="/a2p/assets/opt-in-checkbox.png" alt="Consent checkbox and copy" />
|
||||||
<figcaption>Consent checkbox (non-prechecked) linking to policies</figcaption>
|
<figcaption>Consent checkbox (non-prechecked) linking to policies</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
<figure>
|
|
||||||
<img src="/a2p/assets/otp-sms.png" alt="Example 2FA SMS on device" />
|
|
||||||
<figcaption>Example OTP SMS</figcaption>
|
|
||||||
</figure>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="muted">Carriers are not liable for delayed or undelivered messages.</p>
|
<p class="muted">Carriers are not liable for delayed or undelivered messages.</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
<h2>Reminders (Account Notifications)</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<p><strong>Program:</strong> AptivaAI Reminders (account notifications; non-marketing)</p>
|
||||||
|
<p><strong>Originating number(s):</strong> +1 678-710-3755</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>What we send:</strong> task & milestone reminders the user enables in Settings.</li>
|
||||||
|
<li><strong>Opt-out:</strong> Reply <strong>STOP</strong> to cancel; <strong>HELP</strong> for help.</li>
|
||||||
|
<li><strong>Fees:</strong> Msg & data rates may apply.</li>
|
||||||
|
<li><strong>Frequency:</strong> varies by user settings.</li>
|
||||||
|
<li><strong>Consent:</strong> not a condition of purchase or service.</li>
|
||||||
|
<li><strong>Policies:</strong>
|
||||||
|
<a href="/sms" target="_blank" rel="noreferrer">SMS Terms</a> •
|
||||||
|
<a href="/legal/privacy" target="_blank" rel="noreferrer">Privacy Policy</a> •
|
||||||
|
<a href="/legal/terms" target="_blank" rel="noreferrer">Terms of Service</a>
|
||||||
|
</li>
|
||||||
|
<li><strong>Contact:</strong> <a href="mailto:support@aptivaai.com">support@aptivaai.com</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid" style="margin-top:10px">
|
||||||
|
<figure>
|
||||||
|
<img src="/a2p/assets/reminders-toggle.png"
|
||||||
|
alt="In-app Reminders toggle with non-prechecked consent and policy links">
|
||||||
|
<figcaption>In-app CTA: non-prechecked consent for SMS reminders inside the account.</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Sample Messages</h3>
|
||||||
|
<div class="card">
|
||||||
|
<ul>
|
||||||
|
<li><code>AptivaAI reminder: “[Task name]” is due tomorrow. Reply STOP to cancel, HELP for help.</code></li>
|
||||||
|
<li><code>AptivaAI reminder: “[Milestone]” starts at [3:00 PM] today. Reply STOP to cancel.</code></li>
|
||||||
|
<li><code>AptivaAI reminder: Weekly goal check-in. Reply STOP to cancel, HELP for help.</code></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@ -10,5 +10,7 @@
|
|||||||
</style>
|
</style>
|
||||||
<h1>Privacy Policy</h1>
|
<h1>Privacy Policy</h1>
|
||||||
<p>We collect account information (name, email), an optional phone number to enable SMS, and usage data. We use this information to provide and secure the service, including sending SMS you enable (e.g., verification codes). We share data with processors under contracts that limit use to our instructions. You can opt out of SMS by replying <strong>STOP</strong>.</p>
|
<p>We collect account information (name, email), an optional phone number to enable SMS, and usage data. We use this information to provide and secure the service, including sending SMS you enable (e.g., verification codes). We share data with processors under contracts that limit use to our instructions. You can opt out of SMS by replying <strong>STOP</strong>.</p>
|
||||||
|
<p>We do not share mobile information with third parties or affiliates for marketing or promotional purposes. SMS opt-in data and consent are not shared with any third parties.</p>
|
||||||
|
|
||||||
<p>Contact: <a href="mailto:support@aptivaai.com">support@aptivaai.com</a></p>
|
<p>Contact: <a href="mailto:support@aptivaai.com">support@aptivaai.com</a></p>
|
||||||
<p><small>Last updated: Sep 13, 2025</small></p>
|
<p><small>Last updated: Sep 13, 2025</small></p>
|
||||||
|
@ -15,6 +15,11 @@
|
|||||||
<li><strong>Opt-out:</strong> Reply <strong>STOP</strong> to cancel. Reply <strong>HELP</strong> for help.</li>
|
<li><strong>Opt-out:</strong> Reply <strong>STOP</strong> to cancel. Reply <strong>HELP</strong> for help.</li>
|
||||||
<li><strong>Support:</strong> <a href="mailto:support@aptivaai.com">support@aptivaai.com</a></li>
|
<li><strong>Support:</strong> <a href="mailto:support@aptivaai.com">support@aptivaai.com</a></li>
|
||||||
<li><strong>Carriers:</strong> Not liable for delayed or undelivered messages.</li>
|
<li><strong>Carriers:</strong> Not liable for delayed or undelivered messages.</li>
|
||||||
|
<p><strong>Note:</strong> Consent is not a condition of purchase or service, and you can disable SMS in your account at any time.</p>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
<p>See our <a href="/legal/privacy">Privacy Policy</a> and <a href="/legal/terms">Terms of Service</a>.</p>
|
<p>See our <a href="/legal/privacy">Privacy Policy</a> and <a href="/legal/terms">Terms of Service</a>.</p>
|
||||||
<p><small>Last updated: Sep 13, 2025</small></p>
|
<p><small>Last updated: Sep 13, 2025</small></p>
|
||||||
|
|
||||||
|
<p><strong>Reminders Program (Account Notifications):</strong> Non-marketing reminders you enable in your account (frequency varies). Reply <strong>STOP</strong> to cancel, <strong>HELP</strong> for help. Consent is not a condition of purchase or service.</p>
|
||||||
|
|
||||||
|
@ -14,8 +14,9 @@ function UserProfile() {
|
|||||||
const [loadingAreas, setLoadingAreas] = useState(false);
|
const [loadingAreas, setLoadingAreas] = useState(false);
|
||||||
|
|
||||||
const [phoneE164, setPhoneE164] = useState('');
|
const [phoneE164, setPhoneE164] = useState('');
|
||||||
const [smsOptIn, setSmsOptIn] = useState(false);
|
const [smsRemindersOptIn, setSmsRemindersOptIn] = useState(false);
|
||||||
const [showChangePw, setShowChangePw] = useState(false);
|
const [showChangePw, setShowChangePw] = useState(false);
|
||||||
|
const [phoneVerifiedAt, setPhoneVerifiedAt] = useState(null);
|
||||||
|
|
||||||
// Subscription state
|
// Subscription state
|
||||||
const [sub, setSub] = useState(null);
|
const [sub, setSub] = useState(null);
|
||||||
@ -77,12 +78,14 @@ function UserProfile() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await authFetch('/api/user-profile?fields=' +
|
const res = await authFetch(
|
||||||
[
|
'/api/user-profile?fields=' +
|
||||||
'firstname','lastname','email',
|
[
|
||||||
'zipcode','state','area','career_situation',
|
'firstname','lastname','email',
|
||||||
'phone_e164','sms_opt_in'
|
'zipcode','state','area','career_situation',
|
||||||
].join(','),
|
'phone_e164','sms_opt_in',
|
||||||
|
'phone_verified_at','sms_reminders_opt_in' // may be absent if BE not updated yet
|
||||||
|
].join(','),
|
||||||
{ method: 'GET' }
|
{ method: 'GET' }
|
||||||
);
|
);
|
||||||
if (!res || !res.ok) return;
|
if (!res || !res.ok) return;
|
||||||
@ -96,7 +99,8 @@ function UserProfile() {
|
|||||||
setSelectedArea(data.area || '');
|
setSelectedArea(data.area || '');
|
||||||
setCareerSituation(data.career_situation || '');
|
setCareerSituation(data.career_situation || '');
|
||||||
setPhoneE164(data.phone_e164 || '');
|
setPhoneE164(data.phone_e164 || '');
|
||||||
setSmsOptIn(!!data.sms_opt_in);
|
setSmsRemindersOptIn(!!data.sms_reminders_opt_in); // falls back to false if field not returned
|
||||||
|
setPhoneVerifiedAt(data.phone_verified_at || null);
|
||||||
|
|
||||||
if (data.state) {
|
if (data.state) {
|
||||||
setLoadingAreas(true);
|
setLoadingAreas(true);
|
||||||
@ -150,7 +154,8 @@ function UserProfile() {
|
|||||||
area: selectedArea,
|
area: selectedArea,
|
||||||
careerSituation,
|
careerSituation,
|
||||||
phone_e164: phoneE164 || null,
|
phone_e164: phoneE164 || null,
|
||||||
sms_opt_in: !!smsOptIn,
|
sms_reminders_opt_in: !!smsRemindersOptIn,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -285,7 +290,7 @@ function UserProfile() {
|
|||||||
|
|
||||||
{/* Phone + SMS */}
|
{/* Phone + SMS */}
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<label className="mb-1 block text-sm font-medium text-gray-700">Mobile (E.164)</label>
|
<label className="mb-1 block text-sm font-medium text-gray-700">Mobile</label>
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
placeholder="+15551234567"
|
placeholder="+15551234567"
|
||||||
@ -293,14 +298,27 @@ function UserProfile() {
|
|||||||
onChange={(e) => setPhoneE164(e.target.value)}
|
onChange={(e) => setPhoneE164(e.target.value)}
|
||||||
className="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:border-blue-600 focus:outline-none"
|
className="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:border-blue-600 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
<label className="mt-2 inline-flex items-center gap-2 text-sm">
|
<label className="flex items-start gap-2 text-xs text-gray-700 leading-5 mt-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={smsOptIn}
|
checked={smsRemindersOptIn}
|
||||||
onChange={(e) => setSmsOptIn(e.target.checked)}
|
onChange={e => setSmsRemindersOptIn(e.target.checked)}
|
||||||
/>
|
disabled={!phoneVerifiedAt} // require a verified phone
|
||||||
I agree to receive SMS updates.
|
/>
|
||||||
|
<span>
|
||||||
|
Enable SMS reminders (non-marketing). Message frequency varies. Msg & data rates may apply.
|
||||||
|
Reply <strong>STOP</strong> to cancel, <strong>HELP</strong> for help.
|
||||||
|
Consent is not a condition of purchase or service. See{' '}
|
||||||
|
<a className="underline" href="/sms" target="_blank" rel="noreferrer">SMS Terms</a>,{' '}
|
||||||
|
<a className="underline" href="/legal/privacy" target="_blank" rel="noreferrer">Privacy Policy</a>, and{' '}
|
||||||
|
<a className="underline" href="/legal/terms" target="_blank" rel="noreferrer">Terms</a>.
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
{!phoneVerifiedAt && (
|
||||||
|
<div className="text-[11px] text-gray-500 mt-1">
|
||||||
|
Verify your number on the <a className="underline" href="/verify">Verify</a> page before enabling reminders.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Career Situation */}
|
{/* Career Situation */}
|
||||||
|
@ -126,11 +126,12 @@ export default function Verify() {
|
|||||||
onChange={e => setSmsConsent(e.target.checked)}
|
onChange={e => setSmsConsent(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="sms-consent" className="text-xs text-gray-700 leading-5">
|
<label htmlFor="sms-consent" className="text-xs text-gray-700 leading-5">
|
||||||
By requesting a code, you agree to the{' '}
|
By requesting a code, you agree to receive one-time texts from AptivaAI for account verification and security alerts.
|
||||||
|
Message frequency varies. Msg & data rates may apply. Reply <strong>STOP</strong> to opt out, <strong>HELP</strong> for help.
|
||||||
|
Consent is not a condition of purchase or service. See the{' '}
|
||||||
<a className="underline" href="/sms" target="_blank" rel="noreferrer">SMS Terms</a>,{' '}
|
<a className="underline" href="/sms" target="_blank" rel="noreferrer">SMS Terms</a>,{' '}
|
||||||
<a className="underline" href="/legal/privacy" target="_blank" rel="noreferrer">Privacy Policy</a>,{' '}
|
<a className="underline" href="/legal/privacy" target="_blank" rel="noreferrer">Privacy Policy</a>, and{' '}
|
||||||
and <a className="underline" href="/legal/terms" target="_blank" rel="noreferrer">Terms</a>.{' '}
|
<a className="underline" href="/legal/terms" target="_blank" rel="noreferrer">Terms</a>.
|
||||||
Reply STOP to opt out. Msg & data rates may apply.
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user