knt_base_monetized 4.2.2
knt_base_monetized: ^4.2.2 copied to clipboard
Helper blocs for handling common in-app-purchase flow
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:knt_base_monetized/knt_base_monetized.dart';
import 'package:knt_bloc/knt_bloc.dart';
import 'bloc/subscription_bloc.dart';
import 'data/monetize_repo.dart';
import 'data/purchase_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Knt Monetized Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: BlocProvider(
create: (BuildContext context) => SubscriptionBloc(
ArticleMonetizedRepo(),
entitlementID: Purchases.defaultEntitlementID,
),
child: const MyHomePage(title: 'Knt Monetized Demo'),
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final _iapTag = (MyHomePage).toString();
final _logs = <String>[];
@override
void initState() {
super.initState();
Future.delayed(Duration.zero, _onFetchSku);
}
@override
Widget build(BuildContext context) {
return BlocListener<SubscriptionBloc, BaseState>(
listener: (context, state) {
if (!mounted) {
return;
}
void showSnackBar(String message) {
ScaffoldMessenger.of(context).removeCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration: const Duration(milliseconds: 2811),
content: Text(message),
),
);
}
final stateTag = state.tag;
final timeStamps = DateTime.now();
final newMessageLog = '[${DateFormat.Hms().format(timeStamps)}]: '
'${state.runtimeType} with tag [$stateTag]';
showSnackBar(newMessageLog);
setState(() {
_logs.insert(0, newMessageLog);
});
},
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
mainAxisSize: MainAxisSize.min,
children: [
BlocBuilder<SubscriptionBloc, BaseState>(
buildWhen: (previous, current) {
return current is FetchingOfferingsState ||
current is FetchedOfferingsState ||
current is FetchedOfferingsFailureState;
},
builder: (context, state) => switch (state) {
FetchedOfferingsFailureState() => Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Error: ${state.exception.additionalInfo}',
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 8),
TextButton(
onPressed: _onFetchSku,
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.refresh,
size: 24,
),
SizedBox(width: 8),
Text('Retry'),
],
),
),
],
),
FetchingOfferingsState() => const SizedBox(
height: 240,
child: Center(
child: CircularProgressIndicator(),
),
),
FetchedOfferingsState(:final data) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: _SubscriptionBox(
itemList: data
..sort(
(s1, s2) => s2.price.compareTo(s1.price),
),
onItemPicked: _onItemPicked,
),
),
_ => const SizedBox(),
},
),
const SizedBox(height: 16),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
for (final (index, log) in _logs.indexed)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 20,
height: 20,
margin: const EdgeInsets.symmetric(vertical: 2),
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.green,
),
child: Center(
child: FittedBox(
child: Text(
'${_logs.length - index}',
style: Theme.of(context)
.textTheme
.labelSmall
?.copyWith(color: Colors.white),
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(log),
),
],
),
],
),
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _onFetchSku,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: Text(
'Retry',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Colors.white),
),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: _onRestore,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
),
child: Text(
'Restore',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Colors.white),
),
),
),
],
),
),
const SizedBox(height: 32),
],
),
),
);
}
void _onFetchSku() {
_makeLogGap();
context.read<SubscriptionBloc>().add(FetchOfferingsEvent(tag: _iapTag));
}
void _onItemPicked(SubscriptionItem item) {
_makeLogGap();
context.read<SubscriptionBloc>().add(
RequestSubscriptionEvent(
item,
tag: _iapTag,
),
);
}
void _onRestore() {
_makeLogGap();
context
.read<SubscriptionBloc>()
.add(RestoreSubscriptionEvent(tag: _iapTag));
}
void _makeLogGap() {
setState(() {
_logs.insert(0, '---------------------');
});
}
}
class _SubscriptionBox extends StatelessWidget {
const _SubscriptionBox({
this.itemList = const [],
this.onItemPicked,
});
final List<SubscriptionItem> itemList;
final ValueSetter<SubscriptionItem>? onItemPicked;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final item in itemList)
InkWell(
onTap: () => onItemPicked?.call(item),
child: Container(
margin: const EdgeInsets.only(top: 12),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: (item.period == Period.annually
? Colors.red
: Colors.grey)
.withOpacity(0.3),
),
),
child: Row(
children: [
const Icon(
Icons.shopping_cart,
size: 24,
color: Colors.orange,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.content,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 4),
Text(
'${item.sku} - ${item.localizedPrice}',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Colors.green),
),
],
),
),
],
),
),
),
const SizedBox(height: 16),
],
);
}
}