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
|
||||
|
||||
{
|
||||
"TargetUserId": 23,
|
||||
"notification": {
|
||||
"title": "Push Test",
|
||||
"body": "This is a push test."
|
||||
|
||||
@@ -136,7 +136,23 @@ BluememeUtils.getInitialNotification(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:
|
||||
|
||||
- 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,需要在真机上确认通知打开事件。
|
||||
|
||||
Biometric:
|
||||
@@ -249,6 +268,8 @@ Biometric:
|
||||
- `INVALID_WIFI_OPTIONS`
|
||||
- `WIFI_CONNECT_FAILED`
|
||||
- `FIREBASE_NOT_CONFIGURED`
|
||||
- `APNS_TOKEN_NOT_AVAILABLE`
|
||||
- `APNS_TOKEN_FAILED`
|
||||
- `FCM_TOKEN_FAILED`
|
||||
- `BIOMETRIC_UNAVAILABLE`
|
||||
- `AUTHENTICATION_ERROR`
|
||||
+141
-19
@@ -1,7 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
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)"
|
||||
TIMESTAMP="$(date '+%Y%m%d-%H%M%S')"
|
||||
@@ -23,8 +25,9 @@ Usage:
|
||||
$(basename "$0") /path/to/App.ipa
|
||||
|
||||
Checks:
|
||||
1. embedded.mobileprovision contains ${ENTITLEMENT_KEY}=true
|
||||
2. App executable appears to contain ${ENTITLEMENT_KEY}=true
|
||||
1. embedded.mobileprovision contains ${HOTSPOT_ENTITLEMENT_KEY}=true
|
||||
2. embedded.mobileprovision contains ${APNS_ENTITLEMENT_KEY}=development|production
|
||||
3. App executable appears to contain both signed entitlements
|
||||
|
||||
Notes:
|
||||
Linux cannot run Apple's codesign tool. The executable check is a best-effort
|
||||
@@ -59,22 +62,32 @@ has_command() {
|
||||
extract_plist_from_binary() {
|
||||
local source_file="$1"
|
||||
local target_file="$2"
|
||||
local needle="${3:-}"
|
||||
|
||||
python3 - "$source_file" "$target_file" <<'PY'
|
||||
python3 - "$source_file" "$target_file" "$needle" <<'PY'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
source = Path(sys.argv[1])
|
||||
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()
|
||||
|
||||
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")
|
||||
if start < 0:
|
||||
start = data.find(b"<plist")
|
||||
end = data.find(b"</plist>", start if start >= 0 else 0)
|
||||
|
||||
if start < 0:
|
||||
sys.exit(2)
|
||||
|
||||
end = data.find(b"</plist>", start)
|
||||
if end < 0:
|
||||
sys.exit(3)
|
||||
|
||||
@@ -97,7 +110,28 @@ key = sys.argv[2]
|
||||
data = path.read_bytes()
|
||||
|
||||
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:
|
||||
print("true")
|
||||
sys.exit(0)
|
||||
@@ -123,6 +157,63 @@ except Exception:
|
||||
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() {
|
||||
local app_dir="$1"
|
||||
local info_plist="${app_dir}/Info.plist"
|
||||
@@ -157,7 +248,8 @@ print_section() {
|
||||
echo "==== $* ===="
|
||||
}
|
||||
|
||||
echo "iOS Hotspot Configuration Signature Check"
|
||||
echo "iOS Hotspot/APNs Signature Check"
|
||||
echo "Checker version: ${CHECK_SCRIPT_VERSION}"
|
||||
echo "Started: $(date -Is)"
|
||||
echo "Script: ${BASH_SOURCE[0]}"
|
||||
echo "Log: ${LOG_FILE}"
|
||||
@@ -246,43 +338,71 @@ if [[ "${PROFILE_EXTRACTED}" -eq 0 ]]; then
|
||||
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=$?
|
||||
echo "Profile entitlement value: ${PROFILE_RESULT}"
|
||||
echo "Profile Hotspot entitlement value: ${PROFILE_RESULT}"
|
||||
if [[ "${PROFILE_STATUS}" -eq 0 ]]; then
|
||||
pass "Provisioning profile contains ${ENTITLEMENT_KEY}=true"
|
||||
pass "Provisioning profile contains ${HOTSPOT_ENTITLEMENT_KEY}=true"
|
||||
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
|
||||
|
||||
echo
|
||||
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"
|
||||
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
|
||||
pass "Executable contains entitlement key string: ${ENTITLEMENT_KEY}"
|
||||
if grep -a -q "${HOTSPOT_ENTITLEMENT_KEY}" "${EXECUTABLE_PATH}"; then
|
||||
pass "Executable contains entitlement key string: ${HOTSPOT_ENTITLEMENT_KEY}"
|
||||
else
|
||||
fail "Executable does not contain entitlement key string. Signed app entitlements likely do not include Hotspot Configuration."
|
||||
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"
|
||||
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}"
|
||||
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=$?
|
||||
echo "Executable entitlement value: ${EXEC_RESULT}"
|
||||
echo "Executable Hotspot entitlement value: ${EXEC_RESULT}"
|
||||
if [[ "${EXEC_STATUS}" -eq 0 ]]; then
|
||||
pass "Executable plist candidate contains ${ENTITLEMENT_KEY}=true"
|
||||
pass "Executable plist candidate contains ${HOTSPOT_ENTITLEMENT_KEY}=true"
|
||||
else
|
||||
warn "Executable contains entitlement key, but extracted plist candidate did not prove value=true."
|
||||
warn "Use macOS codesign for authoritative validation."
|
||||
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 "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
|
||||
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."
|
||||
@@ -290,7 +410,9 @@ fi
|
||||
|
||||
print_section "Summary"
|
||||
pass "Provisioning profile Hotspot entitlement: true"
|
||||
pass "Provisioning profile APNs entitlement: ${PROFILE_APNS_RESULT}"
|
||||
pass "Executable Hotspot entitlement key: present"
|
||||
pass "Executable APNs entitlement key: present"
|
||||
echo
|
||||
echo "Full log written to:"
|
||||
echo " ${LOG_FILE}"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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.",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
|
||||
+7
-1
@@ -2,7 +2,7 @@
|
||||
<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.11">
|
||||
version="0.1.12">
|
||||
<name>bluememe-utils</name>
|
||||
<description>Cordova utilities for WiFi, Firebase Cloud Messaging, and biometric unlock.</description>
|
||||
<license>MIT</license>
|
||||
@@ -104,6 +104,12 @@
|
||||
<config-file target="*-Release.plist" parent="com.apple.developer.networking.HotspotConfiguration">
|
||||
<true />
|
||||
</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" />
|
||||
<source-file src="src/ios/CDVBluememeUtils.m" />
|
||||
|
||||
+266
-5
@@ -8,12 +8,18 @@
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
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 ()
|
||||
@property (nonatomic, copy) NSString *notificationCallbackId;
|
||||
@property (nonatomic, strong) NSDictionary *initialNotification;
|
||||
@property (nonatomic, weak) id<UNUserNotificationCenterDelegate> previousNotificationDelegate;
|
||||
@property (nonatomic, strong) NSData *apnsDeviceToken;
|
||||
@property (nonatomic, strong) NSError *apnsRegistrationError;
|
||||
@property (nonatomic, strong) CDVInvokedUrlCommand *pendingFirebaseTokenCommand;
|
||||
@end
|
||||
|
||||
@implementation CDVBluememeUtils
|
||||
@@ -29,6 +35,20 @@ static NSString * const BluememeUtilsPluginVersion = @"0.1.11";
|
||||
self.previousNotificationDelegate = center.delegate;
|
||||
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
|
||||
@@ -159,14 +179,31 @@ static NSString * const BluememeUtilsPluginVersion = @"0.1.11";
|
||||
return;
|
||||
}
|
||||
[[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 {
|
||||
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": @[]}];
|
||||
[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),
|
||||
@"soundEnabled": @((settings.types & UIUserNotificationTypeSound) != 0),
|
||||
@"badgeEnabled": @((settings.types & UIUserNotificationTypeBadge) != 0),
|
||||
@"registeredForRemoteNotifications": @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]),
|
||||
@"apnsTokenAvailable": @(self.apnsDeviceToken.length > 0),
|
||||
@"missing": enabled ? @[] : @[@"notification"]
|
||||
}];
|
||||
}
|
||||
@@ -204,13 +243,102 @@ static NSString * const BluememeUtilsPluginVersion = @"0.1.11";
|
||||
}
|
||||
[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) {
|
||||
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.")];
|
||||
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;
|
||||
}
|
||||
[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}];
|
||||
});
|
||||
}];
|
||||
@@ -716,6 +844,8 @@ static NSString * const BluememeUtilsPluginVersion = @"0.1.11";
|
||||
@"alertEnabled": @(alertEnabled),
|
||||
@"soundEnabled": @(soundEnabled),
|
||||
@"badgeEnabled": @(badgeEnabled),
|
||||
@"registeredForRemoteNotifications": @([[UIApplication sharedApplication] isRegisteredForRemoteNotifications]),
|
||||
@"apnsTokenAvailable": @(self.apnsDeviceToken.length > 0),
|
||||
@"missing": missing
|
||||
};
|
||||
}
|
||||
@@ -746,6 +876,137 @@ static NSString * const BluememeUtilsPluginVersion = @"0.1.11";
|
||||
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
|
||||
{
|
||||
if ([FIRApp defaultApp] != nil) {
|
||||
|
||||
Reference in new issue
Block a user