ios_storekit2 0.0.3
ios_storekit2: ^0.0.3 copied to clipboard
Flutter plugin for iOS in-app purchases using StoreKit 2.
ios_storekit2 #
Flutter plugin for iOS in-app purchases powered by StoreKit 2.
Scope #
- iOS only
- Minimum iOS version: 15.0
- Product loading
- Purchasing
- Current entitlements
- Restore purchases
- Transaction updates stream
- StoreKit Test coverage in the
exampleapp
Install #
dependencies:
ios_storekit2:
path: ../ios_storekit2
Basic Usage #
import 'package:ios_storekit2/ios_storekit2.dart';
final storekit = IosStorekit2();
final products = await storekit.getProducts({
'com.example.monthly',
'com.example.yearly',
'com.example.lifetime',
});
final result = await storekit.purchase('com.example.monthly');
if (result.status == SK2PurchaseStatus.success) {
print(result.transactionId);
print(result.productType);
}
final entitlements = await storekit.getEntitlements();
Error Details #
Native iOS failures still use plugin-level PlatformException.code values such as
FETCH_ERROR, PURCHASE_ERROR, and RESTORE_ERROR, but now also include the
underlying NSError metadata in PlatformException.details:
nativeErrorDomainnativeErrorCodenativeLocalizedDescriptionnativeLocalizedFailureReasonnativeLocalizedRecoverySuggestionnativeHelpAnchornativeErrorUserInfoas a Flutter-safe serialized map
try {
await storekit.restorePurchases();
} on PlatformException catch (e) {
final details = (e.details as Map?)?.cast<String, dynamic>();
print(e.code); // RESTORE_ERROR
print(details?['nativeErrorDomain']);
print(details?['nativeErrorCode']);
print(details?['nativeLocalizedFailureReason']);
print(details?['nativeErrorUserInfo']);
}
Product Model #
SK2ProductTypenow distinguishessubscription,consumable, andnonConsumable- Subscription periods are exposed canonically as
SK2Period(value, unit) - Introductory offers are exposed as
introOffer, not only as trial-specific data
Example:
for (final product in products) {
final subscription = product.subscription;
if (subscription == null) continue;
final period = subscription.period;
print('${period.value} ${period.unit.name}');
final introOffer = subscription.introOffer;
if (introOffer != null) {
print(introOffer.offerType);
}
}
Intro Offers And Trial Sugar #
The canonical fields are:
subscription.introOffersubscription.introOfferEligibilitypurchaseResult.isIntroOfferpurchaseResult.introOfferTypeentitlement.isIntroOfferentitlement.introOfferType
Convenience sugar is still available:
subscription.trialreturns the intro offer only when it is a free trialsubscription.isTrialEligiblemaps eligibility tobool?purchaseResult.isTrialistrueonly for free trialsentitlement.isTrialistrueonly for free trialsSK2TrialInforemains available as a typedef alias toSK2IntroOfferInfo
isTrialEligible is now nullable:
true: eligiblefalse: not eligiblenull: eligibility is unknown, for example on iOS 15
Purchase Result #
Successful purchases now return richer metadata:
productIdproductTypetransactionIdoriginalTransactionIdpurchaseDateexpirationDaterevocationDateownershipTypeisIntroOfferintroOfferType
This makes it easier to log, deduplicate, and reconcile purchases in app code.
Migration Notes #
1. SK2ProductType.oneTime was removed #
Before:
if (product.type == SK2ProductType.oneTime) {
// ...
}
After:
if (product.type == SK2ProductType.nonConsumable) {
// ...
}
If you need consumables, check SK2ProductType.consumable.
2. Use period, not periodDays, as the source of truth #
Before:
final days = product.subscription!.periodDays;
After:
final period = product.subscription!.period;
final value = period.value;
final unit = period.unit;
periodDays still exists as a convenience getter, but it is only an approximation.
3. Use intro-offer APIs instead of assuming every offer is a free trial #
Before:
final trial = product.subscription?.trial;
if (trial != null) {
print('${trial.periodDays}-day trial');
}
After:
final introOffer = product.subscription?.introOffer;
if (introOffer != null) {
print(introOffer.offerType);
}
trial is still available, but only for free trials.
4. isTrialEligible is now bool? #
Before:
if (product.subscription!.isTrialEligible) {
// show trial CTA
}
After:
switch (product.subscription!.introOfferEligibility) {
case SK2EligibilityStatus.eligible:
// show intro offer
break;
case SK2EligibilityStatus.ineligible:
// already used
break;
case SK2EligibilityStatus.unknown:
// platform cannot determine
break;
}
5. Purchase payloads are richer now #
Before:
final result = await storekit.purchase(product.id);
print(result.productId);
After:
final result = await storekit.purchase(product.id);
print(result.productId);
print(result.transactionId);
print(result.originalTransactionId);
print(result.productType);
Testing #
The example app includes StoreKit Test setup and Xcode unit tests for:
- product mapping
- purchase success
- Ask to Buy approve / decline
- restore purchases
- refund / revocation
- expiration
- renewal
- intro offer eligibility
See example/ios/RunnerTests/RunnerTests.swift.