Skip to content

Commit e92746e

Browse files
authored
feat: add subscription status APIs (#522)
This PR implements standardized subscription management APIs as specified in the OpenIAP specification, providing a unified way to check subscription status across iOS and Android platforms with automatic detection of all active subscriptions and platform-specific details like iOS expiration dates and Android auto-renewal status. ## OpenIAP Specification References - [getActiveSubscriptions](https://www.openiap.dev/docs/apis#getactivesubscriptions) - [hasActiveSubscriptions](https://www.openiap.dev/docs/apis#hasactivesubscriptions) - [ActiveSubscription Type](https://www.openiap.dev/docs/types#activesubscription) ## Changes ### New APIs - Add `getActiveSubscriptions()` to retrieve detailed subscription information - Returns list of `ActiveSubscription` objects with platform-specific fields - Supports optional filtering by subscription IDs - Automatically detects all active subscriptions when no filter provided - Add `hasActiveSubscriptions()` for simple boolean subscription checks - Returns `true` if user has any active subscriptions - Supports optional filtering by specific subscription IDs - Handles errors gracefully by returning `false` ### Type System - Add `ActiveSubscription` model with platform-specific fields: - **iOS**: `expirationDateIOS`, `environmentIOS`, `daysUntilExpirationIOS` - **Android**: `autoRenewingAndroid` - **Cross-platform**: `productId`, `isActive`, `willExpireSoon` (within 7 days) ### Code Quality - Rename `IAPPlatform` to `IapPlatform` following Dart naming conventions - Fix type casting issues in `extractPurchased` and `extractItems` utilities - Add proper iOS transaction state mapping in `_convertToPurchase` - Replace `getCurrentPlatform()` calls with instance-based platform detection ### Testing - Add comprehensive unit tests for both iOS and Android platforms - Test subscription filtering functionality - Verify platform-specific field population ## Usage Example ```dart // Get all active subscriptions final subscriptions = await FlutterInappPurchase.instance.getActiveSubscriptions(); // Check specific subscriptions final hasMonthly = await FlutterInappPurchase.instance.hasActiveSubscriptions( subscriptionIds: ['monthly_subscription', 'yearly_subscription'], ); // Access platform-specific information for (final sub in subscriptions) { if (Platform.isIOS && sub.daysUntilExpirationIOS != null) { print('Expires in ${sub.daysUntilExpirationIOS} days'); } if (Platform.isAndroid && sub.autoRenewingAndroid == true) { print('Auto-renewing subscription'); } } ``` ## Breaking Changes None - these are new additions to the API surface. ## Migration Notes - Developers using custom subscription checking logic can migrate to these standardized APIs - The deprecated `checkSubscribed()` method (removed in v6.0.0) can now be replaced with `hasActiveSubscriptions()` ## Related - OpenIAP Discussion: https://github.com/hyochan/openiap.dev/discussions - Reference Implementation: hyochan/expo-iap#158
1 parent f671fe6 commit e92746e

File tree

8 files changed

+526
-84
lines changed

8 files changed

+526
-84
lines changed

.markdownlint.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"MD013": false,
33
"MD024": false,
4+
"MD033": false,
45
"MD040": false,
56
"MD041": false
67
}

README.md

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,6 @@ await iap.initConnection();
6161
final sameIap = FlutterInappPurchase.instance; // Same instance
6262
```
6363

64-
### With Flutter Hooks
65-
66-
```dart
67-
// useIAP hook automatically uses singleton
68-
final iapState = useIAP();
69-
70-
// Access products, purchases, etc.
71-
final products = iapState.products;
72-
final currentPurchase = iapState.currentPurchase;
73-
```
74-
7564
## Sponsors
7665

7766
💼 **[View Our Sponsors](https://openiap.dev/sponsors)**

example/lib/src/screens/error_handling_example.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ class ErrorHandlingExample extends StatelessWidget {
171171
final error = PurchaseError(
172172
code: ErrorCode.eUserCancelled,
173173
message: 'User cancelled the purchase',
174-
platform: IAPPlatform.ios,
174+
platform: IapPlatform.ios,
175175
);
176176

177177
return '''
@@ -190,7 +190,7 @@ isRecoverableError: ${isRecoverableError(error)}
190190
final error = PurchaseError(
191191
code: ErrorCode.eNetworkError,
192192
message: 'Network connection failed',
193-
platform: IAPPlatform.android,
193+
platform: IapPlatform.android,
194194
);
195195

196196
return '''
@@ -249,7 +249,7 @@ isRecoverableError: ${isRecoverableError(error)}
249249
final error = PurchaseError(
250250
code: ErrorCode.eNetworkError,
251251
message: 'Network error occurred',
252-
platform: IAPPlatform.ios,
252+
platform: IapPlatform.ios,
253253
);
254254

255255
return '''
@@ -324,7 +324,7 @@ class PurchaseWithErrorHandling extends StatelessWidget {
324324
throw PurchaseError(
325325
code: ErrorCode.eNetworkError,
326326
message: 'Failed to connect to store',
327-
platform: IAPPlatform.ios,
327+
platform: IapPlatform.ios,
328328
);
329329
} catch (error) {
330330
// Handle the error using our utilities

lib/enums.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
enum Store { none, playStore, amazon, appStore }
55

66
/// Platform detection enum
7-
enum IAPPlatform { ios, android }
7+
enum IapPlatform { ios, android }
88

99
/// Purchase type enum
1010
enum PurchaseType { inapp, subs }

lib/errors.dart

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import 'dart:io';
44
import 'enums.dart';
55

66
/// Get current platform
7-
IAPPlatform getCurrentPlatform() {
7+
IapPlatform getCurrentPlatform() {
88
if (Platform.isIOS) {
9-
return IAPPlatform.ios;
9+
return IapPlatform.ios;
1010
} else if (Platform.isAndroid) {
11-
return IAPPlatform.android;
11+
return IapPlatform.android;
1212
}
1313
throw UnsupportedError('Platform not supported');
1414
}
@@ -89,7 +89,7 @@ class PurchaseError implements Exception {
8989
final String? debugMessage;
9090
final ErrorCode? code;
9191
final String? productId;
92-
final IAPPlatform? platform;
92+
final IapPlatform? platform;
9393

9494
PurchaseError({
9595
String? name,
@@ -104,7 +104,7 @@ class PurchaseError implements Exception {
104104
/// Creates a PurchaseError from platform-specific error data
105105
factory PurchaseError.fromPlatformError(
106106
Map<String, dynamic> errorData,
107-
IAPPlatform platform,
107+
IapPlatform platform,
108108
) {
109109
final errorCode = errorData['code'] != null
110110
? ErrorCodeUtils.fromPlatformCode(errorData['code'], platform)
@@ -175,9 +175,9 @@ class ErrorCodeUtils {
175175
/// Maps a platform-specific error code back to the standardized ErrorCode enum
176176
static ErrorCode fromPlatformCode(
177177
dynamic platformCode,
178-
IAPPlatform platform,
178+
IapPlatform platform,
179179
) {
180-
if (platform == IAPPlatform.ios) {
180+
if (platform == IapPlatform.ios) {
181181
final mapping = ErrorCodeMapping.ios;
182182
for (final entry in mapping.entries) {
183183
if (entry.value == platformCode) {
@@ -196,17 +196,17 @@ class ErrorCodeUtils {
196196
}
197197

198198
/// Maps an ErrorCode enum to platform-specific code
199-
static dynamic toPlatformCode(ErrorCode errorCode, IAPPlatform platform) {
200-
if (platform == IAPPlatform.ios) {
199+
static dynamic toPlatformCode(ErrorCode errorCode, IapPlatform platform) {
200+
if (platform == IapPlatform.ios) {
201201
return ErrorCodeMapping.ios[errorCode] ?? 0;
202202
} else {
203203
return ErrorCodeMapping.android[errorCode] ?? 'E_UNKNOWN';
204204
}
205205
}
206206

207207
/// Checks if an error code is valid for the specified platform
208-
static bool isValidForPlatform(ErrorCode errorCode, IAPPlatform platform) {
209-
if (platform == IAPPlatform.ios) {
208+
static bool isValidForPlatform(ErrorCode errorCode, IapPlatform platform) {
209+
if (platform == IapPlatform.ios) {
210210
return ErrorCodeMapping.ios.containsKey(errorCode);
211211
} else {
212212
return ErrorCodeMapping.android.containsKey(errorCode);

0 commit comments

Comments
 (0)