Skip to content

In-App Feedback Prompt UI - #22052

Merged
salimbraksa merged 23 commits into
task/in-app-feedbackfrom
task/in-app-feedback-ui
Jan 18, 2024
Merged

In-App Feedback Prompt UI#22052
salimbraksa merged 23 commits into
task/in-app-feedbackfrom
task/in-app-feedback-ui

Conversation

@salimbraksa

@salimbraksa salimbraksa commented Nov 16, 2023

Copy link
Copy Markdown
Contributor

Part of #22341

Related PR

This PR depends on:

Description

This PR introduces the In-App Feedback Prompt UI. The prompt becomes visible after a combination of any of these actions is performed at least 20 times:

  1. User taps on an unread notification
  2. User publishes a post
  3. User visits stats
  4. User reads a post
 Positive Feedback Flow Negative Feedback Flow
CleanShot.2024-01-09.at.22.28.11.mp4
CleanShot.2024-01-09.at.22.30.37.mp4

Test Instruction

Positive Feedback Flow

  1. Open WordPressAppDelegate.swift and on line 411, set utility.systemWideSignificantEventCountRequiredForPrompt to 5.
  2. Run Jetpack and Login.
  3. Perform a combination of these actions at least 5 times:
    • User taps on an unread notification
    • User publishes a post
    • User visits stats
    • User reads a post.
  4. Navigate to My Site Dashboard screen.
  5. Expect a "Your feecback matters" alert to appear.
  6. Expect the event app_reviews_saw_prompt to be tracked.
  7. Tap "It's great".
  8. Expect the event app_reviews_liked_app to be tracked.
  9. Expect "Enjoying Jetpack" alert to appear with a component to rate the app.
    • You may notice the Submit button is disabled after rating the app, the reason is that the alert is a native component and the button is disabled in dev environment.
  10. Navigate to any other screen then navigate back to My Site Dashboard.
  11. Expect the In-App Feedback Prompt not to appear.

Negative Feedback Flow

  1. For the In-App Feedback to appear again, you can either delete, and reinstall the Jetpack to clear the cache, or force the prompt to appear programmatically:
    • Open InAppFeedbackPromptCoordinator.swift and on line 25, force shouldShowPromptForAppReview to return true.
  2. Navigate to My Site Dashboard screen.
  3. Run Jetpack and Login.
  4. Expect a "Your feedback matters" alert to appear.
  5. Tap "Not really".
  6. Expect the event app_reviews_didnt_like_app to be tracked.
  7. Expect another alert with title "Help us improve" to appear.
  8. Tap "Sure".
  9. Expect Submit Feedback screen to appear.
  10. Expect the event app_reviews_feedback_screen_opened to be tracked.
  11. Write a feedback.
  12. Tap "Submit".
  13. Expect a success message to appear.
  14. Expect the event app_reviews_feedback_sent <feedback: The app should be improved!> to be tracked.

Cancel Negative Feedback Flow

  1. Trigger the In-App Feedback Prompt
  2. Tap "Not Really"
  3. Tap "Not Now"
  4. Expect the event app_reviews_declined_to_rate_app to be tracked
  5. Repeat 1 to 3
  6. Tap "Sure"
  7. Tap "Cancel"
  8. Expect the event app_reviews_feedback_screen_canceled to be tracked

Regression: Compliance Popover

  1. Make the following code changes to force the compliance popover to appear:
    • Go to CompliancePopoverCoordinator.swift
    • On line 32, comment this code: defaults.didShowCompliancePopup
    • On line 60, force the method to return true.
  2. Run Jetpack and Login.
  3. Expect the Compliance Popover to appear.

Regression Notes

  1. Potential unintended areas of impact
    The presentation of the Compliance Popover should be tested.

  2. What I did to test those areas of impact (or what existing automated tests I relied on)
    Manual Testing.

  3. What automated tests I added (or what prevented me from doing so)
    None.

PR submission checklist:

  • I have completed the Regression Notes.
  • I have considered adding unit tests for my changes.
  • I have considered adding accessibility improvements for my changes.
  • I have considered if this change warrants user-facing release notes and have added them to RELEASE-NOTES.txt if necessary.

UI Changes testing checklist:

  • Portrait and landscape orientations.
  • Light and dark modes.
  • Fonts: Larger, smaller and bold text.
  • High contrast.
  • VoiceOver.
  • Languages with large words or with letters/accents not frequently used in English.
  • Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
  • iPhone and iPad.
  • Multi-tasking: Split view and Slide over. (iPad)

@wpmobilebot

wpmobilebot commented Nov 16, 2023

Copy link
Copy Markdown
Contributor
WordPress Alpha📲 You can test the changes from this Pull Request in WordPress Alpha by scanning the QR code below to install the corresponding build.
App NameWordPress Alpha WordPress Alpha
ConfigurationRelease-Alpha
Build Numberpr22052-46e2143
Version24.0
Bundle IDorg.wordpress.alpha
Commit46e2143
App Center BuildWPiOS - One-Offs #8479
Automatticians: You can use our internal self-serve MC tool to give yourself access to App Center if needed.

@wpmobilebot

wpmobilebot commented Nov 16, 2023

Copy link
Copy Markdown
Contributor
Jetpack Alpha📲 You can test the changes from this Pull Request in Jetpack Alpha by scanning the QR code below to install the corresponding build.
App NameJetpack Alpha Jetpack Alpha
ConfigurationRelease-Alpha
Build Numberpr22052-9b8db01
Version24.0
Bundle IDcom.jetpack.alpha
Commit9b8db01
App Center Buildjetpack-installable-builds #7499
Automatticians: You can use our internal self-serve MC tool to give yourself access to App Center if needed.

@salimbraksa
salimbraksa force-pushed the task/in-app-feedback-ui branch from c71a915 to fbb5165 Compare November 17, 2023 00:56
@salimbraksa
salimbraksa changed the base branch from trunk to task/in-app-feedback November 17, 2023 00:57
@mokagio

mokagio commented Jan 8, 2024

Copy link
Copy Markdown
Contributor

I've been going trough PRs to update them via git merge trunk to include #22270 and run on the new Apple Silicon CI with Xcode 15.1.

I did not work on this one because it's based off another PR.

@salimbraksa
salimbraksa force-pushed the task/in-app-feedback branch 2 times, most recently from a65af62 to 6a98364 Compare January 9, 2024 12:36
@salimbraksa
salimbraksa force-pushed the task/in-app-feedback-ui branch 3 times, most recently from 100425c to 9bdd9c8 Compare January 9, 2024 13:05
@salimbraksa salimbraksa changed the title In-App Feedback Alerts In-App Feedback Prompt Jan 9, 2024
@salimbraksa salimbraksa changed the title In-App Feedback Prompt In-App Feedback Prompt UI Jan 9, 2024
@salimbraksa salimbraksa added this to the Pending milestone Jan 9, 2024
if !unreadNotificationIds.isEmpty {
// Track as significant event for App Rating calculations
AppRatingUtility.shared.incrementSignificantEvent()
}

@salimbraksa salimbraksa Jan 9, 2024

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code was supposed to increment the significant events count when the user visits the Notifications screen and has unread notifications. But that didn't work, the unreadNotificationIds was empty when I visit the screen even though there are unread notifications.

After some digging, I realized the unreadNotificationIds is populated when the user switches to Unread tab, then it is emptied when switching to another tab.

This behavior is buggy in my opinion, and I replaced it with the "User taps on an unread notification" action.

cc @hassaanelgarem

@salimbraksa
salimbraksa force-pushed the task/in-app-feedback-ui branch from a9fa8b9 to 41fa80f Compare January 10, 2024 13:45
/// uses the app in a broad fashion.
///
@objc var systemWideSignificantEventCountRequiredForPrompt: Int = 1
@objc var systemWideSignificantEventCountRequiredForPrompt: Int = 5

@salimbraksa salimbraksa Jan 10, 2024

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is strange. I pushed a commit to revert this change, so locally the value of this variable should be 1, and yet it shows 5 in the PR.

CleanShot 2024-01-10 at 14 56 58@2x

But I moved this file to a different folder, which is probably why Git got confused.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll checkout to your branch and verify

}

func presentIfNeeded() {
guard FeatureFlag.compliancePopover.enabled/*, !defaults.didShowCompliancePopup */else {

@salimbraksa salimbraksa Jan 10, 2024

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change /*, !defaults.didShowCompliancePopup */ was accidentally merged in release/24.0. This PR reverts the change but I'll follow up with another PR targeting 24.0 and adding a Unit Test for the presentIfNeeded method.

// MARK: - Properties

private let appRatingUtility: AppRatingUtility
private let userDefaults: UserDefaults

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UserPersistentRepository can be used instead of UserDefaults as type. It simply adds an abstraction over UserDefaults to allow us injecting.

@salimbraksa salimbraksa Jan 17, 2024

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in cb10a4a. I've removed the userDefaults property as it was not used.

Comment on lines +45 to +47
let yes = UIAlertAction(title: Strings.FeedbackAlert.yes, style: .default) { _ in
self.handlePositiveFeedback(in: controller)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose there's no retain cycle as UIAlertController will be deallocated once the function ends. Though I thought it could make it less prone for a cycle in the future. Up to you.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I'll add [weak self] to this closure and all other UIAlertAction closures.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got this error when adding [weak self]:

'weak' may only be applied to class and class-bound protocol types, not 'InAppFeedbackPromptCoordinator'

That makes sense since InAppFeedbackPromptCoordinator is a struct.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that actually may not be such a great idea as it contains other reference types. So when you pass it around, its value type properties will be copied but the reference types will be modified throughout every copy.

Comment on lines +28 to +33
deinit {
guard !feedbackWasSubmitted else {
return
}
WPAnalytics.track(.appReviewsCanceledFeedbackScreen)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we do this in viewDidDisappear or some other view lifecycle function rather than deinit which is about memory deallocation?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in a9dd8ab

Comment on lines +7 to +8
private var complianceCoordinator: CompliancePopoverCoordinator?
private var inAppFeedbackCoordinator: InAppFeedbackPromptPresenting?

@alpavanoglu alpavanoglu Jan 10, 2024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what kind of structure you have followed but if VC retains Coordinators (whether directly or via ViewModel), these should be unowned variables rather than strongly held. Since we would wish the children Coordinators to be deallocated once their respective VCs are.

I just wanted to point that out.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these should be unowned variables rather than strongly held. Since we would wish the children Coordinators to be deallocated once their respective VCs are.

Their respective VCs don't hold a reference to these children coordinators, so if we mark them as unowned , they will be deallocated immediately.

I have considered instantiating those children coordinators in MySiteOverlaysCoordinator.presentOverlayIfNeeded, but I'm planning to write unit tests for MySiteOverlaysCoordinator and mock those children coordinators dependencies.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. So how are the Coordinators retained? A parent coordinator retains a child coordinator and that one retains its children coordinators? If that's the case, do we also need to deallocate them once the screen is dismissed?

So if Coordinator A (CA) is the parent:

CA -> CB -> CC and the ViewController that is linked to CC is dismissed, is there a mechanism to deallocate CC in place?

@salimbraksa salimbraksa Jan 17, 2024

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alpavanoglu MySiteViewController retains MySiteOverlaysCoordinator, which in turns retains those child coordinators.

MySiteVC > MySiteOverlaysCoordinator > Child Coordinators

Also, none of the coordinators retain a View Controller. And the coordinators will be deallocated when MySiteVC is deallocated.

Keep in mind that the coordinators are only responsible for displaying their respective VCs, but they don't retain them.

@alpavanoglu alpavanoglu Jan 17, 2024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. So if a child coordinator's ViewController is dismissed, the child coordinator itself will not be deallocated because it is being referenced by the parent coordinator. If you think in terms of navigation stack, this would be like a whole flow being retained until their root VC is dismissed/deallocated.

Because of this problem, I personally prefer that each respective VC retains their Coordinator rather than a parent Coordinator retaining children coordinators. Otherwise coordinators have to replicate a deallocation chain that is provided by Navigation Stack out of the box.

Of course all of this is with the assumption that Coordinators are class and not struct. Since they often contain states that change over its lifetime, I find class to be a better fit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally prefer that each respective VC retains their Coordinator rather than a parent Coordinator retaining children coordinators.

I think this is not always possible. In cases like the In-App Feedback coordinator, where the VC is just a UIAlertViewController, we have to create a subclass to retain the Coordinator.

Additionally, the InAppFeedbackCoordinator was inspired by the CompliancePopoverCoordinator, and the latter doesn't retain its VC.

I'm not against your suggestion, but I still don't know what to change exactly. Could you provide a code example to clarify this approach?

@salimbraksa salimbraksa Jan 18, 2024

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you think in terms of navigation stack, this would be like a whole flow being retained until their root VC is dismissed/deallocated.

I understand, but this is not happening in our case. MySiteViewController retains ONLY the coordinators of the screens it is responsible for presenting.

The coordinators are just an alternative to writing the presentation logic within the MySiteViewController.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. The structure confused me to think otherwise. MySiteOverlaysCoordinator is merely a helper to ensure only one coordinator is active at a given time. No action needed then.

Additionally, the InAppFeedbackCoordinator was inspired by the CompliancePopoverCoordinator, and the latter doesn't retain its VC.

Yeah I think it is better that the Coordinator does not retain VCs. Maybe I wasn't articulate enough. Navigation stack already retains VCs down the chain. When Coordinators do it too, we need to deal with deallocating them.

@alpavanoglu

Copy link
Copy Markdown
Contributor

🧪 Test Results 🧪

Positive Feedback Flow

❓ Since I cannot Submit (on simulator), I had to press Cancel. After that, the view is dismissed. And the rest of steps passed.

Negative Feedback Flow

✅ All steps work as expected.

Cancel Negative Feedback Flow

✅ All steps work as expected.

Regression: Compliance Popover

✅ All steps work as expected.

@salimbraksa
salimbraksa force-pushed the task/in-app-feedback-ui branch from 1162ccb to 650d08e Compare January 17, 2024 15:10
@salimbraksa
salimbraksa force-pushed the task/in-app-feedback-ui branch from 650d08e to cb10a4a Compare January 17, 2024 15:11
@salimbraksa salimbraksa modified the milestones: Pending, 24.1 Jan 17, 2024

@alpavanoglu alpavanoglu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving to not block the progress. However I think that my last 2 comments might be important. If after considering them, you think it is safe to go ahead, please feel free to merge.

Thanks for taking care of my earlier comments!

@peril-wordpress-mobile

Copy link
Copy Markdown
Warnings
⚠️ This PR is assigned to a milestone which is closing in less than 4 days Please, make sure to get it merged by then or assign it to a later expiring milestone

Generated by 🚫 dangerJS

@salimbraksa
salimbraksa merged commit 4dc2a78 into task/in-app-feedback Jan 18, 2024
@salimbraksa
salimbraksa deleted the task/in-app-feedback-ui branch January 18, 2024 16:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants