Прежде всего хочу заметить, что я не опытный iOS разработчик, пишу приложение на React Native.
Суть проблемы:
Если мое приложение был удален, и прибыло уведомление pu sh от FCM, я хотел бы отправить сетевой запрос в ответ на действие пользователя в уведомлении, но это не удалось. Код даже не отправляет запрос, потому что журналов на стороне сервера не существует. А пока все работает нормально, если приложение было свернуто или находится в состоянии переднего плана.
Подробности:
Я тестирую текущую функциональность на iOS 10.3.3 .
Вот код:
AppDelegate.m:
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "AppDelegate.h"
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTLinkingManager.h>
@import Firebase;
#import "FirebaseMessagingModule.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
...
[FIRApp configure];
FirebaseMessagingModule* firebaseMessagingModule = [bridge moduleForClass:FirebaseMessagingModule.class];
[FIRMessaging messaging].delegate = firebaseMessagingModule;
/*
Notifications require some setup
*/
if ([UNUserNotificationCenter class] != nil) {
// iOS 10 or later
// For iOS 10 display notification (sent via APNS)
[UNUserNotificationCenter currentNotificationCenter].delegate = firebaseMessagingModule;
UNAuthorizationOptions authOptions = UNAuthorizationOptionAlert |
UNAuthorizationOptionSound | UNAuthorizationOptionBadge;
[[UNUserNotificationCenter currentNotificationCenter]
requestAuthorizationWithOptions:authOptions
completionHandler:^(BOOL granted, NSError * _Nullable error) {
// ...
}];
} else {
// iOS 10 notifications aren't available; fall back to iOS 8-9 notifications.
UIUserNotificationType allNotificationTypes =
(UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge);
UIUserNotificationSettings *settings =
[UIUserNotificationSettings settingsForTypes:allNotificationTypes categories:nil];
[application registerUserNotificationSettings:settings];
}
[application registerForRemoteNotifications];
[firebaseMessagingModule register2FACategory];
return YES;
}
...
@end
FirebaseMessagingModule.h:
//
// FirebaseMessagingModule.h
// bastionpassmobile
//
// Created by Anotn on 25/05/2020.
// Copyright © 2020 Facebook. All rights reserved.
//
#ifndef FirebaseMessagingModule_h
#define FirebaseMessagingModule_h
#endif /* FirebaseMessagingModule_h */
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@import Firebase;
@interface FirebaseMessagingModule : NSObject <RCTBridgeModule, FIRMessagingDelegate, UNUserNotificationCenterDelegate>
@property NSString* token;
- (void)register2FACategory;
@end
FirebaseMessagingModule.m:
//
// FirebaseMessagingModule.m
// bastionpassmobile
//
// Created by Anotn on 25/05/2020.
// Copyright © 2020 Facebook. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "FirebaseMessagingModule.h"
@import Firebase;
#import "constants.h"
#import "MF_Base32Additions.h"
#import <CommonCrypto/CommonCrypto.h>
@implementation FirebaseMessagingModule
RCT_EXPORT_MODULE()
/*
Registers 2FA category for remote notifications.
*/
- (void)register2FACategory
{
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
UNNotificationAction* twofaDeclineAction = [UNNotificationAction actionWithIdentifier:TWOFA_NOTIFICATION_ACTION_DECLINE_ID title:TWOFA_NOTIFICATION_ACTION_DECLINE_LABEL options:UNNotificationActionOptionAuthenticationRequired];
UNNotificationAction* twofaConfirmAction = [UNNotificationAction actionWithIdentifier:TWOFA_NOTIFICATION_ACTION_CONFIRM_ID title:TWOFA_NOTIFICATION_ACTION_CONFIRM_LABEL options:UNNotificationActionOptionAuthenticationRequired];
UNNotificationCategory* twofaCategory = [UNNotificationCategory categoryWithIdentifier:TWOFA_NOTIFICATION_CATEGORY_ID actions:@[twofaDeclineAction, twofaConfirmAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionNone];
NSSet* categories = [NSSet setWithObject:twofaCategory];
[center setNotificationCategories:categories];
}
- (void)messaging:(FIRMessaging *)messaging
didReceiveRegistrationToken:(NSString *)fcmToken
NS_SWIFT_NAME(messaging(_:didReceiveRegistrationToken:))
{
[self setToken:fcmToken];
}
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler __API_AVAILABLE(macos(10.14), ios(10.0), watchos(3.0), tvos(10.0))
{
completionHandler(UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionSound);
}
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler __API_AVAILABLE(macos(10.14), ios(10.0), watchos(3.0)) __API_UNAVAILABLE(tvos)
{
BOOL is2FAConfirm = [response.actionIdentifier isEqualToString:TWOFA_NOTIFICATION_ACTION_CONFIRM_ID];
BOOL is2FADecline = [response.actionIdentifier isEqualToString:TWOFA_NOTIFICATION_ACTION_DECLINE_ID];
if (is2FAConfirm || is2FADecline) {
// Retrieve notification data
NSDictionary* userInfo = response.notification.request.content.userInfo;
NSString* sessionId = userInfo[TWOFA_NOTIFICATION_DATA_SESSIONID_KEY];
NSString* username = userInfo[TWOFA_NOTIFICATION_DATA_USERNAME_KEY];
NSString* stage = userInfo[TWOFA_NOTIFICATION_DATA_STAGE_KEY];
// Retrieve storage data
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSString* authTokensString = [userDefaults valueForKey:STORAGE_KEY_AUTH_TOKENS];
NSData* authTokensData = [authTokensString dataUsingEncoding:NSUTF8StringEncoding];
NSInputStream* authTokensStream = [NSInputStream inputStreamWithData:authTokensData];
[authTokensStream open];
NSData* authTokens = [NSJSONSerialization JSONObjectWithStream:authTokensStream options:0 error:nil];
[authTokensStream close];
NSDictionary<NSString*, NSString*>* clientIds = [authTokens valueForKey:STORAGE_KEY_CLIENT_IDS];
NSString* clientId = [clientIds valueForKey:username];
NSURLSessionConfiguration* urlSessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"twoFAConfirmation"];
urlSessionConfiguration.networkServiceType = NSURLNetworkServiceTypeBackground;
urlSessionConfiguration.discretionary = NO;
urlSessionConfiguration.HTTPAdditionalHeaders = @{
@"Content-Type": @"application/json",
@"Accept": @"application/json",
@"X-Client-Type": @"mobile"
};
NSURLSession* urlSession = [NSURLSession sessionWithConfiguration:urlSessionConfiguration];
NSURL* url = [NSURL URLWithString:[API_URL stringByAppendingString:is2FAConfirm ? API_ENDPOINT_2FA_APPROVE : API_ENDPOINT_2FA_REJECT]];
NSMutableURLRequest* urlRequest = [NSMutableURLRequest requestWithURL:url];
urlRequest.HTTPMethod = @"POST";
urlRequest.allHTTPHeaderFields = @{
@"Content-Type": @"application/json",
@"Accept": @"application/json",
@"X-Client-Type": @"mobile"
};
if (is2FADecline) {
urlRequest.HTTPBody = [NSJSONSerialization dataWithJSONObject:@{
TWOFA_CONFIRMATION_REQUEST_KEY_SESSIONID: sessionId,
TWOFA_CONFIRMATION_REQUEST_KEY_AUTH_CLIENTID: clientId
} options:0 error:nil];
NSURLSessionUploadTask* rejectConfirmationTask = [urlSession uploadTaskWithStreamedRequest:urlRequest];
[rejectConfirmationTask resume];
} else if (is2FAConfirm) {
NSString* hotpSecretsString = [userDefaults valueForKey:STORAGE_KEY_HOTP_SECRETS];
NSData* hotpSecretsData = [hotpSecretsString dataUsingEncoding:NSUTF8StringEncoding];
NSInputStream* hotpSecretsStream = [NSInputStream inputStreamWithData:hotpSecretsData];
[hotpSecretsStream open];
NSData* hotpSecrets = [NSJSONSerialization JSONObjectWithStream:hotpSecretsStream options:0 error:nil];
[hotpSecretsStream close];
NSString* hotpSecret = [hotpSecrets valueForKey:username];
NSString* totpCode = [self getTotpCode:hotpSecret];
urlRequest.HTTPBody = [NSJSONSerialization dataWithJSONObject:@{
TWOFA_CONFIRMATION_REQUEST_KEY_SESSIONID: sessionId,
TWOFA_CONFIRMATION_REQUEST_KEY_AUTH_CLIENTID: clientId,
TWOFA_CONFIRMATION_REQUEST_KEY_TOKEN: totpCode
} options:0 error:nil];
NSURLSessionUploadTask* approveConfirmationTask = [urlSession uploadTaskWithStreamedRequest:urlRequest];
[approveConfirmationTask resume];
}
} else {
NSLog(@"Undefined type of action");
}
completionHandler();
}
/*
Returns totp code for provided hotp secret.
*/
- (NSString*)getTotpCode:(NSString*)hotpSecret
{
NSString* hotpSecretDecoded = [NSString stringFromBase32String:hotpSecret];
const char *cKey = [hotpSecretDecoded cStringUsingEncoding:NSUTF8StringEncoding];
const uint64_t cData = CFSwapInt64([[NSDate date] timeIntervalSince1970] * 1000.0 / 30000);
unsigned char cHMAC[CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, cKey, strlen(cKey), &cData, sizeof(cData), cHMAC);
NSData* hash = [[NSData alloc] initWithBytes:cHMAC length:sizeof(cHMAC)];
int offset = *(int*)[[hash subdataWithRange:NSMakeRange([hash length] - 1, 1)] bytes] & 0x0f;
const long* dbc1 = [[hash subdataWithRange:NSMakeRange(offset, (offset + 4) - offset)] bytes];
long dbc2 = -(~CFSwapInt32(*dbc1) + 1) & 0x7fffffff;
long totp = dbc2 % (long)pow(10, TWOFA_OTP_LENGTH);
NSString* totpCode = @"";
NSString* totpCodeSource = [[NSNumber numberWithLong:totp] stringValue];
while (([totpCode length] + [totpCodeSource length]) < TWOFA_OTP_LENGTH) {
totpCode = [totpCode stringByAppendingString:@"0"];
}
return [totpCode stringByAppendingString:totpCodeSource];
}
/*
Resolves with FCM registration token or rejected if token was not provided.
*/
RCT_REMAP_METHOD(getToken, getTokenResolver:(RCTPromiseResolveBlock)resolve getTokenRejecter:(RCTPromiseRejectBlock)reject)
{
if (self.token) {
resolve(self.token);
} else {
reject(@"0", @"Failed on get token", [[NSError alloc] init]);
}
}
/*
Resolves with FCM registration token or rejected if attempt to retrieve token will be failed.
*/
RCT_REMAP_METHOD(retrieveToken, retrieveTokenResolver:(RCTPromiseResolveBlock)resolve retrieveTokenRejecter:(RCTPromiseRejectBlock)reject)
{
[[FIRInstanceID instanceID] instanceIDWithHandler:^(FIRInstanceIDResult * _Nullable result,
NSError * _Nullable error) {
if (error != nil) {
reject(@"0", @"Failed to retrieve token", [[NSError alloc] init]);
} else {
resolve(result.token);
}
}];
}
@end
Любая помощь будет принята с благодарностью!
[ОБНОВЛЕНИЕ]: тестировщик в нашей компании проверил текущую функциональность и на iOS 13,5 работает, даже если приложение было убито. Может, на это как-то влияют настройки устройства или ограничения старых SDK?