Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
36 changes: 11 additions & 25 deletions .github/workflows/ci-gemini-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ on:
issue_comment:
types: [created]

# Ensure only one run per-issue at a time and avoid duplicates
concurrency:
group: ci-gemini-pr-${{ github.event.issue.number }}
cancel-in-progress: true
Expand All @@ -16,6 +15,7 @@ permissions:
contents: write
issues: write
pull-requests: write
id-token: write

jobs:
analyze-and-pr:
Expand Down Expand Up @@ -46,7 +46,12 @@ jobs:
id: analyze-pr
uses: google-github-actions/run-gemini-cli@v0
timeout-minutes: 20
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# Code Assist settings
use_gemini_code_assist: true
gemini_model: "gemini-2.5-pro"
prompt: |
You are an expert Flutter developer helping with the flutter_inapp_purchase package.

Expand Down Expand Up @@ -91,36 +96,17 @@ jobs:
settings: |
{
"tools": [
{
"name": "gh",
"description": "GitHub CLI for creating branches and PRs"
},
{
"name": "git",
"description": "Git commands for version control"
},
{
"name": "flutter",
"description": "Flutter CLI for testing and analysis"
},
{
"name": "dart",
"description": "Dart CLI for formatting and analysis"
}
{ "name": "gh", "description": "GitHub CLI for creating branches and PRs" },
{ "name": "git", "description": "Git commands for version control" },
{ "name": "flutter", "description": "Flutter CLI for testing and analysis" },
{ "name": "dart", "description": "Dart CLI for formatting and analysis" }
],
"repositories": [
{
"path": ".",
"instructions": "Refer to CLAUDE.md for conventions. Keep changes minimal, add tests, and follow pre-commit checks."
}
{ "path": ".", "instructions": "Refer to CLAUDE.md for conventions. Keep changes minimal, add tests, and follow pre-commit checks." }
],
"model": "gemini-1.5-flash"
}

gemini_api_key: ${{ secrets.GEMINI_API_KEY }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Comment on issue with result
if: always()
uses: actions/github-script@v7
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@ jobs:

- run: flutter pub get

- run: dart format --page-width 80 --trailing-commas automate --output=none --set-exit-if-changed .
- name: Check formatting (excluding generated files)
shell: bash
run: |
set -euo pipefail
files=$(git ls-files '*.dart' | grep -Ev '^lib/types\.dart$|\.g\.dart$|\.freezed\.dart$' || true)
if [[ -z "$files" ]]; then
exit 0
fi
printf '%s\n' "$files" | xargs dart format --page-width 80 --output=none --set-exit-if-changed

# - run: flutter analyze .

Expand Down
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

// Dart & Flutter settings
"dart.flutterSdkPath": null,
"dart.doNotFormat": [
"lib/types.dart"
],
"[dart]": {
"editor.rulers": [80],
"editor.selectionHighlight": false,
Expand Down Expand Up @@ -62,5 +65,5 @@
// Docusaurus specific
"typescript.tsdk": "docs/node_modules/typescript/lib",
"npm.packageManager": "npm",
"cSpell.words": ["inapp", "Inapp"]
"cSpell.words": ["inapp", "Inapp", "skus"]
}
1 change: 1 addition & 0 deletions AGENTS.md
18 changes: 15 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ final allPurchases = await iap.getAvailablePurchases(

## Flutter-Specific Guidelines

### Generated Files

- `lib/types.dart` is generated from the OpenIAP schema. Never edit it by hand.
- Always regenerate via `./scripts/generate-type.sh` so the file stays in sync with the upstream `openiap-dart` package.
- If the generation script fails, fix the script or the upstream source instead of patching the output manually.

### Documentation Style

- **Avoid using emojis** in documentation, especially in headings
Expand All @@ -53,9 +59,9 @@ final allPurchases = await iap.getAvailablePurchases(

Before committing any changes, run these commands in order and ensure ALL pass:

1. **Format check**: `dart format --set-exit-if-changed .`
- This will fail if any files need formatting (exit code 1)
- If it fails, run `dart format .` to fix formatting, then retry
1. **Format check**: `git ls-files '*.dart' | grep -v '^lib/types.dart$' | xargs dart format --page-width 80 --output=none --set-exit-if-changed`
- This matches the CI formatter and skips the generated `lib/types.dart`
- If it fails, run the same command without `--set-exit-if-changed` (or drop the `--output` flag) to auto-format, then retry
- Always format code before committing to maintain consistent style
2. **Lint check**: `flutter analyze`
- Fix any lint issues before committing
Expand All @@ -65,6 +71,12 @@ Before committing any changes, run these commands in order and ensure ALL pass:
4. **Final verification**: Re-run `dart format --set-exit-if-changed .` to confirm no formatting issues
5. Only commit if ALL checks succeed with exit code 0

### Commit Message Convention

- Follow the Angular commit style: `<type>: <short summary>` (50 characters max).
- Use lowercase `type` (e.g., `feat`, `fix`, `docs`, `chore`, `test`).
- Keep the summary concise and descriptive; avoid punctuation at the end.

**Important**:
- Use `--set-exit-if-changed` flag to match CI behavior and catch formatting issues locally before they cause CI failures
- When using generic functions like `showModalBottomSheet`, always specify explicit type arguments (e.g., `showModalBottomSheet<void>`) to avoid type inference errors
Expand Down
5 changes: 4 additions & 1 deletion analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ linter:
- use_rethrow_when_possible

analyzer:
exclude:
- lib/types.dart
- example/**
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
strict-raw-types: true
2 changes: 1 addition & 1 deletion example/ios/Flutter/Flutter.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Pod::Spec.new do |s|
s.license = { :type => 'BSD' }
s.author = { 'Flutter Dev Team' => '[email protected]' }
s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s }
s.ios.deployment_target = '13.0'
s.ios.deployment_target = '12.0'
# Framework linking is handled by Flutter tooling, not CocoaPods.
# Add a placeholder to satisfy `s.dependency 'Flutter'` plugin podspecs.
s.vendored_frameworks = 'path/to/nothing'
Expand Down
6 changes: 3 additions & 3 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ CHECKOUT OPTIONS:
:tag: 1.1.9

SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_inapp_purchase: 919e64e275a3a68ed9a0b7798c3f51e591a1153f
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_inapp_purchase: 2132ba52676c2848b98ea55d26cd7d2284c252a2
openiap: dcfa2a4dcf31259ec4b73658e7d1441babccce66

PODFILE CHECKSUM: 7f77729f2fac4d8b3c65d064abe240d5bd87fe01

COCOAPODS: 1.16.2
COCOAPODS: 1.15.2
10 changes: 9 additions & 1 deletion example/lib/src/screens/available_purchases_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ class _AvailablePurchasesScreenState extends State<AvailablePurchasesScreen> {
return uniquePurchases.values.toList();
}

IapPlatform _platformOrDefault() {
try {
return getCurrentPlatform();
} catch (_) {
return IapPlatform.Android;
}
}

@override
void initState() {
super.initState();
Expand Down Expand Up @@ -121,7 +129,7 @@ class _AvailablePurchasesScreenState extends State<AvailablePurchasesScreen> {

// Load purchase history
List<Purchase> purchaseHistory = [];
if (getCurrentPlatform() == IapPlatform.ios) {
if (_platformOrDefault() == IapPlatform.IOS) {
// iOS: include expired subscriptions as history
purchaseHistory = await _iap.getAvailablePurchases(
const PurchaseOptions(onlyIncludeActiveItemsIOS: false),
Expand Down
41 changes: 21 additions & 20 deletions example/lib/src/screens/builder_demo_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ class _BuilderDemoScreenState extends State<BuilderDemoScreen> {
try {
await _iap.requestPurchaseWithBuilder(
build: (RequestPurchaseBuilder r) => r
..type = ProductType.inapp
..withIOS((RequestPurchaseIOSBuilder i) =>
..type = ProductType.InApp
..withIOS((RequestPurchaseIosBuilder i) =>
i..sku = 'dev.hyo.martie.10bulbs')
..withAndroid((RequestPurchaseAndroidBuilder a) =>
a..skus = ['dev.hyo.martie.10bulbs']),
Expand All @@ -63,8 +63,8 @@ class _BuilderDemoScreenState extends State<BuilderDemoScreen> {
// Use requestPurchaseWithBuilder with type=subs
await _iap.requestPurchaseWithBuilder(
build: (RequestPurchaseBuilder r) => r
..type = ProductType.subs
..withIOS((RequestPurchaseIOSBuilder i) =>
..type = ProductType.Subs
..withIOS((RequestPurchaseIosBuilder i) =>
i..sku = 'dev.hyo.martie.premium')
..withAndroid((RequestPurchaseAndroidBuilder a) =>
a..skus = ['dev.hyo.martie.premium']),
Expand All @@ -86,15 +86,16 @@ class _BuilderDemoScreenState extends State<BuilderDemoScreen> {
try {
// Get existing subscription token if any
final purchases = await _iap.getAvailablePurchases();
final existing = purchases.firstWhere(
(p) => p.productId == 'dev.hyo.martie.premium',
orElse: () => purchases.isNotEmpty
? purchases.first
: Purchase(productId: '', platform: IapPlatform.android),
);
Purchase? existing;
for (final purchase in purchases) {
if (purchase.productId == 'dev.hyo.martie.premium') {
existing = purchase;
break;
}
}

// Android requires an existing purchase token to replace (proration)
final token = existing.purchaseToken;
final token = existing?.purchaseToken;
final hasToken = token != null && token.isNotEmpty;
// Demo: use a default proration mode for upgrade
final int prorationMode = AndroidReplacementMode.withTimeProration.value;
Expand All @@ -108,8 +109,8 @@ class _BuilderDemoScreenState extends State<BuilderDemoScreen> {
..purchaseTokenAndroid = token);

await _iap.requestPurchase(
request: subBuilder.build(),
type: ProductType.subs,
props: subBuilder.build(),
type: ProductType.Subs,
);
setState(() => _status = 'Subscription upgrade initiated');
} else {
Expand All @@ -118,8 +119,8 @@ class _BuilderDemoScreenState extends State<BuilderDemoScreen> {
..withAndroid((RequestSubscriptionAndroidBuilder a) =>
a..skus = ['dev.hyo.martie.premium']);
await _iap.requestPurchase(
request: newSub.build(),
type: ProductType.subs,
props: newSub.build(),
type: ProductType.Subs,
);
setState(() => _status =
'No token/proration; purchased yearly as new subscription');
Expand Down Expand Up @@ -197,8 +198,8 @@ class _BuilderDemoScreenState extends State<BuilderDemoScreen> {
SelectableText(
"""await iap.requestPurchaseWithBuilder(
build: (RequestPurchaseBuilder r) => r
..type = ProductType.inapp
..withIOS((RequestPurchaseIOSBuilder i) => i
..type = ProductType.InApp
..withIOS((RequestPurchaseIosBuilder i) => i
..sku = 'dev.hyo.martie.10bulbs'
..quantity = 1)
..withAndroid((RequestPurchaseAndroidBuilder a) => a
Expand All @@ -208,8 +209,8 @@ class _BuilderDemoScreenState extends State<BuilderDemoScreen> {
// For subscriptions (new purchase):
await iap.requestPurchaseWithBuilder(
build: (RequestPurchaseBuilder r) => r
..type = ProductType.subs
..withIOS((RequestPurchaseIOSBuilder i) => i..sku = 'dev.hyo.martie.premium')
..type = ProductType.Subs
..withIOS((RequestPurchaseIosBuilder i) => i..sku = 'dev.hyo.martie.premium')
..withAndroid((RequestPurchaseAndroidBuilder a) => a..skus = ['dev.hyo.martie.premium']),
);

Expand All @@ -219,7 +220,7 @@ final b = RequestSubscriptionBuilder()
..skus = ['dev.hyo.martie.premium']
..replacementModeAndroid = AndroidReplacementMode.withTimeProration.value
..purchaseTokenAndroid = '<existing_token>');
await iap.requestPurchase(request: b.build(), type: ProductType.subs);""",
await iap.requestPurchase(request: b.build(), type: ProductType.Subs);""",
style: TextStyle(fontFamily: 'monospace', fontSize: 12),
),
],
Expand Down
8 changes: 4 additions & 4 deletions example/lib/src/screens/error_handling_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ class ErrorHandlingExample extends StatelessWidget {
final error = PurchaseError(
code: ErrorCode.UserCancelled,
message: 'User cancelled the purchase',
platform: IapPlatform.ios,
platform: IapPlatform.IOS,
);

return '''
Expand All @@ -190,7 +190,7 @@ isRecoverableError: ${isRecoverableError(error)}
final error = PurchaseError(
code: ErrorCode.NetworkError,
message: 'Network connection failed',
platform: IapPlatform.android,
platform: IapPlatform.Android,
);

return '''
Expand Down Expand Up @@ -249,7 +249,7 @@ isRecoverableError: ${isRecoverableError(error)}
final error = PurchaseError(
code: ErrorCode.NetworkError,
message: 'Network error occurred',
platform: IapPlatform.ios,
platform: IapPlatform.IOS,
);

return '''
Expand Down Expand Up @@ -324,7 +324,7 @@ class PurchaseWithErrorHandling extends StatelessWidget {
throw PurchaseError(
code: ErrorCode.NetworkError,
message: 'Failed to connect to store',
platform: IapPlatform.ios,
platform: IapPlatform.IOS,
);
} catch (error) {
// Handle the error using our utilities
Expand Down
19 changes: 10 additions & 9 deletions example/lib/src/screens/purchase_flow_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class _PurchaseFlowScreenState extends State<PurchaseFlowScreen> {
debugPrint(' Purchase token: ${purchase.purchaseToken}');
debugPrint(' ID: ${purchase.id} (${purchase.id.runtimeType})');
debugPrint(' IDs array: ${purchase.ids}');
if (purchase.platform == IapPlatform.ios) {
if (purchase.platform == IapPlatform.IOS) {
debugPrint(' quantityIOS: ${purchase.quantityIOS}');
debugPrint(
' originalTransactionIdentifierIOS: ${purchase.originalTransactionIdentifierIOS} (${purchase.originalTransactionIdentifierIOS?.runtimeType})');
Expand All @@ -148,18 +148,19 @@ class _PurchaseFlowScreenState extends State<PurchaseFlowScreen> {

if (Platform.isAndroid) {
// For Android, check multiple conditions since fields can be null
bool condition1 = purchase.purchaseState == PurchaseState.purchased;
bool condition2 = (purchase.isAcknowledgedAndroid == false &&
final bool condition1 = purchase.purchaseState == PurchaseState.purchased;
final bool condition2 = purchase.isAcknowledgedAndroid == false &&
purchase.purchaseToken != null &&
purchase.purchaseToken!.isNotEmpty);
bool condition3 =
purchase.purchaseStateAndroid == AndroidPurchaseState.purchased.value;
purchase.purchaseToken!.isNotEmpty &&
purchase.purchaseStateAndroid == AndroidPurchaseState.Purchased.value;
final bool condition3 =
purchase.purchaseStateAndroid == AndroidPurchaseState.Purchased.value;

debugPrint(' Android condition checks:');
debugPrint(' purchaseState == purchased: $condition1');
debugPrint(' unacknowledged with token: $condition2');
debugPrint(
' purchaseStateAndroid == AndroidPurchaseState.purchased: $condition3');
' purchaseStateAndroid == AndroidPurchaseState.Purchased: $condition3');

isPurchased = condition1 || condition2 || condition3;
debugPrint(' Final isPurchased: $isPurchased');
Expand Down Expand Up @@ -332,7 +333,7 @@ Platform: ${error.platform}
// Use fetchProducts with Product type for type-safe list
final products = await _iap.fetchProducts<Product>(
skus: productIds,
type: ProductType.inapp,
type: ProductType.InApp,
);

debugPrint(
Expand Down Expand Up @@ -386,8 +387,8 @@ Platform: ${error.platform}
android: RequestPurchaseAndroid(
skus: [productId],
),
type: ProductType.InApp,
),
type: ProductType.inapp,
);

debugPrint('✅ Purchase request sent successfully');
Expand Down
Loading