Merge branch 'notify-api-sms' into notify-api_endpoint-query

This commit is contained in:
2026-09-13 18:45:36 -06:00
13 changed files with 512 additions and 50 deletions
-2
View File
@@ -1,9 +1,7 @@
#!/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
-2
View File
@@ -1,9 +1,7 @@
#!/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..."
-2
View File
@@ -1,9 +1,7 @@
#!/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)..."
+1 -2
View File
@@ -8,8 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [?] - 2026
### Added
- Full flow for setting up SMS notifications
- SMS notifications talk to the notify-api: phone registration and code
- Full flow for setting up SMS notifications: phone registration and code
verification, delegated alertSearch JWT batches with a send hour, and
revocation to stop the texts
- Push-channel client for the notify-api's alert authorization
+9
View File
@@ -43,6 +43,15 @@ Assumes Xcode and Xcode Command Line Tools are installed.
See [BUILDING.md](BUILDING.md) for comprehensive build instructions for all platforms (Web, Electron, iOS, Android, Docker).
## Tests
```
npm run test:web
# ... or do every check:
npm run test:all
```
## 🛡️ Build Architecture Guard
This project uses **Husky Git hooks** to protect the build system
+113 -1
View File
@@ -153,7 +153,8 @@
"ts-jest": "^29.4.0",
"tsx": "^4.20.4",
"typescript": "~5.2.2",
"vite": "^5.2.0"
"vite": "^5.2.0",
"vue-tsc": "^2.1.10"
}
},
"node_modules/@adraffy/ens-normalize": {
@@ -11605,6 +11606,35 @@
"vue": "^3.2.25"
}
},
"node_modules/@volar/language-core": {
"version": "2.4.28",
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
"integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/source-map": "2.4.28"
}
},
"node_modules/@volar/source-map": {
"version": "2.4.28",
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz",
"integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@volar/typescript": {
"version": "2.4.28",
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz",
"integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/language-core": "2.4.28",
"path-browserify": "^1.0.1",
"vscode-uri": "^3.0.8"
}
},
"node_modules/@vue-leaflet/vue-leaflet": {
"version": "0.10.1",
"license": "MIT",
@@ -11663,6 +11693,17 @@
"@vue/shared": "3.5.13"
}
},
"node_modules/@vue/compiler-vue2": {
"version": "2.7.16",
"resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
"integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
"dev": true,
"license": "MIT",
"dependencies": {
"de-indent": "^1.0.2",
"he": "^1.2.0"
}
},
"node_modules/@vue/devtools-api": {
"version": "6.6.4",
"license": "MIT"
@@ -11890,6 +11931,31 @@
"node": ">=4.0"
}
},
"node_modules/@vue/language-core": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.1.10.tgz",
"integrity": "sha512-DAI289d0K3AB5TUG3xDp9OuQ71CnrujQwJrQnfuZDwo6eGNf0UoRlPuaVNO+Zrn65PC3j0oB2i7mNmVPggeGeQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/language-core": "~2.4.8",
"@vue/compiler-dom": "^3.5.0",
"@vue/compiler-vue2": "^2.7.16",
"@vue/shared": "^3.5.0",
"alien-signals": "^0.2.0",
"minimatch": "^9.0.3",
"muggle-string": "^0.4.1",
"path-browserify": "^1.0.1"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@vue/reactivity": {
"version": "3.5.13",
"license": "MIT",
@@ -12155,6 +12221,13 @@
"ajv": "^6.9.1"
}
},
"node_modules/alien-signals": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-0.2.2.tgz",
"integrity": "sha512-cZIRkbERILsBOXTQmMrxc9hgpxglstn69zm+F1ARf4aPAzdAFYd6sBq87ErO0Fj3DV94tglcyHG5kQz9nDC/8A==",
"dev": true,
"license": "MIT"
},
"node_modules/anser": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz",
@@ -15004,6 +15077,13 @@
"version": "1.11.13",
"license": "MIT"
},
"node_modules/de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
"dev": true,
"license": "MIT"
},
"node_modules/debug": {
"version": "4.3.4",
"license": "MIT",
@@ -23436,6 +23516,13 @@
"license": "Apache-2.0",
"optional": true
},
"node_modules/muggle-string": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
"dev": true,
"license": "MIT"
},
"node_modules/multibase": {
"version": "4.0.6",
"license": "MIT",
@@ -30292,6 +30379,13 @@
"optional": true,
"peer": true
},
"node_modules/vscode-uri": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz",
"integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==",
"dev": true,
"license": "MIT"
},
"node_modules/vue": {
"version": "3.5.13",
"license": "MIT",
@@ -30433,6 +30527,24 @@
"vue": "^3.2.0"
}
},
"node_modules/vue-tsc": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.1.10.tgz",
"integrity": "sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/typescript": "~2.4.8",
"@vue/language-core": "2.1.10",
"semver": "^7.5.4"
},
"bin": {
"vue-tsc": "bin/vue-tsc.js"
},
"peerDependencies": {
"typescript": ">=5.0.0"
}
},
"node_modules/walker": {
"version": "1.0.8",
"devOptional": true,
+3 -1
View File
@@ -10,6 +10,7 @@
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
"type-safety-check": "./scripts/type-safety-check.sh",
"type-check": "tsc --noEmit",
"type-check:vue": "vue-tsc --noEmit",
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
"test:prerequisites": "node scripts/check-prerequisites.js",
"check:dependencies": "./scripts/check-dependencies.sh",
@@ -282,6 +283,7 @@
"ts-jest": "^29.4.0",
"tsx": "^4.20.4",
"typescript": "~5.2.2",
"vite": "^5.2.0"
"vite": "^5.2.0",
"vue-tsc": "^2.1.10"
}
}
+3
View File
@@ -25,6 +25,7 @@
<p :class="textClasses">{{ text }}</p>
<button
v-if="option1Text"
:class="option1ButtonClasses"
@click="handleOption1(close)"
>
@@ -32,6 +33,7 @@
</button>
<button
v-if="option2Text"
:class="option2ButtonClasses"
@click="handleOption2(close)"
>
@@ -39,6 +41,7 @@
</button>
<button
v-if="option3Text"
:class="option3ButtonClasses"
@click="handleOption3(close)"
>
+305
View File
@@ -0,0 +1,305 @@
<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";
import {
normalizePhoneNumber,
registerSmsPhone,
SmsApiError,
smsErrorMessage,
verifySmsPhone,
} from "@/services/notifications/smsNotificationApi";
/**
* 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 -> `POST /notify-sms/phone` texts a one-time code
* 2. Enter the code -> `PUT /notify-sms/phone` confirms possession
*
* On successful verification the callback fires with (true, phoneNumber), the
* number in the E.164 form the service stores, so the caller can then let the
* user turn the SMS notification on. Cancelling fires the callback with
* (false).
*
* A number this identity has already verified comes back from the POST with
* `verified: true` and no text sent, which finishes the dialog in one step.
*/
@Component({
mixins: [PlatformServiceMixin],
})
export default class SmsVerificationDialog extends Vue {
visible = false;
step: "phone" | "code" = "phone";
phoneNumber = "";
sentTo = "";
code = "";
sending = false;
verifying = false;
errorMessage = "";
/** Identity the registration is scoped to; every route is per-DID. */
activeDid = "";
callback: (success: boolean, phoneNumber?: string) => void = () => {};
/**
* Opens the dialog at the phone-entry step.
* @param activeDid - identity the number is registered under
* @param aCallback - fired with (true, phoneNumber) on success, (false) on cancel
* @param prefillPhone - optional number to pre-populate (e.g. re-verifying)
*/
open(
activeDid: string,
aCallback?: (success: boolean, phoneNumber?: string) => void,
prefillPhone = "",
) {
this.activeDid = activeDid;
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 ask the service to text a code.
*
* The masked number the service echoes back is what the user sees; the
* normalized one is what later calls send, since the mask is not a number.
*/
async sendCode(): Promise<void> {
this.errorMessage = "";
const normalized = normalizePhoneNumber(this.phoneNumber);
if (!normalized || normalized.replace(/\D/g, "").length < 7) {
this.errorMessage = "Please enter a valid mobile number.";
return;
}
this.sending = true;
try {
const result = await registerSmsPhone(this.activeDid, normalized);
this.phoneNumber = normalized;
if (result.verified) {
// Already verified under this identity: nothing was texted and there
// is no code to enter.
this.visible = false;
this.callback(true, normalized);
return;
}
this.sentTo = result.phoneNumber || normalized;
// Only a dev server with SMS_DEV_ECHO_CODE returns this; it saves a
// developer with no carrier coverage from a dead-end flow.
this.code = result.devCode || "";
this.step = "code";
} catch (error) {
logger.error("[SmsVerificationDialog] sendCode failed:", error);
this.errorMessage = smsErrorMessage(
error,
"Could not send the code. Please try again.",
);
} finally {
this.sending = false;
}
}
/**
* Ask the service to send a fresh code to the same number. Sends are
* throttled to three per number per hour, so this can be refused.
*/
async resendCode(): Promise<void> {
this.errorMessage = "";
this.sending = true;
try {
const result = await registerSmsPhone(this.activeDid, this.phoneNumber);
this.sentTo = result.phoneNumber || this.sentTo;
// A number already verified comes back with no code, dev or otherwise.
this.code = result.verified ? "" : result.devCode || "";
} catch (error) {
logger.error("[SmsVerificationDialog] resendCode failed:", error);
this.errorMessage = smsErrorMessage(
error,
"Could not resend the code. Please try again.",
);
} finally {
this.sending = false;
}
}
/**
* Confirm the entered code with the service.
*
* A wrong code says how many tries are left; running out clears the code
* server-side, so the only way forward is a fresh send from step one.
*/
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 {
await verifySmsPhone(this.activeDid, this.phoneNumber, this.code);
this.visible = false;
this.callback(true, this.phoneNumber);
} catch (error) {
logger.error("[SmsVerificationDialog] verifyCode failed:", error);
this.errorMessage = smsErrorMessage(
error,
"Could not verify the code. Please try again.",
);
if (
error instanceof SmsApiError &&
error.code === "SMS_CODE_ATTEMPTS_EXHAUSTED"
) {
// The stored code is gone; recovery is another send, not another guess.
this.step = "phone";
this.code = "";
}
} finally {
this.verifying = false;
}
}
onClickCancel(): void {
this.visible = false;
this.callback(false);
}
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>
+12 -3
View File
@@ -59,11 +59,20 @@ export const ACCOUNT_VIEW_CONSTANTS = {
// Notification messages
NOTIFICATIONS: {
NEW_ACTIVITY_INFO: `
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'.
This will notify you with an app message when there is new relevant activity for you personally.
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,
+50 -23
View File
@@ -143,24 +143,48 @@
</button>
</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
<button
class="text-slate-400 fa-fw cursor-pointer"
aria-label="Learn more about New Activity notifications"
@click.stop="showNewActivityNotificationInfo"
@click.stop="showNewActivityNotificationInfo(false)"
>
<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"
aria-label="Toggle New Activity notifications in app"
tabindex="0"
@click.stop.prevent="showNewActivityNotificationChoice()"
>
@@ -196,7 +220,7 @@
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 New Activity Notification…
Edit In-App Time…
</button>
</div>
</div>
@@ -316,6 +340,8 @@
</div>
</section>
<PushNotificationPermission ref="pushNotificationPermission" />
<SmsVerificationDialog ref="smsVerificationDialog" />
<ChoiceButtonDialog ref="smsOptInChoiceDialog" />
<!-- User Profile -->
<section
@@ -958,10 +984,12 @@ 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";
@@ -1048,6 +1076,7 @@ interface PushNotificationPermissionRef {
@Component({
components: {
ChoiceButtonDialog,
EntityIcon,
ImageMethodDialog,
LMap,
@@ -1055,6 +1084,7 @@ interface PushNotificationPermissionRef {
LTileLayer,
PushNotificationPermission,
QuickNav,
SmsVerificationDialog,
TopMessage,
UserNameDialog,
DataExportSection,
@@ -1114,15 +1144,18 @@ export default class AccountViewView extends Vue {
// Notification properties
notifyingNewActivity: boolean = false;
notifyingNewActivityTime: string = "";
// Notify settings hold what the service cannot report back: it has a route
// to list registered numbers but none to read an authorization, so the
// chosen time and the paused/active distinction live here.
/** Verified SMS phone number; kept when smsEnabled turns off, cleared by Forget. */
// SMS channel (opt-in), backed by the notify-api's `/notify-sms` routes.
// Settings hold what the service cannot report back: it has a route to list
// registered numbers but none to read an authorization, so the chosen time
// and the paused/active distinction live here.
/** 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 = "";
/** Guard: one in-flight `/notify-sms` write at a time. */
smsRequestInProgress: boolean = false;
notifyingReminder: boolean = false;
notifyingReminderMessage: string = "";
notifyingReminderTime: string = "";
@@ -1130,13 +1163,6 @@ export default class AccountViewView extends Vue {
editReminderScheduleInProgress: boolean = false;
/** Dev/test only: use 10-minute rollover for daily reminder (plugin rolloverIntervalMinutes). */
reminderFastRolloverForTesting: boolean = false;
// SMS channel (opt-in), backed by the notify-api's `/notify-sms` routes.
/** Master switch: the SMS channel is on (requires a verified phone). */
smsEnabled: boolean = false;
/** Guard: one in-flight `/notify-sms` write at a time. */
smsRequestInProgress: boolean = false;
subscription: PushSubscription | null = null;
// UI state properties
@@ -1479,9 +1505,11 @@ export default class AccountViewView extends Vue {
}
}
async showNewActivityNotificationInfo(): Promise<void> {
async showNewActivityNotificationInfo(isSms: boolean): Promise<void> {
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 () => {
await (this.$router as Router).push({
name: "help-notification-types",
@@ -2315,9 +2343,8 @@ export default class AccountViewView extends Vue {
this.notifyingReminder = false;
this.notifyingReminderMessage = "";
this.notifyingReminderTime = "";
// The SMS channel is deliberately untouched: this runs when the browser's
// web-push subscription has gone missing, but SMS uses other rails.
// web-push subscription has gone missing, and texts do not travel over it.
}
/**
+14 -13
View File
@@ -39,29 +39,29 @@
something, so you can record thanks in here.
</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.
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</h2>
<h2 class="text-xl font-semibold mt-4">New Activity Notifications -- In App</h2>
<div>
<p>
The New Activity Notification will be sent to you when there is new, relevant activity
for you.
The New Activity Notification will be activated in your app 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 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>.)
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">
@@ -108,6 +108,7 @@
</li>
</ul>
</div>
</div>
<!-- eslint-enable -->
</section>
+2 -1
View File
@@ -85,7 +85,8 @@ test('Record an offer', async ({ page }) => {
// click on the number of new offers to go to the list page
await offerNumElem.click();
await expect(page.getByText('New Offers To Your Projects', { exact: true })).toBeVisible();
// "Offer" for exactly one new offer, "Offers" otherwise
await expect(page.getByText(/^New Offers? To Your Projects$/)).toBeVisible();
// get the icon child of the showOffersToUserProjects
await page.getByTestId('showOffersToUserProjects').locator('div > svg.fa-chevron-right').click();
await expect(page.getByText(description)).toBeVisible();