Files
feeddeck/app/lib/widgets/settings/premium/settings_premium_inapp.dart
Rico Berger f8262dbe7f [core] Enable In-App Purchases for Android (#38)
This commit enables in-app purchases for Android, so that a user can
also subscribe to FeedDeck Premium on Android.
2023-10-18 21:04:32 +02:00

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),
),
),
],
);
},
),
);
}
}