braze-react-native-sdk icon indicating copy to clipboard operation
braze-react-native-sdk copied to clipboard

[Bug]: Push Notification doesn't redirect if App is killed and device has been inactive for a few minutes

Open plgrazon opened this issue 8 months ago • 2 comments

Which Platforms?

iOS

Which React Native Version?

0.76.6

Which @braze/react-native-sdk SDK version?

13.1.1

Repro Rate

50%

Steps To Reproduce

  1. Make sure the app is killed and not in the background
  2. Start a push notification campaign with a deep link
  3. Sent test push notification
  4. Leave push notification
  5. Open push notification sometimes it doesn't deep link to the specific screen; sometimes

If I immediately click the notification right after it was sent, it works 100% of the time.

I also followed the sample app setup

Expected Behavior

The app should redirect to the right deep-linked screen even if the app is in a killed state and if the device has been inactive for a long duration.

Actual Incorrect Behavior

Sometimes, when you click on the link, it opens the app but doesn't redirect

Verbose Logs


Additional Information

Helper hook we use in our app entry file to initialize and listen for Braze push notifications.

import Braze from '@braze/react-native-sdk';
import { useEffect } from 'react';
import { BRAZE_DEEP_LINK } from '../constants/mmkv';
import { linking } from '../constants/routes';
import mmkvStorage from '../mmkvStorage';
import { linkTo } from '../navigation/RootNavigation';

export default ({ navigationReady }: { navigationReady: boolean }) => {
    // Handle navigation ready state changes
    useEffect(() => {
        if (navigationReady) {
            const storedUrl = mmkvStorage.getString(BRAZE_DEEP_LINK);
            if (storedUrl && linking?.config) {
                linkTo(storedUrl, linking.config);
            }
        }
    }, [navigationReady]);

    useEffect(() => {
        Braze.requestPushPermission();

        const handleUrl = (url: string) => {
            if (linking?.config && navigationReady) {
                linkTo(url, linking.config);
            } else {
                mmkvStorage.set(BRAZE_DEEP_LINK, url);
            }
        };

        // Handle cold start
        Braze.getInitialPushPayload((payload) => {
            if (payload?.url) {
                handleUrl(payload.url);
            }
        });

        // Handle background/foreground notifications
        const subscription = Braze.addListener('pushNotificationEvent', (payload) => {
            if (payload?.url) {
                handleUrl(payload.url);
            }
        });

        return () => {
            subscription.remove();
        };
    }, []);
};

React Navigation Linking Object

export const linking: LinkingOptions<RootStackParamList> | undefined = {
    prefixes: [
        'https://...'
    ],

    async getInitialURL() {
        const pause = async (timeMs: number) => new Promise((resolve) => setTimeout(resolve, timeMs));
        const url = await Linking.getInitialURL();

        if (checkIfUniversalShortlink(url)) {
            const longUrl = await getLongUrl(url);
            // Wait to make sure navigation is initialized
            await pause(500);
            return longUrl ?? url;
        }

        // Wait to make sure navigation is initialized
        await pause(500);
        return url;
    },

    // Custom function to subscribe to incoming links
    subscribe(listener: (url: string) => void) {
        // First, you may want to do the default deep link handling
        const linkingSubscription = Linking.addEventListener('url', async ({ url }) => {
            if (checkIfBiltShortlink(url)) {
                const longUrl = await getLongUrl(url);
                listener(longUrl ?? url);
            } else {
                listener(url);
            }
        });

        return () => {
            linkingSubscription.remove();
        };
    },
    config,
};

// Braze
#import <BrazeKit/BrazeKit-Swift.h>
#import "BrazeReactBridge.h"
#import <BrazeReactUtils.h>

@implementation AppDelegate

static NSString *const brazeApiKey = @"...";
static NSString *const brazeEndpoint = @"...";
static NSString *const iOSPushAutoEnabledKey = @"iOSPushAutoEnabled";

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // ...
    // Setup Braze
    BRZConfiguration *configuration = [[BRZConfiguration alloc] initWithApiKey:brazeApiKey endpoint:brazeEndpoint];
    
    // Default to automatically setting up push notifications
    BOOL pushAutoEnabled = NO;
    if ([[NSUserDefaults standardUserDefaults] objectForKey:iOSPushAutoEnabledKey]) {
        pushAutoEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:iOSPushAutoEnabledKey];
    }
    if (pushAutoEnabled) {
        NSLog(@"iOS Push Auto enabled.");
        configuration.push.automation = [[BRZConfigurationPushAutomation alloc] initEnablingAllAutomations:YES];
    }

    Braze *braze = [BrazeReactBridge initBraze:configuration];
    AppDelegate.braze = braze;

    if (!pushAutoEnabled) {
        // If the user explicitly disables Push Auto, register for push manually
        NSLog(@"iOS Push Auto disabled - Registering for push manually.");
        [self registerForPushNotifications];
    }

    [[BrazeReactUtils sharedInstance] populateInitialPayloadFromLaunchOptions:launchOptions];
    
    [application registerForRemoteNotifications];
    UNUserNotificationCenter *center = UNUserNotificationCenter.currentNotificationCenter;
    [center setNotificationCategories:BRZNotifications.categories];
    center.delegate = self;
    
    UNAuthorizationOptions options = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge;
    if (@available(iOS 12.0, *)) {
        options = options | UNAuthorizationOptionProvisional;
    }
    
    [center requestAuthorizationWithOptions:options
                          completionHandler:^(BOOL granted, NSError *_Nullable error) {
        NSLog(@"Notification authorization, granted: %d, error: %@)", granted, error);
            if (granted) {
                NSLog(@"Attempting to create bridge class: %@", bridgeClass);
                // Store the bridge in the property instead of local variable
                if (self.brazeBridge) {
                    if (@available(iOS 17.2, *)) {
                       [self.brazeBridge registerActivityType];
                    }
                    NSLog(@"Calling brazeBridge resumeActivities", self.brazeBridge);
                    if (@available(iOS 16.1, *)) {
                       [self.brazeBridge resumeActivities];
                    }
                }
            }
    }];
    // ...
    return YES;
}

#pragma mark - Push Notifications (manual integration)

- (void)registerForPushNotifications {
    UNUserNotificationCenter *center = UNUserNotificationCenter.currentNotificationCenter;
    [center setNotificationCategories:BRZNotifications.categories];
    center.delegate = self;
    [UIApplication.sharedApplication registerForRemoteNotifications];
    // Authorization is requested later in the JavaScript layer via `Braze.requestPushPermission`.
}

// application:didReceiveRemoteNotification:fetchCompletionHandler: 
// [not deprecated, but is superseded by the UNUserNotificationCenterDelegate methods]
// docs: https://reactnative.dev/blog/2024/04/22/release-0.74
// - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
//     BOOL processedByBraze = AppDelegate.braze != nil && [AppDelegate.braze.notifications handleBackgroundNotificationWithUserInfo:userInfo fetchCompletionHandler:completionHandler];
//     if (processedByBraze) {
//         return;
//     }
//     completionHandler(UIBackgroundFetchResultNoData);
// }

- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
         withCompletionHandler:(void (^)(void))completionHandler {
    [[BrazeReactUtils sharedInstance] populateInitialUrlForCategories:response.notification.request.content.userInfo];
    BOOL processedByBraze = AppDelegate.braze != nil && [AppDelegate.braze.notifications handleUserNotificationWithResponse:response withCompletionHandler:completionHandler];
    if (processedByBraze) {
        return;
    }

    completionHandler();
}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center
       willPresentNotification:(UNNotification *)notification
         withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler {
    if (@available(iOS 14.0, *)) {
        completionHandler(UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner);
    } else {
        completionHandler(UNNotificationPresentationOptionAlert);
    }
}

- (void)application:(UIApplication *)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    [AppDelegate.braze.notifications registerDeviceToken:deviceToken];
}

#pragma mark - AppDelegate.braze

static Braze *_braze = nil;

+ (Braze *)braze {
    return _braze;
}

+ (void)setBraze:(Braze *)braze {
    _braze = braze;
}

plgrazon avatar Feb 25 '25 17:02 plgrazon