initial commit (migrated)
This commit is contained in:
commit
b594facb51
143 changed files with 11057 additions and 0 deletions
|
|
@ -0,0 +1,48 @@
|
|||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
|
||||
const _consumableProductIds = <String>{
|
||||
'donate_consumable_low',
|
||||
'donate_consumable_mid',
|
||||
'donate_consumable_high',
|
||||
};
|
||||
|
||||
abstract class InAppPurchaseRemoteDataSource {
|
||||
Stream<List<PurchaseDetails>> get purchaseStream;
|
||||
|
||||
Future<List<ProductDetails>> getConsumableProducts();
|
||||
|
||||
Future<bool> purchaseConsumableProduct(ProductDetails product);
|
||||
|
||||
Future<void> completePurchase(PurchaseDetails purchaseDetails);
|
||||
}
|
||||
|
||||
class InAppPurchaseRemoteDataSourceImpl
|
||||
implements InAppPurchaseRemoteDataSource {
|
||||
InAppPurchaseRemoteDataSourceImpl({required this.iap});
|
||||
|
||||
final InAppPurchase iap;
|
||||
|
||||
@override
|
||||
Stream<List<PurchaseDetails>> get purchaseStream => iap.purchaseStream;
|
||||
|
||||
@override
|
||||
Future<List<ProductDetails>> getConsumableProducts() async {
|
||||
final response = await iap.queryProductDetails(_consumableProductIds);
|
||||
final productDetails = response.productDetails;
|
||||
productDetails.sort((a, b) => a.price.compareTo(b.price));
|
||||
return productDetails;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> purchaseConsumableProduct(ProductDetails product) async {
|
||||
final response = await iap.buyConsumable(
|
||||
purchaseParam: PurchaseParam(productDetails: product),
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> completePurchase(PurchaseDetails purchaseDetails) {
|
||||
return iap.completePurchase(purchaseDetails);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:kuwot/core/error/failure.dart';
|
||||
import 'package:kuwot/features/in_app_purchase/data/data_sources/remote/in_app_purchase_remote_data_source.dart';
|
||||
import 'package:kuwot/features/in_app_purchase/domain/repositories/in_app_purchase_repository.dart';
|
||||
|
||||
class InAppPurchaseRepositoryImpl implements InAppPurchaseRepository {
|
||||
InAppPurchaseRepositoryImpl({required this.inAppPurchaseDataSource});
|
||||
|
||||
final InAppPurchaseRemoteDataSource inAppPurchaseDataSource;
|
||||
|
||||
@override
|
||||
Stream<List<PurchaseDetails>> get purchaseStream =>
|
||||
inAppPurchaseDataSource.purchaseStream;
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<ProductDetails>>> getConsumableProducts() async {
|
||||
try {
|
||||
final response = await inAppPurchaseDataSource.getConsumableProducts();
|
||||
return right(response);
|
||||
} catch (e) {
|
||||
return left(UnknownFailure(message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, bool>> purchaseConsumableProduct(
|
||||
ProductDetails product,
|
||||
) async {
|
||||
try {
|
||||
final response = await inAppPurchaseDataSource.purchaseConsumableProduct(
|
||||
product,
|
||||
);
|
||||
return right(response);
|
||||
} catch (e) {
|
||||
return left(UnknownFailure(message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> completePurchase(
|
||||
PurchaseDetails purchaseDetails,
|
||||
) async {
|
||||
try {
|
||||
await inAppPurchaseDataSource.completePurchase(purchaseDetails);
|
||||
return right(null);
|
||||
} on Exception catch (e) {
|
||||
return left(UnknownFailure(message: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:kuwot/core/error/failure.dart';
|
||||
|
||||
abstract class InAppPurchaseRepository {
|
||||
Stream<List<PurchaseDetails>> get purchaseStream;
|
||||
|
||||
Future<Either<Failure, List<ProductDetails>>> getConsumableProducts();
|
||||
|
||||
Future<Either<Failure, bool>> purchaseConsumableProduct(
|
||||
ProductDetails product,
|
||||
);
|
||||
|
||||
Future<Either<Failure, void>> completePurchase(
|
||||
PurchaseDetails purchaseDetails,
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:kuwot/core/domain/no_params.dart';
|
||||
import 'package:kuwot/core/domain/use_case.dart';
|
||||
import 'package:kuwot/core/error/failure.dart';
|
||||
import 'package:kuwot/features/in_app_purchase/domain/repositories/in_app_purchase_repository.dart';
|
||||
|
||||
class GetConsumableProducts extends UseCase<List<ProductDetails>, NoParams> {
|
||||
final InAppPurchaseRepository repository;
|
||||
|
||||
GetConsumableProducts(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<ProductDetails>>> call(NoParams params) async {
|
||||
return repository.getConsumableProducts();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:kuwot/core/domain/no_params.dart';
|
||||
import 'package:kuwot/core/domain/use_case.dart';
|
||||
import 'package:kuwot/core/error/failure.dart';
|
||||
import 'package:kuwot/features/in_app_purchase/domain/repositories/in_app_purchase_repository.dart';
|
||||
|
||||
class ListenPurchase
|
||||
implements UseCase<Stream<List<PurchaseDetails>>, NoParams> {
|
||||
ListenPurchase(this.repository);
|
||||
|
||||
final InAppPurchaseRepository repository;
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Stream<List<PurchaseDetails>>>> call(NoParams params) {
|
||||
// it always return right because I dont think it will throw an error
|
||||
return Future.value(right(repository.purchaseStream));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:kuwot/core/domain/use_case.dart';
|
||||
import 'package:kuwot/core/error/failure.dart';
|
||||
import 'package:kuwot/features/in_app_purchase/domain/repositories/in_app_purchase_repository.dart';
|
||||
|
||||
class PurchaseConsumableProduct
|
||||
extends UseCase<bool, PurchaseConsumableProductParams> {
|
||||
final InAppPurchaseRepository repository;
|
||||
|
||||
PurchaseConsumableProduct(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, bool>> call(
|
||||
PurchaseConsumableProductParams params,
|
||||
) async {
|
||||
return repository.purchaseConsumableProduct(params.productDetails);
|
||||
}
|
||||
}
|
||||
|
||||
class PurchaseConsumableProductParams extends Equatable {
|
||||
final ProductDetails productDetails;
|
||||
|
||||
const PurchaseConsumableProductParams({required this.productDetails});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [productDetails];
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:kuwot/core/domain/no_params.dart';
|
||||
import 'package:kuwot/core/presentation/bloc/error_state.dart';
|
||||
import 'package:kuwot/features/in_app_purchase/domain/use_case/get_consumable_products.dart';
|
||||
import 'package:kuwot/features/in_app_purchase/domain/use_case/purchase_consumable_product.dart';
|
||||
|
||||
part 'in_app_purchase_events.dart';
|
||||
part 'in_app_purchase_states.dart';
|
||||
|
||||
class InAppPurchaseBloc extends Bloc<InAppPurchaseEvent, InAppPurchaseState> {
|
||||
InAppPurchaseBloc({
|
||||
required this.getConsumableProducts,
|
||||
required this.purchaseConsumableProduct,
|
||||
}) : super(const InAppPurchaseInitialState()) {
|
||||
on<GetConsumableProductsEvent>(_getConsumableProducts);
|
||||
on<PurchaseConsumableProductEvent>(_purchaseConsumableProduct);
|
||||
}
|
||||
|
||||
final GetConsumableProducts getConsumableProducts;
|
||||
final PurchaseConsumableProduct purchaseConsumableProduct;
|
||||
|
||||
Future<void> _getConsumableProducts(
|
||||
GetConsumableProductsEvent event,
|
||||
Emitter<InAppPurchaseState> emit,
|
||||
) async {
|
||||
emit(const GettingConsumableProductsState());
|
||||
final result = await getConsumableProducts(const NoParams());
|
||||
result.fold(
|
||||
(failure) {
|
||||
emit(PurchaseErrorState(message: failure.message));
|
||||
},
|
||||
(products) {
|
||||
emit(ConsumableProductsLoadedState(products));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _purchaseConsumableProduct(
|
||||
PurchaseConsumableProductEvent event,
|
||||
Emitter<InAppPurchaseState> emit,
|
||||
) async {
|
||||
emit(PurchasingConsumableProductState(event.productDetails));
|
||||
final result = await purchaseConsumableProduct(
|
||||
PurchaseConsumableProductParams(productDetails: event.productDetails),
|
||||
);
|
||||
result.fold(
|
||||
(failure) {
|
||||
emit(PurchaseErrorState(message: failure.message));
|
||||
},
|
||||
(success) {
|
||||
emit(ConsumableProductPurchasedState(result: success));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
part of 'in_app_purchase_bloc.dart';
|
||||
|
||||
abstract class InAppPurchaseEvent extends Equatable {
|
||||
const InAppPurchaseEvent();
|
||||
}
|
||||
|
||||
class GetConsumableProductsEvent extends InAppPurchaseEvent {
|
||||
const GetConsumableProductsEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class PurchaseConsumableProductEvent extends InAppPurchaseEvent {
|
||||
final ProductDetails productDetails;
|
||||
|
||||
const PurchaseConsumableProductEvent(this.productDetails);
|
||||
|
||||
@override
|
||||
List<Object> get props => [productDetails];
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
part of 'in_app_purchase_bloc.dart';
|
||||
|
||||
abstract class InAppPurchaseState extends Equatable {
|
||||
const InAppPurchaseState();
|
||||
}
|
||||
|
||||
class InAppPurchaseInitialState extends InAppPurchaseState {
|
||||
const InAppPurchaseInitialState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class GettingConsumableProductsState extends InAppPurchaseState {
|
||||
const GettingConsumableProductsState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class ConsumableProductsLoadedState extends InAppPurchaseState {
|
||||
final List<ProductDetails> products;
|
||||
|
||||
const ConsumableProductsLoadedState(this.products);
|
||||
|
||||
@override
|
||||
List<Object> get props => [products];
|
||||
}
|
||||
|
||||
class PurchasingConsumableProductState extends InAppPurchaseState {
|
||||
final ProductDetails productDetails;
|
||||
|
||||
const PurchasingConsumableProductState(this.productDetails);
|
||||
|
||||
@override
|
||||
List<Object> get props => [productDetails];
|
||||
}
|
||||
|
||||
class ConsumableProductPurchasedState extends InAppPurchaseState {
|
||||
const ConsumableProductPurchasedState({required this.result});
|
||||
|
||||
final bool result;
|
||||
|
||||
@override
|
||||
List<Object> get props => [result];
|
||||
}
|
||||
|
||||
class PurchaseErrorState extends InAppPurchaseState implements ErrorState {
|
||||
@override
|
||||
final String message;
|
||||
|
||||
@override
|
||||
final Exception? cause;
|
||||
|
||||
const PurchaseErrorState({required this.message, this.cause});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message, cause];
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:kuwot/features/in_app_purchase/domain/repositories/in_app_purchase_repository.dart';
|
||||
|
||||
/// Cubit that listens to purchase details stream from [InAppPurchaseRepository]
|
||||
class PurchaseDetailsCubit extends Cubit<List<PurchaseDetails>> {
|
||||
/// Repository to get purchase details stream
|
||||
final InAppPurchaseRepository repository;
|
||||
|
||||
/// Subscription to purchase details stream
|
||||
StreamSubscription<List<PurchaseDetails>>? _purchaseDetailsSubscription;
|
||||
|
||||
PurchaseDetailsCubit(this.repository) : super([]) {
|
||||
// listen to purchase details stream and emit the event to the UI
|
||||
_purchaseDetailsSubscription = repository.purchaseStream.listen((event) {
|
||||
emit(event);
|
||||
|
||||
// complete the purchases
|
||||
event
|
||||
.where((element) => element.status == PurchaseStatus.purchased)
|
||||
.forEach((element) {
|
||||
unawaited(repository.completePurchase(element));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_purchaseDetailsSubscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
92
lib/features/in_app_purchase/presentation/donation_page.dart
Normal file
92
lib/features/in_app_purchase/presentation/donation_page.dart
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:kuwot/features/in_app_purchase/presentation/bloc/in_app_purchase_bloc.dart';
|
||||
|
||||
@RoutePage()
|
||||
class DonationPage extends StatefulWidget {
|
||||
const DonationPage({super.key});
|
||||
|
||||
@override
|
||||
State<DonationPage> createState() => _DonationPageState();
|
||||
}
|
||||
|
||||
class _DonationPageState extends State<DonationPage> {
|
||||
final _donationMessage =
|
||||
'I built this app with love and coffee. If you find it useful, please consider buying me a coffee. Your donation will help me keep the app running and updated. Thank you! ☕';
|
||||
|
||||
final List<ProductDetails> _products = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// get consumable products
|
||||
context.read<InAppPurchaseBloc>().add(const GetConsumableProductsEvent());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Coffee time?')),
|
||||
body: BlocListener<InAppPurchaseBloc, InAppPurchaseState>(
|
||||
listener: (context, state) {
|
||||
if (state is ConsumableProductsLoadedState) {
|
||||
setState(() {
|
||||
_products.clear();
|
||||
_products.addAll(state.products);
|
||||
});
|
||||
}
|
||||
},
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 0, 8, 16),
|
||||
child: Text(_donationMessage),
|
||||
),
|
||||
..._buildProductList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildProductList() {
|
||||
return _products.map((product) {
|
||||
final title = product.title.replaceAll('(Kuwot)', '');
|
||||
final description = product.description.replaceAll(
|
||||
RegExp(r'[\r\n]+'),
|
||||
'',
|
||||
);
|
||||
final productCard = Card(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () => context.read<InAppPurchaseBloc>().add(
|
||||
PurchaseConsumableProductEvent(product),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(title, style: Theme.of(context).textTheme.bodyLarge),
|
||||
Text(description),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
product.price,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return productCard;
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:kuwot/features/in_app_purchase/presentation/bloc/purchase_details_cubit.dart';
|
||||
import 'package:kuwot/utilities.dart';
|
||||
|
||||
class InAppPurchaseListener extends StatelessWidget {
|
||||
const InAppPurchaseListener({required this.child, super.key});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<PurchaseDetailsCubit, List<PurchaseDetails>>(
|
||||
listener: (context, state) {
|
||||
if (state.last.status == PurchaseStatus.purchased) {
|
||||
showSnackbar('Coffee received, thank you! ☕');
|
||||
}
|
||||
|
||||
if (state.last.status == PurchaseStatus.pending) {
|
||||
showSnackbar('Coffee is on the way! ☕');
|
||||
}
|
||||
|
||||
if (state.last.status == PurchaseStatus.error) {
|
||||
showSnackbar('Something went wrong, failed to send coffee 😢');
|
||||
}
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue