mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-22 07:28:14 +00:00
fix(reminder): skip Capacitor permission checks on legacy Android WebView
The legacy `FullscreenActivity` shell exposes the SUPAndroid bridge but hosts no Capacitor bridge, so `LocalNotifications.checkPermissions()` and `checkExactNotificationSetting()` fall back to their web implementations. Web Notifications API is unimplemented in Android WebView (Chromium issue #434712), so `Notification.permission` reads 'default' regardless of the OS POST_NOTIFICATIONS state. That produced two false-positive cold-start snackbars and silently bailed out of the reminder/due-date scheduling effects. Reminders on this path go through AlarmManager via `androidInterface.scheduleNativeReminder`, which the OS gates with POST_NOTIFICATIONS at fire time. Trust it: short-circuit `ensurePermissions()` and `ensureExactAlarmPermission()` to true on the legacy WebView. Trade-off: a user with OS notifications actually disabled gets no in-app warning, but the existing snackbar didn't deep-link to settings anyway. Inject `IS_ANDROID_WEB_VIEW_TOKEN` so the discriminator is overrideable via standard Angular DI in tests, matching the pattern in `task-reminder.effects.ts`. Refs #7408
This commit is contained in:
parent
bd34a20ec9
commit
766ecf2271
2 changed files with 91 additions and 1 deletions
|
|
@ -4,6 +4,7 @@ import { LocalNotificationsWeb } from '@capacitor/local-notifications/dist/esm/w
|
|||
import { CapacitorReminderService } from './capacitor-reminder.service';
|
||||
import { CapacitorPlatformService } from './capacitor-platform.service';
|
||||
import { CapacitorNotificationService } from './capacitor-notification.service';
|
||||
import { IS_ANDROID_WEB_VIEW_TOKEN } from '../../util/is-android-web-view';
|
||||
|
||||
describe('CapacitorReminderService', () => {
|
||||
let service: CapacitorReminderService;
|
||||
|
|
@ -132,6 +133,56 @@ describe('CapacitorReminderService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('legacy Android WebView path (issue #7408)', () => {
|
||||
let legacyService: CapacitorReminderService;
|
||||
let checkExactAlarmSpy: jasmine.Spy;
|
||||
|
||||
beforeEach(() => {
|
||||
const nativePlatformSpy = jasmine.createSpyObj(
|
||||
'CapacitorPlatformService',
|
||||
['hasCapability', 'isIOS'],
|
||||
{
|
||||
platform: 'android',
|
||||
isNative: true,
|
||||
isMobile: true,
|
||||
capabilities: { scheduledNotifications: true },
|
||||
},
|
||||
);
|
||||
checkExactAlarmSpy = spyOn(
|
||||
LocalNotificationsWeb.prototype,
|
||||
'checkExactNotificationSetting',
|
||||
);
|
||||
TestBed.resetTestingModule();
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
CapacitorReminderService,
|
||||
provideMockStore(),
|
||||
{ provide: CapacitorPlatformService, useValue: nativePlatformSpy },
|
||||
{ provide: CapacitorNotificationService, useValue: notificationServiceSpy },
|
||||
// Override the WebView discriminator via DI; `Capacitor.isNativePlatform()`
|
||||
// is naturally false in the test environment, so the legacy-path
|
||||
// condition `_isAndroidWebView && !Capacitor.isNativePlatform()` evaluates true.
|
||||
{ provide: IS_ANDROID_WEB_VIEW_TOKEN, useValue: true },
|
||||
],
|
||||
});
|
||||
legacyService = TestBed.inject(CapacitorReminderService);
|
||||
});
|
||||
|
||||
it('ensurePermissions short-circuits without consulting Capacitor', async () => {
|
||||
// The whole point of issue #7408: avoid the broken Web Notifications API
|
||||
// fallback. We must NOT delegate to _notificationService here.
|
||||
const result = await legacyService.ensurePermissions();
|
||||
expect(result).toBe(true);
|
||||
expect(notificationServiceSpy.ensurePermissions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ensureExactAlarmPermission short-circuits without consulting Capacitor', async () => {
|
||||
const result = await legacyService.ensureExactAlarmPermission();
|
||||
expect(result).toBe(true);
|
||||
expect(checkExactAlarmSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with native platform', () => {
|
||||
let nativeService: CapacitorReminderService;
|
||||
let nativePlatformSpy: jasmine.SpyObj<CapacitorPlatformService>;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { inject, Injectable } from '@angular/core';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { LocalNotifications } from '@capacitor/local-notifications';
|
||||
import { Log } from '../log';
|
||||
import { CapacitorPlatformService } from './capacitor-platform.service';
|
||||
|
|
@ -7,7 +8,10 @@ import {
|
|||
NotificationActionEvent,
|
||||
REMINDER_ACTION_TYPE_ID,
|
||||
} from './capacitor-notification.service';
|
||||
import { IS_ANDROID_WEB_VIEW } from '../../util/is-android-web-view';
|
||||
import {
|
||||
IS_ANDROID_WEB_VIEW,
|
||||
IS_ANDROID_WEB_VIEW_TOKEN,
|
||||
} from '../../util/is-android-web-view';
|
||||
import { androidInterface } from '../../features/android/android-interface';
|
||||
import { Observable } from 'rxjs';
|
||||
import { GlobalConfigService } from '../../features/config/global-config.service';
|
||||
|
|
@ -52,6 +56,9 @@ export class CapacitorReminderService {
|
|||
private _platformService = inject(CapacitorPlatformService);
|
||||
private _notificationService = inject(CapacitorNotificationService);
|
||||
private _globalConfigService = inject(GlobalConfigService);
|
||||
// Injected (vs reading IS_ANDROID_WEB_VIEW directly) so tests can override
|
||||
// it via DI — matches the pattern in `task-reminder.effects.ts`.
|
||||
private _isAndroidWebView = inject(IS_ANDROID_WEB_VIEW_TOKEN);
|
||||
|
||||
/**
|
||||
* Observable that emits when a notification action is performed (iOS).
|
||||
|
|
@ -274,6 +281,16 @@ export class CapacitorReminderService {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (this._isLegacyAndroidWebView()) {
|
||||
// No Capacitor bridge → `LocalNotifications.checkPermissions()` would
|
||||
// fall back to `Notification.permission`, which Android WebView leaves
|
||||
// at 'default' regardless of OS POST_NOTIFICATIONS state (issue #7408).
|
||||
// Reminders here go through AlarmManager via the SUPAndroid bridge; the
|
||||
// OS enforces permission at fire time, so we trust it and skip the
|
||||
// upfront check.
|
||||
return true;
|
||||
}
|
||||
|
||||
const hasPermission = await this._notificationService.ensurePermissions();
|
||||
if (!hasPermission) {
|
||||
return false;
|
||||
|
|
@ -296,6 +313,15 @@ export class CapacitorReminderService {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (this._isLegacyAndroidWebView()) {
|
||||
// Same reasoning as ensurePermissions(): no Capacitor bridge in the
|
||||
// legacy WebView, so the LocalNotifications exact-alarm helpers throw
|
||||
// or return stale data. AlarmManager will fall back to inexact
|
||||
// delivery if SCHEDULE_EXACT_ALARM is denied — acceptable for
|
||||
// reminder UX. Skip the check.
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const exactAlarmStatus = await LocalNotifications.checkExactNotificationSetting();
|
||||
if (exactAlarmStatus?.exact_alarm !== 'granted') {
|
||||
|
|
@ -310,4 +336,17 @@ export class CapacitorReminderService {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True on the legacy `FullscreenActivity`, which exposes the SUPAndroid
|
||||
* bridge but does NOT host a Capacitor bridge. Capacitor plugin calls fall
|
||||
* back to their web implementations there, and the Web Notifications API
|
||||
* is unimplemented in Android WebView — making any LocalNotifications
|
||||
* permission/exact-alarm check unreliable. Reminder scheduling on this
|
||||
* path goes through AlarmManager via `androidInterface.scheduleNativeReminder`,
|
||||
* which the OS gates with POST_NOTIFICATIONS at fire time. See issue #7408.
|
||||
*/
|
||||
private _isLegacyAndroidWebView(): boolean {
|
||||
return this._isAndroidWebView && !Capacitor.isNativePlatform();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue