Skip to content

Jetpack Focus: Enable iOS14+ Stats Widgets and Intents for Jetpack - #19479

Merged
staskus merged 25 commits into
trunkfrom
fix/19460-jetpack-focus-enable-widgets-for-jetpack
Oct 26, 2022
Merged

Jetpack Focus: Enable iOS14+ Stats Widgets and Intents for Jetpack#19479
staskus merged 25 commits into
trunkfrom
fix/19460-jetpack-focus-enable-widgets-for-jetpack

Conversation

@staskus

@staskus staskus commented Oct 18, 2022

Copy link
Copy Markdown
Contributor

Part of #19460

Description

This PR makes >=iOS 14 "Today", "This Week" and "This Year" Stats widgets available for the Jetpack app.

Solution

These are the main tasks accomplished to make Stats widgets work:

  1. Create JetpackStatsWidgets target by duplicating WordPressStatsWidgets.
  • Change display name, bundle identifiers, schemes, and target dependencies
  1. Split part of Constants.h into separate AppConstants.h for each app. It allows defining different UserDefault keys and different file names for saving widget information. Since Jetpack app and WordPress app share the same app group , we need to differentiate these parts.
  2. Create JetpackIntents by duplicating WordPressIntents. Perform similar steps as with JetpackStatsWidgets. It allows supporting site selection in the Edit widget menu.
  3. Add migration. If the user has Jetpack app already installed and logged in, we need to detect this case and initialize widgets after the app's launch.
  4. Have different localization for empty widget states

Testing instructions

Case 1: Fresh install

  1. Install Jetpack
  2. Open Home Screen
  3. Add Today, This Week, This Year widgets - they should be showing Stats for the default site
  4. Click on the widgets - they should open Jetpack app and take to Stats screen
  5. Long click on widgets and tap "Edit Widget" - a list of available sites should show and changing it refreshes the widget.
  6. Come back to the app and logout - the widget should be showing "Please login to Jetpack..." message

Case 2: WordPress app with widgets exist

  1. Install WordPress, log in and add widgets
  2. Install Jetpack, log in to a different account and add widgets
  3. Jetpack app should be showing different information to WordPress app

Case 3: Migration

  1. Install an older version of Jetpack and log in
  2. Install and launch a new version of Jetpack
  3. Add widgets, they should work in all the scenarios as in Case 1

Regression Notes

  1. Potential unintended areas of impact

Breaking WordPress stats widgets

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

Manual testing to ensure that WordPress widgets remain unaffected when interacting with Jetpack widgets

  1. 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.

TBD:

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

Images & Videos

Jetpack.-.schemes.MP4
Jetpack.-.add.widgets.mov

IMG_5393

- Duplicating WordPressStatsWidget target
- Changing Display Name, Bundle Identifier for all the configurations, Info.plist file, Code Signing Entitlements, WPCOM_SCHEME value
… for different apps

- Jetpack and WordPress apps share the same AppGroup
- We need to differentiate between the keys of UserDefaults and file names to save different Widget preferences for Jetpack and WordPress apps
Duplicate and change displayName, bundleIdentifiers, set target dependencies, change schemes
…s widgets

After updating from an old version of Jetpack app that doesn't support Stats widgets to a newer version, required UserDefault flags and other information is not already saved and cannot be accessed by Jetpack Stats Widgets. Detect if these flags are not set and initialize widgets after the app is launched.
@staskus staskus added this to the 21.1 milestone Oct 18, 2022
@staskus
staskus requested a review from guarani October 18, 2022 11:29
@wpmobilebot

wpmobilebot commented Oct 18, 2022

Copy link
Copy Markdown
Contributor
You can test the changes in WordPress from this Pull Request by:
  • Clicking here or scanning the QR code below to access App Center
  • Then installing the build number pr19479-7755426 on your iPhone

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

@staskus
staskus requested review from guarani and removed request for guarani October 18, 2022 15:14
@staskus

staskus commented Oct 18, 2022

Copy link
Copy Markdown
Contributor Author

📝 I noticed there're some issues with provisioning profiles in the buildkite builds. Working on it.

@guarani guarani 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.

Thanks for the PR @staskus! I went through the three test cases and things worked well.

Product bundle identifier

From what I see, the new intent target for Jetpack is what lets the user pick their site while configuring the widget. The product bundle identifier uses com.automattic. for debug/release build configs, but com.jetpack. for alpha and internal. Was this intentional? I also see a potential typo of com.jetpac. for the alpha build config:

Duplicate target dependencies

Looking at the Jetpack target's dependencies, a couple of entries look duplicated (some might pre-date this PR, but I haven't checked). I tested without these and the app seems to work fine.

Potential duplication

It feels like adding the widgets to Jetpack duplicated things (user default keys, localized strings, etc). I understand though that these need to be unique and for some things (esp. localized strings), it might not be desirable or practical to make these dynamic.

Open questions

A couple of open questions which I haven't found time to answer today:

  • Have the changes been tested on iOS 13 (e.g. to ensure that the app handles gracefully the lack of widget support in iOS 13?)
  • Are the intents exposed to Siri and if so does Siri differentiate properly between the apps?
  • Should the widgets and their UI (e.g. for site selection) use a green accent/tint color instead of the current blue color?

I'll hold off approving since the product bundle identifier question looks to be a blocker, although the rest are not.

);
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.jetpac.alpha.JetpackIntents;

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.

This should be com.jetpack.alpha (missing "k")

@staskus

staskus commented Oct 19, 2022

Copy link
Copy Markdown
Contributor Author

Thanks for the review!

The product bundle identifier uses com.automattic. for debug/release build configs, but com.jetpack. for alpha and internal. Was this intentional?

It is intentional from my side. This is how code signing is set up for the main Jetpack target as well as new extensions we're creating.

Looking at the Jetpack target's dependencies, a couple of entries look duplicated

Good observation, thanks! I explicitly added them there and then they were automatically added again when I included extensions into Frameworks, Libraries, and Embedded Content. I will remove duplications.

Potential duplication. It feels like adding the widgets to Jetpack duplicated things (user default keys, localized strings, etc). I understand though that these need to be unique and for some things (esp. localized strings), it might not be desirable or practical to make these dynamic.

I would call it configuration instead of duplication. We need certain things to be different widget targets.

  • We could be explicit about it and have WidgetConfiguration.h instead of AppConstants.h to keep widget-related configuration constants. It could be accessed via AppConfiguration.widgets that we already use, keeping everything in one place.
  • We could use xcconfig files, to be explicit about all the differences and similarities between targets - including bundle identifiers AND constants. Maybe it could be a future consideration.
  • Of course, as you mention, we could also make configuration dynamic. For example., use something like \(AppConfiguration.userDefaultsPrefix)UserDefaultsKey) keys so they would be determined when the extension/app is running and not hard coded. Use %@ in localized strings, where %@ would be the value of the app name. However, I don't think that's desirable, since such differences would be scattered among different code files and would be hard to identify.

In my opinion, we should try defining the differences between Jetpack and WordPress (and their widgets) as explicitly as possible, without scattering them across the codebase and avoiding if/switch statements as much as possible.

These are 2 commits I make these changes in

Let me know what you think about such changes. I can easily revert them if they look a bit overhead.

Have the changes been tested on iOS 13 (e.g. to ensure that the app handles gracefully the lack of widget support in iOS 13?)

Yes. With these changes, Jetpack still doesn't have iOS 13 widgets and WordPress has older Today widgets. My intention is to add older iOS13 widgets in a separate PR.

Are the intents exposed to Siri and if so does Siri differentiate properly between the apps?

The SiriKit intents here are only used to have configurable widgets using "Edit Widget" menu. I don't see Siri voice commands being used in any way.

@staskus staskus closed this Oct 19, 2022
@staskus staskus reopened this Oct 19, 2022
@wpmobilebot

wpmobilebot commented Oct 19, 2022

Copy link
Copy Markdown
Contributor
You can test the changes in Jetpack from this Pull Request by:
  • Clicking here or scanning the QR code below to access App Center
  • Then installing the build number pr19479-7755426 on your iPhone

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

@staskus

staskus commented Oct 19, 2022

Copy link
Copy Markdown
Contributor Author

What was done since the review:

Simulator.Screen.Recording.-.iPhone.14.-.2022-10-19.at.14.01.13.mp4

@staskus

staskus commented Oct 20, 2022

Copy link
Copy Markdown
Contributor Author

I couldn't spot any issues in the code. Can you reproduce it?

Yes, I could. Checking if there's some mismatch between the capabilities in release/alpha/internal profiles.

@staskus staskus changed the title Jetpack Focus: Enable Stats widgets and intents for Jetpack Jetpack Focus: Enable iOS14+ Stats Widgets and Intents for Jetpack Oct 20, 2022
@mokagio
mokagio self-requested a review October 21, 2022 05:54
@mokagio

mokagio commented Oct 21, 2022

Copy link
Copy Markdown
Contributor

I requested a review for myself because I wanted to look at the changes but don't have the time to do it right now.

@peril-wordpress-mobile

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

Generated by 🚫 dangerJS

@staskus

staskus commented Oct 21, 2022

Copy link
Copy Markdown
Contributor Author

I found out that for Release-Alpha builds we still use INTERNAL_BUILD preprocessor macro, which in Constants file chooses groups for Internal build and not for Alpha builds.

#ifdef INTERNAL_BUILD
NSString *const WPAppGroupName                                      = @"group.org.wordpress.internal";
NSString *const WPAppKeychainAccessGroup                    = @"99KV9Z6BKV.org.wordpress.internal";

Maybe it's the reason Widgets cannot load data in AppCenter (alpha) builds. It's hard to tell if such behavior is intentional. I set ALPHA_BUILD preprocessor macro for Release-Alpha and set appropriate group names in Constants.h.

The other possible solution would be to use same groups for both release-alpha or release-internal but that would require changes in profiles.

I can confirm that now Widget data loads on AppCenter version

@staskus
staskus requested a review from guarani October 21, 2022 08:25
@mokagio

mokagio commented Oct 24, 2022

Copy link
Copy Markdown
Contributor

@staskus

I found out that for Release-Alpha builds we still use INTERNAL_BUILD preprocessor macro, which in Constants file chooses groups for Internal build and not for Alpha builds.

...

It's hard to tell if such behavior is intentional.

I haven't looked at the Git history, but that sounds like an oversight to me. Something that was not noticed at the time of adding the alpha build.

The fix you applied seems appropriate and something we should keep going forward. 👍

@mokagio mokagio 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.

None of my comments are blockers, but I'd like to know if you have any comments. Even just ideas on how to follow up later on.

The implementation at the project level is solid, as far as I can see.

I haven't tested the widgets locally, but consider this an approval for me once the other reviewers have a chance to look at the latest changes.

@objc static let homeWidgetAllTimeFilename = "HomeWidgetAllTimeData.plist"
@objc static let homeWidgetThisWeekFilename = "HomeWidgetThisWeekData.plist"
}
}

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.

It's neat to have dedicated settings for WordPress vs Jetpack. Have you thought about how this approach of duplicating a class definition will scale?

In particular, here's something that I'm concerned might happen:

  1. Developer working in Jetpack adds a new value to the AppConfigurationWidget version from Jetpack
  2. They use it in a file shared between Jetpack and WordPress
  3. Everything works fine for them because they are only building Jetpack
  4. The build fails in CI because the value is not available in WordPress

This behavior can be seen in #19502.

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.

Thank you for your comments.

Have you thought about how this approach of duplicating a class definition will scale?

Yes, I thought about it and this is not ideal. Your example is excellent. I had experience in other larger projects where we had to support multiple targets with different configurations. The use case was a bit different than we have with WordPress and Jetpack but technically the problem looks similar.

  • The worst approach was having multiple control statements scattered in the codebase. This made understanding the differences between different configurations hard and any changes needed to be made directly in particular classes.
  • We did try duplicating class definitions but we soon hit the problem you were describing. It's also not 100% transparent as well, which files are configuration files and which ones are included in which targets.
  • It's possible to move the configuration to xcconfig configuration files. It would allow us to have all the configuration, including build settings, in one place. Although that wouldn't guarantee we set all the required fields for all configurations. It could be used in tandem with some approaches in the code.
  • Our configurations could conform to one protocol and the correct configuration be resolved at runtime. This way both configurations could be included in both targets, allowing us to show build error when one of the configurations doesn't conform to a protocol. We ended up using a flavor of this approach in our previous project.

All in all, I think it deserves a wider discussion, how we could set up a configuration in a way that would support our needs going forward. Otherwise, we could end up with a messy solution.

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.

All in all, I think it deserves a wider discussion, how we could set up a configuration in a way that would support our needs going forward. Otherwise, we could end up with a messy solution.

Definitely. I'd be great to address the problem before it happens, but the kind of changes we are discussing are too far-reaching and it would make this PR go stale.

I'm glad to hear about the protocol idea, because that's something I'd been thinking about as well. We'd then need something that at runtime allocates the correct one but that seems simpler to achieve than keeping duplicated classes in sync.

Alternatively, and depending on what the plan long term is for these widgets, it might be beneficial to extract the bulk of the logic in a framework, then have each widget target be a thin wrapper to it and pass a struct with configurations. That would centralize things big time, but does come with an extra layer of complexity in isolating stuff.

We'll see...

Comment thread WordPress/JetpackStatsWidgets/LocalizationConfiguration.swift Outdated
static let unconfiguredViewThisWeekTitle = LocalizableStrings.unconfiguredViewJetpackThisWeekTitle
static let unconfiguredViewAllTimeTitle = LocalizableStrings.unconfiguredViewJetpackAllTimeTitle
}
}

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.

The same consideration about duplicating implementation mentioned for WidgetConfiguration.swift applies here.

@objc static let statsTodayWidgetKeychainTokenKey = "OAuth2Token"
@objc static let statsTodayWidgetKeychainServiceName = "TodayWidget"
@objc static let statsTodayWidgetUserDefaultsSiteIdKey = "WordPressTodayWidgetSiteId"
@objc static let statsHomeWidgetsUserDefaultsSiteIdKey = "WordPressHomeWidgetsSiteId"

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.

Have you considered increasing the granularity of these configurations?

@objc extension AppConfiguration {

    class Widget: NSObject {

        @objc(AppConfigurationWidgetStatsToday)
        class StatsToday: NSObject {
            @objc static let keychainTokenKey = "OAuth2Token"
            @objc static let keychainServiceName = "TodayWidget"
            @objc static let userDefaultsSiteIdKey = "WordPressTodayWidgetSiteId"
        }

        @objc(AppConfigurationWidgetStatsHome)
        class StatsHome: NSObject {
            @objc static let userDefaultsSiteIdKey = "WordPressHomeWidgetsSiteId"
...

@staskus staskus Oct 24, 2022

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 haven't but it would make sense since these keys are meant for different widgets. Although some of the keys are used for both iOS13 and iOS14+ widgets. I'll take a look what makes most sense.

return
}

userDefaults.setValue(AccountHelper.isLoggedIn, forKey: AppConfiguration.Widget.Stats.userDefaultsLoggedInKey)

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.

Note: Check if we could check existence of access token with a static key

@staskus staskus Oct 25, 2022

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.

Unfortunately, the way the widgets are created we still rely on having defaultSiteId and preloaded widgetData when we initialize widgets for the first time. Only later StatsWidgetsService can be used inside of a widget to update the data.

Also, any refactoring we would do, we would still be unable to avoid user launching Jetpack at least once before adding widgets. After the launch correct token is set for AppConfiguration.Widget.Stats.keychainServiceName which is different between WordPress and Jetpack apps. Until then, the Widget wouldn't be able to display or fetch any information.

@guarani guarani Oct 25, 2022

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.

we would still be unable to avoid user launching Jetpack at least once before adding widgets

nitpick: I thought the risk was that the user adds widgets before launching Jetpack (opposite of what's described here). Either way, it sounds like this idea was a dead-end.

@staskus staskus Oct 26, 2022

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 thought the risk was that the user adds widgets before launching Jetpack

I don't see how can we avoid this risk, even if we make changes on what Widgets depends on. For those users who are updating the app from an older version that didn't support widgets, we would still need to set access token in the shared keychain with the expected key. Even though the key would be static, they would still differ from WordPress and would need to be set at least once.

Comment thread Podfile Outdated

@guarani guarani 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.

Widgets couldn't be added now when building from Xcode, but work when installing the App Center builds.

Comment thread Podfile Outdated
return
}

userDefaults.setValue(AccountHelper.isLoggedIn, forKey: AppConfiguration.Widget.Stats.userDefaultsLoggedInKey)

@guarani guarani Oct 25, 2022

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.

we would still be unable to avoid user launching Jetpack at least once before adding widgets

nitpick: I thought the risk was that the user adds widgets before launching Jetpack (opposite of what's described here). Either way, it sounds like this idea was a dead-end.

@guarani

guarani commented Oct 26, 2022

Copy link
Copy Markdown
Contributor

Widgets couldn't be added now when building from Xcode, but work when installing the App Center builds.

Update: After debugging with @staskus the widgets worked again after restarting the device, so I'm re-reviewing now.

@guarani
guarani self-requested a review October 26, 2022 13:52

@guarani guarani 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.

I ran through the test cases and things are working now. It looks like my earlier difficulties adding widgets in #19479 (review), were due to a device quirk because they went away after re-starting the device as you suggested.

Case 1: Fresh install

This works as expected. I noticed a bug that isn't related to this PR and is reproducible on WPiOS 21.0 (TestFlight): On one account with a site with few stats, I saw the "Unable to load site stats" message. The app was showing data in Stats for the site (Most Popular Time and All Time had data, while Today had no data), and the widgets displayed the error message even after visiting Stats. I'm creating an issue for this now (update: see #19519).

Case 2: WordPress app with widgets exist

Works as expected.

Case 3: Migration

Works as expected.


Thanks for your awesome work getting these widgets into the Jetpack app, @staskus! 💯

@staskus
staskus merged commit 3189c40 into trunk Oct 26, 2022
@staskus
staskus deleted the fix/19460-jetpack-focus-enable-widgets-for-jetpack branch October 26, 2022 14:57
class Widget: NSObject {
@objc(AppConfigurationWidgetStats)
class Stats: NSObject {
@objc static let keychainTokenKey = "OAuth2Token"

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.

@staskus I'm working on doing something similar to the constants used by Extensions. Is there a reason for opting to use the same token key for both apps?

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 didn't find it necessary, since it was used in tandem with keychainServiceName which was different for both apps. Did you encounter any problems with that?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants