Tie the in-app New Activity toggle to AlertSearch authorization so turning it on uploads the batch and turning it off deletes it without unregistering the FCM device.
This commit is contained in:
+32
-17
@@ -386,19 +386,28 @@ export default class App extends Vue {
|
||||
// On native (iOS/Android) we don't use web push; the callback handles cancel + state in the view.
|
||||
// The callback is the one passed for this specific modal (New Activity or Daily Reminder), so we only turn off that one.
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
// A false callback means the view refused the off (AlertSearch revoke
|
||||
// failed). Leave the existing "Notifications are off." toast for every
|
||||
// other callback, which returns void.
|
||||
let turnedOff = true;
|
||||
if (notification.callback) {
|
||||
await notification.callback(true);
|
||||
const callbackResult = await notification.callback(true);
|
||||
if (callbackResult === false) {
|
||||
turnedOff = false;
|
||||
}
|
||||
}
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Finished",
|
||||
text: "Notifications are off.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
return true;
|
||||
if (turnedOff) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Finished",
|
||||
text: "Notifications are off.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
return turnedOff;
|
||||
}
|
||||
|
||||
let subscription: PushSubscriptionJSON | null = null;
|
||||
@@ -483,7 +492,17 @@ export default class App extends Vue {
|
||||
return false;
|
||||
});
|
||||
|
||||
const message = pushServerSuccess
|
||||
// Ask the view before announcing success. A callback that returns false
|
||||
// (failed AlertSearch revoke) keeps the existing failure sentence.
|
||||
// Callbacks that return void, including Daily Reminder, keep today's copy.
|
||||
let callbackRefused = false;
|
||||
if (notification.callback) {
|
||||
const callbackResult = await notification.callback(pushServerSuccess);
|
||||
callbackRefused = callbackResult === false;
|
||||
}
|
||||
|
||||
const turnedOff = pushServerSuccess && !callbackRefused;
|
||||
const message = turnedOff
|
||||
? "Notification is off."
|
||||
: "Notification is still on. Try to turn it off again.";
|
||||
|
||||
@@ -497,11 +516,7 @@ export default class App extends Vue {
|
||||
5000,
|
||||
);
|
||||
|
||||
if (notification.callback) {
|
||||
notification.callback(pushServerSuccess);
|
||||
}
|
||||
|
||||
return pushServerSuccess;
|
||||
return turnedOff;
|
||||
} catch (error) {
|
||||
this.$logAndConsole(
|
||||
"Error turning off notifications: " + JSON.stringify(error),
|
||||
|
||||
@@ -79,7 +79,7 @@ export interface NotificationIface {
|
||||
type: string; // "toast" | "info" | "success" | "warning" | "danger"
|
||||
title?: string;
|
||||
text?: string;
|
||||
callback?: (success: boolean) => Promise<void>; // if this triggered an action
|
||||
callback?: (success: boolean) => Promise<void | boolean>; // if this triggered an action
|
||||
noText?: string;
|
||||
onCancel?: (stopAsking?: boolean) => Promise<void>;
|
||||
onNo?: (stopAsking?: boolean) => Promise<void>;
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import type { AlertAuthorizationBatch } from "./alertAuthorizationBatch";
|
||||
import {
|
||||
type AlertAuthorizationDependencies,
|
||||
revokeAlertSearchAuthorization,
|
||||
uploadAlertSearchAuthorization,
|
||||
} from "./alertAuthorization";
|
||||
import { alertSearchNotifyTimeUtcFromLocalNineAm } from "./alertSearchNotifyTime";
|
||||
import {
|
||||
authorizeInAppNewActivity,
|
||||
revokeInAppNewActivity,
|
||||
} from "./newActivityInAppSubscription";
|
||||
|
||||
jest.mock("@/libs/crypto/vc", () => ({
|
||||
ETHR_DID_PREFIX: "did:ethr:",
|
||||
@@ -238,3 +243,141 @@ describe("uploadAlertSearchAuthorization", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function alertAuthorizationUrl(call: unknown[]): string {
|
||||
return String(call[0]);
|
||||
}
|
||||
|
||||
describe("revokeAlertSearchAuthorization", () => {
|
||||
it("deletes alert authorization with the upload path's authenticated headers", async () => {
|
||||
const { dependencies, fetchMock, mintMock, authMock } =
|
||||
createDependencies();
|
||||
|
||||
const result = await revokeAlertSearchAuthorization(dependencies);
|
||||
|
||||
expect(result).toEqual({ ok: true, status: 200 });
|
||||
expect(mintMock).not.toHaveBeenCalled();
|
||||
expect(authMock).toHaveBeenCalledWith(undefined, ACTIVE_DID);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
||||
expect(url).toBe(
|
||||
"https://notification-backend.invalid/notifications/alert-authorization",
|
||||
);
|
||||
expect(init.method).toBe("DELETE");
|
||||
expect(init.body).toBeUndefined();
|
||||
expect(init.headers).toEqual({
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer access-token-fixture",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not unregister the FCM device registration", async () => {
|
||||
const { dependencies, fetchMock } = createDependencies();
|
||||
|
||||
await revokeAlertSearchAuthorization(dependencies);
|
||||
|
||||
const urls = fetchMock.mock.calls.map(alertAuthorizationUrl);
|
||||
expect(urls).toEqual([
|
||||
"https://notification-backend.invalid/notifications/alert-authorization",
|
||||
]);
|
||||
expect(urls.some((url) => url.includes("/notifications/register"))).toBe(
|
||||
false,
|
||||
);
|
||||
expect(fetchMock.mock.calls[0][1].method).not.toBe("POST");
|
||||
});
|
||||
|
||||
it("rejects notification auth bypass before submission", async () => {
|
||||
const { dependencies, fetchMock, mintMock } = createDependencies({
|
||||
bypassAuth: true,
|
||||
});
|
||||
|
||||
const result = await revokeAlertSearchAuthorization(dependencies);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(mintMock).not.toHaveBeenCalled();
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("surfaces a coded refusal without treating it as revoked", async () => {
|
||||
const { dependencies, fetchMock } = createDependencies({
|
||||
response: createResponse(500, {
|
||||
success: false,
|
||||
error: "ALERT_AUTHORIZATION_DELETE_FAILED",
|
||||
message: "Could not delete AlertSearch authorization.",
|
||||
jwt: "must-not-be-returned",
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await revokeAlertSearchAuthorization(dependencies);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
status: 500,
|
||||
errorCode: "ALERT_AUTHORIZATION_DELETE_FAILED",
|
||||
errorMessage: "Could not delete AlertSearch authorization.",
|
||||
});
|
||||
expect(JSON.stringify(result)).not.toContain("must-not-be-returned");
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock.mock.calls[0][1].method).toBe("DELETE");
|
||||
});
|
||||
});
|
||||
|
||||
describe("in-app New Activity AlertSearch subscription", () => {
|
||||
it("uploads AlertSearch authorization when the setting is enabled", async () => {
|
||||
const { dependencies, fetchMock, mintMock } = createDependencies();
|
||||
const reportError = jest.fn();
|
||||
|
||||
const result = await authorizeInAppNewActivity(reportError, dependencies);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(mintMock).toHaveBeenCalledWith(ACTIVE_DID);
|
||||
expect(reportError).not.toHaveBeenCalled();
|
||||
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
||||
expect(url).toBe(
|
||||
"https://notification-backend.invalid/notifications/alert-authorization",
|
||||
);
|
||||
expect(init.method).toBe("POST");
|
||||
});
|
||||
|
||||
it("deletes alert authorization when the setting is disabled", async () => {
|
||||
const { dependencies, fetchMock, mintMock } = createDependencies();
|
||||
const reportError = jest.fn();
|
||||
|
||||
const revoked = await revokeInAppNewActivity(reportError, dependencies);
|
||||
|
||||
expect(revoked).toBe(true);
|
||||
expect(mintMock).not.toHaveBeenCalled();
|
||||
expect(reportError).not.toHaveBeenCalled();
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock.mock.calls[0][1].method).toBe("DELETE");
|
||||
expect(alertAuthorizationUrl(fetchMock.mock.calls[0])).toBe(
|
||||
"https://notification-backend.invalid/notifications/alert-authorization",
|
||||
);
|
||||
expect(
|
||||
fetchMock.mock.calls.some((call) =>
|
||||
alertAuthorizationUrl(call).includes("/notifications/register"),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("reports a failed delete and does not treat it as revoked", async () => {
|
||||
const { dependencies, fetchMock } = createDependencies({
|
||||
response: createResponse(500, {
|
||||
success: false,
|
||||
message: "Could not delete AlertSearch authorization.",
|
||||
}),
|
||||
});
|
||||
const reportError = jest.fn();
|
||||
|
||||
const revoked = await revokeInAppNewActivity(reportError, dependencies);
|
||||
|
||||
expect(revoked).toBe(false);
|
||||
expect(reportError).toHaveBeenCalledTimes(1);
|
||||
expect(reportError).toHaveBeenCalledWith(
|
||||
"Could not delete AlertSearch authorization.",
|
||||
);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock.mock.calls[0][1].method).toBe("DELETE");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
/**
|
||||
* Manual upload of a push-channel AlertSearch authorization, for the
|
||||
* notification debug panel: mint a delegated JWT batch for the active identity
|
||||
* and `POST /notifications/alert-authorization`, with the send time fixed at
|
||||
* 09:00 in the device's zone.
|
||||
* Push-channel AlertSearch authorization against notification-wakeup-service.
|
||||
*
|
||||
* Upload mints a delegated JWT batch for the active identity and
|
||||
* `POST /notifications/alert-authorization`, with the send time fixed at
|
||||
* 09:00 in the device's zone. Revoke sends `DELETE` on that same route and
|
||||
* does not unregister the FCM device. The debug panel uploads manually; the
|
||||
* in-app New Activity toggle uploads on enable and revokes on disable.
|
||||
*
|
||||
* The SMS channel uploads the same body through `authorizeSmsAlertSearch`, and
|
||||
* both mint with `mintAlertAuthorizationBatch`. The wire shapes are in
|
||||
@@ -42,6 +45,19 @@ export type AlertAuthorizationUploadResult =
|
||||
errorCode?: string;
|
||||
};
|
||||
|
||||
/** `DELETE /notifications/alert-authorization` does not carry a batch. */
|
||||
export type AlertAuthorizationRevokeResult =
|
||||
| {
|
||||
ok: true;
|
||||
status: number;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
errorMessage: string;
|
||||
status?: number;
|
||||
errorCode?: string;
|
||||
};
|
||||
|
||||
type FetchNotificationApi = (
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit,
|
||||
@@ -89,6 +105,53 @@ function buildRequestBody(
|
||||
};
|
||||
}
|
||||
|
||||
type DelegatedAlertAccess =
|
||||
| { ok: true; did: string; headers: Record<string, string> }
|
||||
| { ok: false; errorMessage: string };
|
||||
|
||||
/**
|
||||
* Same gate as upload: an active `did:ethr`, no auth bypass, and authenticated
|
||||
* notification headers. `action` only changes the bypass sentence.
|
||||
*/
|
||||
async function requireDelegatedAlertAccess(
|
||||
dependencies: AlertAuthorizationDependencies,
|
||||
action: "uploaded" | "revoked",
|
||||
): Promise<DelegatedAlertAccess> {
|
||||
const did = await dependencies.getActiveDid();
|
||||
if (!did) {
|
||||
return { ok: false, errorMessage: "No active identity is available." };
|
||||
}
|
||||
if (!did.startsWith(ETHR_DID_PREFIX)) {
|
||||
return {
|
||||
ok: false,
|
||||
errorMessage:
|
||||
"AlertSearch authorization requires an active did:ethr identity.",
|
||||
};
|
||||
}
|
||||
if (dependencies.isAuthBypassEnabled()) {
|
||||
return {
|
||||
ok: false,
|
||||
errorMessage: `AlertSearch authorization cannot be ${action} while JWT authentication is skipped.`,
|
||||
};
|
||||
}
|
||||
|
||||
const auth = await dependencies.getAuthHeaders(undefined, did);
|
||||
if (!auth.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
errorMessage: `Authentication unavailable: ${auth.message}`,
|
||||
};
|
||||
}
|
||||
if (!auth.authenticated) {
|
||||
return {
|
||||
ok: false,
|
||||
errorMessage:
|
||||
"AlertSearch authorization requires authenticated notification API headers.",
|
||||
};
|
||||
}
|
||||
return { ok: true, did, headers: auth.headers };
|
||||
}
|
||||
|
||||
/**
|
||||
* The code and message of a refusal, and nothing else from its body. The
|
||||
* service sends an `AlertAuthorizationFailure`: the code, when there is one,
|
||||
@@ -118,73 +181,53 @@ async function readRefusal(
|
||||
};
|
||||
}
|
||||
|
||||
async function authorizationHttpFailure(response: Response): Promise<{
|
||||
ok: false;
|
||||
status: number;
|
||||
errorMessage: string;
|
||||
errorCode?: string;
|
||||
}> {
|
||||
const refusal = await readRefusal(response);
|
||||
const fallback =
|
||||
response.status === 401 || response.status === 403
|
||||
? httpAuthErrorMessage(response.status)
|
||||
: `HTTP ${response.status}`;
|
||||
return {
|
||||
ok: false,
|
||||
status: response.status,
|
||||
errorMessage: refusal.message || fallback,
|
||||
...(refusal.errorCode ? { errorCode: refusal.errorCode } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mint and manually upload a delegated AlertSearch authorization batch.
|
||||
* Mint and upload a delegated AlertSearch authorization batch.
|
||||
* This function never retries and refuses notification auth bypass mode.
|
||||
*/
|
||||
export async function uploadAlertSearchAuthorization(
|
||||
dependencies: AlertAuthorizationDependencies = defaultDependencies,
|
||||
): Promise<AlertAuthorizationUploadResult> {
|
||||
try {
|
||||
const did = await dependencies.getActiveDid();
|
||||
if (!did) {
|
||||
return { ok: false, errorMessage: "No active identity is available." };
|
||||
}
|
||||
if (!did.startsWith(ETHR_DID_PREFIX)) {
|
||||
return {
|
||||
ok: false,
|
||||
errorMessage:
|
||||
"AlertSearch authorization requires an active did:ethr identity.",
|
||||
};
|
||||
}
|
||||
if (dependencies.isAuthBypassEnabled()) {
|
||||
return {
|
||||
ok: false,
|
||||
errorMessage:
|
||||
"AlertSearch authorization cannot be uploaded while JWT authentication is skipped.",
|
||||
};
|
||||
const access = await requireDelegatedAlertAccess(dependencies, "uploaded");
|
||||
if (!access.ok) {
|
||||
return access;
|
||||
}
|
||||
|
||||
const batch = await dependencies.mintBatch(did);
|
||||
const batch = await dependencies.mintBatch(access.did);
|
||||
const timeZone = dependencies.getTimeZone();
|
||||
const body = buildRequestBody(batch, timeZone);
|
||||
|
||||
const auth = await dependencies.getAuthHeaders(undefined, did);
|
||||
if (!auth.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
errorMessage: `Authentication unavailable: ${auth.message}`,
|
||||
};
|
||||
}
|
||||
if (!auth.authenticated) {
|
||||
return {
|
||||
ok: false,
|
||||
errorMessage:
|
||||
"AlertSearch authorization requires authenticated notification API headers.",
|
||||
};
|
||||
}
|
||||
|
||||
const response = await dependencies.fetch(
|
||||
`${dependencies.getBaseUrl()}/notifications/alert-authorization`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: auth.headers,
|
||||
headers: access.headers,
|
||||
body: JSON.stringify(body),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const refusal = await readRefusal(response);
|
||||
const fallback =
|
||||
response.status === 401 || response.status === 403
|
||||
? httpAuthErrorMessage(response.status)
|
||||
: `HTTP ${response.status}`;
|
||||
return {
|
||||
ok: false,
|
||||
status: response.status,
|
||||
errorMessage: refusal.message || fallback,
|
||||
...(refusal.errorCode ? { errorCode: refusal.errorCode } : {}),
|
||||
};
|
||||
return authorizationHttpFailure(response);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -203,3 +246,38 @@ export async function uploadAlertSearchAuthorization(
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the active identity's push AlertSearch authorization batches.
|
||||
* Uses the same URL, auth headers, and refusal handling as upload.
|
||||
* Does not unregister the FCM device. Never retries.
|
||||
*/
|
||||
export async function revokeAlertSearchAuthorization(
|
||||
dependencies: AlertAuthorizationDependencies = defaultDependencies,
|
||||
): Promise<AlertAuthorizationRevokeResult> {
|
||||
try {
|
||||
const access = await requireDelegatedAlertAccess(dependencies, "revoked");
|
||||
if (!access.ok) {
|
||||
return access;
|
||||
}
|
||||
|
||||
const response = await dependencies.fetch(
|
||||
`${dependencies.getBaseUrl()}/notifications/alert-authorization`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: access.headers,
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return authorizationHttpFailure(response);
|
||||
}
|
||||
|
||||
return { ok: true, status: response.status };
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
ok: false,
|
||||
errorMessage: error instanceof Error ? error.message : "Revoke failed.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +38,18 @@ export {
|
||||
export { NotificationService, registerToken } from "./NotificationService";
|
||||
export { NativeNotificationService } from "./NativeNotificationService";
|
||||
export { WebPushNotificationService } from "./WebPushNotificationService";
|
||||
export { uploadAlertSearchAuthorization } from "./alertAuthorization";
|
||||
export type { AlertAuthorizationUploadResult } from "./alertAuthorization";
|
||||
export {
|
||||
revokeAlertSearchAuthorization,
|
||||
uploadAlertSearchAuthorization,
|
||||
} from "./alertAuthorization";
|
||||
export type {
|
||||
AlertAuthorizationRevokeResult,
|
||||
AlertAuthorizationUploadResult,
|
||||
} from "./alertAuthorization";
|
||||
export {
|
||||
authorizeInAppNewActivity,
|
||||
revokeInAppNewActivity,
|
||||
} from "./newActivityInAppSubscription";
|
||||
|
||||
export { configureNativeFetcherIfReady } from "./nativeFetcherConfig";
|
||||
export { runLegacyApiNotificationsCleanupOnce } from "./legacyApiNotificationsCleanup";
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Server AlertSearch subscription for the in-app New Activity toggle.
|
||||
*
|
||||
* Enable uploads through `uploadAlertSearchAuthorization`. Disable revokes
|
||||
* through `revokeAlertSearchAuthorization`. Neither call registers or deletes
|
||||
* the FCM device. Local permission, settings, and the dual schedule stay in
|
||||
* AccountViewView; a failed revoke returns false so that view can leave them
|
||||
* unchanged instead of showing the subscription as off.
|
||||
*/
|
||||
|
||||
import {
|
||||
type AlertAuthorizationDependencies,
|
||||
type AlertAuthorizationRevokeResult,
|
||||
type AlertAuthorizationUploadResult,
|
||||
revokeAlertSearchAuthorization,
|
||||
uploadAlertSearchAuthorization,
|
||||
} from "./alertAuthorization";
|
||||
|
||||
/**
|
||||
* Upload the AlertSearch batch after the in-app setting has been enabled.
|
||||
* A failure is reported and returned; it does not throw.
|
||||
*/
|
||||
export async function authorizeInAppNewActivity(
|
||||
reportError: (message: string) => void,
|
||||
dependencies?: AlertAuthorizationDependencies,
|
||||
): Promise<AlertAuthorizationUploadResult> {
|
||||
const result = await uploadAlertSearchAuthorization(dependencies);
|
||||
if (!result.ok) {
|
||||
reportError(result.errorMessage);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke the push AlertSearch authorization for the in-app toggle.
|
||||
*
|
||||
* @returns false when the delete did not succeed. Callers must not persist
|
||||
* the off setting or announce that notifications are off in that case.
|
||||
*/
|
||||
export async function revokeInAppNewActivity(
|
||||
reportError: (message: string) => void,
|
||||
dependencies?: AlertAuthorizationDependencies,
|
||||
): Promise<boolean> {
|
||||
const result: AlertAuthorizationRevokeResult =
|
||||
await revokeAlertSearchAuthorization(dependencies);
|
||||
if (!result.ok) {
|
||||
reportError(result.errorMessage);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
+1
-1
@@ -187,7 +187,7 @@ export function createNotifyHelpers(notify: NotifyFunction) {
|
||||
// Notification-off modal (for turning off notifications)
|
||||
notificationOff: (
|
||||
title: string,
|
||||
callback: (success: boolean) => Promise<void>,
|
||||
callback: (success: boolean) => Promise<void | boolean>,
|
||||
timeout?: number,
|
||||
) =>
|
||||
notify(
|
||||
|
||||
@@ -1030,6 +1030,7 @@ import { includeDevToolkitRoutes } from "@/utils/includeDevToolkitRoutes";
|
||||
import { AccountSettings, isApiError } from "@/interfaces/accountView";
|
||||
import {
|
||||
NotificationService,
|
||||
authorizeInAppNewActivity,
|
||||
authorizeSmsAlertSearch,
|
||||
buildDualScheduleConfig,
|
||||
configureNativeFetcherIfReady,
|
||||
@@ -1037,6 +1038,7 @@ import {
|
||||
getNotificationApiBaseUrl,
|
||||
listSmsPhones,
|
||||
localTimeTextToUtc,
|
||||
revokeInAppNewActivity,
|
||||
revokeSmsAlertSearch,
|
||||
smsErrorMessage,
|
||||
syncStarredPlansToNativePlugin,
|
||||
@@ -1530,31 +1532,61 @@ export default class AccountViewView extends Vue {
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
await this.scheduleNewActivityDualNotification(timeText);
|
||||
}
|
||||
await authorizeInAppNewActivity((message) => {
|
||||
this.reportAlertSearchAuthorizationFailure("upload", message);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.notify.notificationOff(DAILY_CHECK_TITLE, async (success) => {
|
||||
if (success) {
|
||||
if (
|
||||
Capacitor.isNativePlatform() &&
|
||||
DailyNotification?.cancelDualSchedule
|
||||
) {
|
||||
try {
|
||||
await DailyNotification.cancelDualSchedule();
|
||||
} catch (e) {
|
||||
logger.error("[AccountViewView] cancelDualSchedule failed:", e);
|
||||
}
|
||||
}
|
||||
await this.$saveSettings({
|
||||
notifyingNewActivityTime: "",
|
||||
});
|
||||
this.notifyingNewActivity = false;
|
||||
this.notifyingNewActivityTime = "";
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
// Revoke before clearing the local setting. A failed delete leaves
|
||||
// the toggle on so the dialog does not claim the subscription is gone.
|
||||
const revoked = await revokeInAppNewActivity((message) => {
|
||||
this.reportAlertSearchAuthorizationFailure("revoke", message);
|
||||
});
|
||||
if (!revoked) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
Capacitor.isNativePlatform() &&
|
||||
DailyNotification?.cancelDualSchedule
|
||||
) {
|
||||
try {
|
||||
await DailyNotification.cancelDualSchedule();
|
||||
} catch (e) {
|
||||
logger.error("[AccountViewView] cancelDualSchedule failed:", e);
|
||||
}
|
||||
}
|
||||
await this.$saveSettings({
|
||||
notifyingNewActivityTime: "",
|
||||
});
|
||||
this.notifyingNewActivity = false;
|
||||
this.notifyingNewActivityTime = "";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Surface an AlertSearch upload or revoke failure. The message states the
|
||||
* failure; callers must not follow it with a success announcement.
|
||||
*/
|
||||
private reportAlertSearchAuthorizationFailure(
|
||||
action: "upload" | "revoke",
|
||||
message: string,
|
||||
): void {
|
||||
logger.error(
|
||||
`[AccountViewView] AlertSearch authorization ${action} failed:`,
|
||||
message,
|
||||
);
|
||||
this.notify.error(
|
||||
`AlertSearch authorization ${action} failed: ${message}`,
|
||||
TIMEOUTS.LONG,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the SMS channel as a whole ("Enable Text Messages" / "SMS Text
|
||||
* Message" master switch).
|
||||
|
||||
Reference in New Issue
Block a user