Compare commits
5 Commits
notify-api
...
notify-api
| Author | SHA1 | Date | |
|---|---|---|---|
| 59993cb96a | |||
| 940e851d3c | |||
| b02a1b29c5 | |||
| 3f291b0d63 | |||
| ee8fb80cfe |
@@ -18,6 +18,4 @@ VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
|
|||||||
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
||||||
VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000
|
VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000
|
||||||
#VITE_DEFAULT_PUSH_SERVER... can't be set up with localhost domain
|
#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
|
VITE_PASSKEYS_ENABLED=true
|
||||||
|
|||||||
@@ -11,4 +11,3 @@ VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
|||||||
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
|
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
|
||||||
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch
|
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch
|
||||||
VITE_DEFAULT_PUSH_SERVER=https://timesafari.app
|
VITE_DEFAULT_PUSH_SERVER=https://timesafari.app
|
||||||
VITE_DEFAULT_NOTIFY_API_SERVER=https://notify-api.timesafari.app
|
|
||||||
|
|||||||
@@ -15,5 +15,4 @@ VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch
|
|||||||
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
||||||
VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch
|
VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch
|
||||||
VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app
|
VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app
|
||||||
VITE_DEFAULT_NOTIFY_API_SERVER=https://test-notify-api.timesafari.app
|
|
||||||
VITE_PASSKEYS_ENABLED=true
|
VITE_PASSKEYS_ENABLED=true
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
#
|
#
|
||||||
# Husky Commit Message Hook
|
# Husky Commit Message Hook
|
||||||
# Validates commit message format using commitlint
|
# Validates commit message format using commitlint
|
||||||
#
|
#
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
# Run commitlint but don't fail the commit (|| true)
|
# Run commitlint but don't fail the commit (|| true)
|
||||||
# This provides helpful feedback without blocking commits
|
# This provides helpful feedback without blocking commits
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
#
|
#
|
||||||
# Husky Pre-commit Hook
|
# Husky Pre-commit Hook
|
||||||
# Runs lint-fix and Build Architecture Guard on staged files
|
# Runs lint-fix and Build Architecture Guard on staged files
|
||||||
#
|
#
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
echo "🔍 Running pre-commit hooks..."
|
echo "🔍 Running pre-commit hooks..."
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
#
|
#
|
||||||
# Husky Pre-push Hook
|
# Husky Pre-push Hook
|
||||||
# Runs Build Architecture Guard to check commits being pushed
|
# Runs Build Architecture Guard to check commits being pushed
|
||||||
#
|
#
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
echo "🔍 Running Build Architecture Guard (pre-push)..."
|
echo "🔍 Running Build Architecture Guard (pre-push)..."
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,6 @@ cp .env.example .env.development
|
|||||||
# - VITE_DEFAULT_ENDORSER_API_SERVER
|
# - VITE_DEFAULT_ENDORSER_API_SERVER
|
||||||
# - VITE_DEFAULT_PARTNER_API_SERVER
|
# - VITE_DEFAULT_PARTNER_API_SERVER
|
||||||
# - VITE_DEFAULT_IMAGE_API_SERVER
|
# - VITE_DEFAULT_IMAGE_API_SERVER
|
||||||
# - VITE_DEFAULT_NOTIFY_API_SERVER
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Platform-Specific Development
|
#### Platform-Specific Development
|
||||||
@@ -1651,7 +1650,6 @@ The build system supports multiple environment file patterns for different scena
|
|||||||
VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
||||||
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-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_IMAGE_API_SERVER=https://image-api.timesafari.app
|
||||||
VITE_DEFAULT_NOTIFY_API_SERVER=https://notify-api.timesafari.app
|
|
||||||
|
|
||||||
# Platform Configuration
|
# Platform Configuration
|
||||||
VITE_PLATFORM=web|electron|capacitor
|
VITE_PLATFORM=web|electron|capacitor
|
||||||
@@ -1671,7 +1669,6 @@ VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZY
|
|||||||
VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
|
VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
|
||||||
VITE_DEFAULT_PARTNER_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_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
|
VITE_APP_SERVER=http://localhost:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1682,7 +1679,6 @@ VITE_APP_SERVER=http://localhost:8080
|
|||||||
VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch
|
VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch
|
||||||
VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-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_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
|
VITE_APP_SERVER=https://test.timesafari.app
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1693,7 +1689,6 @@ VITE_APP_SERVER=https://test.timesafari.app
|
|||||||
VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
||||||
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-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_IMAGE_API_SERVER=https://image-api.timesafari.app
|
||||||
VITE_DEFAULT_NOTIFY_API_SERVER=https://notify-api.timesafari.app
|
|
||||||
VITE_APP_SERVER=https://timesafari.app
|
VITE_APP_SERVER=https://timesafari.app
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1951,7 +1946,6 @@ The build system supports multiple environment file patterns:
|
|||||||
VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
||||||
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-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_IMAGE_API_SERVER=https://image-api.timesafari.app
|
||||||
VITE_DEFAULT_NOTIFY_API_SERVER=https://notify-api.timesafari.app
|
|
||||||
|
|
||||||
# Platform Configuration
|
# Platform Configuration
|
||||||
VITE_PLATFORM=web|electron|capacitor
|
VITE_PLATFORM=web|electron|capacitor
|
||||||
|
|||||||
@@ -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).
|
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
|
## [1.3.8] - 2026
|
||||||
### Added
|
### Added
|
||||||
- Device wake-up for notifications
|
- Device wake-up for notifications
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ Create or edit `.env.development` with your computer's IP:
|
|||||||
VITE_DEFAULT_ENDORSER_API_SERVER=http://192.168.1.100:3000
|
VITE_DEFAULT_ENDORSER_API_SERVER=http://192.168.1.100:3000
|
||||||
VITE_DEFAULT_PARTNER_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_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
|
VITE_APP_SERVER=http://192.168.1.100:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -275,9 +275,9 @@ Before ngrok end-to-end testing, confirm:
|
|||||||
|
|
||||||
## 6. Configure the Notification Debug Panel backend override
|
## 6. Configure the Notification Debug Panel backend override
|
||||||
|
|
||||||
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.
|
The app normally calls `APP_SERVER` (from `VITE_APP_SERVER`). For local wakeup testing, override the notification API base URL without rebuilding.
|
||||||
|
|
||||||
For a full panel reference (configuration, URL resolution order, authentication, and troubleshooting), see [notification-debug-panel.md](./notification-debug-panel.md).
|
For a full panel reference (configuration, authentication, and troubleshooting), see [notification-debug-panel.md](./notification-debug-panel.md).
|
||||||
|
|
||||||
### Open the panel
|
### 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.
|
3. Confirm **Backend Status → URL** matches the saved ngrok host.
|
||||||
4. Enable **Test Mode** if using dev backend behavior.
|
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 `DEFAULT_NOTIFY_API_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 production `APP_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"`.
|
**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 `DEFAULT_NOTIFY_API_SERVER`.
|
**Fixes:** Copy new HTTPS URL → **Save Backend URL** in panel; or clear override only if intentionally returning to `APP_SERVER`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -281,9 +281,9 @@ Before ngrok end-to-end testing, confirm:
|
|||||||
|
|
||||||
## 6. Configure the Notification Debug Panel backend override
|
## 6. Configure the Notification Debug Panel backend override
|
||||||
|
|
||||||
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.
|
The app normally calls `APP_SERVER` (from `VITE_APP_SERVER`). For local wakeup testing, override the notification API base URL without rebuilding.
|
||||||
|
|
||||||
For a full panel reference (configuration, URL resolution order, authentication, and troubleshooting), see [notification-debug-panel.md](./notification-debug-panel.md).
|
For a full panel reference (configuration, authentication, and troubleshooting), see [notification-debug-panel.md](./notification-debug-panel.md).
|
||||||
|
|
||||||
### Open the panel
|
### Open the panel
|
||||||
|
|
||||||
@@ -507,7 +507,7 @@ Confirm parameters (token vs deviceId, auth headers) in that repo’s README or
|
|||||||
### Stale ngrok URL
|
### Stale ngrok URL
|
||||||
|
|
||||||
- After restarting ngrok, update **Notification Backend URL** in the panel and tap **Save**
|
- 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 `DEFAULT_NOTIFY_API_SERVER` again
|
- Or clear override (empty field + Save) only if you intend to hit `APP_SERVER` again
|
||||||
|
|
||||||
### Plugin / JWT errors after refresh
|
### Plugin / JWT errors after refresh
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +1,12 @@
|
|||||||
# Notification Debug Panel
|
# Notification Debug Panel
|
||||||
|
|
||||||
**Created:** 2026-07-07
|
**Created:** 2026-07-07
|
||||||
**Updated:** 2026-07-22
|
|
||||||
**Audience:** Developers testing notification registration, refresh, and WAKEUP_PING flows on native (iOS/Android) dev builds.
|
**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.
|
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
|
## Access
|
||||||
|
|
||||||
1. Use a **non-production** build (for example `build:android:dev`, `build:ios:dev`, or `vite dev` with a non-`production` mode).
|
1. Use a **non-production** build (for example `build:android:dev`, `build:ios:dev`, or `vite dev` with a non-`production` mode).
|
||||||
@@ -61,7 +23,7 @@ Settings persist in `localStorage` via `NotificationDebugConfig.ts`:
|
|||||||
|
|
||||||
| Key | Default | Purpose |
|
| Key | Default | Purpose |
|
||||||
|-----|---------|---------|
|
|-----|---------|---------|
|
||||||
| `notificationDebug.backendBaseUrl` | *(unset — use `DEFAULT_NOTIFY_API_SERVER`)* | Override which notification server receives API calls |
|
| `notificationDebug.backendBaseUrl` | *(unset — use `APP_SERVER`)* | Override which notification server receives API calls |
|
||||||
| `notificationDebug.testMode` | `true` | Sent in JSON request bodies (`testMode: true/false`) |
|
| `notificationDebug.testMode` | `true` | Sent in JSON request bodies (`testMode: true/false`) |
|
||||||
| `notificationDebug.bypassAuth` | `false` | When `true`, omit JWT `Authorization` headers on notification API calls |
|
| `notificationDebug.bypassAuth` | `false` | When `true`, omit JWT `Authorization` headers on notification API calls |
|
||||||
|
|
||||||
@@ -71,7 +33,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.
|
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 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.
|
Leave empty to use the built-in default (`APP_SERVER` from `VITE_APP_SERVER`).
|
||||||
|
|
||||||
### Test Mode
|
### Test Mode
|
||||||
|
|
||||||
@@ -97,11 +59,9 @@ The panel **Backend Status** section shows the active URL, `testMode`, and `bypa
|
|||||||
|
|
||||||
Example: `https://test-notify-api.timesafari.app`
|
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 |
|
| Setting | Value |
|
||||||
|---------|-------|
|
|---------|-------|
|
||||||
| **Notification Backend URL** | Empty (use default) or `https://test-notify-api.timesafari.app` |
|
| **Notification Backend URL** | `https://test-notify-api.timesafari.app` |
|
||||||
| **Test Mode** | **ON** (if the server expects `testMode: true`) |
|
| **Test Mode** | **ON** (if the server expects `testMode: true`) |
|
||||||
| **Skip JWT Authentication** | **OFF** |
|
| **Skip JWT Authentication** | **OFF** |
|
||||||
|
|
||||||
@@ -215,9 +175,8 @@ See platform-specific guides for extended ngrok and FCM workflows:
|
|||||||
|
|
||||||
| File | Purpose |
|
| 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/components/dev/NotificationDebugPanel.vue` | Dev UI |
|
||||||
| `src/services/notifications/NotificationDebugConfig.ts` | Base URL resolution, testMode, bypassAuth persistence |
|
| `src/services/notifications/NotificationDebugConfig.ts` | Backend URL, testMode, bypassAuth persistence |
|
||||||
| `src/services/notifications/notificationApiAuth.ts` | JWT vs unauthenticated headers |
|
| `src/services/notifications/notificationApiAuth.ts` | JWT vs unauthenticated headers |
|
||||||
| `src/services/notifications/notificationApiDebugMode.ts` | Auth bypass gate |
|
| `src/services/notifications/notificationApiDebugMode.ts` | Auth bypass gate |
|
||||||
| `src/services/notifications/NotificationDebugService.ts` | Panel action handlers |
|
| `src/services/notifications/NotificationDebugService.ts` | Panel action handlers |
|
||||||
|
|||||||
@@ -133,7 +133,6 @@ VITE_DEFAULT_ENDORSER_API_SERVER=https://dev-api.endorser.ch
|
|||||||
VITE_DEFAULT_IMAGE_API_SERVER=https://dev-image-api.timesafari.app
|
VITE_DEFAULT_IMAGE_API_SERVER=https://dev-image-api.timesafari.app
|
||||||
VITE_DEFAULT_PARTNER_API_SERVER=https://dev-partner-api.endorser.ch
|
VITE_DEFAULT_PARTNER_API_SERVER=https://dev-partner-api.endorser.ch
|
||||||
VITE_DEFAULT_PUSH_SERVER=https://dev.timesafari.app
|
VITE_DEFAULT_PUSH_SERVER=https://dev.timesafari.app
|
||||||
VITE_DEFAULT_NOTIFY_API_SERVER=https://test-notify-api.timesafari.app
|
|
||||||
VITE_PASSKEYS_ENABLED=true
|
VITE_PASSKEYS_ENABLED=true
|
||||||
|
|
||||||
# .env.test
|
# .env.test
|
||||||
@@ -142,7 +141,6 @@ VITE_DEFAULT_ENDORSER_API_SERVER=https://staging-api.endorser.ch
|
|||||||
VITE_DEFAULT_IMAGE_API_SERVER=https://staging-image-api.timesafari.app
|
VITE_DEFAULT_IMAGE_API_SERVER=https://staging-image-api.timesafari.app
|
||||||
VITE_DEFAULT_PARTNER_API_SERVER=https://staging-partner-api.endorser.ch
|
VITE_DEFAULT_PARTNER_API_SERVER=https://staging-partner-api.endorser.ch
|
||||||
VITE_DEFAULT_PUSH_SERVER=https://staging.timesafari.app
|
VITE_DEFAULT_PUSH_SERVER=https://staging.timesafari.app
|
||||||
VITE_DEFAULT_NOTIFY_API_SERVER=https://test-notify-api.timesafari.app
|
|
||||||
VITE_PASSKEYS_ENABLED=true
|
VITE_PASSKEYS_ENABLED=true
|
||||||
|
|
||||||
# .env.production
|
# .env.production
|
||||||
@@ -151,7 +149,6 @@ VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
|||||||
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
|
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
|
||||||
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch
|
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch
|
||||||
VITE_DEFAULT_PUSH_SERVER=https://timesafari.app
|
VITE_DEFAULT_PUSH_SERVER=https://timesafari.app
|
||||||
VITE_DEFAULT_NOTIFY_API_SERVER=https://notify-api.timesafari.app
|
|
||||||
VITE_PASSKEYS_ENABLED=true
|
VITE_PASSKEYS_ENABLED=true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
<p :class="textClasses">{{ text }}</p>
|
<p :class="textClasses">{{ text }}</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
v-if="option1Text"
|
||||||
:class="option1ButtonClasses"
|
:class="option1ButtonClasses"
|
||||||
@click="handleOption1(close)"
|
@click="handleOption1(close)"
|
||||||
>
|
>
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
v-if="option2Text"
|
||||||
:class="option2ButtonClasses"
|
:class="option2ButtonClasses"
|
||||||
@click="handleOption2(close)"
|
@click="handleOption2(close)"
|
||||||
>
|
>
|
||||||
@@ -39,6 +41,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
v-if="option3Text"
|
||||||
:class="option3ButtonClasses"
|
:class="option3ButtonClasses"
|
||||||
@click="handleOption3(close)"
|
@click="handleOption3(close)"
|
||||||
>
|
>
|
||||||
|
|||||||
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>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
v-model="backendUrlDraft"
|
v-model="backendUrlDraft"
|
||||||
type="url"
|
type="url"
|
||||||
class="w-full text-sm px-3 py-2 rounded border border-slate-300 bg-white mb-1"
|
class="w-full text-sm px-3 py-2 rounded border border-slate-300 bg-white mb-1"
|
||||||
placeholder="Leave empty for default (DEFAULT_NOTIFY_API_SERVER)"
|
placeholder="Leave empty for default (APP_SERVER)"
|
||||||
:disabled="busy"
|
:disabled="busy"
|
||||||
@keydown.enter="onSaveBackendUrl"
|
@keydown.enter="onSaveBackendUrl"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -57,11 +57,20 @@ export const ACCOUNT_VIEW_CONSTANTS = {
|
|||||||
// Notification messages
|
// Notification messages
|
||||||
NOTIFICATIONS: {
|
NOTIFICATIONS: {
|
||||||
NEW_ACTIVITY_INFO: `
|
NEW_ACTIVITY_INFO: `
|
||||||
This will only notify you when there is new relevant activity for you personally.
|
This will notify you with an app message 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?
|
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: `
|
REMINDER_INFO: `
|
||||||
This will notify you at a specific time each day.
|
This will notify you at a specific time each day.
|
||||||
Note that it does not give you personalized notifications,
|
Note that it does not give you personalized notifications,
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ export enum AppString {
|
|||||||
TEST1_PUSH_SERVER = "https://test.timesafari.app",
|
TEST1_PUSH_SERVER = "https://test.timesafari.app",
|
||||||
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",
|
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)",
|
NO_CONTACT_NAME = "(no name)",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,10 +47,6 @@ export const DEFAULT_PARTNER_API_SERVER =
|
|||||||
export const DEFAULT_PUSH_SERVER =
|
export const DEFAULT_PUSH_SERVER =
|
||||||
import.meta.env.VITE_DEFAULT_PUSH_SERVER || AppString.PROD_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";
|
export const IMAGE_TYPE_PROFILE = "profile";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Persists overrides in localStorage; production defaults apply when unset.
|
* Persists overrides in localStorage; production defaults apply when unset.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { DEFAULT_NOTIFY_API_SERVER } from "@/constants/app";
|
import { APP_SERVER } from "@/constants/app";
|
||||||
|
|
||||||
const LOG = "[NotificationDebug]";
|
const LOG = "[NotificationDebug]";
|
||||||
const STORAGE_KEY_BACKEND_URL = "notificationDebug.backendBaseUrl";
|
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 Notification API. */
|
/** Backend URL override, or null when using the default app server. */
|
||||||
export function getBackendBaseUrl(): string | null {
|
export function getBackendBaseUrl(): string | null {
|
||||||
const raw = readStorage(STORAGE_KEY_BACKEND_URL);
|
const raw = readStorage(STORAGE_KEY_BACKEND_URL);
|
||||||
if (raw === null) {
|
if (raw === null) {
|
||||||
@@ -101,15 +101,12 @@ export function setBypassAuth(enabled: boolean): void {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Base URL for `/notifications/*` API calls.
|
* Base URL for `/notifications/*` API calls.
|
||||||
* Uses debug override when set; otherwise DEFAULT_NOTIFY_API_SERVER.
|
* Uses debug override when set; otherwise the built-in app server (production default).
|
||||||
*/
|
*/
|
||||||
export function getNotificationApiBaseUrl(): string {
|
export function getNotificationApiBaseUrl(): string {
|
||||||
const override = getBackendBaseUrl();
|
const override = getBackendBaseUrl();
|
||||||
if (override) {
|
if (override) {
|
||||||
return override;
|
return override;
|
||||||
}
|
}
|
||||||
return (
|
return normalizeNotificationBackendUrl(APP_SERVER) ?? APP_SERVER;
|
||||||
normalizeNotificationBackendUrl(DEFAULT_NOTIFY_API_SERVER) ??
|
|
||||||
DEFAULT_NOTIFY_API_SERVER
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,24 +143,48 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between mt-4 mb-2">
|
|
||||||
<!-- label -->
|
<!--
|
||||||
<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">
|
||||||
New Activity Notification
|
New Activity Notification
|
||||||
<button
|
<button
|
||||||
class="text-slate-400 fa-fw cursor-pointer"
|
class="text-slate-400 fa-fw cursor-pointer"
|
||||||
aria-label="Learn more about New Activity notifications"
|
aria-label="Learn more about New Activity notifications"
|
||||||
@click.stop="showNewActivityNotificationInfo"
|
@click.stop="showNewActivityNotificationInfo(false)"
|
||||||
>
|
>
|
||||||
<font-awesome icon="question-circle" aria-hidden="true" />
|
<font-awesome icon="question-circle" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 -->
|
<!-- toggle -->
|
||||||
<div
|
<div
|
||||||
class="relative ml-2 cursor-pointer"
|
class="relative ml-2 cursor-pointer"
|
||||||
role="switch"
|
role="switch"
|
||||||
:aria-checked="notifyingNewActivity"
|
:aria-checked="notifyingNewActivity"
|
||||||
aria-label="Toggle New Activity notifications"
|
aria-label="Toggle New Activity notifications in app"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click.stop.prevent="showNewActivityNotificationChoice()"
|
@click.stop.prevent="showNewActivityNotificationChoice()"
|
||||||
>
|
>
|
||||||
@@ -178,7 +202,7 @@
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="notifyingNewActivity" class="w-full">
|
<div v-if="notifyingNewActivity" class="w-full pl-1">
|
||||||
<div
|
<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"
|
||||||
>
|
>
|
||||||
@@ -191,10 +215,125 @@
|
|||||||
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"
|
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"
|
@click="editNewActivityNotification"
|
||||||
>
|
>
|
||||||
Edit New Activity Notification…
|
Edit In-App Time…
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="mt-2 text-center">
|
||||||
<router-link class="text-sm text-blue-500" to="/help-notifications">
|
<router-link class="text-sm text-blue-500" to="/help-notifications">
|
||||||
Troubleshoot your notifications…
|
Troubleshoot your notifications…
|
||||||
@@ -202,6 +341,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<PushNotificationPermission ref="pushNotificationPermission" />
|
<PushNotificationPermission ref="pushNotificationPermission" />
|
||||||
|
<SmsVerificationDialog ref="smsVerificationDialog" />
|
||||||
|
<ChoiceButtonDialog ref="smsOptInChoiceDialog" />
|
||||||
|
|
||||||
<!-- User Profile -->
|
<!-- User Profile -->
|
||||||
<section
|
<section
|
||||||
@@ -546,7 +687,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Notification Push Server setting - only show on web platforms -->
|
<!-- Notification Push Server setting - only show on web platforms -->
|
||||||
<div v-if="!isNativePlatform">
|
<!-- This will be removed because we're no longer using PWA notifications. -->
|
||||||
|
<div v-if="false">
|
||||||
<h2 class="text-slate-500 text-sm font-bold mb-2">
|
<h2 class="text-slate-500 text-sm font-bold mb-2">
|
||||||
Notification Push Server
|
Notification Push Server
|
||||||
</h2>
|
</h2>
|
||||||
@@ -792,10 +934,12 @@ import { copyToClipboard } from "../services/ClipboardService";
|
|||||||
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||||
import { Capacitor } from "@capacitor/core";
|
import { Capacitor } from "@capacitor/core";
|
||||||
|
|
||||||
|
import ChoiceButtonDialog from "../components/ChoiceButtonDialog.vue";
|
||||||
import EntityIcon from "../components/EntityIcon.vue";
|
import EntityIcon from "../components/EntityIcon.vue";
|
||||||
import ImageMethodDialog from "../components/ImageMethodDialog.vue";
|
import ImageMethodDialog from "../components/ImageMethodDialog.vue";
|
||||||
import PushNotificationPermission from "../components/PushNotificationPermission.vue";
|
import PushNotificationPermission from "../components/PushNotificationPermission.vue";
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
|
import SmsVerificationDialog from "../components/SmsVerificationDialog.vue";
|
||||||
import TopMessage from "../components/TopMessage.vue";
|
import TopMessage from "../components/TopMessage.vue";
|
||||||
import UserNameDialog from "../components/UserNameDialog.vue";
|
import UserNameDialog from "../components/UserNameDialog.vue";
|
||||||
import DataExportSection from "../components/DataExportSection.vue";
|
import DataExportSection from "../components/DataExportSection.vue";
|
||||||
@@ -870,6 +1014,7 @@ interface PushNotificationPermissionRef {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
ChoiceButtonDialog,
|
||||||
EntityIcon,
|
EntityIcon,
|
||||||
ImageMethodDialog,
|
ImageMethodDialog,
|
||||||
LMap,
|
LMap,
|
||||||
@@ -877,6 +1022,7 @@ interface PushNotificationPermissionRef {
|
|||||||
LTileLayer,
|
LTileLayer,
|
||||||
PushNotificationPermission,
|
PushNotificationPermission,
|
||||||
QuickNav,
|
QuickNav,
|
||||||
|
SmsVerificationDialog,
|
||||||
TopMessage,
|
TopMessage,
|
||||||
UserNameDialog,
|
UserNameDialog,
|
||||||
DataExportSection,
|
DataExportSection,
|
||||||
@@ -931,6 +1077,15 @@ export default class AccountViewView extends Vue {
|
|||||||
// Notification properties
|
// Notification properties
|
||||||
notifyingNewActivity: boolean = false;
|
notifyingNewActivity: boolean = false;
|
||||||
notifyingNewActivityTime: string = "";
|
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;
|
notifyingReminder: boolean = false;
|
||||||
notifyingReminderMessage: string = "";
|
notifyingReminderMessage: string = "";
|
||||||
notifyingReminderTime: string = "";
|
notifyingReminderTime: string = "";
|
||||||
@@ -1216,9 +1371,11 @@ export default class AccountViewView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async showNewActivityNotificationInfo(): Promise<void> {
|
async showNewActivityNotificationInfo(isSms: boolean): Promise<void> {
|
||||||
this.notify.confirm(
|
this.notify.confirm(
|
||||||
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.NEW_ACTIVITY_INFO,
|
isSms
|
||||||
|
? ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.NEW_ACTIVITY_SMS_INFO
|
||||||
|
: ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.NEW_ACTIVITY_INFO,
|
||||||
async () => {
|
async () => {
|
||||||
await (this.$router as Router).push({
|
await (this.$router as Router).push({
|
||||||
name: "help-notification-types",
|
name: "help-notification-types",
|
||||||
@@ -1266,6 +1423,166 @@ 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.
|
* Configure native fetcher, sync starred plans, and schedule API-driven dual notification.
|
||||||
*/
|
*/
|
||||||
@@ -1741,6 +2058,10 @@ export default class AccountViewView extends Vue {
|
|||||||
this.notifyingReminder = false;
|
this.notifyingReminder = false;
|
||||||
this.notifyingReminderMessage = "";
|
this.notifyingReminderMessage = "";
|
||||||
this.notifyingReminderTime = "";
|
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,31 +39,70 @@
|
|||||||
something, so you can record thanks in here.
|
something, so you can record thanks in here.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
This is a reliable message, but it doesn't contain any details about
|
This is your own message activated in the app, but it doesn't contain any details about
|
||||||
activity that might be especially interesting to you.
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold mt-4">New Activity Notifications</h2>
|
<h2 class="text-xl font-semibold mt-4">New Activity Notifications -- In App</h2>
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
The New Activity Notification will be sent to you when there is new, relevant activity
|
The New Activity Notification will be activated in your app when there is new, relevant activity
|
||||||
for you.
|
for you.
|
||||||
It will only trigger if something involves you or a project of interest; it will not
|
It will only trigger if something involves you or a project of interest; it will not
|
||||||
bug you for other, general activity.
|
bug you for other, general activity.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
|
||||||
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>
|
|
||||||
</div>
|
</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.
|
||||||
|
</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>
|
</div>
|
||||||
<!-- eslint-enable -->
|
<!-- eslint-enable -->
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user