first commit
This commit is contained in:
commit
4140922530
10 files changed
+2362
No files matched your search
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Bluememe
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
# bluememe-utils
|
||||||
|
|
||||||
|
Cordova plugin for OutSystems 11 mobile apps.
|
||||||
|
|
||||||
|
功能:
|
||||||
|
|
||||||
|
- 保存或连接指定 WiFi 热点
|
||||||
|
- Firebase Cloud Messaging 推送通知
|
||||||
|
- Touch ID / Face ID / Android biometric 解锁,并把解锁结果通知给应用
|
||||||
|
|
||||||
|
插件 ID:`bluememe-utils`
|
||||||
|
|
||||||
|
JS 全局对象:
|
||||||
|
|
||||||
|
- `BluememeUtils`
|
||||||
|
- `cordova.plugins.bluememeUtils`
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
在 OutSystems 11 的 extensibility configuration 中添加插件仓库或本地包后,可使用下面的参数覆盖 Firebase 配置。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"plugin": {
|
||||||
|
"url": "bluememe-utils",
|
||||||
|
"variables": {
|
||||||
|
"ANDROID_FIREBASE_APP_ID": "1:000000000000:android:xxxxxxxxxxxxxxxx",
|
||||||
|
"ANDROID_FIREBASE_API_KEY": "AIza...",
|
||||||
|
"ANDROID_FIREBASE_PROJECT_ID": "your-firebase-project",
|
||||||
|
"ANDROID_FIREBASE_SENDER_ID": "000000000000",
|
||||||
|
"ANDROID_FIREBASE_STORAGE_BUCKET": "your-firebase-project.appspot.com",
|
||||||
|
"IOS_FIREBASE_GOOGLE_APP_ID": "1:000000000000:ios:xxxxxxxxxxxxxxxx",
|
||||||
|
"IOS_FIREBASE_API_KEY": "AIza...",
|
||||||
|
"IOS_FIREBASE_PROJECT_ID": "your-firebase-project",
|
||||||
|
"IOS_FIREBASE_GCM_SENDER_ID": "000000000000",
|
||||||
|
"IOS_FIREBASE_STORAGE_BUCKET": "your-firebase-project.appspot.com",
|
||||||
|
"IOS_FACE_ID_USAGE_DESCRIPTION": "Use Face ID to unlock this application."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以让宿主 App 提供并处理标准 Firebase 配置文件:
|
||||||
|
|
||||||
|
- Android:`google-services.json`
|
||||||
|
- iOS:`GoogleService-Info.plist`
|
||||||
|
|
||||||
|
如果宿主工程已经由 OutSystems、MABS 配置或其他插件初始化 Firebase,本插件会复用已有的 default Firebase app。否则建议使用上面的 plugin variables,让本插件在运行时手动初始化 Firebase。
|
||||||
|
|
||||||
|
## JavaScript API
|
||||||
|
|
||||||
|
所有方法都使用 Cordova callback 风格,适合在 OutSystems client action 中封装。
|
||||||
|
|
||||||
|
```js
|
||||||
|
BluememeUtils.requestWifiPermission(success, error);
|
||||||
|
|
||||||
|
BluememeUtils.saveWifi({
|
||||||
|
ssid: "MyHotspot",
|
||||||
|
password: "password123",
|
||||||
|
security: "wpa2"
|
||||||
|
}, success, error);
|
||||||
|
|
||||||
|
BluememeUtils.connectWifi({
|
||||||
|
ssid: "MyHotspot",
|
||||||
|
password: "password123",
|
||||||
|
security: "wpa2"
|
||||||
|
}, success, error);
|
||||||
|
|
||||||
|
BluememeUtils.removeWifi({ ssid: "MyHotspot", password: "password123", security: "wpa2" }, success, error);
|
||||||
|
BluememeUtils.disconnectWifi(success, error);
|
||||||
|
```
|
||||||
|
|
||||||
|
WiFi options:
|
||||||
|
|
||||||
|
- `ssid`:必填
|
||||||
|
- `password`:开放网络可省略
|
||||||
|
- `security`:`open`、`wep`、`wpa2`、`wpa3`,默认按是否存在 password 推断为 `open` 或 `wpa2`
|
||||||
|
- `hiddenSsid`:可选,Android 支持隐藏 SSID;iOS 当前实现忽略该字段
|
||||||
|
- `bssid`:可选,仅 Android 10+ 临时连接使用
|
||||||
|
- `joinOnce`:可选,仅 iOS;`connectWifi` 默认 `true`,`saveWifi` 默认 `false`
|
||||||
|
|
||||||
|
## Push Notifications
|
||||||
|
|
||||||
|
```js
|
||||||
|
BluememeUtils.requestPushPermission(success, error);
|
||||||
|
BluememeUtils.getFirebaseToken(function (result) {
|
||||||
|
console.log(result.token);
|
||||||
|
}, error);
|
||||||
|
|
||||||
|
BluememeUtils.subscribeToTopic("news", success, error);
|
||||||
|
BluememeUtils.unsubscribeFromTopic("news", success, error);
|
||||||
|
|
||||||
|
BluememeUtils.startNotificationListener(function (event) {
|
||||||
|
if (event.type === "token") {
|
||||||
|
console.log(event.token);
|
||||||
|
}
|
||||||
|
if (event.type === "notification") {
|
||||||
|
console.log(event.notification);
|
||||||
|
}
|
||||||
|
if (event.type === "notificationOpened") {
|
||||||
|
console.log(event.notification);
|
||||||
|
}
|
||||||
|
}, error);
|
||||||
|
|
||||||
|
BluememeUtils.getInitialNotification(success, error);
|
||||||
|
BluememeUtils.stopNotificationListener(success, error);
|
||||||
|
```
|
||||||
|
|
||||||
|
事件也会派发到 `window`:
|
||||||
|
|
||||||
|
- `bluememeutils.token`
|
||||||
|
- `bluememeutils.notification`
|
||||||
|
- `bluememeutils.notificationOpened`
|
||||||
|
- `bluememeutils.unlock`
|
||||||
|
|
||||||
|
也可以用插件内置监听器:
|
||||||
|
|
||||||
|
```js
|
||||||
|
BluememeUtils.on("unlock", function (event) {
|
||||||
|
console.log(event.unlocked);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Biometric Unlock
|
||||||
|
|
||||||
|
```js
|
||||||
|
BluememeUtils.isBiometricAvailable(success, error);
|
||||||
|
|
||||||
|
BluememeUtils.unlockWithBiometrics({
|
||||||
|
reason: "Unlock application",
|
||||||
|
title: "Unlock",
|
||||||
|
subtitle: "",
|
||||||
|
description: "",
|
||||||
|
cancelText: "Cancel",
|
||||||
|
fallbackTitle: "Use Passcode",
|
||||||
|
allowDeviceCredential: true
|
||||||
|
}, function (result) {
|
||||||
|
console.log(result.unlocked, result.biometryType);
|
||||||
|
}, error);
|
||||||
|
```
|
||||||
|
|
||||||
|
`biometryType` 可能值:
|
||||||
|
|
||||||
|
- iOS:`face`、`touch`、`none`、`biometric`
|
||||||
|
- Android:`face`、`fingerprint`、`iris`、`biometric`
|
||||||
|
|
||||||
|
## Platform Notes
|
||||||
|
|
||||||
|
Android WiFi:
|
||||||
|
|
||||||
|
- Android 10+ 不允许第三方应用静默保存并强制切换系统 WiFi。
|
||||||
|
- `saveWifi` 在 Android 10+ 使用 `WifiNetworkSuggestion`,系统可能要求用户批准。
|
||||||
|
- `connectWifi` 在 Android 10+ 使用 `WifiNetworkSpecifier`,这是临时连接,通常只绑定当前应用进程网络。
|
||||||
|
- Android 9 及以下使用旧 `WifiConfiguration` API,可保存并启用网络。
|
||||||
|
- 调用 WiFi API 前先调用 `requestWifiPermission`,并确保系统位置服务开启。
|
||||||
|
|
||||||
|
iOS WiFi:
|
||||||
|
|
||||||
|
- 使用 `NEHotspotConfiguration`。
|
||||||
|
- 必须在 Apple Developer 账号和 App entitlements 中开启 `Hotspot Configuration` capability。
|
||||||
|
- iOS 不提供第三方 App 断开当前 WiFi 的公开 API,因此 `disconnectWifi` 在 iOS 返回错误。
|
||||||
|
|
||||||
|
Firebase:
|
||||||
|
|
||||||
|
- Android 13+ 需要通知权限,调用 `requestPushPermission`。
|
||||||
|
- iOS 需要 Push Notifications capability、Background Modes 中的 Remote notifications,以及 APNs key/certificate 已配置到 Firebase。
|
||||||
|
- 如果项目中还有其他推送插件,也可能同时修改 `UNUserNotificationCenterDelegate` 或 Firebase service,需要在真机上确认通知打开事件。
|
||||||
|
|
||||||
|
Biometric:
|
||||||
|
|
||||||
|
- iOS Face ID 需要 `NSFaceIDUsageDescription`,可通过 `IOS_FACE_ID_USAGE_DESCRIPTION` 设置。
|
||||||
|
- Android 本插件使用平台 `BiometricPrompt`,`unlockWithBiometrics` 支持 Android 9+。
|
||||||
|
|
||||||
|
## Error Shape
|
||||||
|
|
||||||
|
错误 callback 返回对象:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "ERROR_CODE",
|
||||||
|
"message": "Human readable message"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
常见错误:
|
||||||
|
|
||||||
|
- `PERMISSION_REQUIRED`
|
||||||
|
- `INVALID_WIFI_OPTIONS`
|
||||||
|
- `WIFI_CONNECT_FAILED`
|
||||||
|
- `FIREBASE_NOT_CONFIGURED`
|
||||||
|
- `FCM_TOKEN_FAILED`
|
||||||
|
- `BIOMETRIC_UNAVAILABLE`
|
||||||
|
- `AUTHENTICATION_ERROR`
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "bluememe-utils",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Cordova utilities for WiFi, Firebase Cloud Messaging, and biometric unlock for OutSystems 11 apps.",
|
||||||
|
"license": "MIT",
|
||||||
|
"cordova": {
|
||||||
|
"id": "bluememe-utils",
|
||||||
|
"platforms": [
|
||||||
|
"android",
|
||||||
|
"ios"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"cordova",
|
||||||
|
"ecosystem:cordova",
|
||||||
|
"outsystems",
|
||||||
|
"wifi",
|
||||||
|
"firebase",
|
||||||
|
"fcm",
|
||||||
|
"touchid",
|
||||||
|
"faceid",
|
||||||
|
"biometric"
|
||||||
|
],
|
||||||
|
"engines": [
|
||||||
|
{
|
||||||
|
"name": "cordova",
|
||||||
|
"version": ">=9.0.0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
id="bluememe-utils"
|
||||||
|
version="0.1.0">
|
||||||
|
<name>bluememe-utils</name>
|
||||||
|
<description>Cordova utilities for WiFi, Firebase Cloud Messaging, and biometric unlock.</description>
|
||||||
|
<license>MIT</license>
|
||||||
|
<keywords>cordova,outsystems,wifi,firebase,fcm,touchid,faceid,biometric</keywords>
|
||||||
|
|
||||||
|
<js-module src="www/BluememeUtils.js" name="BluememeUtils">
|
||||||
|
<clobbers target="BluememeUtils" />
|
||||||
|
<clobbers target="cordova.plugins.bluememeUtils" />
|
||||||
|
</js-module>
|
||||||
|
|
||||||
|
<preference name="ANDROID_FIREBASE_MESSAGING_VERSION" default="24.1.1" />
|
||||||
|
<preference name="ANDROID_FIREBASE_APP_ID" default="" />
|
||||||
|
<preference name="ANDROID_FIREBASE_API_KEY" default="" />
|
||||||
|
<preference name="ANDROID_FIREBASE_PROJECT_ID" default="" />
|
||||||
|
<preference name="ANDROID_FIREBASE_SENDER_ID" default="" />
|
||||||
|
<preference name="ANDROID_FIREBASE_STORAGE_BUCKET" default="" />
|
||||||
|
|
||||||
|
<preference name="IOS_FIREBASE_MESSAGING_VERSION" default="~> 10.29.0" />
|
||||||
|
<preference name="IOS_FIREBASE_GOOGLE_APP_ID" default="" />
|
||||||
|
<preference name="IOS_FIREBASE_API_KEY" default="" />
|
||||||
|
<preference name="IOS_FIREBASE_PROJECT_ID" default="" />
|
||||||
|
<preference name="IOS_FIREBASE_GCM_SENDER_ID" default="" />
|
||||||
|
<preference name="IOS_FIREBASE_STORAGE_BUCKET" default="" />
|
||||||
|
<preference name="IOS_FACE_ID_USAGE_DESCRIPTION" default="Use Face ID to unlock this application." />
|
||||||
|
|
||||||
|
<platform name="android">
|
||||||
|
<config-file target="res/xml/config.xml" parent="/*">
|
||||||
|
<feature name="BluememeUtils">
|
||||||
|
<param name="android-package" value="com.bluememe.utils.BluememeUtils" />
|
||||||
|
</feature>
|
||||||
|
</config-file>
|
||||||
|
|
||||||
|
<config-file target="AndroidManifest.xml" parent="/manifest">
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
|
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||||
|
</config-file>
|
||||||
|
|
||||||
|
<config-file target="AndroidManifest.xml" parent="/manifest/application">
|
||||||
|
<service
|
||||||
|
android:name="com.bluememe.utils.BluememeMessagingService"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
<meta-data android:name="bluememe.firebase.app_id" android:value="$ANDROID_FIREBASE_APP_ID" />
|
||||||
|
<meta-data android:name="bluememe.firebase.api_key" android:value="$ANDROID_FIREBASE_API_KEY" />
|
||||||
|
<meta-data android:name="bluememe.firebase.project_id" android:value="$ANDROID_FIREBASE_PROJECT_ID" />
|
||||||
|
<meta-data android:name="bluememe.firebase.sender_id" android:value="$ANDROID_FIREBASE_SENDER_ID" />
|
||||||
|
<meta-data android:name="bluememe.firebase.storage_bucket" android:value="$ANDROID_FIREBASE_STORAGE_BUCKET" />
|
||||||
|
<meta-data android:name="com.google.firebase.messaging.default_notification_channel_id" android:value="bluememe_utils_notifications" />
|
||||||
|
</config-file>
|
||||||
|
|
||||||
|
<source-file src="src/android/BluememeUtils.java" target-dir="src/com/bluememe/utils" />
|
||||||
|
<source-file src="src/android/BluememeMessagingService.java" target-dir="src/com/bluememe/utils" />
|
||||||
|
<framework src="com.google.firebase:firebase-messaging:$ANDROID_FIREBASE_MESSAGING_VERSION" />
|
||||||
|
</platform>
|
||||||
|
|
||||||
|
<platform name="ios">
|
||||||
|
<config-file target="config.xml" parent="/*">
|
||||||
|
<feature name="BluememeUtils">
|
||||||
|
<param name="ios-package" value="CDVBluememeUtils" />
|
||||||
|
</feature>
|
||||||
|
</config-file>
|
||||||
|
|
||||||
|
<config-file target="*-Info.plist" parent="NSFaceIDUsageDescription">
|
||||||
|
<string>$IOS_FACE_ID_USAGE_DESCRIPTION</string>
|
||||||
|
</config-file>
|
||||||
|
<config-file target="*-Info.plist" parent="BLUEMEME_FIREBASE_GOOGLE_APP_ID">
|
||||||
|
<string>$IOS_FIREBASE_GOOGLE_APP_ID</string>
|
||||||
|
</config-file>
|
||||||
|
<config-file target="*-Info.plist" parent="BLUEMEME_FIREBASE_API_KEY">
|
||||||
|
<string>$IOS_FIREBASE_API_KEY</string>
|
||||||
|
</config-file>
|
||||||
|
<config-file target="*-Info.plist" parent="BLUEMEME_FIREBASE_PROJECT_ID">
|
||||||
|
<string>$IOS_FIREBASE_PROJECT_ID</string>
|
||||||
|
</config-file>
|
||||||
|
<config-file target="*-Info.plist" parent="BLUEMEME_FIREBASE_GCM_SENDER_ID">
|
||||||
|
<string>$IOS_FIREBASE_GCM_SENDER_ID</string>
|
||||||
|
</config-file>
|
||||||
|
<config-file target="*-Info.plist" parent="BLUEMEME_FIREBASE_STORAGE_BUCKET">
|
||||||
|
<string>$IOS_FIREBASE_STORAGE_BUCKET</string>
|
||||||
|
</config-file>
|
||||||
|
|
||||||
|
<header-file src="src/ios/CDVBluememeUtils.h" />
|
||||||
|
<source-file src="src/ios/CDVBluememeUtils.m" />
|
||||||
|
<framework src="NetworkExtension.framework" />
|
||||||
|
<framework src="LocalAuthentication.framework" />
|
||||||
|
<framework src="UserNotifications.framework" />
|
||||||
|
<framework src="Firebase/Messaging" type="podspec" spec="$IOS_FIREBASE_MESSAGING_VERSION" />
|
||||||
|
</platform>
|
||||||
|
</plugin>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[project]
|
||||||
|
name = "hakuzen"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.14"
|
||||||
|
dependencies = []
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
package com.bluememe.utils;
|
||||||
|
|
||||||
|
import android.app.ActivityManager;
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationChannel;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.firebase.messaging.FirebaseMessagingService;
|
||||||
|
import com.google.firebase.messaging.RemoteMessage;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class BluememeMessagingService extends FirebaseMessagingService {
|
||||||
|
private static final String TAG = "BluememeMessaging";
|
||||||
|
private static final String CHANNEL_ID = "bluememe_utils_notifications";
|
||||||
|
private static final String EXTRA_PAYLOAD = "bluememe_payload";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNewToken(String token) {
|
||||||
|
super.onNewToken(token);
|
||||||
|
BluememeUtils.saveToken(this, token);
|
||||||
|
try {
|
||||||
|
JSONObject event = new JSONObject();
|
||||||
|
event.put("type", "token");
|
||||||
|
event.put("token", token);
|
||||||
|
BluememeUtils.publishEvent(this, event);
|
||||||
|
} catch (JSONException ex) {
|
||||||
|
Log.w(TAG, "Unable to publish token event.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessageReceived(RemoteMessage remoteMessage) {
|
||||||
|
super.onMessageReceived(remoteMessage);
|
||||||
|
JSONObject notification = remoteMessageToJson(remoteMessage);
|
||||||
|
try {
|
||||||
|
JSONObject event = new JSONObject();
|
||||||
|
event.put("type", "notification");
|
||||||
|
event.put("notification", notification);
|
||||||
|
BluememeUtils.publishEvent(this, event);
|
||||||
|
} catch (JSONException ex) {
|
||||||
|
Log.w(TAG, "Unable to publish notification event.", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAppInForeground() && shouldShowNotification(notification)) {
|
||||||
|
showNotification(notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject remoteMessageToJson(RemoteMessage message) {
|
||||||
|
JSONObject json = new JSONObject();
|
||||||
|
try {
|
||||||
|
json.put("messageId", message.getMessageId());
|
||||||
|
json.put("from", message.getFrom());
|
||||||
|
json.put("collapseKey", message.getCollapseKey());
|
||||||
|
json.put("sentTime", message.getSentTime());
|
||||||
|
|
||||||
|
JSONObject data = new JSONObject();
|
||||||
|
for (Map.Entry<String, String> entry : message.getData().entrySet()) {
|
||||||
|
data.put(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
json.put("data", data);
|
||||||
|
|
||||||
|
RemoteMessage.Notification remoteNotification = message.getNotification();
|
||||||
|
if (remoteNotification != null) {
|
||||||
|
JSONObject notification = new JSONObject();
|
||||||
|
notification.put("title", remoteNotification.getTitle());
|
||||||
|
notification.put("body", remoteNotification.getBody());
|
||||||
|
notification.put("tag", remoteNotification.getTag());
|
||||||
|
notification.put("clickAction", remoteNotification.getClickAction());
|
||||||
|
notification.put("channelId", remoteNotification.getChannelId());
|
||||||
|
if (remoteNotification.getImageUrl() != null) {
|
||||||
|
notification.put("imageUrl", remoteNotification.getImageUrl().toString());
|
||||||
|
}
|
||||||
|
json.put("notification", notification);
|
||||||
|
}
|
||||||
|
} catch (JSONException ex) {
|
||||||
|
Log.w(TAG, "Unable to convert RemoteMessage to JSON.", ex);
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldShowNotification(JSONObject payload) {
|
||||||
|
JSONObject notification = payload.optJSONObject("notification");
|
||||||
|
if (notification != null) {
|
||||||
|
return !TextUtils.isEmpty(notification.optString("title", ""))
|
||||||
|
|| !TextUtils.isEmpty(notification.optString("body", ""));
|
||||||
|
}
|
||||||
|
JSONObject data = payload.optJSONObject("data");
|
||||||
|
return data != null
|
||||||
|
&& (!TextUtils.isEmpty(data.optString("title", ""))
|
||||||
|
|| !TextUtils.isEmpty(data.optString("body", "")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showNotification(JSONObject payload) {
|
||||||
|
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
if (notificationManager == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createNotificationChannel(notificationManager);
|
||||||
|
|
||||||
|
String title = valueFromPayload(payload, "title", appLabel());
|
||||||
|
String body = valueFromPayload(payload, "body", "");
|
||||||
|
Intent launchIntent = getPackageManager().getLaunchIntentForPackage(getPackageName());
|
||||||
|
if (launchIntent == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
|
launchIntent.putExtra(EXTRA_PAYLOAD, payload.toString());
|
||||||
|
|
||||||
|
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
flags |= PendingIntent.FLAG_IMMUTABLE;
|
||||||
|
}
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(this, payload.toString().hashCode(), launchIntent, flags);
|
||||||
|
|
||||||
|
int icon = getApplicationInfo().icon == 0 ? android.R.drawable.ic_dialog_info : getApplicationInfo().icon;
|
||||||
|
Notification.Builder builder = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||||
|
? new Notification.Builder(this, CHANNEL_ID)
|
||||||
|
: new Notification.Builder(this);
|
||||||
|
|
||||||
|
builder.setSmallIcon(icon)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(body)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setShowWhen(true);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
|
builder.setStyle(new Notification.BigTextStyle().bigText(body));
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationManager.notify(payload.toString().hashCode(), builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String valueFromPayload(JSONObject payload, String key, String defaultValue) {
|
||||||
|
JSONObject notification = payload.optJSONObject("notification");
|
||||||
|
if (notification != null && !TextUtils.isEmpty(notification.optString(key, ""))) {
|
||||||
|
return notification.optString(key);
|
||||||
|
}
|
||||||
|
JSONObject data = payload.optJSONObject("data");
|
||||||
|
if (data != null && !TextUtils.isEmpty(data.optString(key, ""))) {
|
||||||
|
return data.optString(key);
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String appLabel() {
|
||||||
|
ApplicationInfo applicationInfo = getApplicationInfo();
|
||||||
|
CharSequence label = getPackageManager().getApplicationLabel(applicationInfo);
|
||||||
|
return label == null ? getPackageName() : label.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createNotificationChannel(NotificationManager notificationManager) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NotificationChannel channel = new NotificationChannel(
|
||||||
|
CHANNEL_ID,
|
||||||
|
"Application notifications",
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
);
|
||||||
|
notificationManager.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAppInForeground() {
|
||||||
|
ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
|
||||||
|
if (activityManager == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
List<ActivityManager.RunningAppProcessInfo> processes = activityManager.getRunningAppProcesses();
|
||||||
|
if (processes == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String packageName = getPackageName();
|
||||||
|
for (ActivityManager.RunningAppProcessInfo process : processes) {
|
||||||
|
if (process.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
|
||||||
|
&& packageName.equals(process.processName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,6 @@
|
|||||||
|
#import <Cordova/CDV.h>
|
||||||
|
#import <FirebaseMessaging/FirebaseMessaging.h>
|
||||||
|
#import <UserNotifications/UserNotifications.h>
|
||||||
|
|
||||||
|
@interface CDVBluememeUtils : CDVPlugin <FIRMessagingDelegate, UNUserNotificationCenterDelegate>
|
||||||
|
@end
|
||||||
@@ -0,0 +1,490 @@
|
|||||||
|
#import "CDVBluememeUtils.h"
|
||||||
|
|
||||||
|
#import <FirebaseCore/FirebaseCore.h>
|
||||||
|
#import <LocalAuthentication/LocalAuthentication.h>
|
||||||
|
#import <NetworkExtension/NetworkExtension.h>
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
@interface CDVBluememeUtils ()
|
||||||
|
@property (nonatomic, copy) NSString *notificationCallbackId;
|
||||||
|
@property (nonatomic, strong) NSDictionary *initialNotification;
|
||||||
|
@property (nonatomic, weak) id<UNUserNotificationCenterDelegate> previousNotificationDelegate;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation CDVBluememeUtils
|
||||||
|
|
||||||
|
- (void)pluginInitialize
|
||||||
|
{
|
||||||
|
if ([self ensureFirebaseConfigured:nil]) {
|
||||||
|
[FIRMessaging messaging].delegate = self;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@available(iOS 10.0, *)) {
|
||||||
|
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||||
|
self.previousNotificationDelegate = center.delegate;
|
||||||
|
center.delegate = self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)saveWifi:(CDVInvokedUrlCommand *)command
|
||||||
|
{
|
||||||
|
[self applyWifiCommand:command defaultJoinOnce:NO];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)connectWifi:(CDVInvokedUrlCommand *)command
|
||||||
|
{
|
||||||
|
[self applyWifiCommand:command defaultJoinOnce:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)removeWifi:(CDVInvokedUrlCommand *)command
|
||||||
|
{
|
||||||
|
NSDictionary *options = [self firstDictionaryArgument:command];
|
||||||
|
NSString *ssid = [self stringFromDictionary:options key:@"ssid"];
|
||||||
|
if (ssid.length == 0) {
|
||||||
|
[self sendErrorForCommand:command code:@"INVALID_WIFI_OPTIONS" message:@"ssid is required."];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@available(iOS 11.0, *)) {
|
||||||
|
[[NEHotspotConfigurationManager sharedManager] removeConfigurationForSSID:ssid];
|
||||||
|
[self sendSuccessForCommand:command dictionary:@{@"ssid": ssid, @"removed": @YES}];
|
||||||
|
} else {
|
||||||
|
[self sendErrorForCommand:command code:@"UNSUPPORTED_IOS_VERSION" message:@"WiFi configuration requires iOS 11 or newer."];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)disconnectWifi:(CDVInvokedUrlCommand *)command
|
||||||
|
{
|
||||||
|
[self sendErrorForCommand:command code:@"UNSUPPORTED_PLATFORM_ACTION" message:@"iOS does not expose a public API for disconnecting WiFi from third-party apps."];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)requestWifiPermission:(CDVInvokedUrlCommand *)command
|
||||||
|
{
|
||||||
|
[self sendSuccessForCommand:command dictionary:@{@"type": @"wifi", @"granted": @YES, @"missing": @[]}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)isBiometricAvailable:(CDVInvokedUrlCommand *)command
|
||||||
|
{
|
||||||
|
[self sendSuccessForCommand:command dictionary:[self biometricAvailability]];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)unlockWithBiometrics:(CDVInvokedUrlCommand *)command
|
||||||
|
{
|
||||||
|
NSDictionary *options = [self firstDictionaryArgument:command];
|
||||||
|
LAContext *context = [[LAContext alloc] init];
|
||||||
|
NSString *fallbackTitle = [self stringFromDictionary:options key:@"fallbackTitle"];
|
||||||
|
NSString *cancelTitle = [self stringFromDictionary:options key:@"cancelTitle"];
|
||||||
|
if (fallbackTitle.length > 0) {
|
||||||
|
context.localizedFallbackTitle = fallbackTitle;
|
||||||
|
}
|
||||||
|
if (cancelTitle.length > 0) {
|
||||||
|
context.localizedCancelTitle = cancelTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL allowPasscode = [self boolFromDictionary:options key:@"allowDeviceCredential" defaultValue:YES];
|
||||||
|
LAPolicy policy = allowPasscode ? LAPolicyDeviceOwnerAuthentication : LAPolicyDeviceOwnerAuthenticationWithBiometrics;
|
||||||
|
NSError *availabilityError = nil;
|
||||||
|
if (![context canEvaluatePolicy:policy error:&availabilityError]) {
|
||||||
|
[self sendErrorForCommand:command code:@"BIOMETRIC_UNAVAILABLE" message:(availabilityError.localizedDescription ? availabilityError.localizedDescription : @"Biometric authentication is unavailable.")];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *reason = [self stringFromDictionary:options key:@"reason"];
|
||||||
|
if (reason.length == 0) {
|
||||||
|
reason = @"Unlock";
|
||||||
|
}
|
||||||
|
|
||||||
|
[context evaluatePolicy:policy localizedReason:reason reply:^(BOOL success, NSError *error) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
if (success) {
|
||||||
|
NSDictionary *result = @{@"unlocked": @YES, @"biometryType": [self biometryTypeForContext:context]};
|
||||||
|
[self sendSuccessForCommand:command dictionary:result];
|
||||||
|
NSMutableDictionary *event = [result mutableCopy];
|
||||||
|
event[@"type"] = @"unlock";
|
||||||
|
[self sendEvent:event];
|
||||||
|
} else {
|
||||||
|
[self sendErrorForCommand:command code:@"AUTHENTICATION_ERROR" message:(error.localizedDescription ? error.localizedDescription : @"Authentication failed.")];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)requestPushPermission:(CDVInvokedUrlCommand *)command
|
||||||
|
{
|
||||||
|
NSError *firebaseError = nil;
|
||||||
|
if (![self ensureFirebaseConfigured:&firebaseError]) {
|
||||||
|
[self sendErrorForCommand:command code:@"FIREBASE_NOT_CONFIGURED" message:(firebaseError.localizedDescription ? firebaseError.localizedDescription : @"Firebase is not configured.")];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[FIRMessaging messaging].delegate = self;
|
||||||
|
|
||||||
|
if (@available(iOS 10.0, *)) {
|
||||||
|
UNAuthorizationOptions options = UNAuthorizationOptionAlert | UNAuthorizationOptionBadge | UNAuthorizationOptionSound;
|
||||||
|
[[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError *error) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
if (error != nil) {
|
||||||
|
[self sendErrorForCommand:command code:@"PUSH_PERMISSION_FAILED" message:error.localizedDescription];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[[UIApplication sharedApplication] registerForRemoteNotifications];
|
||||||
|
[self sendSuccessForCommand:command dictionary:@{@"type": @"notification", @"granted": @(granted), @"missing": granted ? @[] : @[@"notification"]}];
|
||||||
|
});
|
||||||
|
}];
|
||||||
|
} else {
|
||||||
|
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound) categories:nil];
|
||||||
|
[[UIApplication sharedApplication] registerUserNotificationSettings:settings];
|
||||||
|
[[UIApplication sharedApplication] registerForRemoteNotifications];
|
||||||
|
[self sendSuccessForCommand:command dictionary:@{@"type": @"notification", @"granted": @YES, @"missing": @[]}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)getFirebaseToken:(CDVInvokedUrlCommand *)command
|
||||||
|
{
|
||||||
|
NSError *firebaseError = nil;
|
||||||
|
if (![self ensureFirebaseConfigured:&firebaseError]) {
|
||||||
|
[self sendErrorForCommand:command code:@"FIREBASE_NOT_CONFIGURED" message:(firebaseError.localizedDescription ? firebaseError.localizedDescription : @"Firebase is not configured.")];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[FIRMessaging messaging].delegate = self;
|
||||||
|
|
||||||
|
[[FIRMessaging messaging] tokenWithCompletion:^(NSString *token, NSError *error) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
if (error != nil || token.length == 0) {
|
||||||
|
[self sendErrorForCommand:command code:@"FCM_TOKEN_FAILED" message:(error.localizedDescription ? error.localizedDescription : @"Failed to get Firebase token.")];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[self sendSuccessForCommand:command dictionary:@{@"token": token}];
|
||||||
|
[self sendEvent:@{@"type": @"token", @"token": token}];
|
||||||
|
});
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)subscribeToTopic:(CDVInvokedUrlCommand *)command
|
||||||
|
{
|
||||||
|
NSString *topic = [command.arguments firstObject];
|
||||||
|
if (![topic isKindOfClass:[NSString class]] || topic.length == 0) {
|
||||||
|
[self sendErrorForCommand:command code:@"INVALID_TOPIC" message:@"topic is required."];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSError *firebaseError = nil;
|
||||||
|
if (![self ensureFirebaseConfigured:&firebaseError]) {
|
||||||
|
[self sendErrorForCommand:command code:@"FIREBASE_NOT_CONFIGURED" message:(firebaseError.localizedDescription ? firebaseError.localizedDescription : @"Firebase is not configured.")];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[[FIRMessaging messaging] subscribeToTopic:topic completion:^(NSError *error) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
if (error != nil) {
|
||||||
|
[self sendErrorForCommand:command code:@"FIREBASE_TASK_FAILED" message:error.localizedDescription];
|
||||||
|
} else {
|
||||||
|
[self sendSuccessForCommand:command dictionary:nil];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)unsubscribeFromTopic:(CDVInvokedUrlCommand *)command
|
||||||
|
{
|
||||||
|
NSString *topic = [command.arguments firstObject];
|
||||||
|
if (![topic isKindOfClass:[NSString class]] || topic.length == 0) {
|
||||||
|
[self sendErrorForCommand:command code:@"INVALID_TOPIC" message:@"topic is required."];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSError *firebaseError = nil;
|
||||||
|
if (![self ensureFirebaseConfigured:&firebaseError]) {
|
||||||
|
[self sendErrorForCommand:command code:@"FIREBASE_NOT_CONFIGURED" message:(firebaseError.localizedDescription ? firebaseError.localizedDescription : @"Firebase is not configured.")];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[[FIRMessaging messaging] unsubscribeFromTopic:topic completion:^(NSError *error) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
if (error != nil) {
|
||||||
|
[self sendErrorForCommand:command code:@"FIREBASE_TASK_FAILED" message:error.localizedDescription];
|
||||||
|
} else {
|
||||||
|
[self sendSuccessForCommand:command dictionary:nil];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)getInitialNotification:(CDVInvokedUrlCommand *)command
|
||||||
|
{
|
||||||
|
[self sendSuccessForCommand:command dictionary:self.initialNotification];
|
||||||
|
self.initialNotification = nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)startNotificationListener:(CDVInvokedUrlCommand *)command
|
||||||
|
{
|
||||||
|
self.notificationCallbackId = command.callbackId;
|
||||||
|
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_NO_RESULT];
|
||||||
|
[result setKeepCallbackAsBool:YES];
|
||||||
|
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)stopNotificationListener:(CDVInvokedUrlCommand *)command
|
||||||
|
{
|
||||||
|
self.notificationCallbackId = nil;
|
||||||
|
[self sendSuccessForCommand:command dictionary:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)applyWifiCommand:(CDVInvokedUrlCommand *)command defaultJoinOnce:(BOOL)defaultJoinOnce
|
||||||
|
{
|
||||||
|
NSDictionary *options = [self firstDictionaryArgument:command];
|
||||||
|
NSString *ssid = [self stringFromDictionary:options key:@"ssid"];
|
||||||
|
NSString *password = [self stringFromDictionary:options key:@"password"];
|
||||||
|
NSString *security = [[self stringFromDictionary:options key:@"security"] lowercaseString];
|
||||||
|
BOOL isWep = [security isEqualToString:@"wep"] || [self boolFromDictionary:options key:@"isWep" defaultValue:NO];
|
||||||
|
BOOL joinOnce = [self boolFromDictionary:options key:@"joinOnce" defaultValue:defaultJoinOnce];
|
||||||
|
|
||||||
|
if (ssid.length == 0) {
|
||||||
|
[self sendErrorForCommand:command code:@"INVALID_WIFI_OPTIONS" message:@"ssid is required."];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@available(iOS 11.0, *)) {
|
||||||
|
NEHotspotConfiguration *configuration = nil;
|
||||||
|
if (password.length == 0 || [security isEqualToString:@"open"]) {
|
||||||
|
configuration = [[NEHotspotConfiguration alloc] initWithSSID:ssid];
|
||||||
|
} else {
|
||||||
|
configuration = [[NEHotspotConfiguration alloc] initWithSSID:ssid passphrase:password isWEP:isWep];
|
||||||
|
}
|
||||||
|
configuration.joinOnce = joinOnce;
|
||||||
|
|
||||||
|
[[NEHotspotConfigurationManager sharedManager] applyConfiguration:configuration completionHandler:^(NSError *error) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
if (error != nil && error.code != NEHotspotConfigurationErrorAlreadyAssociated) {
|
||||||
|
[self sendErrorForCommand:command code:@"WIFI_CONNECT_FAILED" message:(error.localizedDescription ? error.localizedDescription : @"Unable to apply WiFi configuration.")];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSDictionary *result = @{
|
||||||
|
@"ssid": ssid,
|
||||||
|
@"platformMode": @"hotspotConfiguration",
|
||||||
|
@"connected": @YES,
|
||||||
|
@"saved": @(!joinOnce),
|
||||||
|
@"alreadyAssociated": @(error != nil && error.code == NEHotspotConfigurationErrorAlreadyAssociated)
|
||||||
|
};
|
||||||
|
[self sendSuccessForCommand:command dictionary:result];
|
||||||
|
});
|
||||||
|
}];
|
||||||
|
} else {
|
||||||
|
[self sendErrorForCommand:command code:@"UNSUPPORTED_IOS_VERSION" message:@"WiFi configuration requires iOS 11 or newer."];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSDictionary *)biometricAvailability
|
||||||
|
{
|
||||||
|
LAContext *context = [[LAContext alloc] init];
|
||||||
|
NSError *error = nil;
|
||||||
|
BOOL available = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error];
|
||||||
|
NSString *type = [self biometryTypeForContext:context];
|
||||||
|
if (available) {
|
||||||
|
return @{@"available": @YES, @"biometryType": type, @"code": @"OK", @"message": @"Biometric authentication is available."};
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL passcodeAvailable = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:nil];
|
||||||
|
return @{
|
||||||
|
@"available": @NO,
|
||||||
|
@"biometryType": type,
|
||||||
|
@"passcodeAvailable": @(passcodeAvailable),
|
||||||
|
@"code": @"BIOMETRIC_UNAVAILABLE",
|
||||||
|
@"message": error.localizedDescription ? error.localizedDescription : @"Biometric authentication is unavailable."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)biometryTypeForContext:(LAContext *)context
|
||||||
|
{
|
||||||
|
if (@available(iOS 11.0, *)) {
|
||||||
|
if (context.biometryType == LABiometryTypeFaceID) {
|
||||||
|
return @"face";
|
||||||
|
}
|
||||||
|
if (context.biometryType == LABiometryTypeTouchID) {
|
||||||
|
return @"touch";
|
||||||
|
}
|
||||||
|
if (@available(iOS 11.2, *)) {
|
||||||
|
if (context.biometryType == LABiometryTypeNone) {
|
||||||
|
return @"none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return @"biometric";
|
||||||
|
}
|
||||||
|
return @"touch";
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)ensureFirebaseConfigured:(NSError **)error
|
||||||
|
{
|
||||||
|
if ([FIRApp defaultApp] != nil) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *plistPath = [[NSBundle mainBundle] pathForResource:@"GoogleService-Info" ofType:@"plist"];
|
||||||
|
if (plistPath.length > 0) {
|
||||||
|
[FIRApp configure];
|
||||||
|
return [FIRApp defaultApp] != nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *googleAppId = [self infoStringForKey:@"BLUEMEME_FIREBASE_GOOGLE_APP_ID"];
|
||||||
|
NSString *gcmSenderId = [self infoStringForKey:@"BLUEMEME_FIREBASE_GCM_SENDER_ID"];
|
||||||
|
NSString *apiKey = [self infoStringForKey:@"BLUEMEME_FIREBASE_API_KEY"];
|
||||||
|
NSString *projectId = [self infoStringForKey:@"BLUEMEME_FIREBASE_PROJECT_ID"];
|
||||||
|
NSString *storageBucket = [self infoStringForKey:@"BLUEMEME_FIREBASE_STORAGE_BUCKET"];
|
||||||
|
|
||||||
|
if (googleAppId.length == 0 || gcmSenderId.length == 0) {
|
||||||
|
if (error != nil) {
|
||||||
|
*error = [NSError errorWithDomain:@"BluememeUtils" code:1001 userInfo:@{NSLocalizedDescriptionKey: @"Firebase options are missing."}];
|
||||||
|
}
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:googleAppId GCMSenderID:gcmSenderId];
|
||||||
|
if (apiKey.length > 0) {
|
||||||
|
options.APIKey = apiKey;
|
||||||
|
}
|
||||||
|
if (projectId.length > 0) {
|
||||||
|
options.projectID = projectId;
|
||||||
|
}
|
||||||
|
if (storageBucket.length > 0) {
|
||||||
|
options.storageBucket = storageBucket;
|
||||||
|
}
|
||||||
|
options.bundleID = [[NSBundle mainBundle] bundleIdentifier];
|
||||||
|
[FIRApp configureWithOptions:options];
|
||||||
|
return [FIRApp defaultApp] != nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)messaging:(FIRMessaging *)messaging didReceiveRegistrationToken:(NSString *)fcmToken
|
||||||
|
{
|
||||||
|
if (fcmToken.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[self sendEvent:@{@"type": @"token", @"token": fcmToken}];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
||||||
|
willPresentNotification:(UNNotification *)notification
|
||||||
|
withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler API_AVAILABLE(ios(10.0))
|
||||||
|
{
|
||||||
|
[self sendEvent:@{@"type": @"notification", @"notification": [self payloadFromNotification:notification]}];
|
||||||
|
|
||||||
|
if (self.previousNotificationDelegate != nil
|
||||||
|
&& self.previousNotificationDelegate != self
|
||||||
|
&& [self.previousNotificationDelegate respondsToSelector:@selector(userNotificationCenter:willPresentNotification:withCompletionHandler:)]) {
|
||||||
|
[self.previousNotificationDelegate userNotificationCenter:center willPresentNotification:notification withCompletionHandler:completionHandler];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
completionHandler(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionSound | UNNotificationPresentationOptionBadge);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
||||||
|
didReceiveNotificationResponse:(UNNotificationResponse *)response
|
||||||
|
withCompletionHandler:(void (^)(void))completionHandler API_AVAILABLE(ios(10.0))
|
||||||
|
{
|
||||||
|
NSDictionary *payload = [self payloadFromNotification:response.notification];
|
||||||
|
self.initialNotification = payload;
|
||||||
|
[self sendEvent:@{@"type": @"notificationOpened", @"notification": payload}];
|
||||||
|
|
||||||
|
if (self.previousNotificationDelegate != nil
|
||||||
|
&& self.previousNotificationDelegate != self
|
||||||
|
&& [self.previousNotificationDelegate respondsToSelector:@selector(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:)]) {
|
||||||
|
[self.previousNotificationDelegate userNotificationCenter:center didReceiveNotificationResponse:response withCompletionHandler:completionHandler];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
completionHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSDictionary *)payloadFromNotification:(UNNotification *)notification API_AVAILABLE(ios(10.0))
|
||||||
|
{
|
||||||
|
NSMutableDictionary *payload = [NSMutableDictionary dictionary];
|
||||||
|
NSDictionary *userInfo = notification.request.content.userInfo ? notification.request.content.userInfo : @{};
|
||||||
|
payload[@"data"] = userInfo;
|
||||||
|
if (notification.request.identifier.length > 0) {
|
||||||
|
payload[@"identifier"] = notification.request.identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableDictionary *apsNotification = [NSMutableDictionary dictionary];
|
||||||
|
if (notification.request.content.title.length > 0) {
|
||||||
|
apsNotification[@"title"] = notification.request.content.title;
|
||||||
|
}
|
||||||
|
if (notification.request.content.body.length > 0) {
|
||||||
|
apsNotification[@"body"] = notification.request.content.body;
|
||||||
|
}
|
||||||
|
if (notification.request.content.categoryIdentifier.length > 0) {
|
||||||
|
apsNotification[@"category"] = notification.request.content.categoryIdentifier;
|
||||||
|
}
|
||||||
|
if (apsNotification.count > 0) {
|
||||||
|
payload[@"notification"] = apsNotification;
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)sendEvent:(NSDictionary *)event
|
||||||
|
{
|
||||||
|
if (self.notificationCallbackId.length == 0 || event == nil) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:event];
|
||||||
|
[result setKeepCallbackAsBool:YES];
|
||||||
|
[self.commandDelegate sendPluginResult:result callbackId:self.notificationCallbackId];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSDictionary *)firstDictionaryArgument:(CDVInvokedUrlCommand *)command
|
||||||
|
{
|
||||||
|
id first = [command.arguments firstObject];
|
||||||
|
if ([first isKindOfClass:[NSDictionary class]]) {
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
return @{};
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)stringFromDictionary:(NSDictionary *)dictionary key:(NSString *)key
|
||||||
|
{
|
||||||
|
id value = dictionary[key];
|
||||||
|
if ([value isKindOfClass:[NSString class]]) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if ([value respondsToSelector:@selector(stringValue)]) {
|
||||||
|
return [value stringValue];
|
||||||
|
}
|
||||||
|
return @"";
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)boolFromDictionary:(NSDictionary *)dictionary key:(NSString *)key defaultValue:(BOOL)defaultValue
|
||||||
|
{
|
||||||
|
id value = dictionary[key];
|
||||||
|
if ([value isKindOfClass:[NSNumber class]]) {
|
||||||
|
return [value boolValue];
|
||||||
|
}
|
||||||
|
if ([value isKindOfClass:[NSString class]]) {
|
||||||
|
NSString *lowercase = [value lowercaseString];
|
||||||
|
if ([lowercase isEqualToString:@"true"] || [lowercase isEqualToString:@"1"] || [lowercase isEqualToString:@"yes"]) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
if ([lowercase isEqualToString:@"false"] || [lowercase isEqualToString:@"0"] || [lowercase isEqualToString:@"no"]) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)infoStringForKey:(NSString *)key
|
||||||
|
{
|
||||||
|
id value = [[NSBundle mainBundle] objectForInfoDictionaryKey:key];
|
||||||
|
if ([value isKindOfClass:[NSString class]]) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return @"";
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)sendSuccessForCommand:(CDVInvokedUrlCommand *)command dictionary:(NSDictionary *)dictionary
|
||||||
|
{
|
||||||
|
CDVPluginResult *result = dictionary == nil
|
||||||
|
? [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]
|
||||||
|
: [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary];
|
||||||
|
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)sendErrorForCommand:(CDVInvokedUrlCommand *)command code:(NSString *)code message:(NSString *)message
|
||||||
|
{
|
||||||
|
NSDictionary *error = @{@"code": code ? code : @"ERROR", @"message": message ? message : @""};
|
||||||
|
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:error];
|
||||||
|
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
|
||||||
|
}
|
||||||
|
@end
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var exec = require('cordova/exec');
|
||||||
|
var SERVICE = 'BluememeUtils';
|
||||||
|
var eventHandlers = {};
|
||||||
|
|
||||||
|
function noop() {}
|
||||||
|
|
||||||
|
function asWifiOptions(input) {
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
return { ssid: input };
|
||||||
|
}
|
||||||
|
return input || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function call(action, args, success, error) {
|
||||||
|
exec(success || noop, error || noop, SERVICE, action, args || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCustomEvent(name, detail) {
|
||||||
|
var eventName = 'bluememeutils.' + name;
|
||||||
|
|
||||||
|
if (typeof window.CustomEvent === 'function') {
|
||||||
|
return new window.CustomEvent(eventName, { detail: detail });
|
||||||
|
}
|
||||||
|
|
||||||
|
var event = document.createEvent('CustomEvent');
|
||||||
|
event.initCustomEvent(eventName, false, false, detail);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchLocalEvent(event) {
|
||||||
|
if (!event || !event.type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var handlers = eventHandlers[event.type] || [];
|
||||||
|
handlers.slice().forEach(function (handler) {
|
||||||
|
handler(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function') {
|
||||||
|
window.dispatchEvent(createCustomEvent(event.type, event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var api = {
|
||||||
|
saveWifi: function (options, success, error) {
|
||||||
|
call('saveWifi', [asWifiOptions(options)], success, error);
|
||||||
|
},
|
||||||
|
|
||||||
|
connectWifi: function (options, success, error) {
|
||||||
|
call('connectWifi', [asWifiOptions(options)], success, error);
|
||||||
|
},
|
||||||
|
|
||||||
|
disconnectWifi: function (success, error) {
|
||||||
|
call('disconnectWifi', [], success, error);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeWifi: function (options, success, error) {
|
||||||
|
call('removeWifi', [asWifiOptions(options)], success, error);
|
||||||
|
},
|
||||||
|
|
||||||
|
requestWifiPermission: function (success, error) {
|
||||||
|
call('requestWifiPermission', [], success, error);
|
||||||
|
},
|
||||||
|
|
||||||
|
isBiometricAvailable: function (success, error) {
|
||||||
|
call('isBiometricAvailable', [], success, error);
|
||||||
|
},
|
||||||
|
|
||||||
|
unlockWithBiometrics: function (options, success, error) {
|
||||||
|
call('unlockWithBiometrics', [options || {}], success, error);
|
||||||
|
},
|
||||||
|
|
||||||
|
requestPushPermission: function (success, error) {
|
||||||
|
call('requestPushPermission', [], success, error);
|
||||||
|
},
|
||||||
|
|
||||||
|
getFirebaseToken: function (success, error) {
|
||||||
|
call('getFirebaseToken', [], success, error);
|
||||||
|
},
|
||||||
|
|
||||||
|
subscribeToTopic: function (topic, success, error) {
|
||||||
|
call('subscribeToTopic', [topic], success, error);
|
||||||
|
},
|
||||||
|
|
||||||
|
unsubscribeFromTopic: function (topic, success, error) {
|
||||||
|
call('unsubscribeFromTopic', [topic], success, error);
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialNotification: function (success, error) {
|
||||||
|
call('getInitialNotification', [], success, error);
|
||||||
|
},
|
||||||
|
|
||||||
|
startNotificationListener: function (success, error) {
|
||||||
|
call('startNotificationListener', [], function (event) {
|
||||||
|
dispatchLocalEvent(event);
|
||||||
|
if (success) {
|
||||||
|
success(event);
|
||||||
|
}
|
||||||
|
}, error);
|
||||||
|
},
|
||||||
|
|
||||||
|
stopNotificationListener: function (success, error) {
|
||||||
|
call('stopNotificationListener', [], success, error);
|
||||||
|
},
|
||||||
|
|
||||||
|
on: function (type, handler) {
|
||||||
|
if (!eventHandlers[type]) {
|
||||||
|
eventHandlers[type] = [];
|
||||||
|
}
|
||||||
|
eventHandlers[type].push(handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
off: function (type, handler) {
|
||||||
|
var handlers = eventHandlers[type];
|
||||||
|
if (!handlers) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var index = handlers.indexOf(handler);
|
||||||
|
if (index >= 0) {
|
||||||
|
handlers.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = api;
|
||||||
Reference in new issue
Block a user