856 lines
34 KiB
Objective-C
856 lines
34 KiB
Objective-C
#import "CDVBluememeUtils.h"
|
|
|
|
#import <FirebaseCore/FirebaseCore.h>
|
|
#import <LocalAuthentication/LocalAuthentication.h>
|
|
#import <NetworkExtension/NetworkExtension.h>
|
|
#import <TargetConditionals.h>
|
|
#import <UIKit/UIKit.h>
|
|
|
|
static NSString * const BluememeDeviceKeyDefaultsKey = @"bluememe_utils_device_key";
|
|
|
|
@interface CDVBluememeUtils ()
|
|
@property (nonatomic, copy) NSString *notificationCallbackId;
|
|
@property (nonatomic, strong) NSDictionary *initialNotification;
|
|
@property (nonatomic, weak) id<UNUserNotificationCenterDelegate> previousNotificationDelegate;
|
|
@end
|
|
|
|
@implementation CDVBluememeUtils
|
|
|
|
- (void)pluginInitialize
|
|
{
|
|
if ([self ensureFirebaseConfigured:nil]) {
|
|
[FIRMessaging messaging].delegate = self;
|
|
}
|
|
|
|
if (@available(iOS 10.0, *)) {
|
|
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
|
self.previousNotificationDelegate = center.delegate;
|
|
center.delegate = self;
|
|
}
|
|
}
|
|
|
|
- (void)saveWifi:(CDVInvokedUrlCommand *)command
|
|
{
|
|
[self applyWifiCommand:command defaultJoinOnce:NO];
|
|
}
|
|
|
|
- (void)connectWifi:(CDVInvokedUrlCommand *)command
|
|
{
|
|
[self applyWifiCommand:command defaultJoinOnce:YES];
|
|
}
|
|
|
|
- (void)getDeviceInfo:(CDVInvokedUrlCommand *)command
|
|
{
|
|
NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
|
|
NSString *bundleIdentifier = [self safeString:[[NSBundle mainBundle] bundleIdentifier]];
|
|
NSString *appVersion = [self safeString:[infoDictionary objectForKey:@"CFBundleShortVersionString"]];
|
|
NSString *buildNumber = [self safeString:[infoDictionary objectForKey:@"CFBundleVersion"]];
|
|
UIDevice *device = [UIDevice currentDevice];
|
|
NSLocale *locale = [NSLocale currentLocale];
|
|
NSString *localeIdentifier = [self safeString:[locale localeIdentifier]];
|
|
NSString *timeZone = [self safeString:[[NSTimeZone localTimeZone] name]];
|
|
|
|
NSDictionary *result = @{
|
|
@"deviceKey": [self getOrCreateDeviceKey],
|
|
@"platform": @"iOS",
|
|
@"appIdentifier": bundleIdentifier,
|
|
@"appVersion": appVersion,
|
|
@"buildNumber": buildNumber,
|
|
@"osVersion": [self safeString:device.systemVersion],
|
|
@"systemName": [self safeString:device.systemName],
|
|
@"deviceModel": [self safeString:device.model],
|
|
@"model": [self safeString:device.model],
|
|
@"locale": localeIdentifier,
|
|
@"timeZone": timeZone
|
|
};
|
|
[self sendSuccessForCommand:command dictionary:result];
|
|
}
|
|
|
|
- (void)removeWifi:(CDVInvokedUrlCommand *)command
|
|
{
|
|
NSDictionary *options = [self firstDictionaryArgument:command];
|
|
NSString *ssid = [self stringFromDictionary:options key:@"ssid"];
|
|
if (ssid.length == 0) {
|
|
[self sendErrorForCommand:command code:@"INVALID_WIFI_OPTIONS" message:@"ssid is required."];
|
|
return;
|
|
}
|
|
|
|
if (@available(iOS 11.0, *)) {
|
|
[[NEHotspotConfigurationManager sharedManager] removeConfigurationForSSID:ssid];
|
|
[self sendSuccessForCommand:command dictionary:@{@"ssid": ssid, @"removed": @YES}];
|
|
} else {
|
|
[self sendErrorForCommand:command code:@"UNSUPPORTED_IOS_VERSION" message:@"WiFi configuration requires iOS 11 or newer."];
|
|
}
|
|
}
|
|
|
|
- (void)disconnectWifi:(CDVInvokedUrlCommand *)command
|
|
{
|
|
[self sendErrorForCommand:command code:@"UNSUPPORTED_PLATFORM_ACTION" message:@"iOS does not expose a public API for disconnecting WiFi from third-party apps."];
|
|
}
|
|
|
|
- (void)requestWifiPermission:(CDVInvokedUrlCommand *)command
|
|
{
|
|
[self sendSuccessForCommand:command dictionary:@{@"type": @"wifi", @"granted": @YES, @"missing": @[]}];
|
|
}
|
|
|
|
- (void)isBiometricAvailable:(CDVInvokedUrlCommand *)command
|
|
{
|
|
[self sendSuccessForCommand:command dictionary:[self biometricAvailability]];
|
|
}
|
|
|
|
- (void)unlockWithBiometrics:(CDVInvokedUrlCommand *)command
|
|
{
|
|
NSDictionary *options = [self firstDictionaryArgument:command];
|
|
LAContext *context = [[LAContext alloc] init];
|
|
NSString *fallbackTitle = [self stringFromDictionary:options key:@"fallbackTitle"];
|
|
NSString *cancelTitle = [self stringFromDictionary:options key:@"cancelTitle"];
|
|
if (fallbackTitle.length > 0) {
|
|
context.localizedFallbackTitle = fallbackTitle;
|
|
}
|
|
if (cancelTitle.length > 0) {
|
|
context.localizedCancelTitle = cancelTitle;
|
|
}
|
|
|
|
BOOL allowPasscode = [self boolFromDictionary:options key:@"allowDeviceCredential" defaultValue:YES];
|
|
LAPolicy policy = allowPasscode ? LAPolicyDeviceOwnerAuthentication : LAPolicyDeviceOwnerAuthenticationWithBiometrics;
|
|
NSError *availabilityError = nil;
|
|
if (![context canEvaluatePolicy:policy error:&availabilityError]) {
|
|
[self sendErrorForCommand:command code:@"BIOMETRIC_UNAVAILABLE" message:(availabilityError.localizedDescription ? availabilityError.localizedDescription : @"Biometric authentication is unavailable.")];
|
|
return;
|
|
}
|
|
|
|
NSString *reason = [self stringFromDictionary:options key:@"reason"];
|
|
if (reason.length == 0) {
|
|
reason = @"Unlock";
|
|
}
|
|
|
|
[context evaluatePolicy:policy localizedReason:reason reply:^(BOOL success, NSError *error) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (success) {
|
|
NSDictionary *result = @{@"unlocked": @YES, @"biometryType": [self biometryTypeForContext:context]};
|
|
[self sendSuccessForCommand:command dictionary:result];
|
|
NSMutableDictionary *event = [result mutableCopy];
|
|
event[@"type"] = @"unlock";
|
|
[self sendEvent:event];
|
|
} else {
|
|
[self sendErrorForCommand:command code:@"AUTHENTICATION_ERROR" message:(error.localizedDescription ? error.localizedDescription : @"Authentication failed.")];
|
|
}
|
|
});
|
|
}];
|
|
}
|
|
|
|
- (void)requestPushPermission:(CDVInvokedUrlCommand *)command
|
|
{
|
|
NSError *firebaseError = nil;
|
|
if (![self ensureFirebaseConfigured:&firebaseError]) {
|
|
[self sendErrorForCommand:command code:@"FIREBASE_NOT_CONFIGURED" message:(firebaseError.localizedDescription ? firebaseError.localizedDescription : @"Firebase is not configured.")];
|
|
return;
|
|
}
|
|
[FIRMessaging messaging].delegate = self;
|
|
|
|
if (@available(iOS 10.0, *)) {
|
|
UNAuthorizationOptions options = UNAuthorizationOptionAlert | UNAuthorizationOptionBadge | UNAuthorizationOptionSound;
|
|
[[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError *error) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (error != nil) {
|
|
[self sendErrorForCommand:command code:@"PUSH_PERMISSION_FAILED" message:error.localizedDescription];
|
|
return;
|
|
}
|
|
[[UIApplication sharedApplication] registerForRemoteNotifications];
|
|
[self sendSuccessForCommand:command dictionary:@{@"type": @"notification", @"granted": @(granted), @"missing": granted ? @[] : @[@"notification"]}];
|
|
});
|
|
}];
|
|
} else {
|
|
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound) categories:nil];
|
|
[[UIApplication sharedApplication] registerUserNotificationSettings:settings];
|
|
[[UIApplication sharedApplication] registerForRemoteNotifications];
|
|
[self sendSuccessForCommand:command dictionary:@{@"type": @"notification", @"granted": @YES, @"missing": @[]}];
|
|
}
|
|
}
|
|
|
|
- (void)getPushPermissionStatus:(CDVInvokedUrlCommand *)command
|
|
{
|
|
if (@available(iOS 10.0, *)) {
|
|
[[UNUserNotificationCenter currentNotificationCenter] getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *settings) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self sendSuccessForCommand:command dictionary:[self dictionaryFromNotificationSettings:settings]];
|
|
});
|
|
}];
|
|
} else {
|
|
UIUserNotificationSettings *settings = [[UIApplication sharedApplication] currentUserNotificationSettings];
|
|
BOOL enabled = settings.types != UIUserNotificationTypeNone;
|
|
[self sendSuccessForCommand:command dictionary:@{
|
|
@"type": @"notification",
|
|
@"platform": @"iOS",
|
|
@"enabled": @(enabled),
|
|
@"granted": @(enabled),
|
|
@"authorizationStatus": enabled ? @"authorized" : @"denied",
|
|
@"alertEnabled": @((settings.types & UIUserNotificationTypeAlert) != 0),
|
|
@"soundEnabled": @((settings.types & UIUserNotificationTypeSound) != 0),
|
|
@"badgeEnabled": @((settings.types & UIUserNotificationTypeBadge) != 0),
|
|
@"missing": enabled ? @[] : @[@"notification"]
|
|
}];
|
|
}
|
|
}
|
|
|
|
- (void)getFirebaseToken:(CDVInvokedUrlCommand *)command
|
|
{
|
|
NSError *firebaseError = nil;
|
|
if (![self ensureFirebaseConfigured:&firebaseError]) {
|
|
[self sendErrorForCommand:command code:@"FIREBASE_NOT_CONFIGURED" message:(firebaseError.localizedDescription ? firebaseError.localizedDescription : @"Firebase is not configured.")];
|
|
return;
|
|
}
|
|
[FIRMessaging messaging].delegate = self;
|
|
|
|
[[FIRMessaging messaging] tokenWithCompletion:^(NSString *token, NSError *error) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (error != nil || token.length == 0) {
|
|
[self sendErrorForCommand:command code:@"FCM_TOKEN_FAILED" message:(error.localizedDescription ? error.localizedDescription : @"Failed to get Firebase token.")];
|
|
return;
|
|
}
|
|
[self sendSuccessForCommand:command dictionary:@{@"token": token}];
|
|
[self sendEvent:@{@"type": @"token", @"token": token}];
|
|
});
|
|
}];
|
|
}
|
|
|
|
- (void)subscribeToTopic:(CDVInvokedUrlCommand *)command
|
|
{
|
|
NSString *topic = [command.arguments firstObject];
|
|
if (![topic isKindOfClass:[NSString class]] || topic.length == 0) {
|
|
[self sendErrorForCommand:command code:@"INVALID_TOPIC" message:@"topic is required."];
|
|
return;
|
|
}
|
|
NSError *firebaseError = nil;
|
|
if (![self ensureFirebaseConfigured:&firebaseError]) {
|
|
[self sendErrorForCommand:command code:@"FIREBASE_NOT_CONFIGURED" message:(firebaseError.localizedDescription ? firebaseError.localizedDescription : @"Firebase is not configured.")];
|
|
return;
|
|
}
|
|
[[FIRMessaging messaging] subscribeToTopic:topic completion:^(NSError *error) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (error != nil) {
|
|
[self sendErrorForCommand:command code:@"FIREBASE_TASK_FAILED" message:error.localizedDescription];
|
|
} else {
|
|
[self sendSuccessForCommand:command dictionary:nil];
|
|
}
|
|
});
|
|
}];
|
|
}
|
|
|
|
- (void)unsubscribeFromTopic:(CDVInvokedUrlCommand *)command
|
|
{
|
|
NSString *topic = [command.arguments firstObject];
|
|
if (![topic isKindOfClass:[NSString class]] || topic.length == 0) {
|
|
[self sendErrorForCommand:command code:@"INVALID_TOPIC" message:@"topic is required."];
|
|
return;
|
|
}
|
|
NSError *firebaseError = nil;
|
|
if (![self ensureFirebaseConfigured:&firebaseError]) {
|
|
[self sendErrorForCommand:command code:@"FIREBASE_NOT_CONFIGURED" message:(firebaseError.localizedDescription ? firebaseError.localizedDescription : @"Firebase is not configured.")];
|
|
return;
|
|
}
|
|
[[FIRMessaging messaging] unsubscribeFromTopic:topic completion:^(NSError *error) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (error != nil) {
|
|
[self sendErrorForCommand:command code:@"FIREBASE_TASK_FAILED" message:error.localizedDescription];
|
|
} else {
|
|
[self sendSuccessForCommand:command dictionary:nil];
|
|
}
|
|
});
|
|
}];
|
|
}
|
|
|
|
- (void)getInitialNotification:(CDVInvokedUrlCommand *)command
|
|
{
|
|
[self sendSuccessForCommand:command dictionary:self.initialNotification];
|
|
self.initialNotification = nil;
|
|
}
|
|
|
|
- (void)startNotificationListener:(CDVInvokedUrlCommand *)command
|
|
{
|
|
self.notificationCallbackId = command.callbackId;
|
|
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_NO_RESULT];
|
|
[result setKeepCallbackAsBool:YES];
|
|
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
|
|
}
|
|
|
|
- (void)stopNotificationListener:(CDVInvokedUrlCommand *)command
|
|
{
|
|
self.notificationCallbackId = nil;
|
|
[self sendSuccessForCommand:command dictionary:nil];
|
|
}
|
|
|
|
- (void)applyWifiCommand:(CDVInvokedUrlCommand *)command defaultJoinOnce:(BOOL)defaultJoinOnce
|
|
{
|
|
if (![NSThread isMainThread]) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self applyWifiCommand:command defaultJoinOnce:defaultJoinOnce];
|
|
});
|
|
return;
|
|
}
|
|
|
|
NSDictionary *options = [self firstDictionaryArgument:command];
|
|
NSString *ssid = [self stringFromDictionary:options key:@"ssid"];
|
|
NSString *password = [self stringFromDictionary:options key:@"password"];
|
|
NSString *security = [[self stringFromDictionary:options key:@"security"] lowercaseString];
|
|
if (security.length == 0) {
|
|
security = password.length == 0 ? @"open" : @"wpa2";
|
|
}
|
|
BOOL isOpen = [security isEqualToString:@"open"] || [security isEqualToString:@"none"];
|
|
BOOL isWep = [security isEqualToString:@"wep"] || [self boolFromDictionary:options key:@"isWep" defaultValue:NO];
|
|
BOOL joinOnce = [self boolFromDictionary:options key:@"joinOnce" defaultValue:defaultJoinOnce];
|
|
BOOL hiddenSsid = [self boolFromDictionary:options key:@"hiddenSsid" defaultValue:NO];
|
|
BOOL resetExisting = [self boolFromDictionary:options key:@"resetExisting" defaultValue:YES];
|
|
|
|
if (ssid.length == 0) {
|
|
[self sendErrorForCommand:command code:@"INVALID_WIFI_OPTIONS" message:@"ssid is required."];
|
|
return;
|
|
}
|
|
|
|
if (!isOpen && password.length == 0) {
|
|
[self sendErrorForCommand:command code:@"INVALID_WIFI_OPTIONS" message:@"password is required for secured WiFi networks."];
|
|
return;
|
|
}
|
|
|
|
if (!isOpen && !isWep && (password.length < 8 || password.length > 63)) {
|
|
[self sendErrorForCommand:command code:@"INVALID_WIFI_OPTIONS" message:@"WPA/WPA2 passphrase must be 8 to 63 characters."];
|
|
return;
|
|
}
|
|
|
|
#if TARGET_OS_SIMULATOR
|
|
[self sendErrorForCommand:command
|
|
code:@"UNSUPPORTED_IOS_SIMULATOR"
|
|
message:@"iOS WiFi configuration requires a physical device."];
|
|
return;
|
|
#endif
|
|
|
|
if (@available(iOS 11.0, *)) {
|
|
NSDictionary *context = @{
|
|
@"ssid": ssid,
|
|
@"security": security,
|
|
@"passwordLength": @(password.length),
|
|
@"joinOnce": @(joinOnce),
|
|
@"hiddenSsid": @(hiddenSsid),
|
|
@"resetExisting": @(resetExisting)
|
|
};
|
|
[self prepareAndApplyWifiConfigurationForCommand:command
|
|
ssid:ssid
|
|
password:password
|
|
isOpen:isOpen
|
|
isWep:isWep
|
|
joinOnce:joinOnce
|
|
hiddenSsid:hiddenSsid
|
|
resetExisting:resetExisting
|
|
context:context];
|
|
} else {
|
|
[self sendErrorForCommand:command code:@"UNSUPPORTED_IOS_VERSION" message:@"WiFi configuration requires iOS 11 or newer."];
|
|
}
|
|
}
|
|
|
|
- (void)prepareAndApplyWifiConfigurationForCommand:(CDVInvokedUrlCommand *)command
|
|
ssid:(NSString *)ssid
|
|
password:(NSString *)password
|
|
isOpen:(BOOL)isOpen
|
|
isWep:(BOOL)isWep
|
|
joinOnce:(BOOL)joinOnce
|
|
hiddenSsid:(BOOL)hiddenSsid
|
|
resetExisting:(BOOL)resetExisting
|
|
context:(NSDictionary *)context API_AVAILABLE(ios(11.0))
|
|
{
|
|
NEHotspotConfigurationManager *manager = [NEHotspotConfigurationManager sharedManager];
|
|
if (!resetExisting) {
|
|
[self applyWifiConfigurationForCommand:command
|
|
manager:manager
|
|
ssid:ssid
|
|
password:password
|
|
isOpen:isOpen
|
|
isWep:isWep
|
|
joinOnce:joinOnce
|
|
hiddenSsid:hiddenSsid
|
|
context:context];
|
|
return;
|
|
}
|
|
|
|
[manager getConfiguredSSIDsWithCompletionHandler:^(NSArray<NSString *> *configuredSSIDs) {
|
|
BOOL configuredByApp = [configuredSSIDs containsObject:ssid];
|
|
NSMutableDictionary *updatedContext = [context mutableCopy];
|
|
updatedContext[@"configuredByApp"] = @(configuredByApp);
|
|
|
|
void (^apply)(void) = ^{
|
|
[self applyWifiConfigurationForCommand:command
|
|
manager:manager
|
|
ssid:ssid
|
|
password:password
|
|
isOpen:isOpen
|
|
isWep:isWep
|
|
joinOnce:joinOnce
|
|
hiddenSsid:hiddenSsid
|
|
context:updatedContext];
|
|
};
|
|
|
|
if (configuredByApp) {
|
|
[manager removeConfigurationForSSID:ssid];
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(500 * NSEC_PER_MSEC)), dispatch_get_main_queue(), apply);
|
|
} else {
|
|
dispatch_async(dispatch_get_main_queue(), apply);
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)applyWifiConfigurationForCommand:(CDVInvokedUrlCommand *)command
|
|
manager:(NEHotspotConfigurationManager *)manager
|
|
ssid:(NSString *)ssid
|
|
password:(NSString *)password
|
|
isOpen:(BOOL)isOpen
|
|
isWep:(BOOL)isWep
|
|
joinOnce:(BOOL)joinOnce
|
|
hiddenSsid:(BOOL)hiddenSsid
|
|
context:(NSDictionary *)context API_AVAILABLE(ios(11.0))
|
|
{
|
|
NEHotspotConfiguration *configuration = nil;
|
|
if (isOpen) {
|
|
configuration = [[NEHotspotConfiguration alloc] initWithSSID:ssid];
|
|
} else {
|
|
configuration = [[NEHotspotConfiguration alloc] initWithSSID:ssid passphrase:password isWEP:isWep];
|
|
}
|
|
if (@available(iOS 13.0, *)) {
|
|
configuration.hidden = hiddenSsid;
|
|
}
|
|
configuration.joinOnce = joinOnce;
|
|
|
|
[manager applyConfiguration:configuration completionHandler:^(NSError *error) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (error != nil && error.code != NEHotspotConfigurationErrorAlreadyAssociated) {
|
|
[self sendWifiApplyErrorForCommand:command error:error context:context];
|
|
return;
|
|
}
|
|
NSDictionary *result = @{
|
|
@"ssid": ssid,
|
|
@"platformMode": @"hotspotConfiguration",
|
|
@"connected": @YES,
|
|
@"saved": @(!joinOnce),
|
|
@"alreadyAssociated": @(error != nil && error.code == NEHotspotConfigurationErrorAlreadyAssociated)
|
|
};
|
|
[self sendSuccessForCommand:command dictionary:result];
|
|
});
|
|
}];
|
|
}
|
|
|
|
- (void)sendWifiApplyErrorForCommand:(CDVInvokedUrlCommand *)command error:(NSError *)error context:(NSDictionary *)context
|
|
{
|
|
NSMutableDictionary *details = [NSMutableDictionary dictionary];
|
|
if (context != nil) {
|
|
[details addEntriesFromDictionary:context];
|
|
}
|
|
if (error.domain.length > 0) {
|
|
details[@"nativeDomain"] = error.domain;
|
|
}
|
|
details[@"nativeCode"] = @(error.code);
|
|
|
|
NSString *nativeError = [self hotspotConfigurationErrorNameForCode:error.code];
|
|
if (nativeError.length > 0) {
|
|
details[@"nativeError"] = nativeError;
|
|
}
|
|
if (error.localizedFailureReason.length > 0) {
|
|
details[@"nativeFailureReason"] = error.localizedFailureReason;
|
|
}
|
|
if (error.localizedRecoverySuggestion.length > 0) {
|
|
details[@"nativeRecoverySuggestion"] = error.localizedRecoverySuggestion;
|
|
}
|
|
|
|
[self sendErrorForCommand:command
|
|
code:@"WIFI_CONNECT_FAILED"
|
|
message:[self wifiApplyErrorMessageForError:error]
|
|
details:details];
|
|
}
|
|
|
|
- (NSString *)wifiApplyErrorMessageForError:(NSError *)error
|
|
{
|
|
if (![error.domain isEqualToString:NEHotspotConfigurationErrorDomain]) {
|
|
return error.localizedDescription.length > 0 ? error.localizedDescription : @"Unable to apply WiFi configuration.";
|
|
}
|
|
|
|
switch (error.code) {
|
|
case 1:
|
|
return @"Invalid WiFi SSID.";
|
|
case 2:
|
|
return @"Invalid WPA/WPA2/WPA3 passphrase.";
|
|
case 3:
|
|
return @"Invalid WEP passphrase.";
|
|
case 4:
|
|
return @"Invalid EAP settings.";
|
|
case 5:
|
|
return @"Invalid Hotspot 2.0 settings.";
|
|
case 6:
|
|
return @"Invalid Hotspot 2.0 domain name.";
|
|
case 7:
|
|
return @"The user denied the iOS WiFi connection prompt.";
|
|
case 8:
|
|
return @"iOS returned NEHotspotConfigurationErrorInternal. Check that the App ID, entitlements, and provisioning profile include Hotspot Configuration, then test on a physical device.";
|
|
case 9:
|
|
return @"Another iOS WiFi configuration request is already pending.";
|
|
case 10:
|
|
return @"iOS system WiFi configuration failed.";
|
|
case 11:
|
|
return @"iOS returned an unknown WiFi configuration error.";
|
|
case 12:
|
|
return @"joinOnce is not supported for this WiFi configuration.";
|
|
case 14:
|
|
return @"The app must be in the foreground to configure WiFi.";
|
|
case 15:
|
|
return @"Invalid SSID prefix.";
|
|
default:
|
|
return error.localizedDescription.length > 0 ? error.localizedDescription : @"Unable to apply WiFi configuration.";
|
|
}
|
|
}
|
|
|
|
- (NSString *)hotspotConfigurationErrorNameForCode:(NSInteger)code
|
|
{
|
|
switch (code) {
|
|
case 0:
|
|
return @"invalid";
|
|
case 1:
|
|
return @"invalidSSID";
|
|
case 2:
|
|
return @"invalidWPAPassphrase";
|
|
case 3:
|
|
return @"invalidWEPPassphrase";
|
|
case 4:
|
|
return @"invalidEAPSettings";
|
|
case 5:
|
|
return @"invalidHS20Settings";
|
|
case 6:
|
|
return @"invalidHS20DomainName";
|
|
case 7:
|
|
return @"userDenied";
|
|
case 8:
|
|
return @"internal";
|
|
case 9:
|
|
return @"pending";
|
|
case 10:
|
|
return @"systemConfiguration";
|
|
case 11:
|
|
return @"unknown";
|
|
case 12:
|
|
return @"joinOnceNotSupported";
|
|
case 13:
|
|
return @"alreadyAssociated";
|
|
case 14:
|
|
return @"applicationIsNotInForeground";
|
|
case 15:
|
|
return @"invalidSSIDPrefix";
|
|
default:
|
|
return @"";
|
|
}
|
|
}
|
|
|
|
- (NSDictionary *)biometricAvailability
|
|
{
|
|
LAContext *context = [[LAContext alloc] init];
|
|
NSError *error = nil;
|
|
BOOL available = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error];
|
|
NSString *type = [self biometryTypeForContext:context];
|
|
if (available) {
|
|
return @{@"available": @YES, @"biometryType": type, @"code": @"OK", @"message": @"Biometric authentication is available."};
|
|
}
|
|
|
|
BOOL passcodeAvailable = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:nil];
|
|
return @{
|
|
@"available": @NO,
|
|
@"biometryType": type,
|
|
@"passcodeAvailable": @(passcodeAvailable),
|
|
@"code": @"BIOMETRIC_UNAVAILABLE",
|
|
@"message": error.localizedDescription ? error.localizedDescription : @"Biometric authentication is unavailable."
|
|
};
|
|
}
|
|
|
|
- (NSString *)biometryTypeForContext:(LAContext *)context
|
|
{
|
|
if (@available(iOS 11.0, *)) {
|
|
if (context.biometryType == LABiometryTypeFaceID) {
|
|
return @"face";
|
|
}
|
|
if (context.biometryType == LABiometryTypeTouchID) {
|
|
return @"touch";
|
|
}
|
|
if (@available(iOS 11.2, *)) {
|
|
if (context.biometryType == LABiometryTypeNone) {
|
|
return @"none";
|
|
}
|
|
}
|
|
return @"biometric";
|
|
}
|
|
return @"touch";
|
|
}
|
|
|
|
- (NSDictionary *)dictionaryFromNotificationSettings:(UNNotificationSettings *)settings API_AVAILABLE(ios(10.0))
|
|
{
|
|
NSString *status = [self authorizationStatusName:settings.authorizationStatus];
|
|
BOOL enabled = settings.authorizationStatus == UNAuthorizationStatusAuthorized;
|
|
if (@available(iOS 12.0, *)) {
|
|
enabled = enabled || settings.authorizationStatus == UNAuthorizationStatusProvisional;
|
|
}
|
|
if (@available(iOS 14.0, *)) {
|
|
enabled = enabled || settings.authorizationStatus == UNAuthorizationStatusEphemeral;
|
|
}
|
|
|
|
BOOL alertEnabled = settings.alertSetting == UNNotificationSettingEnabled;
|
|
BOOL soundEnabled = settings.soundSetting == UNNotificationSettingEnabled;
|
|
BOOL badgeEnabled = settings.badgeSetting == UNNotificationSettingEnabled;
|
|
NSArray *missing = enabled ? @[] : @[@"notification"];
|
|
|
|
return @{
|
|
@"type": @"notification",
|
|
@"platform": @"iOS",
|
|
@"enabled": @(enabled),
|
|
@"granted": @(enabled),
|
|
@"authorizationStatus": status,
|
|
@"alertEnabled": @(alertEnabled),
|
|
@"soundEnabled": @(soundEnabled),
|
|
@"badgeEnabled": @(badgeEnabled),
|
|
@"missing": missing
|
|
};
|
|
}
|
|
|
|
- (NSString *)authorizationStatusName:(UNAuthorizationStatus)status API_AVAILABLE(ios(10.0))
|
|
{
|
|
switch (status) {
|
|
case UNAuthorizationStatusNotDetermined:
|
|
return @"notDetermined";
|
|
case UNAuthorizationStatusDenied:
|
|
return @"denied";
|
|
case UNAuthorizationStatusAuthorized:
|
|
return @"authorized";
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (@available(iOS 12.0, *)) {
|
|
if (status == UNAuthorizationStatusProvisional) {
|
|
return @"provisional";
|
|
}
|
|
}
|
|
if (@available(iOS 14.0, *)) {
|
|
if (status == UNAuthorizationStatusEphemeral) {
|
|
return @"ephemeral";
|
|
}
|
|
}
|
|
return @"unknown";
|
|
}
|
|
|
|
- (BOOL)ensureFirebaseConfigured:(NSError **)error
|
|
{
|
|
if ([FIRApp defaultApp] != nil) {
|
|
return YES;
|
|
}
|
|
|
|
NSString *plistPath = [[NSBundle mainBundle] pathForResource:@"GoogleService-Info" ofType:@"plist"];
|
|
if (plistPath.length > 0) {
|
|
[FIRApp configure];
|
|
return [FIRApp defaultApp] != nil;
|
|
}
|
|
|
|
NSString *googleAppId = [self infoStringForKey:@"BLUEMEME_FIREBASE_GOOGLE_APP_ID"];
|
|
NSString *gcmSenderId = [self infoStringForKey:@"BLUEMEME_FIREBASE_GCM_SENDER_ID"];
|
|
NSString *apiKey = [self infoStringForKey:@"BLUEMEME_FIREBASE_API_KEY"];
|
|
NSString *projectId = [self infoStringForKey:@"BLUEMEME_FIREBASE_PROJECT_ID"];
|
|
NSString *storageBucket = [self infoStringForKey:@"BLUEMEME_FIREBASE_STORAGE_BUCKET"];
|
|
|
|
if (googleAppId.length == 0 || gcmSenderId.length == 0) {
|
|
if (error != nil) {
|
|
*error = [NSError errorWithDomain:@"BluememeUtils" code:1001 userInfo:@{NSLocalizedDescriptionKey: @"Firebase options are missing."}];
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:googleAppId GCMSenderID:gcmSenderId];
|
|
if (apiKey.length > 0) {
|
|
options.APIKey = apiKey;
|
|
}
|
|
if (projectId.length > 0) {
|
|
options.projectID = projectId;
|
|
}
|
|
if (storageBucket.length > 0) {
|
|
options.storageBucket = storageBucket;
|
|
}
|
|
options.bundleID = [[NSBundle mainBundle] bundleIdentifier];
|
|
[FIRApp configureWithOptions:options];
|
|
return [FIRApp defaultApp] != nil;
|
|
}
|
|
|
|
- (void)messaging:(FIRMessaging *)messaging didReceiveRegistrationToken:(NSString *)fcmToken
|
|
{
|
|
if (fcmToken.length == 0) {
|
|
return;
|
|
}
|
|
[self sendEvent:@{@"type": @"token", @"token": fcmToken}];
|
|
}
|
|
|
|
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
|
willPresentNotification:(UNNotification *)notification
|
|
withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler API_AVAILABLE(ios(10.0))
|
|
{
|
|
[self sendEvent:@{@"type": @"notification", @"notification": [self payloadFromNotification:notification]}];
|
|
|
|
if (self.previousNotificationDelegate != nil
|
|
&& self.previousNotificationDelegate != self
|
|
&& [self.previousNotificationDelegate respondsToSelector:@selector(userNotificationCenter:willPresentNotification:withCompletionHandler:)]) {
|
|
[self.previousNotificationDelegate userNotificationCenter:center willPresentNotification:notification withCompletionHandler:completionHandler];
|
|
return;
|
|
}
|
|
|
|
completionHandler(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionSound | UNNotificationPresentationOptionBadge);
|
|
}
|
|
|
|
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
|
didReceiveNotificationResponse:(UNNotificationResponse *)response
|
|
withCompletionHandler:(void (^)(void))completionHandler API_AVAILABLE(ios(10.0))
|
|
{
|
|
NSDictionary *payload = [self payloadFromNotification:response.notification];
|
|
self.initialNotification = payload;
|
|
[self sendEvent:@{@"type": @"notificationOpened", @"notification": payload}];
|
|
|
|
if (self.previousNotificationDelegate != nil
|
|
&& self.previousNotificationDelegate != self
|
|
&& [self.previousNotificationDelegate respondsToSelector:@selector(userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:)]) {
|
|
[self.previousNotificationDelegate userNotificationCenter:center didReceiveNotificationResponse:response withCompletionHandler:completionHandler];
|
|
return;
|
|
}
|
|
|
|
completionHandler();
|
|
}
|
|
|
|
- (NSDictionary *)payloadFromNotification:(UNNotification *)notification API_AVAILABLE(ios(10.0))
|
|
{
|
|
NSMutableDictionary *payload = [NSMutableDictionary dictionary];
|
|
NSDictionary *userInfo = notification.request.content.userInfo ? notification.request.content.userInfo : @{};
|
|
payload[@"data"] = userInfo;
|
|
if (notification.request.identifier.length > 0) {
|
|
payload[@"identifier"] = notification.request.identifier;
|
|
}
|
|
|
|
NSMutableDictionary *apsNotification = [NSMutableDictionary dictionary];
|
|
if (notification.request.content.title.length > 0) {
|
|
apsNotification[@"title"] = notification.request.content.title;
|
|
}
|
|
if (notification.request.content.body.length > 0) {
|
|
apsNotification[@"body"] = notification.request.content.body;
|
|
}
|
|
if (notification.request.content.categoryIdentifier.length > 0) {
|
|
apsNotification[@"category"] = notification.request.content.categoryIdentifier;
|
|
}
|
|
if (apsNotification.count > 0) {
|
|
payload[@"notification"] = apsNotification;
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
- (void)sendEvent:(NSDictionary *)event
|
|
{
|
|
if (self.notificationCallbackId.length == 0 || event == nil) {
|
|
return;
|
|
}
|
|
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:event];
|
|
[result setKeepCallbackAsBool:YES];
|
|
[self.commandDelegate sendPluginResult:result callbackId:self.notificationCallbackId];
|
|
}
|
|
|
|
- (NSDictionary *)firstDictionaryArgument:(CDVInvokedUrlCommand *)command
|
|
{
|
|
id first = [command.arguments firstObject];
|
|
if ([first isKindOfClass:[NSDictionary class]]) {
|
|
return first;
|
|
}
|
|
return @{};
|
|
}
|
|
|
|
- (NSString *)stringFromDictionary:(NSDictionary *)dictionary key:(NSString *)key
|
|
{
|
|
id value = dictionary[key];
|
|
if ([value isKindOfClass:[NSString class]]) {
|
|
return value;
|
|
}
|
|
if ([value respondsToSelector:@selector(stringValue)]) {
|
|
return [value stringValue];
|
|
}
|
|
return @"";
|
|
}
|
|
|
|
- (NSString *)safeString:(id)value
|
|
{
|
|
if ([value isKindOfClass:[NSString class]]) {
|
|
return value;
|
|
}
|
|
if ([value respondsToSelector:@selector(stringValue)]) {
|
|
return [value stringValue];
|
|
}
|
|
if ([value respondsToSelector:@selector(description)]) {
|
|
return [value description];
|
|
}
|
|
return @"";
|
|
}
|
|
|
|
- (BOOL)boolFromDictionary:(NSDictionary *)dictionary key:(NSString *)key defaultValue:(BOOL)defaultValue
|
|
{
|
|
id value = dictionary[key];
|
|
if ([value isKindOfClass:[NSNumber class]]) {
|
|
return [value boolValue];
|
|
}
|
|
if ([value isKindOfClass:[NSString class]]) {
|
|
NSString *lowercase = [value lowercaseString];
|
|
if ([lowercase isEqualToString:@"true"] || [lowercase isEqualToString:@"1"] || [lowercase isEqualToString:@"yes"]) {
|
|
return YES;
|
|
}
|
|
if ([lowercase isEqualToString:@"false"] || [lowercase isEqualToString:@"0"] || [lowercase isEqualToString:@"no"]) {
|
|
return NO;
|
|
}
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
- (NSString *)infoStringForKey:(NSString *)key
|
|
{
|
|
id value = [[NSBundle mainBundle] objectForInfoDictionaryKey:key];
|
|
if ([value isKindOfClass:[NSString class]]) {
|
|
return value;
|
|
}
|
|
return @"";
|
|
}
|
|
|
|
- (NSString *)getOrCreateDeviceKey
|
|
{
|
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
NSString *existing = [defaults stringForKey:BluememeDeviceKeyDefaultsKey];
|
|
if (existing.length > 0) {
|
|
return existing;
|
|
}
|
|
|
|
NSString *generated = [[NSUUID UUID] UUIDString];
|
|
[defaults setObject:generated forKey:BluememeDeviceKeyDefaultsKey];
|
|
[defaults synchronize];
|
|
return generated;
|
|
}
|
|
|
|
- (void)sendSuccessForCommand:(CDVInvokedUrlCommand *)command dictionary:(NSDictionary *)dictionary
|
|
{
|
|
CDVPluginResult *result = dictionary == nil
|
|
? [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]
|
|
: [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dictionary];
|
|
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
|
|
}
|
|
|
|
- (void)sendErrorForCommand:(CDVInvokedUrlCommand *)command code:(NSString *)code message:(NSString *)message
|
|
{
|
|
[self sendErrorForCommand:command code:code message:message details:nil];
|
|
}
|
|
|
|
- (void)sendErrorForCommand:(CDVInvokedUrlCommand *)command code:(NSString *)code message:(NSString *)message details:(NSDictionary *)details
|
|
{
|
|
NSMutableDictionary *error = details == nil ? [NSMutableDictionary dictionary] : [details mutableCopy];
|
|
error[@"code"] = code ? code : @"ERROR";
|
|
error[@"message"] = message ? message : @"";
|
|
CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:error];
|
|
[self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
|
|
}
|
|
@end
|