add a more comprehensive demo UI workflow for SMS signup
This commit is contained in:
@@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
## [?] - 2026
|
||||
### Added
|
||||
- Full flow for setting up SMS notifications
|
||||
|
||||
|
||||
## [1.3.8] - 2026
|
||||
### Added
|
||||
- Device wake-up for notifications
|
||||
|
||||
284
src/components/SmsVerificationDialog.vue
Normal file
284
src/components/SmsVerificationDialog.vue
Normal file
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<div v-if="visible" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<!-- Step 1: enter phone number -->
|
||||
<div v-if="step === 'phone'">
|
||||
<h1 class="text-xl font-bold text-center mb-2">Verify Your Phone</h1>
|
||||
<p class="text-sm text-slate-500 mb-4">
|
||||
Enter the mobile number where you want to receive New Activity text
|
||||
messages. We'll send a one-time code to confirm it's yours. Standard
|
||||
message and data rates may apply.
|
||||
</p>
|
||||
|
||||
<label for="sms-phone" class="text-slate-500 text-sm font-bold">
|
||||
Mobile number
|
||||
</label>
|
||||
<input
|
||||
id="sms-phone"
|
||||
v-model="phoneNumber"
|
||||
type="tel"
|
||||
inputmode="tel"
|
||||
autocomplete="tel"
|
||||
placeholder="+1 555 123 4567"
|
||||
class="block w-full rounded border border-slate-400 mt-1 mb-1 px-3 py-2"
|
||||
:disabled="sending"
|
||||
@keyup.enter="sendCode()"
|
||||
/>
|
||||
<p v-if="errorMessage" class="text-sm text-red-600 mb-2">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
:class="primaryButtonClasses"
|
||||
:disabled="sending"
|
||||
@click="sendCode()"
|
||||
>
|
||||
<font-awesome v-if="sending" icon="spinner" spin class="mr-1" />
|
||||
{{ sending ? "Sending…" : "Send Code" }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="cancelButtonClasses"
|
||||
:disabled="sending"
|
||||
@click="onClickCancel()"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: enter the code -->
|
||||
<div v-else-if="step === 'code'">
|
||||
<h1 class="text-xl font-bold text-center mb-2">Enter the Code</h1>
|
||||
<p class="text-sm text-slate-500 mb-4">
|
||||
We sent a 6-digit code to
|
||||
<b>{{ sentTo }}</b
|
||||
>. Enter it below to finish verifying your number.
|
||||
</p>
|
||||
|
||||
<label for="sms-code" class="text-slate-500 text-sm font-bold">
|
||||
Verification code
|
||||
</label>
|
||||
<input
|
||||
id="sms-code"
|
||||
v-model="code"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
autocomplete="one-time-code"
|
||||
maxlength="6"
|
||||
placeholder="123456"
|
||||
class="block w-full tracking-[0.4em] text-center text-lg rounded border border-slate-400 mt-1 mb-1 px-3 py-2"
|
||||
:disabled="verifying"
|
||||
@keyup.enter="verifyCode()"
|
||||
/>
|
||||
<p v-if="errorMessage" class="text-sm text-red-600 mb-2">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
|
||||
<div class="text-center mb-2">
|
||||
<button
|
||||
type="button"
|
||||
class="text-sm text-blue-500"
|
||||
:disabled="sending || verifying"
|
||||
@click="resendCode()"
|
||||
>
|
||||
{{ sending ? "Resending…" : "Resend code" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
:class="primaryButtonClasses"
|
||||
:disabled="verifying"
|
||||
@click="verifyCode()"
|
||||
>
|
||||
<font-awesome v-if="verifying" icon="spinner" spin class="mr-1" />
|
||||
{{ verifying ? "Verifying…" : "Verify" }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="cancelButtonClasses"
|
||||
:disabled="verifying"
|
||||
@click="onClickCancel()"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from "vue-facing-decorator";
|
||||
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { logger } from "@/utils/logger";
|
||||
|
||||
/**
|
||||
* SmsVerificationDialog Component
|
||||
*
|
||||
* A two-step modal that walks the user through verifying a mobile number so
|
||||
* they can opt in to SMS "New Activity" notifications:
|
||||
* 1. Enter phone number -> service texts a one-time code
|
||||
* 2. Enter the code -> service confirms the number belongs to them
|
||||
*
|
||||
* On successful verification the callback fires with (true, phoneNumber) so the
|
||||
* caller can then let the user turn the SMS notification on. Cancelling fires
|
||||
* the callback with (false).
|
||||
*
|
||||
* MOCK-UP NOTE: every back-end interaction here is stubbed. The send-code and
|
||||
* verify-code steps only simulate the round-trips locally so the screens can be
|
||||
* exercised end-to-end. See the TODO markers for where the notify-api calls go.
|
||||
*/
|
||||
@Component({
|
||||
mixins: [PlatformServiceMixin],
|
||||
})
|
||||
export default class SmsVerificationDialog extends Vue {
|
||||
visible = false;
|
||||
step: "phone" | "code" = "phone";
|
||||
phoneNumber = "";
|
||||
sentTo = "";
|
||||
code = "";
|
||||
sending = false;
|
||||
verifying = false;
|
||||
errorMessage = "";
|
||||
|
||||
callback: (success: boolean, phoneNumber?: string) => void = () => {};
|
||||
|
||||
/**
|
||||
* Opens the dialog at the phone-entry step.
|
||||
* @param aCallback - fired with (true, phoneNumber) on success, (false) on cancel
|
||||
* @param prefillPhone - optional number to pre-populate (e.g. re-verifying)
|
||||
*/
|
||||
open(
|
||||
aCallback?: (success: boolean, phoneNumber?: string) => void,
|
||||
prefillPhone = "",
|
||||
) {
|
||||
this.callback = aCallback || this.callback;
|
||||
this.step = "phone";
|
||||
this.phoneNumber = prefillPhone;
|
||||
this.sentTo = "";
|
||||
this.code = "";
|
||||
this.sending = false;
|
||||
this.verifying = false;
|
||||
this.errorMessage = "";
|
||||
this.visible = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the phone number and (STUB) ask the service to text a code.
|
||||
*/
|
||||
async sendCode(): Promise<void> {
|
||||
this.errorMessage = "";
|
||||
const normalized = this.normalizePhone(this.phoneNumber);
|
||||
if (!normalized) {
|
||||
this.errorMessage = "Please enter a valid mobile number.";
|
||||
return;
|
||||
}
|
||||
|
||||
this.sending = true;
|
||||
try {
|
||||
// TODO(notify-api): POST /api/sms/register { phoneNumber } -> triggers
|
||||
// the verification SMS. Handle rate-limit / invalid-number responses.
|
||||
await this.stubBackendCall();
|
||||
|
||||
this.phoneNumber = normalized;
|
||||
this.sentTo = normalized;
|
||||
this.code = "";
|
||||
this.step = "code";
|
||||
} catch (error) {
|
||||
logger.error("[SmsVerificationDialog] sendCode failed:", error);
|
||||
this.errorMessage = "Could not send the code. Please try again.";
|
||||
} finally {
|
||||
this.sending = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* (STUB) Ask the service to send a fresh code to the same number.
|
||||
*/
|
||||
async resendCode(): Promise<void> {
|
||||
this.errorMessage = "";
|
||||
this.sending = true;
|
||||
try {
|
||||
// TODO(notify-api): POST /api/sms/register again to re-send the code.
|
||||
await this.stubBackendCall();
|
||||
this.code = "";
|
||||
} catch (error) {
|
||||
logger.error("[SmsVerificationDialog] resendCode failed:", error);
|
||||
this.errorMessage = "Could not resend the code. Please try again.";
|
||||
} finally {
|
||||
this.sending = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the entered code and (STUB) confirm it with the service.
|
||||
*/
|
||||
async verifyCode(): Promise<void> {
|
||||
this.errorMessage = "";
|
||||
if (!/^\d{6}$/.test(this.code.trim())) {
|
||||
this.errorMessage = "Enter the 6-digit code we texted you.";
|
||||
return;
|
||||
}
|
||||
|
||||
this.verifying = true;
|
||||
try {
|
||||
// TODO(notify-api): POST /api/sms/verify { phoneNumber, code }. On a
|
||||
// mismatch, show an "incorrect code" error instead of succeeding. For
|
||||
// this mock any 6-digit code is accepted.
|
||||
await this.stubBackendCall();
|
||||
|
||||
this.visible = false;
|
||||
this.callback(true, this.phoneNumber);
|
||||
} catch (error) {
|
||||
logger.error("[SmsVerificationDialog] verifyCode failed:", error);
|
||||
this.errorMessage = "Could not verify the code. Please try again.";
|
||||
} finally {
|
||||
this.verifying = false;
|
||||
}
|
||||
}
|
||||
|
||||
onClickCancel(): void {
|
||||
this.visible = false;
|
||||
this.callback(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loosely normalize a phone number for display. Real E.164 validation and
|
||||
* formatting will happen server-side once the notify-api is wired in.
|
||||
*/
|
||||
private normalizePhone(raw: string): string {
|
||||
const trimmed = (raw || "").trim();
|
||||
const digits = trimmed.replace(/[^\d]/g, "");
|
||||
if (digits.length < 7) {
|
||||
return "";
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* MOCK-UP helper standing in for a notify-api round-trip. Resolves on the
|
||||
* next tick so the loading states are exercised without a real network call.
|
||||
*/
|
||||
private stubBackendCall(): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, 400));
|
||||
}
|
||||
|
||||
get primaryButtonClasses(): string {
|
||||
return "block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md disabled:opacity-50";
|
||||
}
|
||||
|
||||
get cancelButtonClasses(): string {
|
||||
return "block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md disabled:opacity-50";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -65,6 +65,8 @@ export const ACCOUNT_VIEW_CONSTANTS = {
|
||||
Standard message and data rates may apply, and you can turn it off at any time.
|
||||
Do you want more details?
|
||||
`,
|
||||
SMS_FORGET_PHONE_CONFIRM:
|
||||
"This will remove your phone number and turn off all text messages. You will have to verify a number again to re-enable them. Are you sure?",
|
||||
REMINDER_INFO: `
|
||||
This will notify you at a specific time each day.
|
||||
Note that it does not give you personalized notifications,
|
||||
|
||||
@@ -220,7 +220,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Channel 2: SMS text message (opt-in) -->
|
||||
<!-- Channel 2: SMS text messages (opt-in; gated on a verified phone) -->
|
||||
<div class="flex items-center justify-between mt-3 mb-2 pl-1">
|
||||
<!-- label -->
|
||||
<div>
|
||||
@@ -229,26 +229,23 @@
|
||||
class="fa-fw text-slate-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
SMS Text Message
|
||||
<button
|
||||
class="text-slate-400 fa-fw cursor-pointer"
|
||||
aria-label="Learn more about New Activity SMS notifications"
|
||||
@click.stop="showNewActivityNotificationInfo(true)"
|
||||
>
|
||||
<font-awesome icon="question-circle" aria-hidden="true" />
|
||||
</button>
|
||||
{{
|
||||
notifyingNewActivitySmsPhone
|
||||
? "SMS Text Message"
|
||||
: "Enable Text Messages"
|
||||
}}
|
||||
</div>
|
||||
<!-- toggle -->
|
||||
<!-- master toggle: enables the SMS channel (verifies phone on first use) -->
|
||||
<div
|
||||
class="relative ml-2 cursor-pointer"
|
||||
role="switch"
|
||||
:aria-checked="notifyingNewActivitySms"
|
||||
aria-label="Toggle New Activity notifications via SMS"
|
||||
:aria-checked="smsEnabled"
|
||||
aria-label="Toggle text message delivery"
|
||||
tabindex="0"
|
||||
@click.stop.prevent="showNewActivitySmsNotificationChoice()"
|
||||
@click.stop.prevent="toggleSmsEnabled()"
|
||||
>
|
||||
<input
|
||||
:checked="notifyingNewActivitySms"
|
||||
:checked="smsEnabled"
|
||||
type="checkbox"
|
||||
class="sr-only"
|
||||
readonly
|
||||
@@ -261,26 +258,79 @@
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="notifyingNewActivitySms" class="w-full pl-1">
|
||||
<div v-if="notifyingNewActivitySmsPhone" class="w-full pl-1">
|
||||
<div
|
||||
class="text-sm text-slate-500 mb-2 bg-white rounded px-3 py-2 border border-slate-200"
|
||||
class="text-sm text-slate-500 mb-2 bg-white rounded px-3 py-2 border border-slate-200 flex items-center justify-between gap-2 flex-wrap"
|
||||
>
|
||||
<!-- Label and value must share one line: Vue's whitespace
|
||||
condensing deletes a newline-only text node between elements,
|
||||
which would glue "Texting:" to the number. -->
|
||||
<div>
|
||||
<b>Time:</b>
|
||||
{{ notifyingNewActivitySmsTime.replace(" ", " ") }}
|
||||
<b>Texting:</b> <i>{{ notifyingNewActivitySmsPhone }}</i>
|
||||
</div>
|
||||
<button
|
||||
class="text-red-600"
|
||||
aria-label="Forget phone number and disable text messages"
|
||||
@click="forgetSmsPhoneNumber()"
|
||||
>
|
||||
<font-awesome icon="trash-can" class="fa-fw" aria-hidden="true" />
|
||||
Forget Phone Number
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Per-notification SMS toggles: only available while SMS is enabled -->
|
||||
<div v-if="smsEnabled" class="w-full pl-4">
|
||||
<div class="flex items-center justify-between mt-1 mb-2">
|
||||
<div>
|
||||
<b>Text to:</b>
|
||||
<i>{{ notifyingNewActivitySmsPhone || "(number not set)" }}</i>
|
||||
New Activity Text
|
||||
<button
|
||||
class="text-slate-400 fa-fw cursor-pointer"
|
||||
aria-label="Learn more about New Activity text messages"
|
||||
@click.stop="showNewActivityNotificationInfo(true)"
|
||||
>
|
||||
<font-awesome icon="question-circle" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<!-- toggle -->
|
||||
<div
|
||||
class="relative ml-2 cursor-pointer"
|
||||
role="switch"
|
||||
:aria-checked="notifyingNewActivitySms"
|
||||
aria-label="Toggle New Activity text messages"
|
||||
tabindex="0"
|
||||
@click.stop.prevent="showNewActivitySmsNotificationChoice()"
|
||||
>
|
||||
<input
|
||||
:checked="notifyingNewActivitySms"
|
||||
type="checkbox"
|
||||
class="sr-only"
|
||||
readonly
|
||||
@click.stop.prevent
|
||||
@change.stop.prevent
|
||||
/>
|
||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
||||
<div
|
||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-center">
|
||||
<button
|
||||
class="w-full text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
@click="editNewActivitySmsNotification"
|
||||
<div v-if="notifyingNewActivitySms" class="w-full">
|
||||
<div
|
||||
class="text-sm text-slate-500 mb-2 bg-white rounded px-3 py-2 border border-slate-200"
|
||||
>
|
||||
Edit SMS Time…
|
||||
</button>
|
||||
<div>
|
||||
<b>Time:</b>
|
||||
{{ notifyingNewActivitySmsTime.replace(" ", " ") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-center">
|
||||
<button
|
||||
class="w-full text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
@click="editNewActivitySmsNotification"
|
||||
>
|
||||
Edit SMS Time…
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -291,6 +341,7 @@
|
||||
</div>
|
||||
</section>
|
||||
<PushNotificationPermission ref="pushNotificationPermission" />
|
||||
<SmsVerificationDialog ref="smsVerificationDialog" />
|
||||
|
||||
<!-- User Profile -->
|
||||
<section
|
||||
@@ -886,6 +937,7 @@ import EntityIcon from "../components/EntityIcon.vue";
|
||||
import ImageMethodDialog from "../components/ImageMethodDialog.vue";
|
||||
import PushNotificationPermission from "../components/PushNotificationPermission.vue";
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import SmsVerificationDialog from "../components/SmsVerificationDialog.vue";
|
||||
import TopMessage from "../components/TopMessage.vue";
|
||||
import UserNameDialog from "../components/UserNameDialog.vue";
|
||||
import DataExportSection from "../components/DataExportSection.vue";
|
||||
@@ -967,6 +1019,7 @@ interface PushNotificationPermissionRef {
|
||||
LTileLayer,
|
||||
PushNotificationPermission,
|
||||
QuickNav,
|
||||
SmsVerificationDialog,
|
||||
TopMessage,
|
||||
UserNameDialog,
|
||||
DataExportSection,
|
||||
@@ -1021,12 +1074,15 @@ export default class AccountViewView extends Vue {
|
||||
// Notification properties
|
||||
notifyingNewActivity: boolean = false;
|
||||
notifyingNewActivityTime: string = "";
|
||||
// SMS delivery channel for New Activity (opt-in). MOCK-UP: state is currently
|
||||
// component-local. TODO: persist via settings + register the number and time
|
||||
// with the notify-api (SMS registration/verification flow).
|
||||
// SMS channel (opt-in). MOCK-UP: state is component-local fake data so the
|
||||
// flow can be walked through on an emulator; nothing is persisted. TODO:
|
||||
// register/verify/forget the number and preferences with the notify-api.
|
||||
/** Master switch: the SMS channel is on (requires a verified phone). */
|
||||
smsEnabled: boolean = false;
|
||||
/** Verified phone number; kept when smsEnabled turns off, cleared by Forget. */
|
||||
notifyingNewActivitySmsPhone: string = "";
|
||||
notifyingNewActivitySms: boolean = false;
|
||||
notifyingNewActivitySmsTime: string = "";
|
||||
notifyingNewActivitySmsPhone: string = "";
|
||||
notifyingReminder: boolean = false;
|
||||
notifyingReminderMessage: string = "";
|
||||
notifyingReminderTime: string = "";
|
||||
@@ -1365,37 +1421,99 @@ export default class AccountViewView extends Vue {
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the SMS delivery channel for New Activity notifications.
|
||||
* Toggle the SMS channel as a whole ("Enable Text Messages" / "SMS Text
|
||||
* Message" master switch).
|
||||
*
|
||||
* MOCK-UP: reuses the time-picker dialog so the UI behaves like the in-app
|
||||
* channel, but the picked time is only held in component state for now.
|
||||
* Enabling requires a verified phone number: the user enters a number, the
|
||||
* service texts a code, and the toggle only turns on once the code is
|
||||
* confirmed. Turning the toggle off pauses the channel but keeps the
|
||||
* verified number, so re-enabling skips re-verification; "Forget Phone
|
||||
* Number" is the destructive path.
|
||||
*
|
||||
* TODO (notify-api wiring): on enable, ensure the user has a verified phone
|
||||
* number, then POST the SMS-notification preference to the notify-api with
|
||||
* SMS-specific params (channel="sms", phoneNumber, sendTime) -- the same
|
||||
* alert as the in-app channel, delivered by a different transport. On
|
||||
* disable, tell the notify-api to remove the SMS preference.
|
||||
* MOCK-UP: the verification round-trips are stubbed in
|
||||
* SmsVerificationDialog and all state is component-local.
|
||||
*/
|
||||
async showNewActivitySmsNotificationChoice(): Promise<void> {
|
||||
if (!this.notifyingNewActivitySms) {
|
||||
(
|
||||
this.$refs.pushNotificationPermission as PushNotificationPermission
|
||||
).open(
|
||||
DAILY_CHECK_TITLE,
|
||||
async (success: boolean, timeText: string) => {
|
||||
if (success) {
|
||||
// TODO: verify phone number + register SMS preference with notify-api
|
||||
this.notifyingNewActivitySms = true;
|
||||
this.notifyingNewActivitySmsTime = timeText;
|
||||
}
|
||||
},
|
||||
{ skipSchedule: true },
|
||||
);
|
||||
} else {
|
||||
// TODO: remove SMS preference from notify-api
|
||||
async toggleSmsEnabled(): Promise<void> {
|
||||
if (this.smsEnabled) {
|
||||
// Pause the channel; the verified number stays for easy re-enable.
|
||||
// TODO(notify-api): disable all SMS preferences server-side.
|
||||
this.smsEnabled = false;
|
||||
this.notifyingNewActivitySms = false;
|
||||
this.notifyingNewActivitySmsTime = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.notifyingNewActivitySmsPhone) {
|
||||
// Number already verified: just switch the channel back on.
|
||||
this.smsEnabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
(this.$refs.smsVerificationDialog as SmsVerificationDialog).open(
|
||||
async (success: boolean, phoneNumber?: string) => {
|
||||
if (success && phoneNumber) {
|
||||
this.notifyingNewActivitySmsPhone = phoneNumber;
|
||||
this.smsEnabled = true;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forget the verified phone number entirely: the master toggle turns off
|
||||
* and every SMS notification becomes unavailable until a number is
|
||||
* verified again.
|
||||
*/
|
||||
async forgetSmsPhoneNumber(): Promise<void> {
|
||||
this.notify.confirm(
|
||||
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.SMS_FORGET_PHONE_CONFIRM,
|
||||
async () => {
|
||||
// TODO(notify-api): DELETE /api/sms/register to remove the number
|
||||
// and all SMS preferences server-side.
|
||||
this.notifyingNewActivitySmsPhone = "";
|
||||
this.smsEnabled = false;
|
||||
this.notifyingNewActivitySms = false;
|
||||
this.notifyingNewActivitySmsTime = "";
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the "New Activity Text" notification (only reachable while the
|
||||
* SMS channel is enabled).
|
||||
*
|
||||
* TODO (notify-api wiring): on enable, POST the SMS-notification preference
|
||||
* with SMS-specific params (channel="sms", phoneNumber, sendTime) -- the
|
||||
* same alert as the in-app channel, delivered by a different transport. On
|
||||
* disable, tell the notify-api to remove it.
|
||||
*/
|
||||
async showNewActivitySmsNotificationChoice(): Promise<void> {
|
||||
if (this.notifyingNewActivitySms) {
|
||||
this.notifyingNewActivitySms = false;
|
||||
this.notifyingNewActivitySmsTime = "";
|
||||
return;
|
||||
}
|
||||
await this.promptSmsTimeAndEnable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for the SMS send time (reusing the time-picker dialog) and enable
|
||||
* the SMS channel on success.
|
||||
*
|
||||
* MOCK-UP: the chosen time is held in component state only.
|
||||
*/
|
||||
async promptSmsTimeAndEnable(): Promise<void> {
|
||||
(this.$refs.pushNotificationPermission as PushNotificationPermission).open(
|
||||
DAILY_CHECK_TITLE,
|
||||
async (success: boolean, timeText: string) => {
|
||||
if (success) {
|
||||
// TODO: register SMS preference with notify-api (phone + time)
|
||||
this.notifyingNewActivitySms = true;
|
||||
this.notifyingNewActivitySmsTime = timeText;
|
||||
}
|
||||
},
|
||||
{ skipSchedule: true },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1922,6 +2040,10 @@ export default class AccountViewView extends Vue {
|
||||
this.notifyingReminder = false;
|
||||
this.notifyingReminderMessage = "";
|
||||
this.notifyingReminderTime = "";
|
||||
// SMS channel (mock-up, component-local). TODO: also tell the notify-api.
|
||||
// Leaves the verified phone number in place intentionally.
|
||||
this.notifyingNewActivitySms = false;
|
||||
this.notifyingNewActivitySmsTime = "";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -58,11 +58,43 @@
|
||||
<h2 class="text-xl font-semibold mt-4">New Activity Notifications -- Texts From A Server</h2>
|
||||
<div>
|
||||
<p>
|
||||
This New Activity Notification SMS will be sent to you when there is new, relevant activity
|
||||
for you.
|
||||
This New Activity Notification SMS will be sent to you when there is new, relevant activity
|
||||
for you.
|
||||
It will only trigger if something involves you or a project of interest; it will not
|
||||
bug you for other, general activity.
|
||||
</p>
|
||||
<p class="mt-2">Notes and caveats for text messages:</p>
|
||||
<ul class="list-disc list-outside ml-5 mt-1">
|
||||
<li>
|
||||
Text messages are opt-in: they are only sent after you enter your
|
||||
phone number and confirm it with a verification code.
|
||||
</li>
|
||||
<li>
|
||||
Standard message and data rates from your carrier may apply.
|
||||
</li>
|
||||
<li>
|
||||
Message frequency is at most one New Activity text per day, at the
|
||||
time you choose.
|
||||
</li>
|
||||
<li>
|
||||
You can stop at any time: turn off the toggle in your settings, use
|
||||
"Forget Phone Number" to remove your number entirely, or reply STOP
|
||||
to any message. Reply HELP for help.
|
||||
</li>
|
||||
<li>
|
||||
Delivery is not guaranteed or precisely timed; carriers may delay
|
||||
or drop messages, and neither we nor the carriers are liable for
|
||||
late or undelivered texts.
|
||||
</li>
|
||||
<li>
|
||||
Only US phone numbers are currently supported.
|
||||
</li>
|
||||
<li>
|
||||
Your phone number is used only to send you these notifications; it
|
||||
is stored on the notification server and is deleted when you choose
|
||||
"Forget Phone Number".
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user