firebase / firebase-ios-sdk

Firebase SDK for Apple App Development
https://firebase.google.com
Apache License 2.0
5.59k stars 1.46k forks source link

Not showing next message if only mark as read. #9935

Open igorjacauna opened 2 years ago

igorjacauna commented 2 years ago

Step 1: Describe your environment

Step 2: Describe the problem

We are implementing FIRInAppMessagingDisplay to create custom UI for InAppMessage.

We found a problem with Card message.

When we have a stack of messages, we stuck when a Card message arrives. And after shows Card message no messages shows up

Steps to reproduce:

Have one event called screen_event and 3 messages bind to it, message A of type ImageOnly, message B of typeCard and message C of type Banner.

  1. We trigger the event screen_event when screen get focused. The message A of type ImageOnly is showed and we call impressionDetectedForMessage for it. After that user dismissed or clicked on message.
  2. After while we trigger the screen_event again and the B message is showed and we call impressionDetectedForMessage for it. Our custom component for message of type Card are showed embed to layout, i.e., there is no dismiss button. The user do not interact with the message, once is embeded on layout he scroll the screen for example.
  3. After while we trigger again the event screen_event, but the message C is not showed.

The problem is that after message B of type Card shows up, next message are not showed. But if user interact with message B of type Card and we call messageClicked everything works. And if we trigger the event screen_event again the message C of type Banner shows up.

Seems like impressionDetectedForMessage not working with type Card. The flow of messages only works when call messageClicked for message of type Card for others types impressionDetectedForMessage works fine

rizafran commented 2 years ago

Thanks for reaching out, @igorjacauna. To make sure that we're on the same page, could you provide a minimal repro or code snippet to see how all the 3 messages appear in your app? Also, is the issue reproducible using the latest version?

igorjacauna commented 2 years ago

Hi @rizafran we discovery that the problem is with any type of message. If we only mark it as read, the next message does not shows up, only if we mark as clicked or dismissed.

Just for comparison of behavior, on Android the flow has no problem, the messages from stack show up when we trigger the event screen_event

Setup

I have configured 3 messages that has the event screen_event bind to each one:

  1. Image Only
  2. Card
  3. Modal

On screen we hava a button that fire the event screen_event and when we shows a Card message, we shows a button that mark it as clicked

Explanation:

We click on button that fire the screen_event and the expected behavior is the messages shows up in order. But when Card shows up the flow stuck on it. And the 3rd message only shows if we mark the Card message as clicked.

Steps

  1. Click on button to trigger screen_event
  2. The Image Only shows up
  3. Close the Image Only
  4. Click on button to trigger screen_event
  5. The Card shows up embedded to layout
  6. Do nothing
  7. Click on button to trigger screen_event
  8. Nothing happen
  9. Click on button to mark Card as clicked
  10. Click on button to trigger screen_event
  11. Modal shows up

Every time a message shows up we try to mark it as read. No error shows up on Console

Record

Simulator Screen Recording - iPhone 13 - 2022-06-24 at 15 46 52

rizafran commented 2 years ago

Thanks for sharing the steps, @igorjacauna. As mentioned, is it possible for you to provide a minimal repro that I can run locally or a code snippet on how you display the 3 types of messages? Also, is it reproducible using the latest SDK version?

igorjacauna commented 2 years ago

Hi @rizafran, we tried with the latest SDK version and the problem is present there too.

In our code we have two classes:

To clarify, we use this with ReactNative using Native Modules. And every time we receive a message we send it to ReactNative and when it is rendered on it we call markAsImpressionDetected and if we fire the event to get next message without call markAsClicked or markAsDismissed nothing is coming from Firebase. Only if we call markAsClicked or markAsDismissed. On Android this not happen, it works if we call only markAsImpressionDetected

In AppDelegate.m we set our custom Display class

InAppMessageDisplay *display = [[InAppMessageDisplay alloc] init];
[FIRInAppMessaging inAppMessaging].messageDisplayComponent = display;

InAppMessageDisplay

#import <Foundation/Foundation.h>
#import <Firebase.h>
#import "InAppMessageDisplay.h"
#import "InAppMessageManager.h"

NSString *const kFIRAppReady = @"FIRAppReadyToConfigureSDKNotification";

@implementation InAppMessageDisplay : NSObject

+ (void)load {
  [[NSNotificationCenter defaultCenter] addObserver:self
                                           selector:@selector(didReceiveConfigureSDKNotification:)
                                               name:kFIRAppReady
                                             object:nil];
}

+ (void)didReceiveConfigureSDKNotification:(NSNotification *)notification {

  InAppMessageDisplay *display = [[InAppMessageDisplay alloc] init];
  [FIRInAppMessaging inAppMessaging].messageDisplayComponent = display;
}

- (void)displayMessage:(FIRInAppMessagingDisplayMessage *)messageForDisplay
       displayDelegate:(id<FIRInAppMessagingDisplayDelegate>)displayDelegate {
  if ([messageForDisplay isKindOfClass:[FIRInAppMessagingModalDisplay class]]) {
    InAppMessageManager *emitter = [InAppMessageManager allocWithZone: nil];
    [emitter FIAMReceivedModal:messageForDisplay displayDelegate:displayDelegate];
  } else if ([messageForDisplay isKindOfClass:[FIRInAppMessagingBannerDisplay class]]) {
    InAppMessageManager *emitter = [InAppMessageManager allocWithZone: nil];
    [emitter FIAMReceivedBanner:messageForDisplay displayDelegate:displayDelegate];
  } else if ([messageForDisplay isKindOfClass:[FIRInAppMessagingImageOnlyDisplay class]]) {
    InAppMessageManager *emitter = [InAppMessageManager allocWithZone: nil];
    [emitter FIAMReceivedImageOnly:messageForDisplay displayDelegate:displayDelegate];
  } else if ([messageForDisplay isKindOfClass:[FIRInAppMessagingCardDisplay class]]) {
    InAppMessageManager *emitter = [InAppMessageManager allocWithZone: nil];
    [emitter FIAMReceivedCard:messageForDisplay displayDelegate:displayDelegate];
  }
}
@end

InAppMessageManager

#import <Foundation/Foundation.h>
#import <React/RCTLog.h>
#import "InAppMessageDisplay.h"
#import "InAppMessageManager.h"

@implementation InAppMessageManager

RCT_EXPORT_MODULE();

- (nullable FIRInAppMessagingDisplayMessage *)inAppMessage {
  return nil;
}

- (NSMutableDictionary *) displayDelegateDict {
    if (!_displayDelegateDict) {
      _displayDelegateDict = [NSMutableDictionary new];
    }
    return _displayDelegateDict;
}

- (NSMutableDictionary *) inAppMessageDict {
    if (!_inAppMessageDict) {
      _inAppMessageDict = [NSMutableDictionary new];
    }
    return _inAppMessageDict;
}

+ (id)allocWithZone:(NSZone *)zone {
    static InAppMessageManager *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [super allocWithZone:zone];
    });
    return sharedInstance;
}

- (NSArray<NSString *> *)supportedEvents {
    return @[@"OnDisplayInAppMessage"];
}

RCT_EXPORT_METHOD(markAsImpressionDetected:(NSString *)messageId){
  if ([self.displayDelegateDict objectForKey:messageId]) {
    [[self.displayDelegateDict objectForKey:messageId] impressionDetectedForMessage:[self.inAppMessageDict objectForKey:messageId]];
  }
}

RCT_EXPORT_METHOD(markAsClicked:(NSString *)messageId){
  NSString *actionURL = @"";
  if ([[self.inAppMessageDict objectForKey:messageId] isKindOfClass:[FIRInAppMessagingModalDisplay class]]) {
    FIRInAppMessagingModalDisplay *message = [self.inAppMessageDict objectForKey:messageId];
    if (message.actionURL.absoluteString) {
      actionURL = message.actionURL.absoluteString;
    }
  } else if ([[self.inAppMessageDict objectForKey:messageId] isKindOfClass:[FIRInAppMessagingBannerDisplay class]]) {
    FIRInAppMessagingBannerDisplay *message = [self.inAppMessageDict objectForKey:messageId];
    if (message.actionURL.absoluteString) {
      actionURL = message.actionURL.absoluteString;
    }
  } else if ([[self.inAppMessageDict objectForKey:messageId] isKindOfClass:[FIRInAppMessagingImageOnlyDisplay class]]) {
    FIRInAppMessagingImageOnlyDisplay *message = [self.inAppMessageDict objectForKey:messageId];
    if (message.actionURL.absoluteString) {
      actionURL = message.actionURL.absoluteString;
    }
  } else if ([[self.inAppMessageDict objectForKey:messageId] isKindOfClass:[FIRInAppMessagingCardDisplay class]]) {
    FIRInAppMessagingCardDisplay *message = [self.inAppMessageDict objectForKey:messageId];
    if (message.primaryActionURL.absoluteString) {
      actionURL = message.primaryActionURL.absoluteString;
    }
  }
  if ([self.displayDelegateDict objectForKey:messageId]) {
    NSURL *url = [NSURL URLWithString:actionURL];
    FIRInAppMessagingAction *action =
            [[FIRInAppMessagingAction alloc] initWithActionText:nil
                                                      actionURL:url];
    [[self.displayDelegateDict objectForKey:messageId] messageClicked:[self.inAppMessageDict objectForKey:messageId] withAction: action];
  }
}

RCT_EXPORT_METHOD(markAsDismissed:(NSString *)messageId){
  if ([self.displayDelegateDict objectForKey:messageId]) {
    [[self.displayDelegateDict objectForKey:messageId] messageDismissed:[self.inAppMessageDict objectForKey:messageId] dismissType:FIRInAppMessagingDismissTypeAuto];
  }
}

- (void)FIAMReceivedModal: (FIRInAppMessagingModalDisplay *)inAppMessage
        displayDelegate:(id<FIRInAppMessagingDisplayDelegate>)displayDelegate {

  [self sendEventWithName:@"OnDisplayInAppMessage" body:@{
    @"type":@"Modal"
  }];
  self.displayDelegate = displayDelegate;
  [self.displayDelegateDict setObject:displayDelegate forKey:inAppMessage.campaignInfo.messageID];
  [self.inAppMessageDict setObject:inAppMessage forKey:inAppMessage.campaignInfo.messageID];
}

- (void)FIAMReceivedBanner: (FIRInAppMessagingBannerDisplay *)inAppMessage
        displayDelegate:(id<FIRInAppMessagingDisplayDelegate>)displayDelegate {
  [self sendEventWithName:@"OnDisplayInAppMessage" body:@{
    @"type":@"Banner"
  }];
  self.displayDelegate = displayDelegate;
  [self.displayDelegateDict setObject:displayDelegate forKey:inAppMessage.campaignInfo.messageID];
  [self.inAppMessageDict setObject:inAppMessage forKey:inAppMessage.campaignInfo.messageID];
}

- (void)FIAMReceivedImageOnly: (FIRInAppMessagingImageOnlyDisplay *)inAppMessage
        displayDelegate:(id<FIRInAppMessagingDisplayDelegate>)displayDelegate {
  [self sendEventWithName:@"OnDisplayInAppMessage" body:@{
    @"type":@"ImageOnly"
  }];
  self.displayDelegate = displayDelegate;
  [self.displayDelegateDict setObject:displayDelegate forKey:inAppMessage.campaignInfo.messageID];
  [self.inAppMessageDict setObject:inAppMessage forKey:inAppMessage.campaignInfo.messageID];
}

- (void)FIAMReceivedCard: (FIRInAppMessagingCardDisplay *)inAppMessage
        displayDelegate:(id<FIRInAppMessagingDisplayDelegate>)displayDelegate {
  [self sendEventWithName:@"OnDisplayInAppMessage" body:@{
    @"type":@"Card"
  }];
  self.displayDelegate = displayDelegate;
  [self.displayDelegateDict setObject:displayDelegate forKey:inAppMessage.campaignInfo.messageID];
  [self.inAppMessageDict setObject:inAppMessage forKey:inAppMessage.campaignInfo.messageID];
}

@end
rizafran commented 2 years ago

Hi @igorjacauna, unfortunately, I'm unable to run the app because it looks for InAppMessageManager which I encountered multiple errors. If you could share the InAppMessageManager or if you can create another runnable app that can replicate the issue, that will be great. Thanks.

igorjacauna commented 2 years ago

Hi @rizafran , to help you to reproduce the issue we create a React Native project that has exact architecture of our project and also has the reproducible issue.

To be more specific, the iOS code is on ios directory of the project

We put instructions on README but I will mention it here:


iOS code

The iOS project and code are on ios directory

To run

yarn install

cd ios
pod install

cd ..
yarn ios

If have any error on build, open the ios directory on XCode and build it on XCode. After build try run yarn ios on root dir of project

Uninstall App

Every time we have to try receive again the messages we must uninstall the app before run yarn ios again

Code with workaround

On file App.js we have the logic about receive and mark as read or clicked the message.

If we click on button on screen, we receive a message. If we click again no one message are received.

To workaround we have to uncomment on line 42 of App.js. Remember to uninstall the app and run yarn ios again to start over to receive the messages

rizafran commented 2 years ago

Thanks for the sample app, @igorjacauna. Unfortunately, I'm unable to run the app as I'm not familiar with React Native. However, I tried to create a simple app that uses native iOS SDK, and did the following:

What happened is that my InAppMessagingDisplay protocol was being called only once. Could you confirm if the behavior is the same as yours?

Lucasfrota commented 2 years ago

Hi @rizafran , I'm working on the same project with @igorjacauna , I have tried to run the project without the custom FIAM class, indeed the problem doesn’t seem to happen in this condition, however for the porpoises of the project we're working on we need to use a custom FIAM class, considering this problem doesn’t occur without the custom class we realized that it may be a problem on our implementation of this class so we are doing some investigations, but just to make sure we're in the same page, this is the problematic behavior from the perspective of our custom class: In our custom class we created the method displayMessage we expect this method to be called when an event linked to a message is triggered, however for some reason when we have two or more messages linked to the same event the method is only called once no matter how much times we trigger the event linked to the messages unless we call the method messageClicked, once we call messageClicked to mark the first message as clicked and trigger the event again the method displayMessage will be called bringing a new message, So in summary looks like once the first message is shown the rest of the messages are being ignored unless we mark the first message as clicked, is it really what should be happening?

rizafran commented 2 years ago

Thanks for clarifying, @Lucasfrota. It seems like we're encountering the same behavior. I'll inform the engineer about this to see if it's a bug or if something's missing in the code implementation.

rizafran commented 2 years ago

Sorry for the delay in response. I just got an update from the engineer and it seems that the code you shared is working as expected. When the displayMessage method is called, FIAM considers the message as shown to the user until one of displayDelegate's messageDismissed, messageClicked, and displayError is called. Before this, FIAM refuses to show any more messages.

I'll be closing this ticket for now, but feel free to comment if you have any other questions.

Nsouza31 commented 2 years ago

Hello @rizafran I'm the product manager working with @igorjacauna and @Lucasfrota at the in app messing implementation. We felt extremelly surprised with your last comment in this thread about the behavior of iOS SDK, given the fact that at Android SDK seems to have the behavior which satisfied our expection about marking the messaging as delivered once it is displayed. Should the behavior of the same point beeing different on Android and iOS?

As I said before we based this expectation on Android but also on firebase documentation {compose a campaign -> Scheduling your message}:

By default, a campaign is not shown after it has been viewed by (that is, impressed on) the user once. Or, you can set the frequency of messages in days (Maximum 1 time per day)

We see some risks about assuming that on iOS SDK the user needs to "dismiss" or "click to interact":

  1. We made tests and once a user has a message received and closes the app without interacting with it the queue of messaging don´t goes to next messaging as expectated, if I have others campaign aafter that they won't be shown as planned during the user journey;

  2. If the user put the app in background after viewing the message if he return to the App he will view the same messaging breaking major rule of seeing one message at most once a day.

He need to understand if it is the behavior we should expect for iOS SDK because it creates a risk we didn´t mapped before based on Firebase In-app documentation and seems to be a no-go issue because of right risk of spamming our iOS users and ineffectiveness of our campaigns calendar.

It's high important to considerer that in-app messaging is one of the most important peaces of our strategy to growth the engagement of 10 million active users on our app and the partneship with the biggest retail ecommerces in Brazil.

Could you check the topics I broght in this comment and give us a feedback.

paulb777 commented 2 years ago

@Nsouza31 Thanks for the clarification. The behavior difference between Android and iOS is something that we've had in the backlog to address. We're still working out the plan and will update here when it's determined.

eldhosembabu commented 2 years ago

Hi @Nsouza31 ,

We had an internal discussion regarding the issue with our product managers and the outcome is that the iOS SDK is working as expected and for the Android SDK, it is a bug which we have added to our roadmap for fixing. In the mean time, we will consider this issue as a feature request.