Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

[in_app_purchase] Ensure the introductoryPriceMicros field is transported as a String. #4370

Merged
merged 5 commits into from Sep 28, 2021

Conversation

mvanbeusekom
Copy link
Contributor

In the current version the introductoryPriceMicros field is transported from JAVA to Dart as a long type. This is causing a runtime error on the Dart side as it is expected to be of type String (see comment here).

This PR will resolve the problem by making sure the introductoryPriceMicros field is converted to a String type before it is transported to Dart.

Ideally we would change the Dart API to expect the field to be a int, however this would introduce a breaking change. Therefore I opted for the conversion from long to String on the JAVA side.

Fixes the problem discussed here: #4364 (comment)

If you had to change anything in the flutter/tests repo, include a link to the migration guide as per the breaking change policy.

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the relevant style guides and ran the auto-formatter. (Note that unlike the flutter/flutter repo, the flutter/plugins repo does use dart format.)
  • I signed the CLA.
  • The title of the PR starts with the name of the plugin surrounded by square brackets, e.g. [shared_preferences]
  • I listed at least one issue that this PR fixes in the description above.
  • I updated pubspec.yaml with an appropriate new version according to the pub versioning philosophy.
  • I updated CHANGELOG.md to add a description of the change.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test exempt.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@781flyingdutchman
Copy link

781flyingdutchman commented Sep 22, 2021

I understand the desire to return a String to avoid a breaking change, but the priceAmountMicros and originalPriceAmountMicros fields are both type int so having introductoryPriceAmountMicros now suddenly be a String is very inconsistent. My understanding is that the introductoryPriceAmountMicros field was never populated correctly due to the wrong key being used to extract it from JSON, so couldn't we argue that the field effectively wasn't supported until the commit from two days ago (which turned out to not work)? In that case I would suggest changing the field to be an int and thus keeping it consistent with the other ...priceAmountMicros fields. If there ever was a moment to make this change, it is now, before the field is actually being used.

@mvanbeusekom
Copy link
Contributor Author

/cc @cyanglaz, @stuartmorgan, how do you feel about the comment from @781flyingdutchman? I agree with him it would make the API a more consistent API, however it will still result in a breaking change since we would change the type of a public field. Even though this field was never populated correctly, developers could still access it.

@stuartmorgan
Copy link
Contributor

My understanding is that the introductoryPriceAmountMicros field was never populated correctly due to the wrong key being used to extract it from JSON, so couldn't we argue that the field effectively wasn't supported until the commit from two days ago (which turned out to not work)?

There's a difference between code that compiles but returned nothing at runtime and code that doesn't compile. It appears from blame that people have had four months, not two days, to write code that would fail to build if this changed. It's a breaking change.

@mvanbeusekom This isn't forwarded through the cross-platform API, right; i.e., someone must directly use in_app_purchase_android to interact with this? If so, then can't we do a breaking change to just the android package, and pick that up in a minor update to the app-facing package?

(We should still do a bugfix in a bugfix update first though, so that the last version people will get that's resolvable with existing dependencies isn't actively broken.)

// Make sure the `introductoryPriceAmountMicros` field is returned as a `String` value as the
// Dart side expects it as `String`. Changing the type in Dart would introduce a breaking change.
info.put(
"introductoryPriceAmountMicros", String.valueOf(detail.getIntroductoryPriceAmountMicros()));
Copy link
Contributor

Choose a reason for hiding this comment

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

Changing this on the Java side will make a better fix later harder to do. Can't we change the transport name, and have a String getter on the dart side that returns the underlying int value as a String?

Choose a reason for hiding this comment

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

@stuartmorgan I understand changing to int would technically be a breaking change, but only for people who read and use the field - and that field would always have returned an empty string. I therefore expect the number of builds that break to be very very small.

Is a compromise to change the field to an int and also add a getter introductoryPriceAmountMicrosAsString that does the conversion so the very few people who read the original field and expect a string can simply change their field to the new getter?

Copy link
Contributor

Choose a reason for hiding this comment

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

@stuartmorgan I understand changing to int would technically be a breaking change, but only for people who read and use the field

I'm well aware that it's only a breaking change for people who reference the field; please re-read my comment.

I therefore expect the number of builds that break to be very very small.

That may be true (or may not; you don't really know how many people did things like try to use it, find that it didn't seem to work for them, and added a fallback but left the code in place), but the definition of a non-breaking change in semantic versioning is not "I don't think it'll break many people".

Is a compromise to change the field to an int and also add a getter introductoryPriceAmountMicrosAsString that does the conversion so the very few people who read the original field and expect a string can simply change their field to the new getter?

No, because that's a breaking change.

There is no way to change the type of the existing getter without it being a breaking change; this is not a matter of discussion, it's an objective fact. And we're not going to make an obviously breaking change without adjusting the version accordingly, because we do not deliberately violate semantic versioning in our plugins.

Choose a reason for hiding this comment

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

Got it - didn't mean to come across as pushing, was just trying to help. I have absolutely no skin in this game (I came across the original error, I don't use the field at all).

If I may make one comment, it would be that the impact of the original change (that caused the error) was quite severe, in that it rendered the entire package unusable for Android users. Ideally, this would have been caught by tests before bumping the version. I am saying this because I had the same thing happen with the wakelock package just a few days ago, and if you have a flutter app with many package dependencies then these bugs as a result of version updates are very disruptive and difficult to discover and trace back to the offending package. It leads me to consider locking packages to a specific version (instead of using the caret notation to benefit from upgrades), and that is not a good developer experience.

Copy link
Contributor

Choose a reason for hiding this comment

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

If I may make one comment, it would be that the impact of the original change (that caused the error) was quite severe, in that it rendered the entire package unusable for Android users. Ideally, this would have been caught by tests before bumping the version.

Yes, it's not clear to me yet why that PR passed our automated tests (of which we have quite a few). That's something we'll be looking into.

I am saying this because I had the same thing happen with the wakelock package just a few days ago

That was a third-party package, and as far as I can tell from the nature of that bug, that plugin:

  • has no automated testing of any kind, and
  • was never launched on iOS after the change

We cannot control what third-party package authors do, but it was a very different scenario. That bug could not have passed our CI.

@cyanglaz
Copy link
Contributor

cyanglaz commented Sep 22, 2021

I would suggest the following (cooperating with what @stuartmorgan has commented):

  1. Introduce a new field in dart: final int introductoryPriceAmountMicros, which gets the value directly from java side, and also has the same name as in java.
  2. Mark introductoryPriceMicros as deprecated, make it return the string value of introductoryPriceAmountMicros
  3. This will be a minor bump.

Then open another PR, making a breaking change (major version bump). This PR removes the deprecated introductoryPriceMicros field.

As for testing, I'm not aware that we have anything specific setup to make sure method channels work end to end (I might have missed something?). In iOS, we have StoreKit Tests, which is a UITests. It does cover the end to end case. However, StoreKit Test equivalent does not exist in Play Billing Library just yet.

@stuartmorgan
Copy link
Contributor

I didn't realize we didn't have integration tests for this plugin. In that case, we should look into setting up "native end to end" tests as discussed in the testing doc, where we could mock out the Play dependencies.

@stuartmorgan
Copy link
Contributor

(We should also seriously evaluate using Pigeon here in the medium term, so that we have less room to make type errors, if we don't have sufficient test coverage.)

@mvanbeusekom mvanbeusekom force-pushed the issue/90244 branch 3 times, most recently from d4482c5 to 15bcf56 Compare September 22, 2021 19:59
@mvanbeusekom
Copy link
Contributor Author

@stuartmorgan, @cyanglaz, I have updated the PR to introduce the introductoryPriceAmountMicros field using the correct type and deprecating the introductoryPriceMicros field which used the wrong type. I would appreciate it if you could have another look.

@@ -1,6 +1,6 @@
## 0.1.4+7
Copy link
Contributor

Choose a reason for hiding this comment

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

This was published; it shouldn't be removed from the changelog.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right and fixed it.

required this.introductoryPriceMicros,
@Deprecated('Use `introductoryPriceAmountMicros` parameter instead')
String introductoryPriceMicros = '',
this.introductoryPriceAmountMicros = 0,
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the better option here is to make this int introductoryPriceAmountMicros,, and have the constructor body do:

introductoryPriceAmountMicros = introductoryPriceAmountMicros ?? int.tryParse(introductoryPriceMicros) ?? 0

Then you don't need all the complicated logic in the getter, or _introductoryPriceMicros.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure that will work as the introductoryPriceAmountMicros parameter a not nullable and should always have a value. We could make it nullable but that means we need to later make it non-nullable again when we introduce the breaking change.

Copy link
Contributor

Choose a reason for hiding this comment

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

We'll be changing it to required at that point presumably to match everything else, and it will already be a breaking change in general, so that doesn't seem like a significant issue. But it's not a big deal either way.

@Deprecated('Use `introductoryPriceAmountMicros` instead.')
@JsonKey(ignore: true)
String get introductoryPriceMicros => _introductoryPriceMicros.isEmpty
? introductoryPriceAmountMicros != 0
Copy link
Contributor

@cyanglaz cyanglaz Sep 22, 2021

Choose a reason for hiding this comment

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

I didn't get why we needed the nested part. If introductoryPriceAmountMicros is 0, can't introductoryPriceMicros just be '0'?
Wouldn't the below work?

String get introductoryPriceMicros => _introductoryPriceMicros.isEmpty ? 
                                                                  introductoryPriceAmountMicros.toString() :
                                                                  _introductoryPriceMicros;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes it would work, but the original behavior (although wrong) was to return an empty string as default value. I tried to stick as close as possible to that using the nested condition.

Copy link
Contributor

@cyanglaz cyanglaz Sep 22, 2021

Choose a reason for hiding this comment

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

The original behavior always returns empty string because of the incorrect KV mapping, right?

If returning empty string is wrong, we don't need to worry about the wrong behavior. This PR suppose to "fix" it :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes that makes sense. In that case the inner condition can indeed be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link
Contributor

@cyanglaz cyanglaz left a comment

Choose a reason for hiding this comment

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

LGTM, let's wait for @stuartmorgan's approval on the things he commented.

@@ -68,8 +72,15 @@ class SkuDetailsWrapper {
final String introductoryPrice;

/// [introductoryPrice] in micro-units 990000
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add a new comment saying something similar to the android doc:
/// Returns 0 if the SKU is not a subscription or doesn't have an introductory period.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, good catch. I think that is a valuable addition.

Copy link
Contributor

@stuartmorgan stuartmorgan left a comment

Choose a reason for hiding this comment

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

LGTM

@mvanbeusekom mvanbeusekom added the waiting for tree to go green (Use "autosubmit") This PR is approved and tested, but waiting for the tree to be green to land. label Sep 27, 2021
@fluttergithubbot
Copy link

This pull request is not suitable for automatic merging in its current state.

  • The status or check suite publishable has failed. Please fix the issues identified (or deflake) before re-applying this label.

@fluttergithubbot fluttergithubbot removed the waiting for tree to go green (Use "autosubmit") This PR is approved and tested, but waiting for the tree to be green to land. label Sep 27, 2021
Introduced the `SkuDetailsWrapper.introductoryPriceAmountMicros` field of the correct type (`int`) and deprecated the `SkuDetailsWrapper.introductoryPriceMicros` field.
Added extra documentation explaining that the default return value for
the `SkuDetailsWrapper.introductoryPriceAmountMicros` field will be 0 in
case the product is not a subscription or doesn't have an introductory
price associated with it.
@mvanbeusekom mvanbeusekom added the waiting for tree to go green (Use "autosubmit") This PR is approved and tested, but waiting for the tree to be green to land. label Sep 28, 2021
@fluttergithubbot fluttergithubbot merged commit f48f8db into flutter:master Sep 28, 2021
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Sep 28, 2021
@mvanbeusekom mvanbeusekom deleted the issue/90244 branch September 28, 2021 08:30
NickalasB added a commit to NickalasB/plugins that referenced this pull request Sep 30, 2021
* master:
  [google_maps_flutter] Add Marker drag events (flutter#2838)
  [flutter_plugin_tools] Validate pubspec description (flutter#4396)
  Add file_selector to the repo list (flutter#4395)
  [in_app_purchase] Fix in_app_purchase_android/README.md (flutter#4363)
  [google_maps_flutter_web] Add Marker drag events (flutter#4385)
  [webview_flutter] Fixed todos in FlutterWebView.java (flutter#4394)
  Handle `PurchaseStatus.restored` correctly in example. (flutter#4393)
  Handle restored purchases in iOS example app (flutter#4392)
  [file_selector] Remove custom analysis options (flutter#4382)
  [flutter_plugin_tools] Check licenses in Kotlin (flutter#4373)
  Fixed _CastError when running example App (flutter#4390)
  [in_app_purchase] Ensure the introductoryPriceMicros field is transported as a String. (flutter#4370)
  Load navigation controls immediately. (flutter#4377)
  [camera] Fix IllegalStateException being thrown in Android implementation when switching activities. (flutter#4319)

# Conflicts:
#	packages/webview_flutter/webview_flutter/CHANGELOG.md
#	packages/webview_flutter/webview_flutter_android/CHANGELOG.md
mgonzalezc pushed a commit to mgonzalezc/plugins that referenced this pull request Oct 12, 2021
* master: (1126 commits)
  [webview_flutter] Adjust test URLs again (flutter#4407)
  [google_sign_in] Add serverAuthCode attribute to google_sign_in_platform_interface user data (flutter#4179)
  [camera] Add filter for unsupported cameras on Android (flutter#4418)
  [webview_flutter] Update webview platform interface with new methods for running JavaScript. (flutter#4401)
  [webview_flutter] Add zoomEnabled to webview flutter platform interface (flutter#4404)
  [ci] Remove obsolete Dockerfile (flutter#4405)
  Fix order-dependant platform interface tests (flutter#4406)
  [google_maps_flutter]: LatLng longitude loses precision in constructor #90574 (flutter#4374)
  [google_maps_flutter] Add Marker drag events (flutter#2838)
  [flutter_plugin_tools] Validate pubspec description (flutter#4396)
  Add file_selector to the repo list (flutter#4395)
  [in_app_purchase] Fix in_app_purchase_android/README.md (flutter#4363)
  [google_maps_flutter_web] Add Marker drag events (flutter#4385)
  [webview_flutter] Fixed todos in FlutterWebView.java (flutter#4394)
  Handle `PurchaseStatus.restored` correctly in example. (flutter#4393)
  Handle restored purchases in iOS example app (flutter#4392)
  [file_selector] Remove custom analysis options (flutter#4382)
  [flutter_plugin_tools] Check licenses in Kotlin (flutter#4373)
  Fixed _CastError when running example App (flutter#4390)
  [in_app_purchase] Ensure the introductoryPriceMicros field is transported as a String. (flutter#4370)
  ...

# Conflicts:
#	packages/quick_actions/ios/Classes/FLTQuickActionsPlugin.m
amantoux pushed a commit to amantoux/plugins that referenced this pull request Dec 11, 2021
KyleFin pushed a commit to KyleFin/plugins that referenced this pull request Dec 21, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
cla: yes p: in_app_purchase platform-android waiting for tree to go green (Use "autosubmit") This PR is approved and tested, but waiting for the tree to be green to land.
Projects
None yet
5 participants