[in_app_purchase] platform interface improvement #3821
Conversation
/// (using `extends`) ensures that the subclass will get the default implementation, while | ||
/// platform implementations that `implements` this interface will be broken by newly added | ||
/// [InAppPurchasePlatformAddition] methods. | ||
abstract class InAppPurchasePlatformAddition extends PlatformInterface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm making the InAppPurchasePlatformAddition
also a PlatformInterface
.
We didn't talk about this offline but I think this makes sense. Please share your thoughts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the advantage of doing this? It is not at all clear to me that if we added new methods to PlatformInterface
they would be applicable to these addition classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is mainly to take advantage of using the token verification of the PlatformInterface
.
I couldn't think of a scenario where we need to add some methods in the "PlatformAddition" that will not be applicable to all addition classes.
Also, Now thinking about the usage of other "PlatformInterface"s, I'm not confident with InAppPurchasePlatformAddition
being a "PlatformInterface". Maybe for this plugin i'll just duplicate the token verification code instead of extending PlatformInterface
.
WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't the verifyToken
meant to enforce users extend
the class instead of implement
ing it?
Is this a concern for the InAppPurchasePlatformAddition
class?
I don't think we will need to be afraid of the InAppPurchasePlatformAddition
class to have any methods. Meaning I don't expect any methods to be added in the future that would break implementing classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do need some sort of enforcement to make sure the addition is not a type of InAppPurchasePlatform
.
Make sure an addition class to extend InAppPurchasePlatformAddition
is one of the solutions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do need some sort of enforcement to make sure the addition is not a type of
InAppPurchasePlatform
.
Why do we need to enforce that? The concern is purely around pubspec dependency cycles with endorsement IIRC, in which case it's the pubspec cycle, not the classes, that are the issue. This seems like a very indirect way of trying to enforce a lack of cycles (and is both not sufficient to prevent a cycle, and not necessary in the case of an in-package implementation if this is something we adopt as a general solution).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the addition is a type of InAppPurchasePlatform
, the plugin user would easily make a mistake to access the InAppPurchasePlatform
object (e.g. InAppPurchasePlatformAndroid) and call the unified API there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I'm not following. Can you give a snippet of what that mistake would look like in code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's say we allow an InAppPurchasePlatform
to also be a type of InAppPurchasePlatformAddition
, the implementer could potentially create a class like this:
class InAppPurchasePlatformGooglePlay extends InAppPurchasePlatform implements InAppPurchasePlatformAddition {
@override
Future<void> buyConsumable(...){...}
Future<void> someAdditionMethod(...){...}
}
Then the user could potentially use this class to do something like:
InAppPurchasePlatformGooglePlay googlePlayAddition = InAppPurchaseConnection.instance.getPlatformAddition<InAppPurchasePlatformGooglePlay>();
googlePlayAddition.buyConsumable(...);
In this case, the user is accessing the platform interface API directly, which we want to avoid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to go out of our way to forbid that construction. It's a bad idea, but only in the same way that, say, making a bunch of implementation details of a InAppPurchaseGoogleplayAdditions
public instead of private would be. I.e., it's the implementor of the addition class designing their own extension API badly.
What I wanted to avoid was the design where we forced extensions to use that, so that every extension would be required to have that poor design. If we make it so that it's easy to do the right thing, and model the right thing in our plugins, that should be enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, only thing I am thinking about is adding another test to verify that accessing instance
throws a LateInitializationError
when accessing it before it is initialized.
Sounds good, weird tho the code wasn't able to find |
I tested it on dartpad.dev since I wasn't sure what would happen ;) |
@mvanbeusekom Seems like the class |
@@ -24,15 +24,16 @@ abstract class InAppPurchasePlatform extends PlatformInterface { | |||
/// The instance of [InAppPurchasePlatform] to use. | |||
/// | |||
/// Defaults to `null`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is no longer correct. Instead, you should document that it must be set before using.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
|
||
static InAppPurchasePlatform? _instance; | ||
// Should only be accessed after setter is called. | ||
static late InAppPurchasePlatform _instance; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be either above or below the getter and setter, rather than between them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
// ignore: avoid_classes_with_only_static_members | ||
import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; | ||
import 'package:plugin_platform_interface/plugin_platform_interface.dart'; | ||
|
||
/// The interface that platform implementations must implement when they want to | ||
/// provide platform specific in_app_purchase features. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: platform-specific
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done. Also fixed some other appearance in the plugin.
/// (using `extends`) ensures that the subclass will get the default implementation, while | ||
/// platform implementations that `implements` this interface will be broken by newly added | ||
/// [InAppPurchasePlatformAddition] methods. | ||
abstract class InAppPurchasePlatformAddition extends PlatformInterface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the advantage of doing this? It is not at all clear to me that if we added new methods to PlatformInterface
they would be applicable to these addition classes.
@@ -36,5 +52,13 @@ abstract class InAppPurchasePlatformAddition { | |||
/// } | |||
/// } | |||
/// ``` | |||
static InAppPurchasePlatformAddition? instance; | |||
static InAppPurchasePlatformAddition get instance => _instance; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why have you made this non-nullable? Why should a platform with no additions need to set this up?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah makes sense! Made this nullable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stuartmorgan Updated per your comments. PTAL :)
@@ -24,15 +24,16 @@ abstract class InAppPurchasePlatform extends PlatformInterface { | |||
/// The instance of [InAppPurchasePlatform] to use. | |||
/// | |||
/// Defaults to `null`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
|
||
static InAppPurchasePlatform? _instance; | ||
// Should only be accessed after setter is called. | ||
static late InAppPurchasePlatform _instance; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
// ignore: avoid_classes_with_only_static_members | ||
import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; | ||
import 'package:plugin_platform_interface/plugin_platform_interface.dart'; | ||
|
||
/// The interface that platform implementations must implement when they want to | ||
/// provide platform specific in_app_purchase features. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done. Also fixed some other appearance in the plugin.
/// (using `extends`) ensures that the subclass will get the default implementation, while | ||
/// platform implementations that `implements` this interface will be broken by newly added | ||
/// [InAppPurchasePlatformAddition] methods. | ||
abstract class InAppPurchasePlatformAddition extends PlatformInterface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is mainly to take advantage of using the token verification of the PlatformInterface
.
I couldn't think of a scenario where we need to add some methods in the "PlatformAddition" that will not be applicable to all addition classes.
Also, Now thinking about the usage of other "PlatformInterface"s, I'm not confident with InAppPurchasePlatformAddition
being a "PlatformInterface". Maybe for this plugin i'll just duplicate the token verification code instead of extending PlatformInterface
.
WDYT?
@@ -36,5 +52,13 @@ abstract class InAppPurchasePlatformAddition { | |||
/// } | |||
/// } | |||
/// ``` | |||
static InAppPurchasePlatformAddition? instance; | |||
static InAppPurchasePlatformAddition get instance => _instance; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah makes sense! Made this nullable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stuartmorgan Updated with review. comments about not making InAppPurchasePlatformAddition
a platform interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM with nits.
/// provide platform-specific in_app_purchase features. | ||
/// | ||
/// Platforms that wants to introduce platform-specific public APIs should create | ||
/// a class that either extend or implements [InAppPurchasePlatformAddition]. Then replace |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/replace ... with/set ... to/ (by default there's no instance, so there's nothing to replace)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
/// a class that either extend or implements [InAppPurchasePlatformAddition]. Then replace | ||
/// the [InAppPurchasePlatformAddition.instance] with an instance of that class. | ||
/// | ||
/// All the APIs added by [InAppPurchasePlatformAddition] implementers will be accessed from |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/implementers/implementations/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
/// | ||
/// All the APIs added by [InAppPurchasePlatformAddition] implementers will be accessed from | ||
/// [InAppPurchasePlatformAdditionProvider.getPlatformAddition] by the client APPs. | ||
/// To avoid client APPs to directly call the [InAppPurchasePlatform] APIs, an [InAppPurchasePlatformAddition] implementation must not |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"To avoid clients directly calling [InAppPurchasePlatform] APIs, an [InAppPurchasePlatformAddition] implementation should not be"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
static InAppPurchasePlatformAddition? instance; | ||
static InAppPurchasePlatformAddition? get instance => _instance; | ||
|
||
/// Set the instance to a desired [InAppPurchasePlatformAddition] implementation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sets
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
|
||
/// Set the instance to a desired [InAppPurchasePlatformAddition] implementation. | ||
/// | ||
/// The `instance` must not be a type of [InAppPurchasePlatform]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/must/should/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -18,7 +18,7 @@ class ProductDetailsResponse { | |||
|
|||
/// The list of identifiers that are in the `identifiers` of [InAppPurchasePlatform.queryProductDetails] but failed to be fetched. | |||
/// | |||
/// There's multiple platform specific reasons that product information could fail to be fetched, | |||
/// There's multiple platform-specific reasons that product information could fail to be fetched, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/There's/There are/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
class MockInAppPurchasePlatformAddition extends Mock | ||
with | ||
// ignore: prefer_mixin | ||
MockPlatformInterfaceMixin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This shouldn't still be needed, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Adds some improvements to the platform interface. 1. Make `InAppPurchasePlatformAddition` a subclass of `PlatformInterface` to take advantage of the existing token checking logic. (I also consider `InAppPurchasePlatformAddition` a `PlatformInterface` because it works in a similar way. 2. Make the `instance` variable `late` as we should never access it before setter. 3. Add tests for `InAppPurchasePlatformAddition` part of flutter/flutter#78525
Adds some improvements to the platform interface.
InAppPurchasePlatformAddition
a subclass ofPlatformInterface
to take advantage of the existing token checking logic. (I also considerInAppPurchasePlatformAddition
aPlatformInterface
because it works in a similar way.instance
variablelate
as we should never access it before setter.InAppPurchasePlatformAddition
part of flutter/flutter#78525
Pre-launch Checklist
dart format
. See plugin_tool format)[shared_preferences]
///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.