Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation of purchase collection. #308

Merged
merged 12 commits into from
Jul 29, 2024
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ a beta or production release, they must be documented here).

## [Unreleased]

### Added
- Creation of the structure for implementing paid decks.
- Now the deck can be paid for, just add the `productInfo` and also if it is `isPremium`.

### Added
- Visually Opinionated Buttons (Primary, Secondary and Text).

Expand Down
46 changes: 0 additions & 46 deletions assets/collections/0_chatGPT_premium.json
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Esse é um exemplo, melhor não subir ele junto, manter para testes, ou fazer um deck real pra já inserirmos

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acho q renomeia esse cara pra mock, pra depois nós tirarmos ele

This file was deleted.

4 changes: 0 additions & 4 deletions assets/collections/bdd_fundamentos_01.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@
"tests",
"bdd"
],
"productInfo": {
"price": null,
"productId": null
},
"contributors": [
{
"name": "Nicolas Nascimento",
Expand Down
4 changes: 0 additions & 4 deletions assets/collections/comecando_com_git.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@
"git",
"versionamento"
],
"productInfo": {
"price": null,
"productId": null
},
"contributors": [
{
"name": "@matuella",
Expand Down
4 changes: 0 additions & 4 deletions assets/collections/ecossistema_do_flutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@
"framework",
"cross-platform"
],
"productInfo": {
"price": null,
"productId": null
},
"contributors": [
{
"name": "@matuella",
Expand Down
4 changes: 0 additions & 4 deletions assets/collections/fundamentos_scrum.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@
"agile",
"scrum"
],
"productInfo": {
"price": null,
"productId": null
},
"contributors": [
{
"name": "Olympus",
Expand Down
4 changes: 0 additions & 4 deletions assets/collections/guia_scrum.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@
"agile",
"scrum"
],
"productInfo": {
"price": null,
"productId": null
},
"contributors": [
{
"name": "Daniel Wildt",
Expand Down
4 changes: 0 additions & 4 deletions assets/collections/kotlin_fundamentos_01.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@
"kotlin",
"linguagem de programação"
],
"productInfo": {
"price": null,
"productId": null
},
"contributors": [
{
"name": "Lucas Montano",
Expand Down
4 changes: 0 additions & 4 deletions assets/collections/manifesto_agil.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@
"tags": [
"agile"
],
"productInfo": {
"price": null,
"productId": null
},
"contributors": [
{
"name": "Daniel Wildt",
Expand Down
4 changes: 0 additions & 4 deletions assets/collections/swift_fundamentos_01.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@
"swift",
"linguagem de programação"
],
"productInfo": {
"price": null,
"productId": null
},
"contributors": [
{
"name": "@matuella",
Expand Down
4 changes: 1 addition & 3 deletions lib/application/constants/strings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ const collectionsStartNow = 'Começar Agora';
const collectionPurchase = 'Deseja comprar este Deck?';
const collectionSuccessPurchase = 'Deck comprado com sucesso!';
String collectionPurchaseDeck(double price) => 'Comprar deck - R\$ ${price.toStringAsFixed(2)}';
// TODO(joao): check the best place to store the const double.
const double collectionPrice = 0.99;

String collectionsEmptyTitleSegment(CollectionsSegment segment) {
switch (segment) {
Expand Down Expand Up @@ -100,7 +98,7 @@ const jumpTo = 'Pular para';
// Tags Component
//
const tags = 'Tags';
const premium = 'PREMIUM';
const premium = 'Premium';
const tagsHint = 'Adicione as tags...';
const suggestions = 'Sugestões';
const addTags = 'Adicionar Tags';
Expand Down
32 changes: 18 additions & 14 deletions lib/application/pages/details/collection_details_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,25 +97,29 @@ class CollectionDetailsPage extends ConsumerWidget {

sections.add(resourcesSection);

final studyNowButton = PrimaryElevatedButton(
onPressed: () => readCoordinator(ref).navigateToCollectionExecution(id, isNestedNavigation: false),
text: strings.detailsStudyNow.toUpperCase(),
);

final purchaseDeckButton = SecondaryElevatedButton(
backgroundColor: memoTheme.secondarySwatch,
text: strings.collectionPurchaseDeck(state.metadata.price),
onPressed: () async => _collectionPurchaseBottomSheet(
context,
() => ref.watch(collectionDetailsVM(id).notifier).purchaseCollection(state.metadata.id),
),
);
Widget actionButton() {
if (state.isPurchased) {
return PrimaryElevatedButton(
onPressed: () => readCoordinator(ref).navigateToCollectionExecution(id, isNestedNavigation: false),
text: strings.detailsStudyNow.toUpperCase(),
);
} else {
return SecondaryElevatedButton(
backgroundColor: memoTheme.secondarySwatch,
text: strings.collectionPurchaseDeck(state.metadata.price!),
onPressed: () async => _collectionPurchaseBottomSheet(
context,
() => ref.watch(collectionDetailsVM(id).notifier).purchaseCollection(state.metadata.id),
),
);
}
}

final fixedBottomAction = ThemedBottomContainer(
child: ColoredBox(
color: memoTheme.neutralSwatch.shade800,
child: SafeArea(
child: (state.isPurchased ? studyNowButton : purchaseDeckButton),
child: actionButton(),
).withSymmetricalPadding(
context,
vertical: Spacing.small,
Expand Down
5 changes: 2 additions & 3 deletions lib/application/view-models/app_vm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,8 @@ class AppVMImpl extends AppVM {
splashMinDuration,
]);

await Future.wait<dynamic>([
collectionPurchaseServices.updatePurchasesIfNeeded(),
]);
// This dependency needs to be loaded after the "dependent dependencies" as it depends on checking purchases that cannot be loaded in parallel.
await collectionPurchaseServices.updatePurchasesIfNeeded();

value = AsyncValue.data(appState);
}
Expand Down
12 changes: 6 additions & 6 deletions lib/application/view-models/item_metadata.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ abstract class CollectionItem extends ItemMetadata {
required this.category,
required this.tags,
required this.isPremium,
required this.price,
this.price,
});

final String id;
Expand All @@ -30,7 +30,7 @@ abstract class CollectionItem extends ItemMetadata {
final List<String> tags;

final bool isPremium;
final double price;
final double? price;

@override
List<Object?> get props => [id, name, category, tags, isPremium];
Expand All @@ -45,7 +45,7 @@ class CompletedCollectionItem extends CollectionItem {
required String category,
required List<String> tags,
required bool isPremium,
required double price,
double? price,
}) : super(
id: id,
name: name,
Expand All @@ -72,7 +72,7 @@ class IncompleteCollectionItem extends CollectionItem {
required String category,
required List<String> tags,
required bool isPremium,
required double price,
double? price,
}) : super(
id: id,
name: name,
Expand Down Expand Up @@ -104,7 +104,7 @@ CollectionItem mapStatusToMetadata(CollectionStatus status) {
category: collection.category,
tags: collection.tags,
isPremium: collection.isPremium,
price: collection.productInfo.price,
price: collection.productInfo?.price,
);
} else {
return IncompleteCollectionItem(
Expand All @@ -115,7 +115,7 @@ CollectionItem mapStatusToMetadata(CollectionStatus status) {
category: collection.category,
tags: collection.tags,
isPremium: collection.isPremium,
price: collection.productInfo.price,
price: collection.productInfo?.price,
);
}
}
2 changes: 1 addition & 1 deletion lib/application/widgets/theme/collection_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class CollectionCard extends ConsumerWidget {
/// List of tags associated with this collection.
final List<String> tags;

///
/// `true` If the collection is available for purchase.
final bool isPremium;

/// If this widget should draw a border for this card.
Expand Down
2 changes: 1 addition & 1 deletion lib/data/repositories/collection_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class CollectionRepositoryImpl implements CollectionRepository {
CollectionKeys.uniqueMemosAmount: collection.uniqueMemosAmount,
CollectionKeys.uniqueMemoExecutionsAmount: collection.uniqueMemoExecutionsAmount,
CollectionKeys.isPremium: collection.isPremium,
CollectionKeys.productInfo: _productInfoSerializer.to(collection.productInfo),
if (collection.isPremium) CollectionKeys.productInfo: _productInfoSerializer.to(collection.productInfo!),
},
)
.toList(),
Expand Down
24 changes: 16 additions & 8 deletions lib/data/repositories/purchase_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,31 @@ abstract class PurchaseRepository {
/// Purchase products in the app with the store ID [storeId] for the local user.
Future<void> purchaseInApp({required String storeId});

/// Receives purchase information made by the user.
Future<List<String>> getPurchasesInfo();
/// Fetches a list of RevenueCat purchase information strings by user id.
///
/// This function retrieves purchase information asynchronously
/// and returns a list of strings, where each string represents a purchase.
Future<List<String>> getUserPurchases();

/// Check which products are available for purchase.
Future<List<String>> isAvailable();

/// Updates the collection with the [id] to be premium or not.
/// Updates the collection with the [purchaseId] to be premium or not.
Future<void> updatePurchase({required String purchaseId});

Future<List<String>> getPurchaseProducts();
/// Fetches a list of purchased product IDs from the local database.
///
/// This function asynchronously retrieves all the purchases stored in the local database
/// and extracts the product IDs from each purchase record.
Future<List<String>> getPurchasedProductsIds();
}

class PurchaseRepositoryImpl implements PurchaseRepository {
PurchaseRepositoryImpl(this._db, this._purchaseGateway);

final SembastDatabase _db;
final _purchasesStore = 'purchases';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
final _purchasesStore = 'purchases';
final _purchasesStore = 'purchases';
final _purchaseIdKey = 'purchaseId';

final _purchaseIdKey = 'purchasesId';

final PurchaseGateway _purchaseGateway;

Expand All @@ -31,7 +39,7 @@ class PurchaseRepositoryImpl implements PurchaseRepository {
);

@override
Future<List<String>> getPurchasesInfo() async {
Future<List<String>> getUserPurchases() async {
final info = await _purchaseGateway.purchasesInfo();
return info.map((purchase) => purchase).toList();
}
Expand All @@ -46,14 +54,14 @@ class PurchaseRepositoryImpl implements PurchaseRepository {
Future<void> updatePurchase({required String purchaseId}) => _db.put(
id: purchaseId,
object: <String, dynamic>{
'purchasesId': purchaseId,
_purchaseIdKey: purchaseId,
},
store: _purchasesStore,
);

@override
Future<List<String>> getPurchaseProducts() async {
Future<List<String>> getPurchasedProductsIds() async {
final purchases = await _db.getAll(store: _purchasesStore);
return purchases.map((purchase) => purchase['purchasesId'] as String).toList();
return purchases.map((purchase) => purchase[_purchaseIdKey] as String).toList();
}
}
5 changes: 3 additions & 2 deletions lib/data/serializers/collection_memos_serializer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ class CollectionMemosSerializer implements Serializer<CollectionMemos, Map<Strin
final category = json[CollectionMemosKeys.category] as String;
final isPremium = json[CollectionMemosKeys.isPremium] as bool?;

final productInfo = productInfoSerializer.from(json[CollectionMemosKeys.productInfo] as Map<String, dynamic>);
final rawProductInfo = json[CollectionMemosKeys.productInfo] as Map<String, dynamic>?;
final productInfo = rawProductInfo != null ? productInfoSerializer.from(rawProductInfo) : null;

final tags = List<String>.from(json[CollectionMemosKeys.tags] as List);

Expand Down Expand Up @@ -62,6 +63,6 @@ class CollectionMemosSerializer implements Serializer<CollectionMemos, Map<Strin
CollectionMemosKeys.memosMetadata: collection.memosMetadata.map(memoMetadataSerializer.to).toList(),
CollectionMemosKeys.contributors: collection.contributors.map(contributorSerializer.to).toList(),
CollectionMemosKeys.isPremium: collection.isPremium,
CollectionMemosKeys.productInfo: productInfoSerializer.to(collection.productInfo),
if (collection.isPremium) CollectionMemosKeys.productInfo: productInfoSerializer.to(collection.productInfo!),
};
}
Loading