From 61e04723f3a44a75b02ca6e074a16310022f5a9b Mon Sep 17 00:00:00 2001 From: aria Date: Thu, 21 May 2026 13:27:31 +0900 Subject: [PATCH] updated JavaScript utils file to include new methods. created a unified flow to be an example. --- README.md | 16 ++-- example/unified-push-flow.js | 118 ++++++++++++++++++++++++++++ package.json | 1 + www/BluememeUtils.js | 148 +++++++++++++++++++++++++---------- 4 files changed, 232 insertions(+), 51 deletions(-) create mode 100644 example/unified-push-flow.js diff --git a/README.md b/README.md index 85365b0..2acf6d4 100644 --- a/README.md +++ b/README.md @@ -113,9 +113,13 @@ WiFi options: ```js BluememeUtils.requestPushPermission(success, error); BluememeUtils.getPushPermissionStatus(success, error); +BluememeUtils.ensurePushPermission(success, error); BluememeUtils.getFirebaseToken(function (result) { console.log(result.token); }, error); +BluememeUtils.getFirebaseTokenWithPermission(function (result) { + console.log(result.token); +}, error); BluememeUtils.subscribeToTopic("news", success, error); BluememeUtils.unsubscribeFromTopic("news", success, error); @@ -136,23 +140,19 @@ BluememeUtils.getInitialNotification(success, error); BluememeUtils.stopNotificationListener(success, error); ``` -`getPushPermissionStatus` 只读取当前系统通知状态,不弹授权框。可用于填充后端 `NotificationsEnabled`。iOS 返回 `authorizationStatus: "notDetermined"` 时,说明还没有触发系统通知授权弹窗,需要先调用 `requestPushPermission`,用户授权后再调用 `getFirebaseToken`。 +`getPushPermissionStatus` 只读取当前系统通知状态,不弹授权框。可用于填充后端 `NotificationsEnabled`。`ensurePushPermission` 会先读取权限状态;如果未授权,再调用系统权限请求;授权成功后才继续。`getFirebaseTokenWithPermission` 会按同一套流程先确认通知权限,再调用 `getFirebaseToken`。 -iOS 获取 FCM token 的推荐顺序: +Android 和 iOS 获取 FCM token 都建议使用同一套顺序: ```js -BluememeUtils.requestPushPermission(function (permission) { - if (permission.enabled !== true) { - return; - } - +BluememeUtils.ensurePushPermission(function () { BluememeUtils.getFirebaseToken(function (result) { console.log(result.token); }, error); }, error); ``` -iOS 上 FCM token 依赖 APNs device token。插件会在 `getFirebaseToken` 内等待 Cordova 收到 APNs token 后再向 Firebase 请求 FCM token;如果 APNs token 超时或注册失败,会返回 `APNS_TOKEN_NOT_AVAILABLE` 或 `APNS_TOKEN_FAILED`。 +完整示例流程见 `example/unified-push-flow.js`。iOS 上 FCM token 依赖 APNs device token。插件会在 `getFirebaseToken` 内等待 Cordova 收到 APNs token 后再向 Firebase 请求 FCM token;如果 APNs token 超时或注册失败,会返回 `APNS_TOKEN_NOT_AVAILABLE` 或 `APNS_TOKEN_FAILED`。 返回示例: diff --git a/example/unified-push-flow.js b/example/unified-push-flow.js new file mode 100644 index 0000000..a87fbb7 --- /dev/null +++ b/example/unified-push-flow.js @@ -0,0 +1,118 @@ +'use strict'; + +(function (root) { + function getBluememeUtils() { + var cordovaPlugin = root.cordova + && root.cordova.plugins + && root.cordova.plugins.bluememeUtils; + var plugin = root.BluememeUtils || cordovaPlugin; + + if (!plugin) { + throw new Error('BluememeUtils is not available. Run this flow after deviceready.'); + } + return plugin; + } + + function toPromise(work) { + return new Promise(function (resolve, reject) { + work(resolve, reject); + }); + } + + function call(methodName) { + var args = Array.prototype.slice.call(arguments, 1); + + return toPromise(function (resolve, reject) { + var plugin = getBluememeUtils(); + plugin[methodName].apply(plugin, args.concat(resolve, reject)); + }); + } + + function isPushPermissionGranted(permission) { + return !!permission && (permission.enabled === true || permission.granted === true); + } + + function permissionRequiredError(permission) { + return { + code: 'PERMISSION_REQUIRED', + message: 'Notification permission is required before Firebase messaging setup.', + permission: permission || null + }; + } + + function ensurePushPermission() { + return call('getPushPermissionStatus').then(function (status) { + if (isPushPermissionGranted(status)) { + return status; + } + + return call('requestPushPermission').then(function () { + return call('getPushPermissionStatus'); + }).then(function (updatedStatus) { + if (isPushPermissionGranted(updatedStatus)) { + return updatedStatus; + } + throw permissionRequiredError(updatedStatus); + }); + }); + } + + function subscribeTopics(topics) { + return (topics || []).reduce(function (chain, topic) { + return chain.then(function (results) { + return call('subscribeToTopic', topic).then(function () { + results.push(topic); + return results; + }); + }); + }, Promise.resolve([])); + } + + function startListener(onNotificationEvent, onError) { + if (typeof onNotificationEvent !== 'function') { + return; + } + + getBluememeUtils().startNotificationListener(onNotificationEvent, onError); + } + + function setupFirebaseMessaging(options, success, error) { + options = options || {}; + + var flow = ensurePushPermission() + .then(function (permission) { + startListener(options.onNotificationEvent, error || options.onError); + return call('getFirebaseToken').then(function (tokenResult) { + return { + permission: permission, + token: tokenResult && tokenResult.token, + tokenResult: tokenResult + }; + }); + }) + .then(function (context) { + return subscribeTopics(options.topics).then(function (topics) { + context.topics = topics; + return context; + }); + }) + .then(function (context) { + return call('getInitialNotification').then(function (initialNotification) { + context.initialNotification = initialNotification || null; + return context; + }); + }); + + if (typeof success === 'function' || typeof error === 'function') { + flow.then(success, error); + } + + return flow; + } + + root.BluememePushFlow = { + ensurePushPermission: ensurePushPermission, + isPushPermissionGranted: isPushPermissionGranted, + setupFirebaseMessaging: setupFirebaseMessaging + }; +})(window); diff --git a/package.json b/package.json index 2f39926..b094884 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "license": "MIT", "files": [ "plugin.xml", + "example/", "src/", "www/", "README.md", diff --git a/www/BluememeUtils.js b/www/BluememeUtils.js index e33a59c..f6d9e49 100644 --- a/www/BluememeUtils.js +++ b/www/BluememeUtils.js @@ -1,11 +1,21 @@ 'use strict'; var exec = require('cordova/exec'); + var SERVICE = 'BluememeUtils'; -var eventHandlers = {}; +var EVENT_PREFIX = 'bluememeutils.'; +var eventHandlers = Object.create(null); function noop() {} +function isFunction(value) { + return typeof value === 'function'; +} + +function callbackOrNoop(callback) { + return isFunction(callback) ? callback : noop; +} + function asWifiOptions(input) { if (typeof input === 'string') { return { ssid: input }; @@ -14,11 +24,35 @@ function asWifiOptions(input) { } function call(action, args, success, error) { - exec(success || noop, error || noop, SERVICE, action, args || []); + exec(callbackOrNoop(success), callbackOrNoop(error), SERVICE, action, args || []); +} + +function noArgs(action) { + return function (success, error) { + call(action, [], success, error); + }; +} + +function oneArg(action, normalize) { + return function (value, success, error) { + call(action, [normalize ? normalize(value) : value], success, error); + }; +} + +function isPermissionGranted(permission) { + return !!permission && (permission.enabled === true || permission.granted === true); +} + +function permissionError(type, permission) { + return { + code: 'PERMISSION_REQUIRED', + message: type + ' permission is required.', + permission: permission || null + }; } function createCustomEvent(name, detail) { - var eventName = 'bluememeutils.' + name; + var eventName = EVENT_PREFIX + name; if (typeof window.CustomEvent === 'function') { return new window.CustomEvent(eventName, { detail: detail }); @@ -36,7 +70,13 @@ function dispatchLocalEvent(event) { var handlers = eventHandlers[event.type] || []; handlers.slice().forEach(function (handler) { - handler(event); + try { + handler(event); + } catch (ex) { + setTimeout(function () { + throw ex; + }, 0); + } }); if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function') { @@ -44,50 +84,70 @@ function dispatchLocalEvent(event) { } } +function ensurePushPermission(success, error) { + var onSuccess = callbackOrNoop(success); + var onError = callbackOrNoop(error); + + api.getPushPermissionStatus(function (status) { + if (isPermissionGranted(status)) { + onSuccess(status); + return; + } + + api.requestPushPermission(function () { + api.getPushPermissionStatus(function (updatedStatus) { + if (isPermissionGranted(updatedStatus)) { + onSuccess(updatedStatus); + return; + } + onError(permissionError('notification', updatedStatus)); + }, onError); + }, onError); + }, onError); +} + +function getFirebaseTokenWithPermission(success, error) { + var onSuccess = callbackOrNoop(success); + var onError = callbackOrNoop(error); + + ensurePushPermission(function (permission) { + api.getFirebaseToken(function (result) { + if (result && typeof result === 'object') { + result.permission = permission; + } + onSuccess(result); + }, onError); + }, onError); +} + var api = { - getDeviceInfo: function (success, error) { - call('getDeviceInfo', [], success, error); - }, + getDeviceInfo: noArgs('getDeviceInfo'), - saveWifi: function (options, success, error) { - call('saveWifi', [asWifiOptions(options)], success, error); - }, + saveWifi: oneArg('saveWifi', asWifiOptions), - connectWifi: function (options, success, error) { - call('connectWifi', [asWifiOptions(options)], success, error); - }, + connectWifi: oneArg('connectWifi', asWifiOptions), - disconnectWifi: function (success, error) { - call('disconnectWifi', [], success, error); - }, + disconnectWifi: noArgs('disconnectWifi'), - removeWifi: function (options, success, error) { - call('removeWifi', [asWifiOptions(options)], success, error); - }, + removeWifi: oneArg('removeWifi', asWifiOptions), - requestWifiPermission: function (success, error) { - call('requestWifiPermission', [], success, error); - }, + requestWifiPermission: noArgs('requestWifiPermission'), - isBiometricAvailable: function (success, error) { - call('isBiometricAvailable', [], success, error); - }, + isBiometricAvailable: noArgs('isBiometricAvailable'), - unlockWithBiometrics: function (options, success, error) { - call('unlockWithBiometrics', [options || {}], success, error); - }, + unlockWithBiometrics: oneArg('unlockWithBiometrics', function (options) { + return options || {}; + }), - requestPushPermission: function (success, error) { - call('requestPushPermission', [], success, error); - }, + requestPushPermission: noArgs('requestPushPermission'), - getPushPermissionStatus: function (success, error) { - call('getPushPermissionStatus', [], success, error); - }, + getPushPermissionStatus: noArgs('getPushPermissionStatus'), - getFirebaseToken: function (success, error) { - call('getFirebaseToken', [], success, error); - }, + ensurePushPermission: ensurePushPermission, + + getFirebaseToken: noArgs('getFirebaseToken'), + + getFirebaseTokenWithPermission: getFirebaseTokenWithPermission, subscribeToTopic: function (topic, success, error) { call('subscribeToTopic', [topic], success, error); @@ -97,9 +157,7 @@ var api = { call('unsubscribeFromTopic', [topic], success, error); }, - getInitialNotification: function (success, error) { - call('getInitialNotification', [], success, error); - }, + getInitialNotification: noArgs('getInitialNotification'), startNotificationListener: function (success, error) { call('startNotificationListener', [], function (event) { @@ -110,15 +168,19 @@ var api = { }, error); }, - stopNotificationListener: function (success, error) { - call('stopNotificationListener', [], success, error); - }, + stopNotificationListener: noArgs('stopNotificationListener'), on: function (type, handler) { + if (!isFunction(handler)) { + return noop; + } if (!eventHandlers[type]) { eventHandlers[type] = []; } eventHandlers[type].push(handler); + return function () { + api.off(type, handler); + }; }, off: function (type, handler) {