add apns supports.
This commit is contained in:
1 parent
855d24bc64
commit
21c392db8f
6 files changed
+439
-28
No files matched your search
@@ -4,6 +4,7 @@ POST https://tokyohakuzencoltd-dev.outsystemsenterprise.com/BluememeCordovaCore/
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
|
"TargetUserId": 23,
|
||||||
"notification": {
|
"notification": {
|
||||||
"title": "Push Test",
|
"title": "Push Test",
|
||||||
"body": "This is a push test."
|
"body": "This is a push test."
|
||||||
|
|||||||
@@ -136,7 +136,23 @@ BluememeUtils.getInitialNotification(success, error);
|
|||||||
BluememeUtils.stopNotificationListener(success, error);
|
BluememeUtils.stopNotificationListener(success, error);
|
||||||
```
|
```
|
||||||
|
|
||||||
`getPushPermissionStatus` 只读取当前系统通知状态,不弹授权框。可用于填充后端 `NotificationsEnabled`。
|
`getPushPermissionStatus` 只读取当前系统通知状态,不弹授权框。可用于填充后端 `NotificationsEnabled`。iOS 返回 `authorizationStatus: "notDetermined"` 时,说明还没有触发系统通知授权弹窗,需要先调用 `requestPushPermission`,用户授权后再调用 `getFirebaseToken`。
|
||||||
|
|
||||||
|
iOS 获取 FCM token 的推荐顺序:
|
||||||
|
|
||||||
|
```js
|
||||||
|
BluememeUtils.requestPushPermission(function (permission) {
|
||||||
|
if (permission.enabled !== true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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`。
|
||||||
|
|
||||||
返回示例:
|
返回示例:
|
||||||
|
|
||||||
@@ -224,7 +240,10 @@ iOS WiFi:
|
|||||||
Firebase:
|
Firebase:
|
||||||
|
|
||||||
- Android 13+ 需要通知权限,调用 `requestPushPermission`。
|
- Android 13+ 需要通知权限,调用 `requestPushPermission`。
|
||||||
- iOS 需要 Push Notifications capability、Background Modes 中的 Remote notifications,以及 APNs key/certificate 已配置到 Firebase。
|
- iOS 需要 Apple Developer App ID 开启 Push Notifications,并使用重新生成的 provisioning profile 构建。
|
||||||
|
- 插件会向 Cordova iOS entitlements 写入 `aps-environment`:Debug 为 `development`,Release 为 `production`。仍建议在构建后的 IPA 中检查签名 entitlements 是否包含该 key。
|
||||||
|
- Firebase Console 需要在 Project Settings > Cloud Messaging > Apple app configuration 中上传 APNs Auth Key `.p8` 或 APNs certificate。推荐使用 APNs Auth Key。
|
||||||
|
- Background Modes 中的 Remote notifications 只影响后台静默推送/后台唤醒,不是获取 APNs token 和 FCM token 的前置条件。
|
||||||
- 如果项目中还有其他推送插件,也可能同时修改 `UNUserNotificationCenterDelegate` 或 Firebase service,需要在真机上确认通知打开事件。
|
- 如果项目中还有其他推送插件,也可能同时修改 `UNUserNotificationCenterDelegate` 或 Firebase service,需要在真机上确认通知打开事件。
|
||||||
|
|
||||||
Biometric:
|
Biometric:
|
||||||
@@ -249,6 +268,8 @@ Biometric:
|
|||||||
- `INVALID_WIFI_OPTIONS`
|
- `INVALID_WIFI_OPTIONS`
|
||||||
- `WIFI_CONNECT_FAILED`
|
- `WIFI_CONNECT_FAILED`
|
||||||
- `FIREBASE_NOT_CONFIGURED`
|
- `FIREBASE_NOT_CONFIGURED`
|
||||||
|
- `APNS_TOKEN_NOT_AVAILABLE`
|
||||||
|
- `APNS_TOKEN_FAILED`
|
||||||
- `FCM_TOKEN_FAILED`
|
- `FCM_TOKEN_FAILED`
|
||||||
- `BIOMETRIC_UNAVAILABLE`
|
- `BIOMETRIC_UNAVAILABLE`
|
||||||
- `AUTHENTICATION_ERROR`
|
- `AUTHENTICATION_ERROR`
|
||||||
+141
-19
@@ -1,7 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
ENTITLEMENT_KEY="com.apple.developer.networking.HotspotConfiguration"
|
HOTSPOT_ENTITLEMENT_KEY="com.apple.developer.networking.HotspotConfiguration"
|
||||||
|
APNS_ENTITLEMENT_KEY="aps-environment"
|
||||||
|
CHECK_SCRIPT_VERSION="2026-04-23.3"
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
TIMESTAMP="$(date '+%Y%m%d-%H%M%S')"
|
TIMESTAMP="$(date '+%Y%m%d-%H%M%S')"
|
||||||
@@ -23,8 +25,9 @@ Usage:
|
|||||||
$(basename "$0") /path/to/App.ipa
|
$(basename "$0") /path/to/App.ipa
|
||||||
|
|
||||||
Checks:
|
Checks:
|
||||||
1. embedded.mobileprovision contains ${ENTITLEMENT_KEY}=true
|
1. embedded.mobileprovision contains ${HOTSPOT_ENTITLEMENT_KEY}=true
|
||||||
2. App executable appears to contain ${ENTITLEMENT_KEY}=true
|
2. embedded.mobileprovision contains ${APNS_ENTITLEMENT_KEY}=development|production
|
||||||
|
3. App executable appears to contain both signed entitlements
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
Linux cannot run Apple's codesign tool. The executable check is a best-effort
|
Linux cannot run Apple's codesign tool. The executable check is a best-effort
|
||||||
@@ -59,22 +62,32 @@ has_command() {
|
|||||||
extract_plist_from_binary() {
|
extract_plist_from_binary() {
|
||||||
local source_file="$1"
|
local source_file="$1"
|
||||||
local target_file="$2"
|
local target_file="$2"
|
||||||
|
local needle="${3:-}"
|
||||||
|
|
||||||
python3 - "$source_file" "$target_file" <<'PY'
|
python3 - "$source_file" "$target_file" "$needle" <<'PY'
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
source = Path(sys.argv[1])
|
source = Path(sys.argv[1])
|
||||||
target = Path(sys.argv[2])
|
target = Path(sys.argv[2])
|
||||||
|
needle = sys.argv[3].encode("utf-8") if len(sys.argv) > 3 and sys.argv[3] else b""
|
||||||
data = source.read_bytes()
|
data = source.read_bytes()
|
||||||
|
|
||||||
|
needle_index = data.find(needle) if needle else -1
|
||||||
|
if needle_index >= 0:
|
||||||
|
start = data.rfind(b"<?xml", 0, needle_index)
|
||||||
|
if start < 0:
|
||||||
|
start = data.rfind(b"<plist", 0, needle_index)
|
||||||
|
end = data.find(b"</plist>", needle_index)
|
||||||
|
else:
|
||||||
start = data.find(b"<?xml")
|
start = data.find(b"<?xml")
|
||||||
if start < 0:
|
if start < 0:
|
||||||
start = data.find(b"<plist")
|
start = data.find(b"<plist")
|
||||||
|
end = data.find(b"</plist>", start if start >= 0 else 0)
|
||||||
|
|
||||||
if start < 0:
|
if start < 0:
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
end = data.find(b"</plist>", start)
|
|
||||||
if end < 0:
|
if end < 0:
|
||||||
sys.exit(3)
|
sys.exit(3)
|
||||||
|
|
||||||
@@ -97,7 +110,28 @@ key = sys.argv[2]
|
|||||||
data = path.read_bytes()
|
data = path.read_bytes()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
value = plistlib.loads(data).get(key)
|
plist = plistlib.loads(data)
|
||||||
|
|
||||||
|
values = []
|
||||||
|
|
||||||
|
def collect(obj):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
if key in obj:
|
||||||
|
values.append(obj.get(key))
|
||||||
|
entitlements = obj.get("Entitlements")
|
||||||
|
if isinstance(entitlements, dict) and key in entitlements:
|
||||||
|
values.append(entitlements.get(key))
|
||||||
|
for value in obj.values():
|
||||||
|
if isinstance(value, (dict, list, tuple)):
|
||||||
|
collect(value)
|
||||||
|
elif isinstance(obj, (list, tuple)):
|
||||||
|
for value in obj:
|
||||||
|
if isinstance(value, (dict, list, tuple)):
|
||||||
|
collect(value)
|
||||||
|
|
||||||
|
collect(plist)
|
||||||
|
|
||||||
|
value = values[0] if values else None
|
||||||
if value is True:
|
if value is True:
|
||||||
print("true")
|
print("true")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
@@ -123,6 +157,63 @@ except Exception:
|
|||||||
PY
|
PY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_plist_key_string_allowed() {
|
||||||
|
local plist_file="$1"
|
||||||
|
local key="$2"
|
||||||
|
shift 2
|
||||||
|
|
||||||
|
python3 - "$plist_file" "$key" "$@" <<'PY'
|
||||||
|
import plistlib
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
path = Path(sys.argv[1])
|
||||||
|
key = sys.argv[2]
|
||||||
|
allowed = set(sys.argv[3:])
|
||||||
|
data = path.read_bytes()
|
||||||
|
|
||||||
|
def finish(value):
|
||||||
|
if value is None:
|
||||||
|
print("missing")
|
||||||
|
sys.exit(1)
|
||||||
|
print(value)
|
||||||
|
if str(value) in allowed:
|
||||||
|
sys.exit(0)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
try:
|
||||||
|
plist = plistlib.loads(data)
|
||||||
|
values = []
|
||||||
|
|
||||||
|
def collect(obj):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
if key in obj:
|
||||||
|
values.append(obj.get(key))
|
||||||
|
entitlements = obj.get("Entitlements")
|
||||||
|
if isinstance(entitlements, dict) and key in entitlements:
|
||||||
|
values.append(entitlements.get(key))
|
||||||
|
for value in obj.values():
|
||||||
|
if isinstance(value, (dict, list, tuple)):
|
||||||
|
collect(value)
|
||||||
|
elif isinstance(obj, (list, tuple)):
|
||||||
|
for value in obj:
|
||||||
|
if isinstance(value, (dict, list, tuple)):
|
||||||
|
collect(value)
|
||||||
|
|
||||||
|
collect(plist)
|
||||||
|
finish(values[0] if values else None)
|
||||||
|
except Exception:
|
||||||
|
text = data.decode("utf-8", errors="ignore")
|
||||||
|
pattern = re.compile(
|
||||||
|
r"<key>\s*" + re.escape(key) + r"\s*</key>\s*<string>\s*([^<]+?)\s*</string>",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
match = pattern.search(text)
|
||||||
|
finish(match.group(1).strip() if match else None)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
find_main_executable() {
|
find_main_executable() {
|
||||||
local app_dir="$1"
|
local app_dir="$1"
|
||||||
local info_plist="${app_dir}/Info.plist"
|
local info_plist="${app_dir}/Info.plist"
|
||||||
@@ -157,7 +248,8 @@ print_section() {
|
|||||||
echo "==== $* ===="
|
echo "==== $* ===="
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "iOS Hotspot Configuration Signature Check"
|
echo "iOS Hotspot/APNs Signature Check"
|
||||||
|
echo "Checker version: ${CHECK_SCRIPT_VERSION}"
|
||||||
echo "Started: $(date -Is)"
|
echo "Started: $(date -Is)"
|
||||||
echo "Script: ${BASH_SOURCE[0]}"
|
echo "Script: ${BASH_SOURCE[0]}"
|
||||||
echo "Log: ${LOG_FILE}"
|
echo "Log: ${LOG_FILE}"
|
||||||
@@ -246,43 +338,71 @@ if [[ "${PROFILE_EXTRACTED}" -eq 0 ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PROFILE_RESULT="$(check_plist_key_true "${PROFILE_PLIST}" "${ENTITLEMENT_KEY}" 2>&1)"
|
PROFILE_RESULT="$(check_plist_key_true "${PROFILE_PLIST}" "${HOTSPOT_ENTITLEMENT_KEY}" 2>&1)"
|
||||||
PROFILE_STATUS=$?
|
PROFILE_STATUS=$?
|
||||||
echo "Profile entitlement value: ${PROFILE_RESULT}"
|
echo "Profile Hotspot entitlement value: ${PROFILE_RESULT}"
|
||||||
if [[ "${PROFILE_STATUS}" -eq 0 ]]; then
|
if [[ "${PROFILE_STATUS}" -eq 0 ]]; then
|
||||||
pass "Provisioning profile contains ${ENTITLEMENT_KEY}=true"
|
pass "Provisioning profile contains ${HOTSPOT_ENTITLEMENT_KEY}=true"
|
||||||
else
|
else
|
||||||
fail "Provisioning profile does not contain ${ENTITLEMENT_KEY}=true"
|
fail "Provisioning profile does not contain ${HOTSPOT_ENTITLEMENT_KEY}=true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
PROFILE_APNS_RESULT="$(check_plist_key_string_allowed "${PROFILE_PLIST}" "${APNS_ENTITLEMENT_KEY}" development production 2>&1)"
|
||||||
|
PROFILE_APNS_STATUS=$?
|
||||||
|
echo "Profile APNs entitlement value: ${PROFILE_APNS_RESULT}"
|
||||||
|
if [[ "${PROFILE_APNS_STATUS}" -eq 0 ]]; then
|
||||||
|
pass "Provisioning profile contains ${APNS_ENTITLEMENT_KEY}=${PROFILE_APNS_RESULT}"
|
||||||
|
else
|
||||||
|
fail "Provisioning profile does not contain ${APNS_ENTITLEMENT_KEY}=development|production"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Profile entitlement context:"
|
echo "Profile entitlement context:"
|
||||||
grep -n -A2 -B1 "${ENTITLEMENT_KEY}" "${PROFILE_PLIST}" || true
|
grep -n -A2 -B1 "${HOTSPOT_ENTITLEMENT_KEY}" "${PROFILE_PLIST}" || true
|
||||||
|
grep -n -A2 -B1 "${APNS_ENTITLEMENT_KEY}" "${PROFILE_PLIST}" || true
|
||||||
|
|
||||||
print_section "Signed App Entitlements"
|
print_section "Signed App Entitlements"
|
||||||
echo "Linux cannot run Apple's codesign. This is a best-effort scan of the app executable."
|
echo "Linux cannot run Apple's codesign. This is a best-effort scan of the app executable."
|
||||||
|
|
||||||
if grep -a -q "${ENTITLEMENT_KEY}" "${EXECUTABLE_PATH}"; then
|
if grep -a -q "${HOTSPOT_ENTITLEMENT_KEY}" "${EXECUTABLE_PATH}"; then
|
||||||
pass "Executable contains entitlement key string: ${ENTITLEMENT_KEY}"
|
pass "Executable contains entitlement key string: ${HOTSPOT_ENTITLEMENT_KEY}"
|
||||||
else
|
else
|
||||||
fail "Executable does not contain entitlement key string. Signed app entitlements likely do not include Hotspot Configuration."
|
fail "Executable does not contain entitlement key string. Signed app entitlements likely do not include Hotspot Configuration."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if grep -a -q "${APNS_ENTITLEMENT_KEY}" "${EXECUTABLE_PATH}"; then
|
||||||
|
pass "Executable contains entitlement key string: ${APNS_ENTITLEMENT_KEY}"
|
||||||
|
else
|
||||||
|
fail "Executable does not contain entitlement key string. Signed app entitlements likely do not include APNs aps-environment."
|
||||||
|
fi
|
||||||
|
|
||||||
EXEC_ENTITLEMENTS_PLIST="${WORK_DIR}/signed-entitlements-heuristic.plist"
|
EXEC_ENTITLEMENTS_PLIST="${WORK_DIR}/signed-entitlements-heuristic.plist"
|
||||||
if extract_plist_from_binary "${EXECUTABLE_PATH}" "${EXEC_ENTITLEMENTS_PLIST}"; then
|
if extract_plist_from_binary "${EXECUTABLE_PATH}" "${EXEC_ENTITLEMENTS_PLIST}" "${HOTSPOT_ENTITLEMENT_KEY}"; then
|
||||||
echo "Extracted executable plist candidate: ${EXEC_ENTITLEMENTS_PLIST}"
|
echo "Extracted executable plist candidate: ${EXEC_ENTITLEMENTS_PLIST}"
|
||||||
EXEC_RESULT="$(check_plist_key_true "${EXEC_ENTITLEMENTS_PLIST}" "${ENTITLEMENT_KEY}" 2>&1)"
|
EXEC_RESULT="$(check_plist_key_true "${EXEC_ENTITLEMENTS_PLIST}" "${HOTSPOT_ENTITLEMENT_KEY}" 2>&1)"
|
||||||
EXEC_STATUS=$?
|
EXEC_STATUS=$?
|
||||||
echo "Executable entitlement value: ${EXEC_RESULT}"
|
echo "Executable Hotspot entitlement value: ${EXEC_RESULT}"
|
||||||
if [[ "${EXEC_STATUS}" -eq 0 ]]; then
|
if [[ "${EXEC_STATUS}" -eq 0 ]]; then
|
||||||
pass "Executable plist candidate contains ${ENTITLEMENT_KEY}=true"
|
pass "Executable plist candidate contains ${HOTSPOT_ENTITLEMENT_KEY}=true"
|
||||||
else
|
else
|
||||||
warn "Executable contains entitlement key, but extracted plist candidate did not prove value=true."
|
warn "Executable contains entitlement key, but extracted plist candidate did not prove value=true."
|
||||||
warn "Use macOS codesign for authoritative validation."
|
warn "Use macOS codesign for authoritative validation."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
EXEC_APNS_RESULT="$(check_plist_key_string_allowed "${EXEC_ENTITLEMENTS_PLIST}" "${APNS_ENTITLEMENT_KEY}" development production 2>&1)"
|
||||||
|
EXEC_APNS_STATUS=$?
|
||||||
|
echo "Executable APNs entitlement value: ${EXEC_APNS_RESULT}"
|
||||||
|
if [[ "${EXEC_APNS_STATUS}" -eq 0 ]]; then
|
||||||
|
pass "Executable plist candidate contains ${APNS_ENTITLEMENT_KEY}=${EXEC_APNS_RESULT}"
|
||||||
|
else
|
||||||
|
warn "Executable contains APNs entitlement key, but extracted plist candidate did not prove value=development|production."
|
||||||
|
warn "Use macOS codesign for authoritative validation."
|
||||||
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Executable entitlement context:"
|
echo "Executable entitlement context:"
|
||||||
grep -n -A2 -B1 "${ENTITLEMENT_KEY}" "${EXEC_ENTITLEMENTS_PLIST}" || true
|
grep -n -A2 -B1 "${HOTSPOT_ENTITLEMENT_KEY}" "${EXEC_ENTITLEMENTS_PLIST}" || true
|
||||||
|
grep -n -A2 -B1 "${APNS_ENTITLEMENT_KEY}" "${EXEC_ENTITLEMENTS_PLIST}" || true
|
||||||
else
|
else
|
||||||
warn "Could not extract XML plist from executable. Key string was present, but value=true was not proven."
|
warn "Could not extract XML plist from executable. Key string was present, but value=true was not proven."
|
||||||
warn "Use macOS codesign for authoritative validation."
|
warn "Use macOS codesign for authoritative validation."
|
||||||
@@ -290,7 +410,9 @@ fi
|
|||||||
|
|
||||||
print_section "Summary"
|
print_section "Summary"
|
||||||
pass "Provisioning profile Hotspot entitlement: true"
|
pass "Provisioning profile Hotspot entitlement: true"
|
||||||
|
pass "Provisioning profile APNs entitlement: ${PROFILE_APNS_RESULT}"
|
||||||
pass "Executable Hotspot entitlement key: present"
|
pass "Executable Hotspot entitlement key: present"
|
||||||
|
pass "Executable APNs entitlement key: present"
|
||||||
echo
|
echo
|
||||||
echo "Full log written to:"
|
echo "Full log written to:"
|
||||||
echo " ${LOG_FILE}"
|
echo " ${LOG_FILE}"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bluememe-utils",
|
"name": "bluememe-utils",
|
||||||
"version": "0.1.11",
|
"version": "0.1.12",
|
||||||
"description": "Cordova utilities for WiFi, Firebase Cloud Messaging, and biometric unlock for OutSystems 11 apps.",
|
"description": "Cordova utilities for WiFi, Firebase Cloud Messaging, and biometric unlock for OutSystems 11 apps.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
+7
-1
@@ -2,7 +2,7 @@
|
|||||||
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
|
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
id="bluememe-utils"
|
id="bluememe-utils"
|
||||||
version="0.1.11">
|
version="0.1.12">
|
||||||
<name>bluememe-utils</name>
|
<name>bluememe-utils</name>
|
||||||
<description>Cordova utilities for WiFi, Firebase Cloud Messaging, and biometric unlock.</description>
|
<description>Cordova utilities for WiFi, Firebase Cloud Messaging, and biometric unlock.</description>
|
||||||
<license>MIT</license>
|
<license>MIT</license>
|
||||||
@@ -104,6 +104,12 @@
|
|||||||
<config-file target="*-Release.plist" parent="com.apple.developer.networking.HotspotConfiguration">
|
<config-file target="*-Release.plist" parent="com.apple.developer.networking.HotspotConfiguration">
|
||||||
<true />
|
<true />
|
||||||
</config-file>
|
</config-file>
|
||||||
|
<config-file target="*-Debug.plist" parent="aps-environment">
|
||||||
|
<string>development</string>
|
||||||
|
</config-file>
|
||||||
|
<config-file target="*-Release.plist" parent="aps-environment">
|
||||||
|
<string>production</string>
|
||||||
|
</config-file>
|
||||||
|
|
||||||
<header-file src="src/ios/CDVBluememeUtils.h" />
|
<header-file src="src/ios/CDVBluememeUtils.h" />
|
||||||
<source-file src="src/ios/CDVBluememeUtils.m" />
|
<source-file src="src/ios/CDVBluememeUtils.m" />
|
||||||
|
|||||||
+266
-5
@@ -8,12 +8,18 @@
|
|||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
static NSString * const BluememeDeviceKeyDefaultsKey = @"bluememe_utils_device_key";
|
static NSString * const BluememeDeviceKeyDefaultsKey = @"bluememe_utils_device_key";
|
||||||
static NSString * const BluememeUtilsPluginVersion = @"0.1.11";
|
static NSString * const BluememeUtilsPluginVersion = @"0.1.12";
|
||||||
|
static NSString * const BluememeCordovaRemoteNotificationName = @"CDVRemoteNotification";
|
||||||
|
static NSString * const BluememeCordovaRemoteNotificationErrorName = @"CDVRemoteNotificationError";
|
||||||
|
static NSTimeInterval const BluememeAPNSTokenTimeoutSeconds = 10.0;
|
||||||
|
|
||||||
@interface CDVBluememeUtils ()
|
@interface CDVBluememeUtils ()
|
||||||
@property (nonatomic, copy) NSString *notificationCallbackId;
|
@property (nonatomic, copy) NSString *notificationCallbackId;
|
||||||
@property (nonatomic, strong) NSDictionary *initialNotification;
|
@property (nonatomic, strong) NSDictionary *initialNotification;
|
||||||
@property (nonatomic, weak) id<UNUserNotificationCenterDelegate> previousNotificationDelegate;
|
@property (nonatomic, weak) id<UNUserNotificationCenterDelegate> previousNotificationDelegate;
|
||||||
|
@property (nonatomic, strong) NSData *apnsDeviceToken;
|
||||||
|
@property (nonatomic, strong) NSError *apnsRegistrationError;
|
||||||
|
@property (nonatomic, strong) CDVInvokedUrlCommand *pendingFirebaseTokenCommand;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation CDVBluememeUtils
|
@implementation CDVBluememeUtils
|
||||||
@@ -29,6 +35,20 @@ static NSString * const BluememeUtilsPluginVersion = @"0.1.11";
|
|||||||
self.previousNotificationDelegate = center.delegate;
|
self.previousNotificationDelegate = center.delegate;
|
||||||
center.delegate = self;
|
center.delegate = self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
|
selector:@selector(remoteNotificationRegistered:)
|
||||||
|
name:BluememeCordovaRemoteNotificationName
|
||||||
|
object:nil];
|
||||||
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||||
|
selector:@selector(remoteNotificationRegistrationFailed:)
|
||||||
|
name:BluememeCordovaRemoteNotificationErrorName
|
||||||
|
object:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dealloc
|
||||||
|
{
|
||||||
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)saveWifi:(CDVInvokedUrlCommand *)command
|
- (void)saveWifi:(CDVInvokedUrlCommand *)command
|
||||||
@@ -159,14 +179,31 @@ static NSString * const BluememeUtilsPluginVersion = @"0.1.11";
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
[[UIApplication sharedApplication] registerForRemoteNotifications];
|
[[UIApplication sharedApplication] registerForRemoteNotifications];
|
||||||
[self sendSuccessForCommand:command dictionary:@{@"type": @"notification", @"granted": @(granted), @"missing": granted ? @[] : @[@"notification"]}];
|
[[UNUserNotificationCenter currentNotificationCenter] getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *settings) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
NSMutableDictionary *result = [[self dictionaryFromNotificationSettings:settings] mutableCopy];
|
||||||
|
result[@"granted"] = @(granted);
|
||||||
|
result[@"registeredForRemoteNotifications"] = @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]);
|
||||||
|
result[@"apnsTokenAvailable"] = @(self.apnsDeviceToken.length > 0);
|
||||||
|
[self sendSuccessForCommand:command dictionary:result];
|
||||||
|
});
|
||||||
|
}];
|
||||||
});
|
});
|
||||||
}];
|
}];
|
||||||
} else {
|
} else {
|
||||||
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound) categories:nil];
|
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound) categories:nil];
|
||||||
[[UIApplication sharedApplication] registerUserNotificationSettings:settings];
|
[[UIApplication sharedApplication] registerUserNotificationSettings:settings];
|
||||||
[[UIApplication sharedApplication] registerForRemoteNotifications];
|
[[UIApplication sharedApplication] registerForRemoteNotifications];
|
||||||
[self sendSuccessForCommand:command dictionary:@{@"type": @"notification", @"granted": @YES, @"missing": @[]}];
|
[self sendSuccessForCommand:command dictionary:@{
|
||||||
|
@"type": @"notification",
|
||||||
|
@"platform": @"iOS",
|
||||||
|
@"enabled": @YES,
|
||||||
|
@"granted": @YES,
|
||||||
|
@"authorizationStatus": @"authorized",
|
||||||
|
@"registeredForRemoteNotifications": @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]),
|
||||||
|
@"apnsTokenAvailable": @(self.apnsDeviceToken.length > 0),
|
||||||
|
@"missing": @[]
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +227,8 @@ static NSString * const BluememeUtilsPluginVersion = @"0.1.11";
|
|||||||
@"alertEnabled": @((settings.types & UIUserNotificationTypeAlert) != 0),
|
@"alertEnabled": @((settings.types & UIUserNotificationTypeAlert) != 0),
|
||||||
@"soundEnabled": @((settings.types & UIUserNotificationTypeSound) != 0),
|
@"soundEnabled": @((settings.types & UIUserNotificationTypeSound) != 0),
|
||||||
@"badgeEnabled": @((settings.types & UIUserNotificationTypeBadge) != 0),
|
@"badgeEnabled": @((settings.types & UIUserNotificationTypeBadge) != 0),
|
||||||
|
@"registeredForRemoteNotifications": @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]),
|
||||||
|
@"apnsTokenAvailable": @(self.apnsDeviceToken.length > 0),
|
||||||
@"missing": enabled ? @[] : @[@"notification"]
|
@"missing": enabled ? @[] : @[@"notification"]
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
@@ -204,13 +243,102 @@ static NSString * const BluememeUtilsPluginVersion = @"0.1.11";
|
|||||||
}
|
}
|
||||||
[FIRMessaging messaging].delegate = self;
|
[FIRMessaging messaging].delegate = self;
|
||||||
|
|
||||||
|
if (@available(iOS 10.0, *)) {
|
||||||
|
[[UNUserNotificationCenter currentNotificationCenter] getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *settings) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
if (![self notificationAuthorizationAllowsFirebaseToken:settings.authorizationStatus]) {
|
||||||
|
NSMutableDictionary *details = [[self dictionaryFromNotificationSettings:settings] mutableCopy];
|
||||||
|
details[@"apnsTokenAvailable"] = @(self.apnsDeviceToken.length > 0);
|
||||||
|
details[@"registeredForRemoteNotifications"] = @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]);
|
||||||
|
[self sendErrorForCommand:command
|
||||||
|
code:@"PERMISSION_REQUIRED"
|
||||||
|
message:@"Call requestPushPermission and wait for notification authorization before getFirebaseToken."
|
||||||
|
details:details];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[self registerForRemoteNotificationsAndFetchFirebaseTokenForCommand:command];
|
||||||
|
});
|
||||||
|
}];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self registerForRemoteNotificationsAndFetchFirebaseTokenForCommand:command];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)registerForRemoteNotificationsAndFetchFirebaseTokenForCommand:(CDVInvokedUrlCommand *)command
|
||||||
|
{
|
||||||
|
if (self.apnsDeviceToken.length > 0) {
|
||||||
|
[FIRMessaging messaging].APNSToken = self.apnsDeviceToken;
|
||||||
|
[self fetchFirebaseTokenForCommand:command];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.pendingFirebaseTokenCommand != nil) {
|
||||||
|
[self sendErrorForCommand:self.pendingFirebaseTokenCommand
|
||||||
|
code:@"FCM_TOKEN_REQUEST_REPLACED"
|
||||||
|
message:@"A newer getFirebaseToken request replaced this pending request."
|
||||||
|
details:@{
|
||||||
|
@"apnsTokenAvailable": @NO,
|
||||||
|
@"registeredForRemoteNotifications": @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications])
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pendingFirebaseTokenCommand = command;
|
||||||
|
self.apnsRegistrationError = nil;
|
||||||
|
[[UIApplication sharedApplication] registerForRemoteNotifications];
|
||||||
|
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(BluememeAPNSTokenTimeoutSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||||
|
if (self.pendingFirebaseTokenCommand != command) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pendingFirebaseTokenCommand = nil;
|
||||||
|
if (self.apnsRegistrationError != nil) {
|
||||||
|
[self sendErrorForCommand:command
|
||||||
|
code:@"APNS_TOKEN_FAILED"
|
||||||
|
message:(self.apnsRegistrationError.localizedDescription ? self.apnsRegistrationError.localizedDescription : @"APNs registration failed.")
|
||||||
|
details:[self detailsForNativeError:self.apnsRegistrationError includeAPNSState:YES]];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self sendErrorForCommand:command
|
||||||
|
code:@"APNS_TOKEN_NOT_AVAILABLE"
|
||||||
|
message:@"APNs device token was not received before fetching the FCM token. Check Push Notifications capability, aps-environment entitlement, provisioning profile, and Firebase APNs key configuration."
|
||||||
|
details:@{
|
||||||
|
@"apnsTokenAvailable": @NO,
|
||||||
|
@"registeredForRemoteNotifications": @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]),
|
||||||
|
@"timeoutSeconds": @(BluememeAPNSTokenTimeoutSeconds)
|
||||||
|
}];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)fetchFirebaseTokenForCommand:(CDVInvokedUrlCommand *)command
|
||||||
|
{
|
||||||
|
if (self.apnsDeviceToken.length > 0) {
|
||||||
|
[FIRMessaging messaging].APNSToken = self.apnsDeviceToken;
|
||||||
|
}
|
||||||
|
|
||||||
[[FIRMessaging messaging] tokenWithCompletion:^(NSString *token, NSError *error) {
|
[[FIRMessaging messaging] tokenWithCompletion:^(NSString *token, NSError *error) {
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
if (error != nil || token.length == 0) {
|
if (error != nil || token.length == 0) {
|
||||||
[self sendErrorForCommand:command code:@"FCM_TOKEN_FAILED" message:(error.localizedDescription ? error.localizedDescription : @"Failed to get Firebase token.")];
|
NSMutableDictionary *details = [NSMutableDictionary dictionary];
|
||||||
|
details[@"apnsTokenAvailable"] = @(self.apnsDeviceToken.length > 0);
|
||||||
|
details[@"registeredForRemoteNotifications"] = @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]);
|
||||||
|
if (error != nil) {
|
||||||
|
[details addEntriesFromDictionary:[self detailsForNativeError:error includeAPNSState:NO]];
|
||||||
|
}
|
||||||
|
[self sendErrorForCommand:command
|
||||||
|
code:@"FCM_TOKEN_FAILED"
|
||||||
|
message:(error.localizedDescription ? error.localizedDescription : @"Failed to get Firebase token.")
|
||||||
|
details:details];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
[self sendSuccessForCommand:command dictionary:@{@"token": token}];
|
[self sendSuccessForCommand:command dictionary:@{
|
||||||
|
@"token": token,
|
||||||
|
@"platform": @"iOS",
|
||||||
|
@"apnsTokenAvailable": @(self.apnsDeviceToken.length > 0),
|
||||||
|
@"registeredForRemoteNotifications": @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications])
|
||||||
|
}];
|
||||||
[self sendEvent:@{@"type": @"token", @"token": token}];
|
[self sendEvent:@{@"type": @"token", @"token": token}];
|
||||||
});
|
});
|
||||||
}];
|
}];
|
||||||
@@ -716,6 +844,8 @@ static NSString * const BluememeUtilsPluginVersion = @"0.1.11";
|
|||||||
@"alertEnabled": @(alertEnabled),
|
@"alertEnabled": @(alertEnabled),
|
||||||
@"soundEnabled": @(soundEnabled),
|
@"soundEnabled": @(soundEnabled),
|
||||||
@"badgeEnabled": @(badgeEnabled),
|
@"badgeEnabled": @(badgeEnabled),
|
||||||
|
@"registeredForRemoteNotifications": @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]),
|
||||||
|
@"apnsTokenAvailable": @(self.apnsDeviceToken.length > 0),
|
||||||
@"missing": missing
|
@"missing": missing
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -746,6 +876,137 @@ static NSString * const BluememeUtilsPluginVersion = @"0.1.11";
|
|||||||
return @"unknown";
|
return @"unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (BOOL)notificationAuthorizationAllowsFirebaseToken:(UNAuthorizationStatus)status API_AVAILABLE(ios(10.0))
|
||||||
|
{
|
||||||
|
if (status == UNAuthorizationStatusAuthorized) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
if (@available(iOS 12.0, *)) {
|
||||||
|
if (status == UNAuthorizationStatusProvisional) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (@available(iOS 14.0, *)) {
|
||||||
|
if (status == UNAuthorizationStatusEphemeral) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)remoteNotificationRegistered:(NSNotification *)notification
|
||||||
|
{
|
||||||
|
NSData *tokenData = [self dataFromAPNSTokenObject:notification.object];
|
||||||
|
if (tokenData.length == 0) {
|
||||||
|
tokenData = [self dataFromAPNSTokenObject:notification.userInfo[@"deviceToken"]];
|
||||||
|
}
|
||||||
|
if (tokenData.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.apnsDeviceToken = tokenData;
|
||||||
|
self.apnsRegistrationError = nil;
|
||||||
|
if ([FIRApp defaultApp] != nil) {
|
||||||
|
[FIRMessaging messaging].APNSToken = tokenData;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.pendingFirebaseTokenCommand != nil) {
|
||||||
|
CDVInvokedUrlCommand *command = self.pendingFirebaseTokenCommand;
|
||||||
|
self.pendingFirebaseTokenCommand = nil;
|
||||||
|
[self fetchFirebaseTokenForCommand:command];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)remoteNotificationRegistrationFailed:(NSNotification *)notification
|
||||||
|
{
|
||||||
|
NSError *error = nil;
|
||||||
|
if ([notification.object isKindOfClass:[NSError class]]) {
|
||||||
|
error = notification.object;
|
||||||
|
} else if ([notification.userInfo[@"error"] isKindOfClass:[NSError class]]) {
|
||||||
|
error = notification.userInfo[@"error"];
|
||||||
|
}
|
||||||
|
|
||||||
|
self.apnsRegistrationError = error;
|
||||||
|
|
||||||
|
if (self.pendingFirebaseTokenCommand != nil) {
|
||||||
|
CDVInvokedUrlCommand *command = self.pendingFirebaseTokenCommand;
|
||||||
|
self.pendingFirebaseTokenCommand = nil;
|
||||||
|
[self sendErrorForCommand:command
|
||||||
|
code:@"APNS_TOKEN_FAILED"
|
||||||
|
message:(error.localizedDescription ? error.localizedDescription : @"APNs registration failed.")
|
||||||
|
details:[self detailsForNativeError:error includeAPNSState:YES]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSData *)dataFromAPNSTokenObject:(id)object
|
||||||
|
{
|
||||||
|
if ([object isKindOfClass:[NSData class]]) {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
if (![object isKindOfClass:[NSString class]]) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *tokenString = (NSString *)object;
|
||||||
|
NSRange angleStart = [tokenString rangeOfString:@"<"];
|
||||||
|
NSRange angleEnd = [tokenString rangeOfString:@">" options:NSBackwardsSearch];
|
||||||
|
if (angleStart.location != NSNotFound && angleEnd.location != NSNotFound && angleEnd.location > angleStart.location) {
|
||||||
|
tokenString = [tokenString substringWithRange:NSMakeRange(angleStart.location + 1, angleEnd.location - angleStart.location - 1)];
|
||||||
|
} else {
|
||||||
|
NSRange bytesRange = [tokenString rangeOfString:@"bytes = 0x"];
|
||||||
|
if (bytesRange.location != NSNotFound) {
|
||||||
|
tokenString = [tokenString substringFromIndex:bytesRange.location + bytesRange.length];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableString *hex = [NSMutableString string];
|
||||||
|
NSCharacterSet *hexCharacters = [NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdefABCDEF"];
|
||||||
|
for (NSUInteger index = 0; index < tokenString.length; index++) {
|
||||||
|
unichar character = [tokenString characterAtIndex:index];
|
||||||
|
if ([hexCharacters characterIsMember:character]) {
|
||||||
|
[hex appendFormat:@"%C", character];
|
||||||
|
} else if ([tokenString rangeOfString:@"bytes = 0x"].location != NSNotFound) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hex.length == 0 || (hex.length % 2) != 0) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableData *data = [NSMutableData dataWithCapacity:(hex.length / 2)];
|
||||||
|
for (NSUInteger index = 0; index < hex.length; index += 2) {
|
||||||
|
NSString *byteString = [hex substringWithRange:NSMakeRange(index, 2)];
|
||||||
|
unsigned int byte = 0;
|
||||||
|
NSScanner *scanner = [NSScanner scannerWithString:byteString];
|
||||||
|
if (![scanner scanHexInt:&byte]) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
unsigned char value = (unsigned char)byte;
|
||||||
|
[data appendBytes:&value length:1];
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSDictionary *)detailsForNativeError:(NSError *)error includeAPNSState:(BOOL)includeAPNSState
|
||||||
|
{
|
||||||
|
NSMutableDictionary *details = [NSMutableDictionary dictionary];
|
||||||
|
if (includeAPNSState) {
|
||||||
|
details[@"apnsTokenAvailable"] = @(self.apnsDeviceToken.length > 0);
|
||||||
|
details[@"registeredForRemoteNotifications"] = @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]);
|
||||||
|
}
|
||||||
|
if (error.domain.length > 0) {
|
||||||
|
details[@"nativeDomain"] = error.domain;
|
||||||
|
}
|
||||||
|
if (error != nil) {
|
||||||
|
details[@"nativeCode"] = @(error.code);
|
||||||
|
}
|
||||||
|
if (error.localizedDescription.length > 0) {
|
||||||
|
details[@"nativeError"] = error.localizedDescription;
|
||||||
|
}
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
- (BOOL)ensureFirebaseConfigured:(NSError **)error
|
- (BOOL)ensureFirebaseConfigured:(NSError **)error
|
||||||
{
|
{
|
||||||
if ([FIRApp defaultApp] != nil) {
|
if ([FIRApp defaultApp] != nil) {
|
||||||
|
|||||||
Reference in new issue
Block a user