Compare commits
6 Commits
notify-api
...
notify-api
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c400b8797 | |||
|
|
d5c357b291 | ||
|
|
86611fe50d | ||
|
|
4152012838 | ||
|
|
25110e3eea | ||
|
|
0ecd4c6dd7 |
@@ -18,4 +18,6 @@ VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
|
||||
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
||||
VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000
|
||||
#VITE_DEFAULT_PUSH_SERVER... can't be set up with localhost domain
|
||||
# Using shared test notify API (no local notify server by default).
|
||||
VITE_DEFAULT_NOTIFY_API_SERVER=https://test-notify-api.timesafari.app
|
||||
VITE_PASSKEYS_ENABLED=true
|
||||
|
||||
@@ -11,3 +11,4 @@ VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
||||
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
|
||||
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch
|
||||
VITE_DEFAULT_PUSH_SERVER=https://timesafari.app
|
||||
VITE_DEFAULT_NOTIFY_API_SERVER=https://notify-api.timesafari.app
|
||||
|
||||
@@ -15,4 +15,5 @@ VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch
|
||||
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
||||
VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch
|
||||
VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app
|
||||
VITE_DEFAULT_NOTIFY_API_SERVER=https://test-notify-api.timesafari.app
|
||||
VITE_PASSKEYS_ENABLED=true
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Husky Commit Message Hook
|
||||
# Validates commit message format using commitlint
|
||||
#
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
# Run commitlint but don't fail the commit (|| true)
|
||||
# This provides helpful feedback without blocking commits
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Husky Pre-commit Hook
|
||||
# Runs lint-fix and Build Architecture Guard on staged files
|
||||
#
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
echo "🔍 Running pre-commit hooks..."
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Husky Pre-push Hook
|
||||
# Runs Build Architecture Guard to check commits being pushed
|
||||
#
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
echo "🔍 Running Build Architecture Guard (pre-push)..."
|
||||
|
||||
|
||||
@@ -164,6 +164,7 @@ cp .env.example .env.development
|
||||
# - VITE_DEFAULT_ENDORSER_API_SERVER
|
||||
# - VITE_DEFAULT_PARTNER_API_SERVER
|
||||
# - VITE_DEFAULT_IMAGE_API_SERVER
|
||||
# - VITE_DEFAULT_NOTIFY_API_SERVER
|
||||
```
|
||||
|
||||
#### Platform-Specific Development
|
||||
@@ -1650,6 +1651,7 @@ The build system supports multiple environment file patterns for different scena
|
||||
VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
||||
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch
|
||||
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
|
||||
VITE_DEFAULT_NOTIFY_API_SERVER=https://notify-api.timesafari.app
|
||||
|
||||
# Platform Configuration
|
||||
VITE_PLATFORM=web|electron|capacitor
|
||||
@@ -1669,6 +1671,7 @@ VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZY
|
||||
VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
|
||||
VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000
|
||||
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
||||
VITE_DEFAULT_NOTIFY_API_SERVER=https://test-notify-api.timesafari.app
|
||||
VITE_APP_SERVER=http://localhost:8080
|
||||
```
|
||||
|
||||
@@ -1679,6 +1682,7 @@ VITE_APP_SERVER=http://localhost:8080
|
||||
VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch
|
||||
VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch
|
||||
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
||||
VITE_DEFAULT_NOTIFY_API_SERVER=https://test-notify-api.timesafari.app
|
||||
VITE_APP_SERVER=https://test.timesafari.app
|
||||
```
|
||||
|
||||
@@ -1689,6 +1693,7 @@ VITE_APP_SERVER=https://test.timesafari.app
|
||||
VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
||||
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch
|
||||
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
|
||||
VITE_DEFAULT_NOTIFY_API_SERVER=https://notify-api.timesafari.app
|
||||
VITE_APP_SERVER=https://timesafari.app
|
||||
```
|
||||
|
||||
@@ -1946,6 +1951,7 @@ The build system supports multiple environment file patterns:
|
||||
VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
||||
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch
|
||||
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
|
||||
VITE_DEFAULT_NOTIFY_API_SERVER=https://notify-api.timesafari.app
|
||||
|
||||
# Platform Configuration
|
||||
VITE_PLATFORM=web|electron|capacitor
|
||||
|
||||
@@ -6,11 +6,6 @@ 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
|
||||
|
||||
@@ -135,6 +135,7 @@ Create or edit `.env.development` with your computer's IP:
|
||||
VITE_DEFAULT_ENDORSER_API_SERVER=http://192.168.1.100:3000
|
||||
VITE_DEFAULT_PARTNER_API_SERVER=http://192.168.1.100:3000
|
||||
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
||||
VITE_DEFAULT_NOTIFY_API_SERVER=https://test-notify-api.timesafari.app
|
||||
VITE_APP_SERVER=http://192.168.1.100:8080
|
||||
```
|
||||
|
||||
|
||||
@@ -14,24 +14,24 @@ End-to-end flow when testing New Activity / silent wake on a physical Android ph
|
||||
|
||||
```text
|
||||
┌─────────────────────┐ HTTPS ┌──────────────────────┐
|
||||
│ Mac (localhost) │ ◄───────────── │ ngrok edge │
|
||||
│ notification- │ tunnel │ (public HTTPS URL) │
|
||||
│ Mac (localhost) │ ◄───────────── │ ngrok edge │
|
||||
│ notification- │ tunnel │ (public HTTPS URL) │
|
||||
│ wakeup-service │ └──────────┬───────────┘
|
||||
└──────────┬──────────┘ │
|
||||
│ │ fetch
|
||||
│ POST /notifications/refresh │ POST /notifications/register
|
||||
│ ▼
|
||||
│ │ fetch
|
||||
│ POST /notifications/refresh │ POST /notifications/register
|
||||
│ ▼
|
||||
│ ┌──────────────────────┐
|
||||
│ │ crowd-funder-for- │
|
||||
│ │ time-pwa (Capacitor │
|
||||
│ │ Android on device) │
|
||||
│ │ crowd-funder-for- │
|
||||
│ │ time-pwa (Capacitor │
|
||||
│ │ Android on device) │
|
||||
│ └──────────┬───────────┘
|
||||
│ │
|
||||
│ FCM data message (WAKEUP_PING) │ daily-notification-plugin
|
||||
▼ ▼ (local schedule replace)
|
||||
┌─────────────────────┐ ┌──────────────────────┐
|
||||
│ Firebase Cloud │ ──FCM────────► │ Android device │
|
||||
│ Messaging │ direct │ app.timesafari.app │
|
||||
│ Firebase Cloud │ ──FCM────────► │ Android device │
|
||||
│ Messaging │ direct │ app.timesafari.app │
|
||||
└─────────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
@@ -275,9 +275,9 @@ Before ngrok end-to-end testing, confirm:
|
||||
|
||||
## 6. Configure the Notification Debug Panel backend override
|
||||
|
||||
The app normally calls `APP_SERVER` (from `VITE_APP_SERVER`). For local wakeup testing, override the notification API base URL without rebuilding.
|
||||
The app normally calls `DEFAULT_NOTIFY_API_SERVER` (from `VITE_DEFAULT_NOTIFY_API_SERVER`, falling back to `AppString.PROD_NOTIFY_API_SERVER`). That is independent of `APP_SERVER`. For local wakeup testing, override the notification API base URL in the Debug Panel without rebuilding.
|
||||
|
||||
For a full panel reference (configuration, authentication, and troubleshooting), see [notification-debug-panel.md](./notification-debug-panel.md).
|
||||
For a full panel reference (configuration, URL resolution order, authentication, and troubleshooting), see [notification-debug-panel.md](./notification-debug-panel.md).
|
||||
|
||||
### Open the panel
|
||||
|
||||
@@ -578,7 +578,7 @@ curl -sS -w "\nHTTP %{http_code}\n" "$BASE/health"
|
||||
3. Confirm **Backend Status → URL** matches the saved ngrok host.
|
||||
4. Enable **Test Mode** if using dev backend behavior.
|
||||
|
||||
**Expected outcome:** **Active** URL in the panel equals your ngrok `https://…` host. Subsequent app requests use that base (not production `APP_SERVER`) for `/notifications/register` and `/notifications/refresh`.
|
||||
**Expected outcome:** **Active** URL in the panel equals your ngrok `https://…` host. Subsequent app requests use that base (not `DEFAULT_NOTIFY_API_SERVER`) for `/notifications/register` and `/notifications/refresh`.
|
||||
|
||||
---
|
||||
|
||||
@@ -893,7 +893,7 @@ Compare with panel **Backend Status** and Event Log error text.
|
||||
|
||||
**Verification:** Compare panel URL to current `ngrok http` **Forwarding** line; `curl -sS "$NEW_URL/health"`.
|
||||
|
||||
**Fixes:** Copy new HTTPS URL → **Save Backend URL** in panel; or clear override only if intentionally returning to `APP_SERVER`.
|
||||
**Fixes:** Copy new HTTPS URL → **Save Backend URL** in panel; or clear override only if intentionally returning to `DEFAULT_NOTIFY_API_SERVER`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -281,9 +281,9 @@ Before ngrok end-to-end testing, confirm:
|
||||
|
||||
## 6. Configure the Notification Debug Panel backend override
|
||||
|
||||
The app normally calls `APP_SERVER` (from `VITE_APP_SERVER`). For local wakeup testing, override the notification API base URL without rebuilding.
|
||||
The app normally calls `DEFAULT_NOTIFY_API_SERVER` (from `VITE_DEFAULT_NOTIFY_API_SERVER`, falling back to `AppString.PROD_NOTIFY_API_SERVER`). That is independent of `APP_SERVER`. For local wakeup testing, override the notification API base URL in the Debug Panel without rebuilding.
|
||||
|
||||
For a full panel reference (configuration, authentication, and troubleshooting), see [notification-debug-panel.md](./notification-debug-panel.md).
|
||||
For a full panel reference (configuration, URL resolution order, authentication, and troubleshooting), see [notification-debug-panel.md](./notification-debug-panel.md).
|
||||
|
||||
### Open the panel
|
||||
|
||||
@@ -507,7 +507,7 @@ Confirm parameters (token vs deviceId, auth headers) in that repo’s README or
|
||||
### Stale ngrok URL
|
||||
|
||||
- After restarting ngrok, update **Notification Backend URL** in the panel and tap **Save**
|
||||
- Or clear override (empty field + Save) only if you intend to hit `APP_SERVER` again
|
||||
- Or clear override (empty field + Save) only if you intend to hit `DEFAULT_NOTIFY_API_SERVER` again
|
||||
|
||||
### Plugin / JWT errors after refresh
|
||||
|
||||
|
||||
@@ -1,12 +1,50 @@
|
||||
# Notification Debug Panel
|
||||
|
||||
**Created:** 2026-07-07
|
||||
**Updated:** 2026-07-22
|
||||
**Audience:** Developers testing notification registration, refresh, and WAKEUP_PING flows on native (iOS/Android) dev builds.
|
||||
|
||||
The **Notification Debug Panel** is a dev-only UI for exercising the same notification orchestration paths the production app uses: FCM token registration, backend refresh, wakeup handling, and local schedule inspection. It does not duplicate scheduling logic.
|
||||
|
||||
---
|
||||
|
||||
## Notification API base URL
|
||||
|
||||
Notification HTTP calls (`/notifications/register`, `/notifications/refresh`, `/debug/send-wakeup`, etc.) do **not** use `APP_SERVER`. They use a dedicated Notification API host, resolved at runtime by `getNotificationApiBaseUrl()` in `NotificationDebugConfig.ts`.
|
||||
|
||||
### Configuration constants
|
||||
|
||||
| Symbol | Location | Purpose |
|
||||
|--------|----------|---------|
|
||||
| `VITE_DEFAULT_NOTIFY_API_SERVER` | `.env.development` / `.env.test` / `.env.production` | Build-time default Notification API URL for that Vite mode (same pattern as other `VITE_DEFAULT_*` backends) |
|
||||
| `DEFAULT_NOTIFY_API_SERVER` | `src/constants/app.ts` | Runtime constant: `import.meta.env.VITE_DEFAULT_NOTIFY_API_SERVER \|\| AppString.PROD_NOTIFY_API_SERVER` |
|
||||
| `AppString.PROD_NOTIFY_API_SERVER` | `src/constants/app.ts` | Hardcoded production fallback: `https://notify-api.timesafari.app` |
|
||||
| `AppString.TEST_NOTIFY_API_SERVER` | `src/constants/app.ts` | Hardcoded test host: `https://test-notify-api.timesafari.app` (for explicit UI/debug use; not the automatic fallback) |
|
||||
|
||||
Production, test, and development builds get different Notification API URLs from their respective `.env.*` files. Runtime request code always goes through `DEFAULT_NOTIFY_API_SERVER` (via `getNotificationApiBaseUrl()`), not by reading the env var directly at each call site.
|
||||
|
||||
Typical values today:
|
||||
|
||||
| Build / env file | `VITE_DEFAULT_NOTIFY_API_SERVER` |
|
||||
|------------------|----------------------------------|
|
||||
| `.env.production` | `https://notify-api.timesafari.app` |
|
||||
| `.env.test` | `https://test-notify-api.timesafari.app` |
|
||||
| `.env.development` | `https://test-notify-api.timesafari.app` |
|
||||
|
||||
### URL resolution order
|
||||
|
||||
`getNotificationApiBaseUrl()` selects the base URL in this order:
|
||||
|
||||
1. **Debug Panel backend override** — `localStorage` key `notificationDebug.backendBaseUrl` (set via **Save Backend URL** or `setBackendBaseUrl()`)
|
||||
2. **`VITE_DEFAULT_NOTIFY_API_SERVER`** — baked into the build as part of `DEFAULT_NOTIFY_API_SERVER`
|
||||
3. **`AppString.PROD_NOTIFY_API_SERVER`** — hardcoded fallback when the env var is unset (`https://notify-api.timesafari.app`)
|
||||
|
||||
Clearing the Debug Panel override (empty field + Save) returns the app to step 2 / 3 (`DEFAULT_NOTIFY_API_SERVER`). The override never changes auth behavior by itself.
|
||||
|
||||
`APP_SERVER` / `VITE_APP_SERVER` remain for deep links and the main app web host only — not for notification API traffic.
|
||||
|
||||
---
|
||||
|
||||
## Access
|
||||
|
||||
1. Use a **non-production** build (for example `build:android:dev`, `build:ios:dev`, or `vite dev` with a non-`production` mode).
|
||||
@@ -23,7 +61,7 @@ Settings persist in `localStorage` via `NotificationDebugConfig.ts`:
|
||||
|
||||
| Key | Default | Purpose |
|
||||
|-----|---------|---------|
|
||||
| `notificationDebug.backendBaseUrl` | *(unset — use `APP_SERVER`)* | Override which notification server receives API calls |
|
||||
| `notificationDebug.backendBaseUrl` | *(unset — use `DEFAULT_NOTIFY_API_SERVER`)* | Override which notification server receives API calls |
|
||||
| `notificationDebug.testMode` | `true` | Sent in JSON request bodies (`testMode: true/false`) |
|
||||
| `notificationDebug.bypassAuth` | `false` | When `true`, omit JWT `Authorization` headers on notification API calls |
|
||||
|
||||
@@ -33,7 +71,7 @@ All notification API requests (`/notifications/register`, `/notifications/refres
|
||||
|
||||
Paste a base URL (no trailing slash) and tap **Save Backend URL**. This changes **only** which server the app calls (`getNotificationApiBaseUrl()`). It does **not** disable JWT authentication.
|
||||
|
||||
Leave empty to use the built-in default (`APP_SERVER` from `VITE_APP_SERVER`).
|
||||
Leave empty to use the configured build default (`DEFAULT_NOTIFY_API_SERVER`, from `VITE_DEFAULT_NOTIFY_API_SERVER` or `AppString.PROD_NOTIFY_API_SERVER`). The Debug Panel override still wins whenever a non-empty URL is saved.
|
||||
|
||||
### Test Mode
|
||||
|
||||
@@ -59,9 +97,11 @@ The panel **Backend Status** section shows the active URL, `testMode`, and `bypa
|
||||
|
||||
Example: `https://test-notify-api.timesafari.app`
|
||||
|
||||
On development and test builds, this host is already the default via `VITE_DEFAULT_NOTIFY_API_SERVER`. You can leave **Notification Backend URL** empty, or paste the same URL explicitly.
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| **Notification Backend URL** | `https://test-notify-api.timesafari.app` |
|
||||
| **Notification Backend URL** | Empty (use default) or `https://test-notify-api.timesafari.app` |
|
||||
| **Test Mode** | **ON** (if the server expects `testMode: true`) |
|
||||
| **Skip JWT Authentication** | **OFF** |
|
||||
|
||||
@@ -175,8 +215,9 @@ See platform-specific guides for extended ngrok and FCM workflows:
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/constants/app.ts` | `DEFAULT_NOTIFY_API_SERVER`, `PROD_NOTIFY_API_SERVER`, `TEST_NOTIFY_API_SERVER` |
|
||||
| `src/components/dev/NotificationDebugPanel.vue` | Dev UI |
|
||||
| `src/services/notifications/NotificationDebugConfig.ts` | Backend URL, testMode, bypassAuth persistence |
|
||||
| `src/services/notifications/NotificationDebugConfig.ts` | Base URL resolution, testMode, bypassAuth persistence |
|
||||
| `src/services/notifications/notificationApiAuth.ts` | JWT vs unauthenticated headers |
|
||||
| `src/services/notifications/notificationApiDebugMode.ts` | Auth bypass gate |
|
||||
| `src/services/notifications/NotificationDebugService.ts` | Panel action handlers |
|
||||
|
||||
@@ -133,6 +133,7 @@ VITE_DEFAULT_ENDORSER_API_SERVER=https://dev-api.endorser.ch
|
||||
VITE_DEFAULT_IMAGE_API_SERVER=https://dev-image-api.timesafari.app
|
||||
VITE_DEFAULT_PARTNER_API_SERVER=https://dev-partner-api.endorser.ch
|
||||
VITE_DEFAULT_PUSH_SERVER=https://dev.timesafari.app
|
||||
VITE_DEFAULT_NOTIFY_API_SERVER=https://test-notify-api.timesafari.app
|
||||
VITE_PASSKEYS_ENABLED=true
|
||||
|
||||
# .env.test
|
||||
@@ -141,6 +142,7 @@ VITE_DEFAULT_ENDORSER_API_SERVER=https://staging-api.endorser.ch
|
||||
VITE_DEFAULT_IMAGE_API_SERVER=https://staging-image-api.timesafari.app
|
||||
VITE_DEFAULT_PARTNER_API_SERVER=https://staging-partner-api.endorser.ch
|
||||
VITE_DEFAULT_PUSH_SERVER=https://staging.timesafari.app
|
||||
VITE_DEFAULT_NOTIFY_API_SERVER=https://test-notify-api.timesafari.app
|
||||
VITE_PASSKEYS_ENABLED=true
|
||||
|
||||
# .env.production
|
||||
@@ -149,6 +151,7 @@ VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
||||
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
|
||||
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch
|
||||
VITE_DEFAULT_PUSH_SERVER=https://timesafari.app
|
||||
VITE_DEFAULT_NOTIFY_API_SERVER=https://notify-api.timesafari.app
|
||||
VITE_PASSKEYS_ENABLED=true
|
||||
```
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
<p :class="textClasses">{{ text }}</p>
|
||||
|
||||
<button
|
||||
v-if="option1Text"
|
||||
:class="option1ButtonClasses"
|
||||
@click="handleOption1(close)"
|
||||
>
|
||||
@@ -33,7 +32,6 @@
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="option2Text"
|
||||
:class="option2ButtonClasses"
|
||||
@click="handleOption2(close)"
|
||||
>
|
||||
@@ -41,7 +39,6 @@
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="option3Text"
|
||||
:class="option3ButtonClasses"
|
||||
@click="handleOption3(close)"
|
||||
>
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
<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>
|
||||
@@ -11,7 +11,7 @@
|
||||
v-model="backendUrlDraft"
|
||||
type="url"
|
||||
class="w-full text-sm px-3 py-2 rounded border border-slate-300 bg-white mb-1"
|
||||
placeholder="Leave empty for default (APP_SERVER)"
|
||||
placeholder="Leave empty for default (DEFAULT_NOTIFY_API_SERVER)"
|
||||
:disabled="busy"
|
||||
@keydown.enter="onSaveBackendUrl"
|
||||
/>
|
||||
|
||||
@@ -57,20 +57,11 @@ export const ACCOUNT_VIEW_CONSTANTS = {
|
||||
// Notification messages
|
||||
NOTIFICATIONS: {
|
||||
NEW_ACTIVITY_INFO: `
|
||||
This will notify you with an app message when there is new relevant activity for you personally.
|
||||
This will only notify you when there is new relevant activity for you personally.
|
||||
Note that it runs on your device and many factors may affect delivery,
|
||||
so if you want a reliable but simple daily notification then choose a 'Reminder'.
|
||||
Do you want more details?
|
||||
`,
|
||||
NEW_ACTIVITY_SMS_INFO: `
|
||||
This is opt-in: by turning it on you agree to receive text messages when there is new relevant activity for you personally.
|
||||
Standard message and data rates may apply, and you can turn it off at any time.
|
||||
Do you want more details?
|
||||
`,
|
||||
SMS_OPT_IN_CHOICE: `
|
||||
This is opt-in: by entering your phone number you agree to receive text messages when there is new relevant activity for you personally.
|
||||
Standard message and data rates may apply, and you can turn it off at any time.
|
||||
`,
|
||||
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,
|
||||
|
||||
@@ -26,6 +26,9 @@ export enum AppString {
|
||||
TEST1_PUSH_SERVER = "https://test.timesafari.app",
|
||||
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",
|
||||
|
||||
PROD_NOTIFY_API_SERVER = "https://notify-api.timesafari.app",
|
||||
TEST_NOTIFY_API_SERVER = "https://test-notify-api.timesafari.app",
|
||||
|
||||
NO_CONTACT_NAME = "(no name)",
|
||||
}
|
||||
|
||||
@@ -47,6 +50,10 @@ export const DEFAULT_PARTNER_API_SERVER =
|
||||
export const DEFAULT_PUSH_SERVER =
|
||||
import.meta.env.VITE_DEFAULT_PUSH_SERVER || AppString.PROD_PUSH_SERVER;
|
||||
|
||||
export const DEFAULT_NOTIFY_API_SERVER =
|
||||
import.meta.env.VITE_DEFAULT_NOTIFY_API_SERVER ||
|
||||
AppString.PROD_NOTIFY_API_SERVER;
|
||||
|
||||
export const IMAGE_TYPE_PROFILE = "profile";
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Persists overrides in localStorage; production defaults apply when unset.
|
||||
*/
|
||||
|
||||
import { APP_SERVER } from "@/constants/app";
|
||||
import { DEFAULT_NOTIFY_API_SERVER } from "@/constants/app";
|
||||
|
||||
const LOG = "[NotificationDebug]";
|
||||
const STORAGE_KEY_BACKEND_URL = "notificationDebug.backendBaseUrl";
|
||||
@@ -45,7 +45,7 @@ function writeStorage(key: string, value: string | null): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** Backend URL override, or null when using the default app server. */
|
||||
/** Backend URL override, or null when using the default Notification API. */
|
||||
export function getBackendBaseUrl(): string | null {
|
||||
const raw = readStorage(STORAGE_KEY_BACKEND_URL);
|
||||
if (raw === null) {
|
||||
@@ -101,12 +101,15 @@ export function setBypassAuth(enabled: boolean): void {
|
||||
|
||||
/**
|
||||
* Base URL for `/notifications/*` API calls.
|
||||
* Uses debug override when set; otherwise the built-in app server (production default).
|
||||
* Uses debug override when set; otherwise DEFAULT_NOTIFY_API_SERVER.
|
||||
*/
|
||||
export function getNotificationApiBaseUrl(): string {
|
||||
const override = getBackendBaseUrl();
|
||||
if (override) {
|
||||
return override;
|
||||
}
|
||||
return normalizeNotificationBackendUrl(APP_SERVER) ?? APP_SERVER;
|
||||
return (
|
||||
normalizeNotificationBackendUrl(DEFAULT_NOTIFY_API_SERVER) ??
|
||||
DEFAULT_NOTIFY_API_SERVER
|
||||
);
|
||||
}
|
||||
|
||||
@@ -143,48 +143,24 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
New Activity Notification: two delivery channels for the same alert.
|
||||
Both let the user pick a time; they differ only in how the alert is
|
||||
delivered (and therefore in the parameters passed to the notify-api):
|
||||
- In-App: on-device notification via the daily-notification-plugin.
|
||||
- SMS: text message sent by the notify-api (opt-in).
|
||||
-->
|
||||
<div class="mt-4 mb-1">
|
||||
<div class="font-semibold">
|
||||
<div class="flex items-center justify-between mt-4 mb-2">
|
||||
<!-- label -->
|
||||
<div>
|
||||
New Activity Notification
|
||||
<button
|
||||
class="text-slate-400 fa-fw cursor-pointer"
|
||||
aria-label="Learn more about New Activity notifications"
|
||||
@click.stop="showNewActivityNotificationInfo(false)"
|
||||
@click.stop="showNewActivityNotificationInfo"
|
||||
>
|
||||
<font-awesome icon="question-circle" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500">
|
||||
Get told about new relevant activity for you. Choose a delivery
|
||||
channel below -- you can turn on either one, or both.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Channel 1: In-App notification -->
|
||||
<div class="flex items-center justify-between mt-3 mb-2 pl-1">
|
||||
<!-- label -->
|
||||
<div>
|
||||
<font-awesome
|
||||
icon="comment"
|
||||
class="fa-fw text-slate-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
In-App Notification
|
||||
</div>
|
||||
<!-- toggle -->
|
||||
<div
|
||||
class="relative ml-2 cursor-pointer"
|
||||
role="switch"
|
||||
:aria-checked="notifyingNewActivity"
|
||||
aria-label="Toggle New Activity notifications in app"
|
||||
aria-label="Toggle New Activity notifications"
|
||||
tabindex="0"
|
||||
@click.stop.prevent="showNewActivityNotificationChoice()"
|
||||
>
|
||||
@@ -202,7 +178,7 @@
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="notifyingNewActivity" class="w-full pl-1">
|
||||
<div v-if="notifyingNewActivity" class="w-full">
|
||||
<div
|
||||
class="text-sm text-slate-500 mb-2 bg-white rounded px-3 py-2 border border-slate-200"
|
||||
>
|
||||
@@ -215,125 +191,10 @@
|
||||
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="editNewActivityNotification"
|
||||
>
|
||||
Edit In-App Time…
|
||||
Edit New Activity Notification…
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<font-awesome
|
||||
icon="message"
|
||||
class="fa-fw text-slate-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{
|
||||
notifyingNewActivitySmsPhone
|
||||
? "SMS Text Message"
|
||||
: "Enable Text Messages"
|
||||
}}
|
||||
</div>
|
||||
<!-- master toggle: enables the SMS channel (verifies phone on first use) -->
|
||||
<div
|
||||
class="relative ml-2 cursor-pointer"
|
||||
role="switch"
|
||||
:aria-checked="smsEnabled"
|
||||
aria-label="Toggle text message delivery"
|
||||
tabindex="0"
|
||||
@click.stop.prevent="toggleSmsEnabled()"
|
||||
>
|
||||
<input
|
||||
:checked="smsEnabled"
|
||||
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 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 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>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>
|
||||
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 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"
|
||||
>
|
||||
<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>
|
||||
|
||||
<div class="mt-2 text-center">
|
||||
<router-link class="text-sm text-blue-500" to="/help-notifications">
|
||||
Troubleshoot your notifications…
|
||||
@@ -341,8 +202,6 @@
|
||||
</div>
|
||||
</section>
|
||||
<PushNotificationPermission ref="pushNotificationPermission" />
|
||||
<SmsVerificationDialog ref="smsVerificationDialog" />
|
||||
<ChoiceButtonDialog ref="smsOptInChoiceDialog" />
|
||||
|
||||
<!-- User Profile -->
|
||||
<section
|
||||
@@ -687,8 +546,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Notification Push Server setting - only show on web platforms -->
|
||||
<!-- This will be removed because we're no longer using PWA notifications. -->
|
||||
<div v-if="false">
|
||||
<div v-if="!isNativePlatform">
|
||||
<h2 class="text-slate-500 text-sm font-bold mb-2">
|
||||
Notification Push Server
|
||||
</h2>
|
||||
@@ -934,12 +792,10 @@ import { copyToClipboard } from "../services/ClipboardService";
|
||||
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
|
||||
import ChoiceButtonDialog from "../components/ChoiceButtonDialog.vue";
|
||||
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";
|
||||
@@ -1014,7 +870,6 @@ interface PushNotificationPermissionRef {
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
ChoiceButtonDialog,
|
||||
EntityIcon,
|
||||
ImageMethodDialog,
|
||||
LMap,
|
||||
@@ -1022,7 +877,6 @@ interface PushNotificationPermissionRef {
|
||||
LTileLayer,
|
||||
PushNotificationPermission,
|
||||
QuickNav,
|
||||
SmsVerificationDialog,
|
||||
TopMessage,
|
||||
UserNameDialog,
|
||||
DataExportSection,
|
||||
@@ -1077,15 +931,6 @@ export default class AccountViewView extends Vue {
|
||||
// Notification properties
|
||||
notifyingNewActivity: boolean = false;
|
||||
notifyingNewActivityTime: string = "";
|
||||
// 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 = "";
|
||||
notifyingReminder: boolean = false;
|
||||
notifyingReminderMessage: string = "";
|
||||
notifyingReminderTime: string = "";
|
||||
@@ -1371,11 +1216,9 @@ export default class AccountViewView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
async showNewActivityNotificationInfo(isSms: boolean): Promise<void> {
|
||||
async showNewActivityNotificationInfo(): Promise<void> {
|
||||
this.notify.confirm(
|
||||
isSms
|
||||
? ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.NEW_ACTIVITY_SMS_INFO
|
||||
: ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.NEW_ACTIVITY_INFO,
|
||||
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.NEW_ACTIVITY_INFO,
|
||||
async () => {
|
||||
await (this.$router as Router).push({
|
||||
name: "help-notification-types",
|
||||
@@ -1423,166 +1266,6 @@ export default class AccountViewView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the SMS channel as a whole ("Enable Text Messages" / "SMS Text
|
||||
* Message" master switch).
|
||||
*
|
||||
* Enabling first shows an opt-in choice (see more info / enter phone
|
||||
* number / cancel) before any phone number is requested. Only "Enter
|
||||
* Phone Number" proceeds to SmsVerificationDialog, where the toggle turns
|
||||
* on once the texted 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 that
|
||||
* forces a full opt-in + re-verification next time.
|
||||
*
|
||||
* MOCK-UP: the verification round-trips are stubbed in
|
||||
* SmsVerificationDialog and all state is component-local.
|
||||
*/
|
||||
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.smsOptInChoiceDialog as ChoiceButtonDialog).open({
|
||||
title: "Enable Text Messages",
|
||||
text: ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.SMS_OPT_IN_CHOICE,
|
||||
option1Text: "See More Info",
|
||||
option2Text: "Enter Phone Number",
|
||||
onOption1: async () => {
|
||||
await (this.$router as Router).push({
|
||||
name: "help-notification-types",
|
||||
});
|
||||
},
|
||||
onOption2: () => {
|
||||
(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 },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the time for the SMS delivery channel.
|
||||
*
|
||||
* MOCK-UP: updates component state only. TODO: PUT the updated sendTime to
|
||||
* the notify-api SMS preference.
|
||||
*/
|
||||
async editNewActivitySmsNotification(): Promise<void> {
|
||||
const dialog = this.$refs
|
||||
.pushNotificationPermission as PushNotificationPermission;
|
||||
|
||||
dialog.open(
|
||||
DAILY_CHECK_TITLE,
|
||||
async (success: boolean, timeText: string) => {
|
||||
if (!success) return;
|
||||
// TODO: PUT updated sendTime to notify-api SMS preference
|
||||
this.notifyingNewActivitySmsTime = timeText;
|
||||
this.notify.success(
|
||||
"New Activity SMS time updated.",
|
||||
TIMEOUTS.STANDARD,
|
||||
);
|
||||
},
|
||||
{ skipSchedule: true },
|
||||
);
|
||||
|
||||
// Pre-populate the dialog with the current SMS time
|
||||
setTimeout(() => {
|
||||
const timeMatch = this.notifyingNewActivitySmsTime.match(
|
||||
/(\d+):(\d+)\s*(AM|PM)/i,
|
||||
);
|
||||
if (timeMatch) {
|
||||
let hour = parseInt(timeMatch[1], 10);
|
||||
const minute = timeMatch[2];
|
||||
const isAm = timeMatch[3].toUpperCase() === "AM";
|
||||
if (hour === 12) {
|
||||
hour = 12;
|
||||
} else if (hour > 12) {
|
||||
hour = hour - 12;
|
||||
}
|
||||
const dialogComponent =
|
||||
dialog as unknown as PushNotificationPermissionRef;
|
||||
if (dialogComponent) {
|
||||
dialogComponent.hourInput = hour.toString();
|
||||
dialogComponent.minuteInput = minute;
|
||||
dialogComponent.hourAm = isAm;
|
||||
}
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure native fetcher, sync starred plans, and schedule API-driven dual notification.
|
||||
*/
|
||||
@@ -2058,10 +1741,6 @@ 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 = "";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -39,70 +39,31 @@
|
||||
something, so you can record thanks in here.
|
||||
</p>
|
||||
<p>
|
||||
This is your own message activated in the app, but it doesn't contain any details about
|
||||
This is a reliable message, but it doesn't contain any details about
|
||||
activity that might be especially interesting to you.
|
||||
Note that the timing is not precise: the device may deliver it a bit later than scheduled.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl font-semibold mt-4">New Activity Notifications -- In App</h2>
|
||||
<h2 class="text-xl font-semibold mt-4">New Activity Notifications</h2>
|
||||
<div>
|
||||
<p>
|
||||
The New Activity Notification will be activated in your app when there is new, relevant activity
|
||||
The New Activity Notification 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>
|
||||
</div>
|
||||
|
||||
<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.
|
||||
It will only trigger if something involves you or a project of interest; it will not
|
||||
bug you for other, general activity.
|
||||
This type is not as reliable as a Reminder Notification because mobile devices often
|
||||
suppress such notifications to save battery. (If you want to quickly check for relevant
|
||||
activity daily, use the Reminder Notification and open the app and look for a large green
|
||||
button that points out new activity that is personal to you. We are working on other
|
||||
ways to notify you more reliably.
|
||||
<router-link class="text-blue-500" to="/help">
|
||||
go here to follow us or contact us
|
||||
<font-awesome icon="chevron-right" class="fa-fw"></font-awesome>
|
||||
</router-link>.)
|
||||
</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>
|
||||
<li>
|
||||
See our
|
||||
<a href="https://timesafari.app/help-terms" target="_blank" class="text-blue-500">
|
||||
Terms & Conditions and Privacy Policy docs here.
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- eslint-enable -->
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user