Skip to content

Conversation

@joshheald
Copy link
Contributor

@joshheald joshheald commented May 10, 2023

Closes: #9679

Description

Apologies that this is slightly long. If it would help, I could split the ModalOverlay from the JustInTimeMessageModal, however the overlay would not be easily testable.

We want to show a modal-style Just in Time Message, for a higher-visibility message. This PR adds a template, and modal overlay display style, to provide that messaging.

Tapping the CTA navigates to a web view or universal link in the same way as banner JITMs do

Tapping the Maybe later button dismisses the JITM modal, and logs the dismissal with the JITM system

Tapping outside the modal temporarily dismisses the JITM modal, but does not log the dismissal with the JITM system.

N.B. – on the My Store screen, store onboarding has priority over Just in Time Messages. Originally this was because they used the same space, and getting a store set up fully would generally come before IPP-related actions, which is what JITMs were/are used for. This restriction continues, and still applies to modal JITMs even though they don't share space.

Testing instructions

Test setup for this is non-trivial until the JITM message definitions are added - sorry! There are two options:

  1. Proxyman/Charles breakpoint and modify the response (easier, but requires intervention for each request): PCYsg-Ffl-p2
  2. Set up a WPcom sandbox and ephemeral site with test JITM definitions: P91TBi-a5J-p2

In either case, you'll also need to host the images somewhere, and use the correct conventions when adding them to the JITM definition/JSON response.

Using a proxy

Using Charles or Proxyman, set a breakpoint on the following endpoint pattern: https://public-api.wordpress.com/rest/v1.1/jetpack-blogs/*/rest-api/?json=true&locale=*&path=/jetpack/v4/jitm*

Modify the response JSON using the following sample JSON response:

{
  "data": [
    {
      "content": {
        "message": "Accept in-person contactless payments on your iPhone",
        "icon": "",
        "iconPath": null,
        "list": [],
        "description": "Tap to Pay on iPhone is quick, secure, and simple to set up in WooCommerce Payments - no extra terminals or card readers needed.",
        "classes": "",
        "title": "",
        "disclaimer": []
      },
      "CTA": {
        "message": "Set up Tap to Pay",
        "hook": "",
        "newWindow": true,
        "primary": true,
        "link": "https://woocommerce.com/mobile/payments/tap-to-pay"
      },
      "template": "modal",
      "ttl": 300,
      "id": "woomobile_tap_to_pay_wcpay_set_up_test_1",
      "feature_class": "woomobile_tap_to_pay_wcpay_set_up",
      "expires": 30,
      "max_dismissal": 50,
      "activate_module": null,
      "is_dismissible": true,
      "is_user_created_by_partner": null,
      "message_expiration": null,
      "assets": {
        "background_image_url": "https://{EPHEMERAL-SITE-HOST}/wp-content/uploads/2023/05/[email protected]",
        "background_image_dark_url": "https://{EPHEMERAL-SITE-HOST}/wp-content/uploads/2023/05/[email protected]",
        "badge_image_url": "https://{EPHEMERAL-SITE-HOST}/wp-content/uploads/2023/05/[email protected]",
        "badge_image_dark_url": "https://{EPHEMERAL-SITE-HOST}/wp-content/uploads/2023/05/[email protected]"
      },
      "url": "https://jetpack.com/redirect/?source=jitm-woomobile_tap_to_pay_wcpay_set_up_test_1",
      "jitm_stats_url": "https://pixel.wp.com/b.gif?v=wpcom2&rand=fe304a7d0cabbf0466f5d554a6f9de3a&x_jetpack-jitm=woomobile_tap_to_pay_wcpay_set_up_test_1"
    }
  ]
}

Using a sandbox

A sample JITM definition (for the sandbox option) is below:

public_html/wp-content/mu-plugins/0-sandbox.php

<?php
function sandbox_jitm_rules( $rules, $cache ) {
	return array(
		( new \JITM\Message( 'woomobile_tap_to_pay_wcpay_set_up_test_1', 'woomobile_tap_to_pay_wcpay_set_up', $cache ) )
			->show( 'Accept in-person contactless payments on your iPhone',
					'Tap to Pay on iPhone is quick, secure, and simple to set up in WooCommerce Payments - no extra terminals or card readers needed.' )
			->message_path( '/woomobile:my_store:admin_notices/' )
			->with_asset( 'background_image_url', 'https://{EPHEMERAL-SITE-HOST}/wp-content/uploads/2023/05/[email protected]' )
			->with_asset( 'background_image_dark_url', 'https://{EPHEMERAL-SITE-HOST}/wp-content/uploads/2023/05/[email protected]' )
			->with_asset( 'badge_image_url', 'https://{EPHEMERAL-SITE-HOST}/wp-content/uploads/2023/05/[email protected]' )
			->with_asset( 'badge_image_dark_url', 'https://{EPHEMERAL-SITE-HOST}/wp-content/uploads/2023/05/[email protected]' )
			->plugin_active( 'woocommerce-payments/woocommerce-payments.php' )
			->with_CTA( 'Set up Tap to Pay',
						'',
						'https://woocommerce.com/mobile/payments/tap-to-pay' )
			->for_sites_of_types( array( 'wpcom', 'jetpack', 'atomic' ) )
			->with_wc_country( 'US' )
			->max_dismissals( 50 )
                        ->with_template( 'modal' )
			->show_again_after( 30 )
			->priority( 150 ),
	);
}
add_filter( 'jetpack_jitm_rules', 'sandbox_jitm_rules', 10, 2 );

Image hosting – I uploaded the images to my ephemeral site's media library, and used their URLs in the definition/response. Replace {EPHEMERAL-SITE-HOST} with your site's domain if you follow the same route. While it remains up, you're welcome to use pale-pirate-peach.atomicsites.blog if you like, just to save a step of hosting the images.

Testing in the app

Whichever route you choose:

Functionality: Deeplinking to Set up TTP

  1. Launch the app
  2. Observe that the JITM modal is shown
  3. Tap the CTA – observe that the Set up Tap to Pay on iPhone flow is opened

Functionality: Dismissal

  1. Launch the app
  2. Observe that the JITM modal is shown
  3. Tap outside the modal – observe that it is dismissed
  4. Pull to refresh – observe that the modal is shown again (we may want to change this soon!)
  5. Tap the Maybe later button – observe that the modal is dismissed
  6. Pull to refresh – observe that the modal is not shown until a pull to refresh 30s after the maybe later is tapped (real JITMs will have a delay measured in days/weeks)

Functionality: Opening a web view

Change the network response/JITM definition so that the CTA URL is https://woocommerce.com/products/hardware/us

  1. Launch the app
  2. Observe that the JITM modal is shown
  3. Tap the CTA – observe that the web view is opened and redirects to the M2 reader page

Appearance

  1. Observe that the specified image is shown for your current light/dark mode
  2. Switch display mode
  3. Observe that the specified images are updated
  4. Remove the image urls from the response – observe that the default image is used

Screenshots

JITM.modal.mp4

  • I have considered if this change warrants user-facing release notes and have added them to RELEASE-NOTES.txt if necessary.

@joshheald joshheald added type: enhancement A request for an enhancement. feature: mobile payments Related to mobile payments / card present payments / Woo Payments. feature: jitm Related to Just In Time Messages labels May 10, 2023
@joshheald joshheald added this to the 13.6 milestone May 10, 2023
@joshheald joshheald changed the title Issue/9679 modal jit ms [Just in Time Messages] Add Modal JITM template support for Tap to Pay campaign May 10, 2023
@wpmobilebot
Copy link
Collaborator

wpmobilebot commented May 10, 2023

You can test the changes from this Pull Request by:
  • Clicking here or scanning the QR code below to access App Center
  • Then installing the build number pr9694-8a13be0 on your iPhone

If you need access to App Center, please ask a maintainer to add you.

joshheald added 5 commits May 11, 2023 10:59
Because of the additional delay in dismissing the modal JITM, universal links to Set up Tap to Pay on iPhone only got as far as the payments menu.

This was due to using a short async delay to trigger the navigation to the Set up TTPoI flow – which works with a banner JITM, but triggers before the screen is presented when called from a modal JITM, so `navigationController` was still nil. That meant there was nothing to show the Set up TTPoI flow modal from.

I’ve added a callback in the `viewDidLoad` for the view controller which allows us to trigger the navigation step at the right time, i.e. when the menu screen is fully loaded.
@joshheald joshheald marked this pull request as ready for review May 11, 2023 10:55
@joshheald joshheald requested a review from iamgabrielma May 11, 2023 10:55
@peril-woocommerce
Copy link

Warnings
⚠️ PR has more than 500 lines of code changing. Consider splitting into smaller PRs if possible.

Generated by 🚫 dangerJS

@iamgabrielma iamgabrielma self-assigned this May 12, 2023
@iamgabrielma
Copy link
Contributor

👋 I have started to check into this but I haven't got into the code yet, during testing I found 2 cases that I cannot replicate, so I'm not sure if I'm following the instructions correctly:

✅ Functionality: Deeplinking to Set up TTP
✅ Functionality: Dismissal
❌ Functionality: Opening a web view: By changing the CTA link in the response to https://woocommerce.com/products/hardware/us I get no response when I tap the button. I only change the link property within the CTA chunk, let me know if I should modify something else:

Click me
{
  "data": [
    {
      "content": {
        "message": "Accept in-person contactless payments on your iPhone",
        "icon": "",
        "iconPath": null,
        "list": [],
        "description": "Tap to Pay on iPhone is quick, secure, and simple to set up in WooCommerce Payments - no extra terminals or card readers needed.",
        "classes": "",
        "title": "",
        "disclaimer": []
      },
      "CTA": {
        "message": "Set up Tap to Pay",
        "hook": "",
        "newWindow": true,
        "primary": true,
        "link": "https://woocommerce.com/products/hardware/us"
      },
      "template": "modal",
      "ttl": 300,
      "id": "woomobile_tap_to_pay_wcpay_set_up_test_1",
      "feature_class": "woomobile_tap_to_pay_wcpay_set_up",
      "expires": 30,
      "max_dismissal": 50,
      "activate_module": null,
      "is_dismissible": true,
      "is_user_created_by_partner": null,
      "message_expiration": null,
      "assets": {
        "background_image_url": "https://pale-pirate-peach.atomicsites.blog/wp-content/uploads/2023/05/[email protected]",
        "background_image_dark_url": "https://pale-pirate-peach.atomicsites.blog/wp-content/uploads/2023/05/[email protected]",
        "badge_image_url": "https://pale-pirate-peach.atomicsites.blog/wp-content/uploads/2023/05/[email protected]",
        "badge_image_dark_url": "https://pale-pirate-peach.atomicsites.blog/wp-content/uploads/2023/05/[email protected]"
      },
      "url": "https://jetpack.com/redirect/?source=jitm-woomobile_tap_to_pay_wcpay_set_up_test_1",
      "jitm_stats_url": "https://pixel.wp.com/b.gif?v=wpcom2&rand=fe304a7d0cabbf0466f5d554a6f9de3a&x_jetpack-jitm=woomobile_tap_to_pay_wcpay_set_up_test_1"
    }
  ]
}

❌ Appearance: Removing the image urls from the response – observe that the default image is used: Removing the URLs from the response does not show the default images:

Click me
{
  "data": [
    {
      "content": {
        "message": "Accept in-person contactless payments on your iPhone",
        "icon": "",
        "iconPath": null,
        "list": [],
        "description": "Tap to Pay on iPhone is quick, secure, and simple to set up in WooCommerce Payments - no extra terminals or card readers needed.",
        "classes": "",
        "title": "",
        "disclaimer": []
      },
      "CTA": {
        "message": "Set up Tap to Pay",
        "hook": "",
        "newWindow": true,
        "primary": true,
        "link": "https://woocommerce.com/mobile/payments/tap-to-pay"
      },
      "template": "modal",
      "ttl": 300,
      "id": "woomobile_tap_to_pay_wcpay_set_up_test_1",
      "feature_class": "woomobile_tap_to_pay_wcpay_set_up",
      "expires": 30,
      "max_dismissal": 50,
      "activate_module": null,
      "is_dismissible": true,
      "is_user_created_by_partner": null,
      "message_expiration": null,
      "assets": {
        "background_image_url": "",
        "background_image_dark_url": "",
        "badge_image_url": "",
        "badge_image_dark_url": "g"
      },
      "url": "https://jetpack.com/redirect/?source=jitm-woomobile_tap_to_pay_wcpay_set_up_test_1",
      "jitm_stats_url": "https://pixel.wp.com/b.gif?v=wpcom2&rand=fe304a7d0cabbf0466f5d554a6f9de3a&x_jetpack-jitm=woomobile_tap_to_pay_wcpay_set_up_test_1"
    }
  ]
}

We also seem to have a failing test: test_when_no_messages_are_received_existing_messages_are_removed. I ran CI twice and the same one has failed, so I tried locally and fails as well when doing this assertion: XCTAssertNil(viewModel.announcementViewModel), as the viewModel contains objects:

(lldb) po viewModel.announcementViewModel
▿ Optional<AnnouncementCardViewModelProtocol>
  ▿ some : <JustInTimeMessageViewModel: 0x130d1b420>

@joshheald
Copy link
Contributor Author

Thanks for looking at it @iamgabrielma ... I'll try to help with what's going on but... it works on my machine 😬

webview-and-default-image.mp4

❌ Functionality: Opening a web view: By changing the CTA link in the response to https://woocommerce.com/products/hardware/us I get no response when I tap the button. I only change the link property within the CTA chunk, let me know if I should modify something else

Your JSON looks correct to me. When I set the link to the M2 product page in my sandbox, tapping the button dismisses the modal and shows the web view. Here's some JSON which I've copied from a response my sandbox made to the app, so used the actual rule definition:

{
  "data": [
    {
      "content": {
        "message": "Accept contactless payments with WooCommerce Payments",
        "icon": "",
        "iconPath": null,
        "list": [],
        "description": "Tap to Pay on iPhone is quick, secure and simple to set up - no extra terminals or card readers needed.",
        "classes": "",
        "title": "",
        "disclaimer": []
      },
      "CTA": {
        "message": "Set up Tap to Pay",
        "hook": "",
        "newWindow": true,
        "primary": true,
        "link": "https://woocommerce.com/products/hardware/us"
      },
      "template": "modal",
      "ttl": 300,
      "id": "woomobile_tap_to_pay_wcpay_set_up_test_1",
      "feature_class": "woomobile_tap_to_pay_wcpay_set_up",
      "expires": 30,
      "max_dismissal": 50,
      "activate_module": null,
      "is_dismissible": true,
      "is_user_created_by_partner": null,
      "message_expiration": null,
      "url": "https://jetpack.com/redirect/?source=jitm-woomobile_tap_to_pay_wcpay_set_up_test_1&#038;site=pale-pirate-peach.atomicsites.blog&#038;u=1",
      "jitm_stats_url": "https://pixel.wp.com/b.gif?v=wpcom2&rand=b8d84113470da77dca1b06079de95480&x_jetpack-jitm=woomobile_tap_to_pay_wcpay_set_up_test_1"
    }
  ]
}

❌ Appearance: Removing the image urls from the response – observe that the default image is used: Removing the URLs from the response does not show the default images:

This time your JSON doesn't look like what I intended, but should still work. I wasn't specific enough here, really I meant for you to remove the background_image_url key and value, not just return an empty string.

I've tried with both cases though, and it shows the megaphone image as per the default when I do it.

This has made me think a bit though: we should probably show no image in this case, because it's less decorative and more part of the content here. Showing a JITM modal without an image seems like a valid thing to want to do, so I'll change that behaviour... though perhaps not on this PR.

Two thoughts:

  • Is there any chance you're modifying the wrong sequenced response in Proxyman/Charles? I.e. could you check with an app breakpoint as well that the app is getting the network response you intend it to be?
  • Could you check for any console log messages about displaying a modal while something else is already presented? That is a risk I thought I'd covered with a recent commit, but it could be stopping the web view modal from showing if I... didn't.

We also seem to have a failing test: test_when_no_messages_are_received_existing_messages_are_removed. I ran CI twice and the same one has failed, so I tried locally and fails as well when doing this assertion: XCTAssertNil(viewModel.announcementViewModel), as the viewModel contains objects:

Argh sorry. I'll sort that.

I think we probably need to bump this to 13.7 at this point

@joshheald joshheald modified the milestones: 13.6, 13.7 May 12, 2023
Copy link
Contributor

@iamgabrielma iamgabrielma left a comment

Choose a reason for hiding this comment

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

Is there any chance you're modifying the wrong sequenced response in Proxyman/Charles? I.e. could you check with an app breakpoint as well that the app is getting the network response you intend it to be?

It is possible, today when I tried again it didn't work the first time but then started working for all my other tests. The response I used is the same, so perhaps due to Proxyman configuration, as I had to reconfigure the certificates and such. It "works on my machine" now as well 🤷🏻‍♂️

Could you check for any console log messages about displaying a modal while something else is already presented? That is a risk I thought I'd covered with a recent commit, but it could be stopping the web view modal from showing if I... didn't.

That seems to be the issue, I've written down the error in the comment here, with some attempted (but ultimately failed) fix. IMHO this detail can be handled on a different PR if needed 🚢

Comment on lines +28 to +34
extension JustInTimeMessageTemplate {
/// Returns a "ready to use" type filled with fake values.
///
public static func fake() -> JustInTimeMessageTemplate {
.banner
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Much cleaner for tests 💯

}


extension JustInTimeMessageViewModel: AnnouncementCardViewModelProtocol {
Copy link
Contributor

Choose a reason for hiding this comment

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

TIL that we cannot make this extension's access control private because is declaring protocol conformances, it makes sense, but I didn't realized until the compiler told me :D

self.modalJustInTimeMessageHostingController = modalController
modalController.view.backgroundColor = .clear
modalController.modalPresentationStyle = .overFullScreen
self.present(modalController, animated: true)
Copy link
Contributor

Choose a reason for hiding this comment

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

As you guessed on #9694 (comment), there's indeed an error logged in the console about attempting to present the web view when we're already presenting the JITM UIKit wrapper:

WooCommerce[59729:46069113] [Presentation] Attempt to present <_TtGC7SwiftUI19UIHostingControllerV11WooCommerce12WebViewSheet_: 0x12d101000> on <WooCommerce.MainTabBarController: 0x127879e00> (from <WooCommerce.DashboardViewController: 0x12c05da00>) which is already presenting <_TtGC11WooCommerce36ConstraintsUpdatingHostingControllerVS_28JustInTimeMessageModal_UIKit_: 0x12c0b9600>.

I did a quick experiment by switching .present(_:) to .show(_:) instead, so the system handles the navigation:

Suggested change
self.present(modalController, animated: true)
self.show(modalController, sender: self)

This fixes the dismissal and redirection issue to what we expect, however the overlay is still in the middle, so definitely not a final fix 😅 :

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't really want to change the presentation style for this... the JITM modal already dismisses itself when the CTA is tapped, but I've added a second call to do that from the DashboardViewController right before the web view is presented as an extra check.

N.B. I couldn't reproduce this issue .

Comment on lines +63 to +64
#if DEBUG // to avoid importing things we don't need in the implementation
import Yosemite
Copy link
Contributor

Choose a reason for hiding this comment

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

That's a nice way to deal with importing the module just for the preview 💯

Comment on lines +21 to +22
@unknown default:
EmptyView()
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we add a log, or track this case somehow? Otherwise we might fail to catch on time new AdaptiveAsyncImage cases when/if are introduced

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, because:

  1. we can't do that in the middle of a View.body
  2. we don't need to, because @unknown default means the compiler will still produce a warning for any unhandled cases. So if another case gets added in future, we'll know about it when we build the app.

Comment on lines +22 to +44
Color.black.opacity(0.5)
.edgesIgnoringSafeArea(.all)
.onTapGesture {
dismiss()
}
.transition(.opacity)
.animation(.easeInOut(duration: 0.1), value: internalIsPresented)

GeometryReader { geometry in
VStack {
content()
.padding(16)
.frame(width: geometry.size.width * 0.75)
.frame(maxHeight: geometry.size.height * 0.8)
.fixedSize(horizontal: false, vertical: true) // these three modifiers define the container size
.background(Color(.tertiarySystemBackground))
.cornerRadius(10)
.shadow(radius: 10)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) // Ensures the container is centred
}
.transition(.move(edge: .bottom))
.animation(.easeInOut(duration: 0.25), value: internalIsPresented)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we move the numbers to a Constants extension?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is one of those places where I feel like it would make the code less readable... and there's no (real) repetition, because the two 10s wouldn't be the same constant anyway.

joshheald added 2 commits May 15, 2023 17:40
If a modal JITM is showing when we open the webview, we’ll get an error and the webview won’t open. This shouldn’t really happen, because the JITM modal already dismisses itself when we tap its CTA, but just in case we can do a pre-emptive dismissal before showing the webview.
@joshheald joshheald enabled auto-merge May 15, 2023 16:56
@joshheald joshheald disabled auto-merge May 15, 2023 16:56
@joshheald joshheald enabled auto-merge May 15, 2023 17:10
@joshheald joshheald merged commit de7e112 into trunk May 15, 2023
@joshheald joshheald deleted the issue/9679-modal-JITMs branch May 15, 2023 20:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature: jitm Related to Just In Time Messages feature: mobile payments Related to mobile payments / card present payments / Woo Payments. type: enhancement A request for an enhancement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Just in Time Messages] Add support for Modal template messages

4 participants