mirror of
https://github.com/feeddeck/feeddeck.git
synced 2026-03-11 17:47:47 -05:00
This commit enables in-app purchases for Android, so that a user can also subscribe to FeedDeck Premium on Android.
255 lines
8.7 KiB
Dart
255 lines
8.7 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:purchases_flutter/purchases_flutter.dart';
|
|
import 'package:supabase_flutter/supabase_flutter.dart' as supabase;
|
|
|
|
import 'package:feeddeck/models/profile.dart';
|
|
import 'package:feeddeck/repositories/profile_repository.dart';
|
|
import 'package:feeddeck/repositories/settings_repository.dart';
|
|
import 'package:feeddeck/utils/constants.dart';
|
|
import 'package:feeddeck/utils/fd_icons.dart';
|
|
import 'package:feeddeck/widgets/general/elevated_button_progress_indicator.dart';
|
|
|
|
class SettingsPremiumInApp extends StatefulWidget {
|
|
const SettingsPremiumInApp({super.key});
|
|
|
|
@override
|
|
State<SettingsPremiumInApp> createState() => _SettingsPremiumInAppState();
|
|
}
|
|
|
|
class _SettingsPremiumInAppState extends State<SettingsPremiumInApp> {
|
|
late Future<Offering?> _futureFetchOfferings;
|
|
bool _isLoading = false;
|
|
|
|
/// [_fetchOfferings] is used to fetch the Stripe checkout session
|
|
/// link. For that we have to call the `stripe-create-checkout-session-v1`
|
|
/// Supabase edge function. If the link is generated successfully, the
|
|
/// function returns the url, which can then be opened by the user.
|
|
Future<Offering?> _fetchOfferings() async {
|
|
if (Platform.isAndroid) {
|
|
await Purchases.configure(
|
|
PurchasesConfiguration(
|
|
SettingsRepository().revenueCatGooglePlayKey,
|
|
)..appUserID = supabase.Supabase.instance.client.auth.currentUser!.id,
|
|
);
|
|
} else if (Platform.isMacOS || Platform.isIOS) {
|
|
await Purchases.configure(
|
|
PurchasesConfiguration(
|
|
SettingsRepository().revenueCatAppStoreKey,
|
|
)..appUserID = supabase.Supabase.instance.client.auth.currentUser!.id,
|
|
);
|
|
}
|
|
|
|
Offerings offerings = await Purchases.getOfferings();
|
|
if (offerings.current != null) {
|
|
return offerings.current;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// [_purchase] is used to purchase the provided [package]. If the purchase
|
|
/// was successful, the user is notified. If the purchase failed, the user is
|
|
/// notified as well.
|
|
Future<void> _purchase(Package package) async {
|
|
try {
|
|
setState(() {
|
|
_isLoading = true;
|
|
});
|
|
|
|
CustomerInfo customerInfo = await Purchases.purchasePackage(package);
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
|
|
if (!customerInfo.entitlements.all.containsKey('FeedDeck Premium')) {
|
|
throw Exception('FeedDeck Premium entitlement not found.');
|
|
}
|
|
|
|
if (customerInfo.entitlements.all['FeedDeck Premium']!.isActive) {
|
|
if (!mounted) return;
|
|
Provider.of<ProfileRepository>(
|
|
context,
|
|
listen: false,
|
|
).setTier(FDProfileTier.premium);
|
|
}
|
|
|
|
if (!mounted) return;
|
|
Navigator.of(context).pop();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
duration: Duration(seconds: 10),
|
|
backgroundColor: Constants.primary,
|
|
showCloseIcon: true,
|
|
content: Text(
|
|
'FeedDeck Premium was successfully purchased.',
|
|
style: TextStyle(color: Constants.onPrimary),
|
|
),
|
|
),
|
|
);
|
|
} on PlatformException catch (err) {
|
|
final errorCode = PurchasesErrorHelper.getErrorCode(err);
|
|
if (errorCode == PurchasesErrorCode.purchaseCancelledError) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
Navigator.of(context).pop();
|
|
} else {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
Navigator.of(context).pop();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
duration: const Duration(seconds: 10),
|
|
backgroundColor: Constants.error,
|
|
showCloseIcon: true,
|
|
content: Text(
|
|
'In-app purchase failed: ${err.message}',
|
|
style: const TextStyle(color: Constants.onError),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} catch (err) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
Navigator.of(context).pop();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
duration: const Duration(seconds: 10),
|
|
backgroundColor: Constants.error,
|
|
showCloseIcon: true,
|
|
content: Text(
|
|
'In-app purchase failed: ${err.toString()}',
|
|
style: const TextStyle(color: Constants.onError),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
setState(() {
|
|
_futureFetchOfferings = _fetchOfferings();
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
automaticallyImplyLeading: false,
|
|
shape: const Border(
|
|
bottom: BorderSide(
|
|
color: Constants.dividerColor,
|
|
width: 1,
|
|
),
|
|
),
|
|
title: const Text('FeedDeck Premium'),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(
|
|
Icons.close,
|
|
),
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
body: FutureBuilder(
|
|
future: _futureFetchOfferings,
|
|
builder: (
|
|
BuildContext context,
|
|
AsyncSnapshot<Offering?> snapshot,
|
|
) {
|
|
return Column(
|
|
children: [
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
|
child: SingleChildScrollView(
|
|
child: snapshot.connectionState == ConnectionState.none ||
|
|
snapshot.connectionState ==
|
|
ConnectionState.waiting ||
|
|
snapshot.hasError ||
|
|
snapshot.data == null ||
|
|
snapshot.data?.monthly == null
|
|
? const Text('Loading ...')
|
|
: MarkdownBody(
|
|
selectable: true,
|
|
data: '''
|
|
You are currently using the free version of FeedDeck, which allows you to add up
|
|
to 10 sources for the first 7 days. After that trial period your sources will
|
|
not be updated anymore.
|
|
|
|
To use FeedDeck after the trial period with up to 1000 sources, you need to
|
|
upgrade to a premium account. The premium account costs
|
|
${snapshot.data?.monthly?.storeProduct.priceString} per month and can be
|
|
canceled at any time.
|
|
''',
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(
|
|
height: Constants.spacingSmall,
|
|
),
|
|
const Divider(
|
|
color: Constants.dividerColor,
|
|
height: 1,
|
|
thickness: 1,
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.all(Constants.spacingMiddle),
|
|
child: ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Constants.primary,
|
|
foregroundColor: Constants.onPrimary,
|
|
maximumSize: const Size.fromHeight(
|
|
Constants.elevatedButtonSize,
|
|
),
|
|
minimumSize: const Size.fromHeight(
|
|
Constants.elevatedButtonSize,
|
|
),
|
|
),
|
|
label: Text(
|
|
snapshot.data?.monthly?.storeProduct.priceString != null
|
|
? 'Subscribe to FeedDeck Premium for ${snapshot.data?.monthly?.storeProduct.priceString}'
|
|
: 'Subscribe to FeedDeck Premium',
|
|
),
|
|
onPressed: snapshot.connectionState == ConnectionState.none ||
|
|
snapshot.connectionState == ConnectionState.waiting ||
|
|
snapshot.hasError ||
|
|
snapshot.data == null ||
|
|
snapshot.data?.monthly == null ||
|
|
_isLoading
|
|
? null
|
|
: () => _purchase(snapshot.data!.monthly!),
|
|
icon: snapshot.connectionState == ConnectionState.none ||
|
|
snapshot.connectionState == ConnectionState.waiting ||
|
|
snapshot.hasError ||
|
|
snapshot.data == null ||
|
|
snapshot.data?.monthly == null ||
|
|
_isLoading
|
|
? const ElevatedButtonProgressIndicator()
|
|
: const Icon(FDIcons.feeddeck),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|