add apns supports.

This commit is contained in:
aria committed 2026-04-23 15:38:20 +09:00
1 parent 855d24bc64
commit 21c392db8f
6 files changed
+442 -31

No files matched your search

+1
View File
@@ -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."
+23 -2
View File
@@ -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`
+144 -22
View File
@@ -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()
start = data.find(b"<?xml") needle_index = data.find(needle) if needle else -1
if start < 0: if needle_index >= 0:
start = data.find(b"<plist") 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: 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
View File
@@ -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
View File
@@ -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
View File
@@ -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) {