|
|
@@ -80,6 +80,55 @@ export class AppNotificationService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 权限被拒后,把用户**引导到系统通知设置页**(2026-11-15)。
|
|
|
+ *
|
|
|
+ * ## 为什么需要
|
|
|
+ *
|
|
|
+ * Android 13+ 通知权限如果被用户点了「拒绝」,**再次调用 `requestPermissions()`
|
|
|
+ * 不会再弹窗**(系统会直接返回 denied),必须去系统设置里手动打开。
|
|
|
+ * 不做引导的话,用户会觉得「这个 App 就是不给我推送」,也不知道去哪开。
|
|
|
+ *
|
|
|
+ * ## 跳哪个页面
|
|
|
+ *
|
|
|
+ * 用 `App.openSettings()` 打开**本 App 的系统设置页**。
|
|
|
+ * 为什么不直接跳「通知」子页:那需要 `Settings.ACTION_APP_NOTIFICATION_SETTINGS`
|
|
|
+ * (Android 原生 Intent),Capacitor 没有暴露;而 App 设置页里
|
|
|
+ * 第一屏就是「通知」入口,路径很短,也不会跳错到别的设置页。
|
|
|
+ *
|
|
|
+ * 🔴 对照真实踩坑:`@capacitor/local-notifications` 因为
|
|
|
+ * `isExactNotification` 默认为 true,会**主动跳「闹钟和提醒」**页 —— 那才是错的。
|
|
|
+ */
|
|
|
+ async openNotificationSettings(): Promise<void> {
|
|
|
+ if (!isNativeApp()) return;
|
|
|
+ try {
|
|
|
+ /*
|
|
|
+ * 走我们自己的原生插件 `AppSettingsPlugin`(见 android/.../AppSettingsPlugin.java)。
|
|
|
+ * `@capacitor/app` v8 **没有** `openSettings()`,所以必须自建。
|
|
|
+ * 插件内部用 `ACTION_APPLICATION_DETAILS_SETTINGS` 打开本 App 的设置页。
|
|
|
+ */
|
|
|
+ const { registerPlugin } = await import('@capacitor/core');
|
|
|
+ const AppSettings = registerPlugin<{ open(): Promise<void> }>('AppSettings');
|
|
|
+ await AppSettings.open();
|
|
|
+ } catch {
|
|
|
+ /* 插件未注册 / 非 Android:静默,不崩 */
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 当前是否已授权(不发请求,只查询;未知时返回 null) */
|
|
|
+ async isGranted(): Promise<boolean | null> {
|
|
|
+ if (!isNativeApp()) return null;
|
|
|
+ if (this.granted !== null) return this.granted;
|
|
|
+ try {
|
|
|
+ const { LocalNotifications } = await import('@capacitor/local-notifications');
|
|
|
+ const cur = await LocalNotifications.checkPermissions();
|
|
|
+ this.granted = cur.display === 'granted';
|
|
|
+ return this.granted;
|
|
|
+ } catch {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 申请通知权限。
|
|
|
*
|
|
|
@@ -95,6 +144,11 @@ export class AppNotificationService {
|
|
|
this.granted = true;
|
|
|
return true;
|
|
|
}
|
|
|
+ /*
|
|
|
+ * 已被拒绝过一次 → 系统不再弹窗,直接返回 denied。
|
|
|
+ * 此时 `this.granted` 会记成 false,上层可用 `isGranted()` 区分
|
|
|
+ * 「还没问过」与「问过被拒」,从而决定是否给引导提示。
|
|
|
+ */
|
|
|
const res = await LocalNotifications.requestPermissions();
|
|
|
this.granted = res.display === 'granted';
|
|
|
return this.granted;
|
|
|
@@ -131,6 +185,23 @@ export class AppNotificationService {
|
|
|
autoCancel: true,
|
|
|
smallIcon: 'ic_stat_icon_config_sample',
|
|
|
channelId: 'fmode-default',
|
|
|
+ /*
|
|
|
+ * 🔴 必须显式关掉「精确通知」(2026-11-15 真机踩坑)。
|
|
|
+ *
|
|
|
+ * 插件源码 `LocalNotification.kt:185`:
|
|
|
+ * n.isExactNotification = jsonObject.getBoolean("isExactNotification", true) ?: true
|
|
|
+ * **默认值是 true**。不传这个字段,插件就把它当精确定时闹钟,
|
|
|
+ * 于是走 `ACTION_REQUEST_SCHEDULE_EXACT_ALARM`,
|
|
|
+ * 把用户**强制带到系统的「闹钟和提醒」设置页**。
|
|
|
+ *
|
|
|
+ * 症状:用户首次收到通知时莫名跳到闹钟设置,完全不理解为什么。
|
|
|
+ * 更坑的是——从 manifest 移除 SCHEDULE_EXACT_ALARM 权限**并不能阻止**,
|
|
|
+ * 因为跳转是插件在 `schedule()` 里主动 `startActivityForResult` 发起的。
|
|
|
+ *
|
|
|
+ * 我们是**即时通知**,不需要毫秒级定时,所以传 false。
|
|
|
+ * 关掉后插件走 `AlarmManager.set()`(非精确),通知照样按时弹。
|
|
|
+ */
|
|
|
+ isExactNotification: false,
|
|
|
},
|
|
|
],
|
|
|
});
|