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
8 changes: 8 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@
"cwd": "${workspaceFolder}/example",
"flutterMode": "release",
"args": []
},
{
"name": "Example (iOS Device)",
"type": "dart",
"request": "launch",
"program": "lib/main.dart",
"cwd": "${workspaceFolder}/example",
"args": []
}
],
"compounds": []
Expand Down
16 changes: 15 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
import groovy.json.JsonSlurper

def openiapVersionsFile = new File(rootDir.parentFile, 'openiap-versions.json')
static File locateOpeniapVersionsFile(File startDir) {
File current = startDir
while (current != null) {
File candidate = new File(current, 'openiap-versions.json')
if (candidate.exists()) {
return candidate
}
current = current.getParentFile()
}
throw new GradleException(
"Unable to locate openiap-versions.json starting from ${startDir.absolutePath}"
)
}

def openiapVersionsFile = locateOpeniapVersionsFile(rootDir)
def openiapVersions = new JsonSlurper().parse(openiapVersionsFile)
def openiapGoogleVersion = openiapVersions['google']

Expand Down
16 changes: 8 additions & 8 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ PODS:
- Flutter (1.0.0)
- flutter_inapp_purchase (0.0.1):
- Flutter
- openiap (= 1.1.12)
- openiap (1.1.12)
- openiap (= 1.2.2)
- openiap (1.2.2)

DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_inapp_purchase (from `.symlinks/plugins/flutter_inapp_purchase/ios`)
- openiap (from `https://github.com/hyodotdev/openiap-apple.git`, tag `1.1.12`)
- openiap (from `https://github.com/hyodotdev/openiap-apple.git`, tag `1.2.2`)

EXTERNAL SOURCES:
Flutter:
Expand All @@ -17,18 +17,18 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_inapp_purchase/ios"
openiap:
:git: https://github.com/hyodotdev/openiap-apple.git
:tag: 1.1.12
:tag: 1.2.2

CHECKOUT OPTIONS:
openiap:
:git: https://github.com/hyodotdev/openiap-apple.git
:tag: 1.1.12
:tag: 1.2.2

SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_inapp_purchase: 8e8e96cc14bc141e3ffd432012fc9d4e347f8f71
openiap: e207e50cc83dc28cd00c3701421a12eacdbd163a
flutter_inapp_purchase: 50cbf415ba7058ee0042620d433cddd2ff4cb09d
openiap: ea0f37fb1d08cd982ee4dfc219bd2e9c0656822c

PODFILE CHECKSUM: 9446ef2faab77e656d46b3aaf8938ab324919b93
PODFILE CHECKSUM: 55b4ebbe613025bc8eab828c4569a30387a623c1

COCOAPODS: 1.16.2
150 changes: 121 additions & 29 deletions example/lib/src/screens/available_purchases_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ class _AvailablePurchasesScreenState extends State<AvailablePurchasesScreen> {
List<Purchase> _availablePurchases = [];
List<Purchase> _purchaseHistory = [];
bool _loading = false;
bool _historyLoading = false;
bool _connected = false;
String? _error;
String? _historyError;

/// Convert various date formats to milliseconds timestamp
int _parseTimestamp(dynamic date) {
Expand Down Expand Up @@ -100,14 +102,15 @@ class _AvailablePurchasesScreenState extends State<AvailablePurchasesScreen> {
});
await _loadPurchases();
} catch (e) {
if (!mounted) {
debugPrint('Failed to initialize IAP connection: $e');
return;
}
setState(() {
_error = e.toString();
});
debugPrint('Failed to initialize IAP connection: $e');
} finally {
setState(() {
_loading = false;
});
debugPrint('Failed to initialize IAP connection: $e');
}
}

Expand All @@ -130,35 +133,90 @@ class _AvailablePurchasesScreenState extends State<AvailablePurchasesScreen> {
// Remove duplicates by productId, keeping the most recent one
final deduplicatedPurchases = _deduplicatePurchases(availablePurchases);

// Load purchase history
List<Purchase> purchaseHistory = [];
if (_platformOrDefault() == IapPlatform.IOS) {
// iOS: include expired subscriptions as history
purchaseHistory = await _iap.getAvailablePurchases(
const PurchaseOptions(onlyIncludeActiveItemsIOS: false),
);
} else {
// Android (GPB v8+): history of consumed items is not available via API
// Show active purchases only
purchaseHistory = [];
if (!mounted) {
return;
}
debugPrint('Loaded ${purchaseHistory.length} purchases from history');

setState(() {
_availablePurchases = deduplicatedPurchases;
_purchaseHistory = purchaseHistory;
_loading = false;
});

debugPrint(
'After deduplication: ${_availablePurchases.length} unique active purchases');
'After deduplication: ${deduplicatedPurchases.length} unique active purchases');

if (_platformOrDefault() == IapPlatform.IOS) {
unawaited(_loadPurchaseHistory());
} else if (mounted) {
setState(() {
_purchaseHistory = [];
});
}
} catch (e) {
if (!mounted) {
debugPrint('Error loading purchases: $e');
return;
}
setState(() {
_error = e.toString();
_loading = false;
_historyLoading = false;
});
debugPrint('Error loading purchases: $e');
}
}

Future<void> _loadPurchaseHistory() async {
setState(() {
_historyLoading = true;
_historyError = null;
});

Timer? warningTimer;
warningTimer = Timer(const Duration(seconds: 12), () {
if (!mounted || !_historyLoading || _historyError != null) {
return;
}
setState(() {
_historyError =
'Fetching purchase history is taking longer than expected. Still waiting...';
});
});

try {
final purchaseHistory = await _iap.getAvailablePurchases(
const PurchaseOptions(
onlyIncludeActiveItemsIOS: false,
alsoPublishToEventListenerIOS: false,
),
);
debugPrint('Loaded ${purchaseHistory.length} purchases from history');

if (!mounted) {
warningTimer?.cancel();
return;
}

setState(() {
_purchaseHistory = purchaseHistory;
_historyError = null;
});
} catch (e) {
debugPrint('Error loading purchase history: $e');
if (!mounted) {
warningTimer?.cancel();
return;
}
setState(() {
_historyError = e.toString();
});
} finally {
warningTimer?.cancel();
if (!mounted) {
return;
}
setState(() {
_loading = false;
_historyLoading = false;
});
}
}
Expand Down Expand Up @@ -477,14 +535,31 @@ class _AvailablePurchasesScreenState extends State<AvailablePurchasesScreen> {
const SizedBox(height: 24),
],

// Purchase History Section
if (_purchaseHistory.isNotEmpty) ...[
const Text(
'Purchase History',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
// Purchase History Section (iOS only)
if (_platformOrDefault() == IapPlatform.IOS) ...[
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Expanded(
child: Text(
'Purchase History',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
OutlinedButton.icon(
onPressed:
_historyLoading ? null : _loadPurchaseHistory,
icon: const Icon(Icons.history),
label: Text(
_purchaseHistory.isEmpty
? 'Load History'
: 'Reload History',
),
),
],
),
const Text(
'All purchases including consumed items',
Expand All @@ -494,13 +569,30 @@ class _AvailablePurchasesScreenState extends State<AvailablePurchasesScreen> {
),
),
const SizedBox(height: 8),
..._purchaseHistory
.map((item) => _buildPurchaseHistoryItem(item)),
if (_historyLoading)
const Padding(
padding: EdgeInsets.symmetric(vertical: 24),
child: Center(child: CircularProgressIndicator()),
)
else if (_historyError != null)
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(
_historyError!,
style:
const TextStyle(color: Colors.red, fontSize: 12),
),
),
if (_purchaseHistory.isNotEmpty)
..._purchaseHistory
.map((item) => _buildPurchaseHistoryItem(item)),
const SizedBox(height: 24),
],

// Empty State
if (_availablePurchases.isEmpty &&
_purchaseHistory.isEmpty &&
!_historyLoading &&
_error == null) ...[
Center(
child: Column(
Expand Down
7 changes: 1 addition & 6 deletions example/lib/src/screens/purchase_flow_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -363,12 +363,7 @@ Product ID: ${error.productId ?? 'unknown'}
),
);

List<Product> products;
if (result is FetchProductsResultProducts) {
products = result.value ?? const <Product>[];
} else {
products = const <Product>[];
}
final products = result.inAppProducts();

debugPrint('📦 Received ${products.length} products from fetchProducts');

Expand Down
Loading