Skip to content

Workflow Description: CallKeep and React Native VoIP Push Notification HAVE PROBLEM  #847

@TranHongPhucBlockify

Description

@TranHongPhucBlockify
  • [ v ] - Initial Setup: The app has been built and successfully obtained a VoIP token.

  • [ v ] - App State: The app is swiped out of the multitasking screen (background or terminated state).

  • [ v ] - VoIP Notification: The VoIP notification is sent to the backend (BE).

  • [ v ] - CallKeep Screen: The CallKeep screen appears natively on the device when the notification is received.

  • [ v ] - Accept Call: When the user accepts the call by pressing the "Accept" button, the app is woken up from the background/terminated state.

  • [ x ] - Issue: After the app wakes up, the screen appears blank (white screen) with almost no content. The login screen fails to load properly.

Image Image

Image

#import "AppDelegate.h"

#import <Firebase.h>
#import <UserNotifications/UserNotifications.h>
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTPushNotificationManager.h>
#import <RNBranch/RNBranch.h>
#import <React/RCTLinkingManager.h>

// ✅ Thêm PushKit + CallKeep
#import <PushKit/PushKit.h>
#import "RNCallKeep.h"
#import "RNVoipPushNotificationManager.h"

#if DEBUG
#ifdef FB_SONARKIT_ENABLED
#import <FlipperKit/FlipperClient.h>
#import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
#import <FlipperKitLayoutPlugin/SKDescriptorMapper.h>
#import <FlipperKitNetworkPlugin/FlipperKitNetworkPlugin.h>
#import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>
#import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
#import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
#endif
#endif

#if DEBUG && __has_include(<EXDevLauncher/EXDevLauncherController.h>)
#import <EXDevLauncher/EXDevLauncherController.h>
#endif

@interface AppDelegate () // ✅ giữ nguyên y chang
@EnD

@implementation AppDelegate

  • (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // ===== 1. Firebase setup =====
    [FIRApp configure];

    // ===== 2. iOS Push setup =====
    UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
    center.delegate = self;
    UNAuthorizationOptions authOptions = UNAuthorizationOptionAlert |
    UNAuthorizationOptionSound |
    UNAuthorizationOptionBadge;
    [center requestAuthorizationWithOptions:authOptions completionHandler:^(BOOL granted, NSError * _Nullable error) {
    if (granted) {
    NSLog(@"🔔 Push permission granted");
    } else {
    NSLog(@"🚫 Push permission denied: %@", error);
    }
    }];
    [application registerForRemoteNotifications];

    // ✅ PushKit VoIP Registry
    PKPushRegistry *voipRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
    voipRegistry.delegate = self;
    voipRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];

    // ===== 3. React Native + Branch setup =====
    self.moduleName = @"Bluefish";
    NSString *foxCodeFromBundle = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"fox_code"];
    NSString *foxCode = foxCodeFromBundle ?: @"debug";

    [RNBranch initSessionWithLaunchOptions:launchOptions isReferrable:YES];

    RCTBridge *bridge = [self.reactDelegate createBridgeWithDelegate:self launchOptions:launchOptions];
    RCTRootView *rootView = [self.reactDelegate createRootViewWithBridge:bridge
    moduleName:self.moduleName
    initialProperties:@{
    @"foxCode": foxCode,
    @"appName": @"Bluefish"
    }];
    rootView.backgroundColor = [UIColor colorNamed:@"ThemeColors"];

    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    UIViewController *rootViewController = [self.reactDelegate createRootViewController];
    rootViewController.view = rootView;
    self.window.rootViewController = rootViewController;
    [self.window makeKeyAndVisible];

    // Keep splash screen
    UIView *launchScreenView = [[[NSBundle mainBundle] loadNibNamed:@"LaunchScreen" owner:self options:nil] objectAtIndex:0];
    launchScreenView.frame = self.window.bounds;
    rootView.loadingView = launchScreenView;

    [self initializeFlipper:application];

    [super application:application didFinishLaunchingWithOptions:launchOptions];
    return YES;
    }

#pragma mark – Flipper

  • (void)initializeFlipper:(UIApplication *)application {
    #if DEBUG
    #ifdef FB_SONARKIT_ENABLED
    FlipperClient *client = [FlipperClient sharedClient];
    SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults];
    [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]];
    [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]];
    [client addPlugin:[FlipperKitReactPlugin new]];
    [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]];
    [client start];
    #endif
    #endif
    }

#pragma mark – URL handlers for Branch

  • (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
    #if DEBUG
    if ([EXDevLauncherController.sharedInstance onDeepLink:url options:options]) {
    return YES;
    }
    #endif
    NSLog(@"🔥 App nhận deeplink: %@", url.absoluteString);
    return [RCTLinkingManager application:app openURL:url options:options];
    }

  • (BOOL)application:(UIApplication *)application
    continueUserActivity:(NSUserActivity *)userActivity
    restorationHandler:(void(^)(NSArray * __nullable restorableObjects))restorationHandler {
    return [RNCallKeep application:application
    continueUserActivity:userActivity
    restorationHandler:restorationHandler];

}

#pragma mark – APNs registration

  • (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    [FIRMessaging messaging].APNSToken = deviceToken;
    [RCTPushNotificationManager didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
    }

  • (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
    [RCTPushNotificationManager didFailToRegisterForRemoteNotificationsWithError:error];
    NSLog(@"❌ Failed to register for APNs: %@", error);
    }

#pragma mark – UNUserNotificationCenterDelegate

  • (void)userNotificationCenter:(UNUserNotificationCenter *)center
    willPresentNotification:(UNNotification *)notification
    withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler {
    completionHandler(UNNotificationPresentationOptionAlert |
    UNNotificationPresentationOptionSound |
    UNNotificationPresentationOptionBadge);
    }

  • (void)userNotificationCenter:(UNUserNotificationCenter *)center
    didReceiveNotificationResponse:(UNNotificationResponse *)response
    withCompletionHandler:(void (^)(void))completionHandler {
    NSDictionary *userInfo = response.notification.request.content.userInfo;
    [RCTPushNotificationManager didReceiveRemoteNotification:userInfo fetchCompletionHandler:^(UIBackgroundFetchResult result){}];
    completionHandler();
    }

#pragma mark – Legacy RN PushNotificationManager

  • (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings {
    [RCTPushNotificationManager didRegisterUserNotificationSettings:notificationSettings];
    }

  • (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
    fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
    NSLog(@"📩 Background Push Payload Received: %@", userInfo);
    [RCTPushNotificationManager didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
    }

  • (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {
    [RCTPushNotificationManager didReceiveLocalNotification:notification];
    }

#pragma mark – React Native bridge

  • (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
    #if DEBUG
    return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
    #else
    return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
    #endif
    }

#pragma mark – PushKit VoIP

  • (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type {
    [RNVoipPushNotificationManager didUpdatePushCredentials:credentials forType:type];

    // ✅ Log VoIP Token rõ ràng 64 ký tự
    NSString *voipToken = [credentials.token.description stringByReplacingOccurrencesOfString:@" " withString:@""];
    NSLog(@"📞 VoIP Push token: %@", voipToken);
    }

  • (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload
    forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion {

    [RNVoipPushNotificationManager didReceiveIncomingPushWithPayload:payload forType:type];

    // ✅ Nếu app bị kill → cần tạo lại window và rootView
    if (!self.window || !self.window.rootViewController) {
    NSLog(@"🧠 App was killed. Reinitializing window and rootView...");
    RCTBridge *bridge = [self.reactDelegate createBridgeWithDelegate:self launchOptions:nil];
    RCTRootView *rootView = [self.reactDelegate createRootViewWithBridge:bridge
    moduleName:self.moduleName
    initialProperties:nil];
    rootView.backgroundColor = [UIColor whiteColor];

    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    UIViewController *rootVC = [self.reactDelegate createRootViewController];
    rootVC.view = rootView;
    self.window.rootViewController = rootVC;
    [self.window makeKeyAndVisible];
    }

    NSDictionary *content = [payload.dictionaryPayload valueForKey:@"aps"];
    NSDictionary *alert = [content valueForKey:@"alert"];
    NSDictionary *items = [alert valueForKey:@"items"];

    NSString *uuid = [[[NSUUID UUID] UUIDString] lowercaseString];
    NSString *callerName = [items valueForKey:@"peer_caller_id_name"] ?: @"Unknown";
    NSString *handle = [items valueForKey:@"peer_caller_id_number"] ?: @"Unknown";
    NSDictionary *extra = payload.dictionaryPayload;

    [RNCallKeep reportNewIncomingCall:uuid
    handle:handle
    handleType:@"generic"
    hasVideo:NO
    localizedCallerName:callerName
    supportsHolding:YES
    supportsDTMF:YES
    supportsGrouping:YES
    supportsUngrouping:YES
    fromPushKit:YES
    payload:extra
    withCompletionHandler:completion];

    // ✅ ✅ CHỜ BRIDGE KHỞI ĐỘNG SAU COLD START
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    RCTBridge *bridge = [self.reactDelegate createBridgeWithDelegate:self launchOptions:nil];
    if (bridge != nil) {
    [bridge enqueueJSCall:@"RCTDeviceEventEmitter"
    method:@"emit"
    args:@[@"BridgeReadyAfterVoIP", @{}]
    completion:NULL];

    NSLog(@"✅ BridgeReadyAfterVoIP event sent to JS");
    

    } else {
    NSLog(@"❌ Failed to initialize RN bridge after VoIP push");
    }
    });
    }

@EnD

#import <Firebase.h>
#import <Expo/Expo.h>
#import <React/RCTBridgeDelegate.h>
#import <UIKit/UIKit.h>

// ✅ Thêm từ mẫu
#import <PushKit/PushKit.h>
#import <UserNotifications/UserNotifications.h>
#import "RNCallKeep.h"
#import "RNVoipPushNotificationManager.h"

@interface AppDelegate : EXAppDelegateWrapper <UIApplicationDelegate, RCTBridgeDelegate, PKPushRegistryDelegate, UNUserNotificationCenterDelegate>

@Property (nonatomic, strong) UIWindow *window;

@EnD

Hope everyone gives their opinions to help overcome this problem. Thank God, Amen.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions