Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,30 @@ Before committing any changes, run these commands in order and ensure ALL pass:

**Manual check script**: You can also run `./scripts/pre-commit-checks.sh` to manually execute all checks.

### Code Coverage (Codecov)

This project uses Codecov with two checks: **codecov/patch** (new/modified lines) and **codecov/project** (overall project coverage). Both must pass for CI to succeed.

**When adding or modifying code:**

1. Always write tests for new code paths - aim for full branch coverage of your changes
2. After writing tests, run coverage locally to verify:

```bash
flutter test --coverage
dart run tool/filter_coverage.dart
```

3. The project target is `auto` (must not decrease from base branch). If codecov/project fails, add more tests until overall coverage meets or exceeds the base
4. Focus coverage on:
- All new public methods and their error paths
- Both `on PlatformException catch` and generic `catch` blocks
- Extension methods and utility functions
- Edge cases (null values, empty strings, missing fields)
5. Files with 0% coverage are easy wins - prioritize testing those first when coverage needs improvement

**Configuration:** `.codecov.yml` - `lib/types.dart` is ignored (generated file).

### Commit Message Convention

- Follow the Angular commit style: `<type>: <short summary>` (50 characters max).
Expand Down
112 changes: 91 additions & 21 deletions lib/flutter_inapp_purchase.dart
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,11 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi {
await _channel.invokeMethod('initConnection', config);
_isInitialized = true;
return true;
} on PlatformException catch (error) {
throw _purchaseErrorFromPlatformException(
error,
'initialize IAP connection',
);
} catch (error) {
throw PurchaseError(
code: gentype.ErrorCode.NotPrepared,
Expand All @@ -265,6 +270,11 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi {

_isInitialized = false;
return true;
} on PlatformException catch (error) {
throw _purchaseErrorFromPlatformException(
error,
'end IAP connection',
);
} catch (error) {
throw PurchaseError(
code: gentype.ErrorCode.ServiceError,
Expand Down Expand Up @@ -466,10 +476,13 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi {
code: gentype.ErrorCode.IapNotAvailable,
message: 'requestPurchase is not supported on this platform',
);
} on PlatformException catch (error) {
throw _purchaseErrorFromPlatformException(
error,
'request purchase',
);
} catch (error) {
if (error is PurchaseError) {
rethrow;
}
if (error is PurchaseError) rethrow;
throw PurchaseError(
code: gentype.ErrorCode.ServiceError,
message: 'Failed to request purchase: ${error.toString()}',
Expand Down Expand Up @@ -568,7 +581,13 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi {
}

return await resolvePurchases();
} on PlatformException catch (error) {
throw _purchaseErrorFromPlatformException(
error,
'get available purchases',
);
} catch (error) {
if (error is PurchaseError) rethrow;
throw PurchaseError(
code: gentype.ErrorCode.ServiceError,
message: 'Failed to get available purchases: ${error.toString()}',
Expand All @@ -587,10 +606,12 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi {
'getStorefront',
);
return storefront ?? '';
} catch (error) {
debugPrint(
'[getStorefront] Failed to get storefront on ${_platform.operatingSystem}: $error',
} on PlatformException catch (error) {
throw _purchaseErrorFromPlatformException(
error,
'get storefront',
);
} catch (error) {
throw PurchaseError(
code: gentype.ErrorCode.ServiceError,
message: 'Failed to get storefront: ${error.toString()}',
Expand Down Expand Up @@ -618,10 +639,13 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi {
code: gentype.ErrorCode.ServiceError,
message: 'Failed to get storefront country code',
);
} on PlatformException catch (error) {
throw _purchaseErrorFromPlatformException(
error,
'get storefront',
);
} catch (error) {
if (error is PurchaseError) {
rethrow;
}
if (error is PurchaseError) rethrow;
throw PurchaseError(
code: gentype.ErrorCode.ServiceError,
message: 'Failed to get storefront: ${error.toString()}',
Expand Down Expand Up @@ -832,11 +856,17 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi {
try {
await channel.invokeMethod('presentCodeRedemptionSheetIOS');
return true;
} on PlatformException catch (error) {
throw _purchaseErrorFromPlatformException(
error,
'present code redemption sheet',
);
} catch (error) {
if (error is PurchaseError) rethrow;
throw PurchaseError(
code: gentype.ErrorCode.ServiceError,
message:
'Failed to present code redemption sheet: ${error.toString()}',
message: 'Failed to present code redemption sheet: '
'${error.toString()}',
);
}
};
Expand All @@ -854,11 +884,17 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi {
try {
await channel.invokeMethod('showManageSubscriptionsIOS');
return const <gentype.PurchaseIOS>[];
} on PlatformException catch (error) {
throw _purchaseErrorFromPlatformException(
error,
'show manage subscriptions',
);
} catch (error) {
if (error is PurchaseError) rethrow;
throw PurchaseError(
code: gentype.ErrorCode.ServiceError,
message:
'Failed to show manage subscriptions: ${error.toString()}',
message: 'Failed to show manage subscriptions: '
'${error.toString()}',
);
}
};
Expand Down Expand Up @@ -1619,7 +1655,13 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi {
);
return inApps;
}
} on PlatformException catch (error) {
throw _purchaseErrorFromPlatformException(
error,
'fetch products',
);
} catch (error) {
if (error is PurchaseError) rethrow;
throw PurchaseError(
code: gentype.ErrorCode.ServiceError,
message: 'Failed to fetch products: ${error.toString()}',
Expand Down Expand Up @@ -1676,6 +1718,26 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi {
}
};

/// Convert a PlatformException to a PurchaseError with proper error code
PurchaseError _purchaseErrorFromPlatformException(
PlatformException error,
String operation,
) {
final platform = _platform.isIOS || _platform.isMacOS
? gentype.IapPlatform.IOS
: gentype.IapPlatform.Android;
final errorCode = errors.ErrorCodeUtils.fromPlatformCode(
error.code,
platform,
);
return PurchaseError(
code: errorCode,
platform: platform,
message: 'Failed to $operation [${error.code}]: '
'${error.message ?? error.details}',
);
}

/// Recursively convert platform channel Map/List to proper Dart types
dynamic _deepConvertMap(dynamic value) {
if (value is Map) {
Expand Down Expand Up @@ -1767,13 +1829,17 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi {
}

return _parseActiveSubscriptions(result);
} on PlatformException catch (error) {
throw _purchaseErrorFromPlatformException(
error,
'get active subscriptions',
);
} catch (error) {
if (error is PurchaseError) {
rethrow;
}
if (error is PurchaseError) rethrow;
throw PurchaseError(
code: gentype.ErrorCode.ServiceError,
message: 'Failed to get active subscriptions: ${error.toString()}',
message: 'Failed to get active subscriptions: '
'${error.toString()}',
);
}
};
Expand Down Expand Up @@ -2024,13 +2090,17 @@ class FlutterInappPurchase with RequestPurchaseBuilderApi {
'isEligibleForExternalPurchaseCustomLinkIOS',
);
return result ?? false;
} on PlatformException catch (error) {
throw _purchaseErrorFromPlatformException(
error,
'check eligibility for ExternalPurchaseCustomLink',
);
} catch (error) {
debugPrint(
'isEligibleForExternalPurchaseCustomLinkIOS error: $error');
if (error is PurchaseError) rethrow;
throw PurchaseError(
code: gentype.ErrorCode.ServiceError,
message:
'Failed to check eligibility for ExternalPurchaseCustomLink: $error',
message: 'Failed to check eligibility for '
'ExternalPurchaseCustomLink: $error',
);
}
};
Expand Down
Loading