I'm using the in_app_purchase package, but I only can get one plan inside the subscriptions
I have 3 subscriptions:
Basic subscription
Premium subscription
Enterprise subscription
And inside each subscription, I want to have 2 plans:
Month plan
Year plan
I always get the plan that has the "backward compatibility"("This will be the baseline returned by the deprecated Google Play Billing Library method querySkuDetailsAsync()") enabled.
Is any way to get all plans, or do I have to have 6 subscriptions with only 1 plan in each one?
Edit:
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_i18n/flutter_i18n.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart';
import 'package:in_app_purchase_storekit/store_kit_wrappers.dart';
import 'package:motorline_home/widgets/materials/appbar/appbar_title_widget.dart';
import 'package:motorline_home/widgets/materials/pop_button_widget.dart';
import 'package:rxdart/subjects.dart';
class SubscriptionPage extends StatefulWidget {
const SubscriptionPage({
Key? key,
}) : super(key: key);
#override
State<SubscriptionPage> createState() => _SubscriptionPageState();
}
class _SubscriptionPageState extends State<SubscriptionPage> {
// In app subscriptions
InAppPurchase _inAppPurchase = InAppPurchase.instance;
late StreamSubscription<List<PurchaseDetails>> _inAppPurchaseSubscription;
StreamController<List<ProductDetails>> _streamGooglePlaySubscriptions =
BehaviorSubject();
final List<String> _subscriptionsIDs = [
"basic",
"premium",
"enterprise",
];
#override
void initState() {
super.initState();
// In app purchase subscription
_inAppPurchaseSubscription =
_inAppPurchase.purchaseStream.listen((purchaseDetailsList) {
_listenToPurchaseUpdated(purchaseDetailsList);
}, onDone: () {
print("In app purchase onDone");
_inAppPurchaseSubscription.cancel();
}, onError: (error) {
print("In app purchase error: ${error.toString()}");
// handle error here.
_inAppPurchaseSubscription.cancel();
});
// Initialize in app purchase
_initializeInAppPurchase();
}
#override
void dispose() {
if (Platform.isIOS) {
final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition =
_inAppPurchase
.getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
iosPlatformAddition.setDelegate(null);
}
// Cancel in app purchase listener
_inAppPurchaseSubscription.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: AppBarTitleWidget(
title: FlutterI18n.translate(context, "subscriptions"),
),
leading: PopButtonWidget(),
),
// Body
body: Container(),
);
}
void _initializeInAppPurchase() async {
print("Initializing in app purchase");
bool available = await _inAppPurchase.isAvailable();
print("In app purchase initialized: $available");
if (available) {
if (Platform.isIOS) {
final InAppPurchaseStoreKitPlatformAddition iosPlatformAddition =
_inAppPurchase
.getPlatformAddition<InAppPurchaseStoreKitPlatformAddition>();
await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate());
}
// Get subscriptions
List<ProductDetails> subscriptions = await _getSubscriptions(
productIds:
_subscriptionsIDs.toSet(),
);
// Sort by price
subscriptions.sort((a, b) => a.rawPrice.compareTo(b.rawPrice));
// Add subscriptions to stream
_streamGooglePlaySubscriptions.add(subscriptions);
// DEBUG: Print subscriptions
print("In app purchase subscription subscriptions: ${subscriptions}");
for (var subscription in subscriptions) {
print("In app purchase plan: ${subscription.id}: ${subscription.rawPrice}");
print("In app purchase description: ${subscription.description}");
// HOW GET ALL PLANS IN EACH SUBSCRIPTION ID?
}
await InAppPurchase.instance.restorePurchases();
}
}
// In app purchase updates
void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
// If purchase is pending
if (purchaseDetails.status == PurchaseStatus.pending) {
print("In app purchase pending...");
// Show pending ui
} else {
if (purchaseDetails.status == PurchaseStatus.canceled) {
print("In app purchase cancelled");
}
// If purchase failed
if (purchaseDetails.status == PurchaseStatus.error) {
print("In app purchase error");
// Show error
} else if (purchaseDetails.status == PurchaseStatus.purchased ||
purchaseDetails.status == PurchaseStatus.restored) {
print("In app purchase restored or purchased");
}
if (purchaseDetails.pendingCompletePurchase) {
debugPrint("In app purchase complete purchased");
debugPrint(
"In app purchase purchase id : ${purchaseDetails.purchaseID}");
debugPrint(
"In app purchase server data : ${purchaseDetails.verificationData.serverVerificationData}");
debugPrint(
"In app purchase local data : ${purchaseDetails.verificationData.localVerificationData}");
// Verify purchase on backend
try {
// VALIDADE PURCHASE IN BACKEND
} catch (error) {
debugPrint("In app purchase error: ${error.toString()}");
}
}
}
});
}
// Get subscription
Future<List<ProductDetails>> _getSubscriptions(
{required Set<String> productIds}) async {
ProductDetailsResponse response =
await _inAppPurchase.queryProductDetails(productIds);
return response.productDetails;
}
}
/// Example implementation of the
/// [`SKPaymentQueueDelegate`](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate?language=objc).
///
/// The payment queue delegate can be implementated to provide information
/// needed to complete transactions.
class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper {
#override
bool shouldContinueTransaction(
SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront) {
return true;
}
#override
bool shouldShowPriceConsent() {
return false;
}
}
What you like to do is not possible. You need to create for each subscription a new plan, you can't say a Premium Subscription does have a yearly and a monthly plan.
This is not possible at the moment since it's not supported by official plugin (and we don't know when it will be):
https://github.com/flutter/flutter/issues/110909
As they mentioned, solution is one plan per subscription with the extra logic necessary in your app (e.g. to detect if subscription with ID PLUS-year is an upgrade of PLUS-month)
Related
hello i am using in_app_purchase: ^3.1.4 official flutter plugin for purchasing item in my app its non_consumable product user can buy it only one time. i have test this app on internal testing.On internal testing it was working fine but issue was its not working production release anyone know what issue here is my code below please check now
final Set<String> _productIds = {'urdubible'};
late List<ProductDetails> _products;
bool _isPurchased = false;
#override
void initState() {
super.initState();
_checkPurchaseStatus();
// initialize the in-app purchase plugin
InAppPurchase .instance.purchaseStream
.listen((List<PurchaseDetails> purchases) {
// handle any purchase updates
_handlePurchaseUpdates(purchases);
});
// load the available products
_loadProducts();
}
Future<void> _loadProducts() async {
// get the product details from the store
ProductDetailsResponse response =
await InAppPurchase.instance.queryProductDetails(_productIds);
// save the product details to state
setState(() {
_products = response.productDetails;
});
}
void _handlePurchaseUpdates(List<PurchaseDetails> purchases) {
for (PurchaseDetails purchase in purchases) {
switch (purchase.status) {
case PurchaseStatus.purchased:
_savePurchaseStatus(true);
// handle the purchase, e.g. update UI, save the purchase to database, etc.
setState(() {
_isPurchased = true;
});
break;
case PurchaseStatus.error:
// handle any purchase errors
break;
case PurchaseStatus.canceled:
// handle cancelled purchases
break;
}
}
}
Future<void> _purchaseProduct() async {
if (_products.isNotEmpty) {
// initiate the purchase flow
final PurchaseParam purchaseParam =
PurchaseParam(productDetails: _products.first);
InAppPurchase.instance.buyNonConsumable(purchaseParam: purchaseParam);
}
}
Future<void> _savePurchaseStatus(bool isPurchased) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setBool('isPurchased', isPurchased);
}
Future<void> _checkPurchaseStatus() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool? isPurchased = prefs.getBool('isPurchased');
if (isPurchased != null && isPurchased) {
setState(() {
_isPurchased = true;
});
}
}
in my case I am facing issue with
buildTypes {
release {
signingConfig signingConfigs.debug
minifyEnabled false
shrinkResources false
useProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
After disabling shrink resources it's working for me
Buying a product works fine but when I try to get the bought products on startup of the app the listening is not working.
The logger.i("purchaseInit") message is not displayed.
purchaseInit() {
StreamSubscription<List<PurchaseDetails>> _subscription;
final Stream purchaseUpdated = InAppPurchase.instance.purchaseStream;
_subscription = purchaseUpdated.listen((purchaseDetailsList) {
logger.i("purchaseInit"); // not showing up
_listenToPurchaseUpdated(purchaseDetailsList);
}, onDone: () {
_subscription.cancel();
}, onError: (error) {
// handle error
});}
void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.pending) {
logger.i("_showPendingUI()");
} else {
if (purchaseDetails.status == PurchaseStatus.error) {
logger.i(purchaseDetails.error);
} else if (purchaseDetails.status == PurchaseStatus.purchased ||
purchaseDetails.status == PurchaseStatus.restored) {
logger.i(purchaseDetails);
}
if (purchaseDetails.pendingCompletePurchase) {
await InAppPurchase.instance.completePurchase(purchaseDetails);
}
}
});
}
It seems, you are just listening to purchases by subscribing to the stream. But previous purchases are not emitted by the stream upon startup of the app or upon subscribing to it.
In order to get a list of previous purchases, you must trigger a restore.
await InAppPurchase.instance.restorePurchases();
This will re-emit the previous purchases of non-consumable products and your listener will be able to handle them.
However, I suggest that you try to keep track of what the user purchased yourself and only call restorePurchases() if necessary.
https://pub.dev/packages/in_app_purchase#restoring-previous-purchases
I use in-app purchase in my app. Hassle free purchase. But when I want to buy the same product for the second time, this Play Store shows me "You Already Have This Item". I want my products to be bought again and again. I don't have any idea how I can solve it. Thank you very much for the help. I left my source codes below.
InAppPurchase iap = InAppPurchase.instance;
bool available = true;
List<ProductDetails> products = [];
List<PurchaseDetails> purchases = [];
StreamSubscription subscription;
int credits = 0;
#override
void initState() {
initialize();
super.initState();
}
#override
void dispose() {
subscription.cancel();
super.dispose();
}
void initialize() async {
available = await iap.isAvailable();
if (available) {
await getProducts();
verifyPurchase();
subscription = iap.purchaseStream.listen((data) => setState(() {
print('New Purchase');
purchases.addAll(data);
verifyPurchase();
}));
}
}
Future<void> getProducts() async {
Set<String> ids = Set.from(['buy_300_wecoin']);
ProductDetailsResponse response = await iap.queryProductDetails(ids);
setState(() {
products = response.productDetails;
});
}
PurchaseDetails hasPurchased(String productID) {
return purchases.firstWhere((purchase) => purchase.productID == productID,
orElse: () => null);
}
bool verifyPurchase() {
PurchaseDetails purchase = hasPurchased('buy_300_wecoin');
if (purchase != null && purchase.status == PurchaseStatus.purchased) {
return true;
}
}
void buyProduct(ProductDetails prod) {
final PurchaseParam purchaseParam = PurchaseParam(productDetails: prod);
iap.buyConsumable(purchaseParam: purchaseParam, autoConsume: false);
}
you should use Consumable In-App Purchase
A consumable product is one that a user consumes to receive in-app content, such as in-game currency. When a user consumes the product, your app dispenses the associated content, and the user can
then purchase the item again.
Note that the App Store does not have any APIs for querying consumable products, and Google Play considers consumable products to no longer be owned once they're marked as consumed and fails to return them here. For restoring these across devices you'll need to persist them on your own server and query that as well.
Try in_app_purchase library and read about consumable IAP using this library.
I am doing an app in flutter and I am working on the authentication part. I want to know how I can keep my user logged in after I reload the app. Now the thing is that my app has 2 kinds of users (Client and Driver). So each has its own space, like sign in and sign up and main (after logging in).
This is the code that I used for logging.
class Initializer extends StatefulWidget {
// Access to this Screen
static String id = 'initializer';
#override
_InitializerState createState() => _InitializerState();
}
class _InitializerState extends State<Initializer> {
// Firebase Stuff
final _auth = FirebaseAuth.instance;
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
User _user;
// To Check if There's a Driver
bool isDriver = true;
void getCurrentUser() async {
try {
final getCurrentUser = _auth.currentUser;
if (getCurrentUser != null) {
getUserKind();
_user = getCurrentUser;
}
} catch (e) {
print(e);
}
}
getUserKind() async {
try {
// To fetch Database for Driver
final QuerySnapshot checkOfDriver =
await _firestore.collection('driver').where('uid', isEqualTo: _user.uid).get().catchError((error) {
print(error);
});
if (checkOfDriver.docs.isEmpty)
setState(() {
isDriver = false;
});
else
setState(() {
isDriver = true;
});
} catch (e) {
print(e);
return null;
}
}
#override
void setState(fn) {
if (mounted) {
super.setState(fn);
}
}
#override
void initState() {
super.initState();
getCurrentUser();
}
#override
Widget build(BuildContext context) {
getCurrentUser();
SizeConfig().init(context);
return _user == null
? WelcomeScreen()
: isDriver
? DriverMain()
: ClientMain();
}
}
It's actually working but not properly, because when I reload the app while I'm logging in as a Client, the app shows me DriverMain at the beginning for one second then it switches to the right side which is ClientMain and that causes me some errors sometimes, and it's not an efficient work anyway.
So, what I should add to the code or ...
Firebase already persists the users credentials, and restores them automatically when the app restarts.
But this is an asynchronous process, as it requires a call to the server. By the time your getCurrentUser = _auth.currentUser code runs, that asynchronous process hasn't finished yet, so you get null.
To properly respond to the auth state being restored (and other changes), you'll want to use an auth state change listener as shown in the documentation on authentication state:
FirebaseAuth.instance
.authStateChanges()
.listen((User? user) {
if (user == null) {
print('User is currently signed out!');
} else {
print('User is signed in!');
}
});
If you want to use this in your UI, you'll typically wrap it in a StreamBuilder instead of calling listen yourself.
Programmatically generated dynamic links are not properly catched by
FirebaseDynamicLinks.instance.getInitialLink().
if the app is closed. However, if the app is open it is properly detected by the listener for new incoming dynamic links. It is not clear to me if it is a setup problem, how I generate the dynamic link.
To Reproduce
First set up Firebase for Flutter project as documented. Then to set up a dynamic link:
/// See also
/// https://firebase.google.com/docs/dynamic-links/use-cases/rewarded-referral
/// how to implement referral schemes using Firebase.
Future<ShortDynamicLink> buildDynamicLink(String userId) async {
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
final String packageName = packageInfo.packageName;
var androidParams = AndroidParameters(
packageName: packageInfo.packageName,
minimumVersion: Constants.androidVersion, // app version and not the Android OS version
);
var iosParams = IosParameters(
bundleId: packageInfo.packageName,
minimumVersion: Constants.iosVersion, // app version and not the iOS version
appStoreId: Constants.iosAppStoreId,
);
var socialMetaTagParams = SocialMetaTagParameters(
title: 'Referral Link',
description: 'Referred app signup',
);
var dynamicLinkParams = DynamicLinkParameters(
uriPrefix: 'https://xxxxxx.page.link',
link: Uri.parse('https://www.xxxxxxxxx${Constants.referralLinkPath}?${Constants.referralLinkParam}=$userId'),
androidParameters: androidParams,
iosParameters: iosParams,
socialMetaTagParameters: socialMetaTagParams,
);
return dynamicLinkParams.buildShortLink();
}
This dynamic link then can be shared with other new users.
I listen for initial links at app startup and then for new incoming links.
1) The link properly opens the app if the app is not running but the getInitialLink does not get it.
2) If the app is open the link is properly caught by the listener and all works.
Here is the very simple main.dart that I used to verify 1) that the initial link is not found with FirebaseDynamicLinks.instance.getInitialLink().
void main() async {
WidgetsFlutterBinding.ensureInitialized();
PendingDynamicLinkData linkData = await FirebaseDynamicLinks.instance.getInitialLink();
String link = linkData?.link.toString();
runApp(MyTestApp(link: link));
}
class MyTestApp extends StatelessWidget {
final String link;
MyTestApp({this.link});
#override
Widget build(BuildContext context) {
return MaterialApp(
builder: (BuildContext context, Widget child) {
return Scaffold(
body: Container(
child: Center(
child: Text('Initial dynamic Firebase link: $link')
),
),
);
}
);
}
}
Expected behavior
The link should open the app and trigger FirebaseDynamicLinks.instance.getInitialLink()..
Additional context
I hope properly configured Firebase project with Firebase console. To verify this I created a dynamic link to be used with Firebase Auth 'signup by email link' and these dynamic links are working as expected, also when the app is not open.
The point here is that the referral dynamic link that I generate programmatically is opening the app when it is closed but is then not caught by FirebaseDynamicLinks.instance.getInitialLink(), and to make things more confusing, works as expected if the app is open. In that case it is caught by the listener FirebaseDynamicLinks.instance.onLink.
I also set up the WidgetsBindingObserver in Flutter to handle that callback as required, when the app gets its focus back.
Any help is greatly appreciated. Debugging is very tricky, as you need to do it on a real device and not in the simulator. To make things worse, I did not figure out how to attach a debugger while the dynamic link opens the app. This means I am also stuck in investigating this issue further.
In The FirebaseDynamicLinks Two Methods 1) getInitialLink() 2) onLink().
If When Your App Is Open And You Click On Dynamic Link Then Will Be Call FirebaseDynamicLinks.instance.onLink(), If Your App Is Killed Or Open From PlayStore Then You Get From FirebaseDynamicLinks.instance.getInitialLink();.
First Of You Need To Initialise Instance Of FirebaseDynamicLinks.instance.
static void initDynamicLinks() async {
final PendingDynamicLinkData data =
await FirebaseDynamicLinks.instance.getInitialLink();
final Uri deepLink = data?.link;
if (deepLink != null && deepLink.queryParameters != null) {
SharedPrefs.setValue("param", deepLink.queryParameters["param"]);
}
FirebaseDynamicLinks.instance.onLink(
onSuccess: (PendingDynamicLinkData dynamicLink) async {
final Uri deepLink = dynamicLink?.link;
if (deepLink != null && deepLink.queryParameters != null) {
SharedPrefs.setValue("param", deepLink.queryParameters["param]);
}
}, onError: (OnLinkErrorException e) async {
print(e.message);
});
}
Initialize Link Listener. This works for me.
class _MainAppState extends State<MainApp> {
Future<void> initDynamicLinks() async {
print("Initial DynamicLinks");
FirebaseDynamicLinks dynamicLinks = FirebaseDynamicLinks.instance;
// Incoming Links Listener
dynamicLinks.onLink.listen((dynamicLinkData) {
final Uri uri = dynamicLinkData.link;
final queryParams = uri.queryParameters;
if (queryParams.isNotEmpty) {
print("Incoming Link :" + uri.toString());
// your code here
} else {
print("No Current Links");
// your code here
}
});
// Search for Firebase Dynamic Links
PendingDynamicLinkData? data = await dynamicLinks
.getDynamicLink(Uri.parse("https://yousite.page.link/refcode"));
final Uri uri = data!.link;
if (uri != null) {
print("Found The Searched Link: " + uri.toString());
// your code here
} else {
print("Search Link Not Found");
// your code here
}
}
Future<void> initFirebase() async {
print("Initial Firebase");
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
// await Future.delayed(Duration(seconds: 3));
initDynamicLinks();
}
#override
initState() {
print("INITSTATE to INITIALIZE FIREBASE");
super.initState();
initFirebase();
}
I tried Rohit's answer and because several people face the same issue I add here some more details. I created a stateful widget that I place pretty much at the top of the widget tree just under material app:
class DynamicLinkWidget extends StatefulWidget {
final Widget child;
DynamicLinkWidget({this.child});
#override
State<StatefulWidget> createState() => DynamicLinkWidgetState();
}
class DynamicLinkWidgetState extends State<DynamicLinkWidget> with WidgetsBindingObserver {
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
locator.get<DynamicLinkService>().initDynamicLinks();
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container(child: widget.child);
}
}
I use the getit package to inject services. The dynamic link service is roughly like this:
class DynamicLinkService {
final UserDataService userDataService;
final ValueNotifier<bool> isLoading = ValueNotifier<bool>(false);
final BehaviorSubject<DynamicLinkError> _errorController = BehaviorSubject<DynamicLinkError>();
Stream<DynamicLinkError> get errorStream => _errorController.stream;
DynamicLinkService({#required this.userDataService});
void initDynamicLinks() async {
final PendingDynamicLinkData data = await FirebaseDynamicLinks.instance.getInitialLink();
final Uri deepLink = data?.link;
if (deepLink != null) {
processDynamicLink(deepLink);
}
FirebaseDynamicLinks.instance.onLink(
onSuccess: (PendingDynamicLinkData dynamicLink) async {
final Uri deepLink = dynamicLink?.link;
if (deepLink != null) {
print('=====> incoming deep link: <${deepLink.toString()}>');
processDynamicLink(deepLink);
}
},
onError: (OnLinkErrorException error) async {
throw PlatformException(
code: error.code,
message: error.message,
details: error.details,
);
}
);
}
Future<void> processDynamicLink(Uri deepLink) async {
if (deepLink.path == Constants.referralLinkPath && deepLink.queryParameters.containsKey(Constants.referrerLinkParam)) {
var referrer = referrerFromDynamicLink(deepLink);
userDataService.processReferrer(referrer);
} else {
await FirebaseEmailSignIn.processDynamicLink(
deepLink: deepLink,
isLoading: isLoading,
onError: this.onError
);
}
}
void onError(DynamicLinkError error) {
_errorController.add(error);
}
}
You see that my app has to process two types of dynamic link, one is for email link signup, the other link is our referral link that is used to link users together and allow us to understand who introduced a new user to us. This setup works now for us. Hope it helps others too.