Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5172f8e
build: измена конфигурация ios проекта в связи с изменением аккаунта …
neekeetuh May 29, 2025
4584621
feat: добавлена тестовая версия логики покупки про версии через аппстор
neekeetuh May 29, 2025
12f85bb
fix: удаление возможности пользователя со статусом про попадать на эк…
neekeetuh May 29, 2025
0c4e4d4
refactor: вынесение констант
neekeetuh May 29, 2025
384b694
fix: вынесение цены про версии в локализацию + исправление ошибок лок…
neekeetuh May 30, 2025
ad3d1f5
refactor: удаление ненужного метода в инициализации провайдера про ве…
neekeetuh May 30, 2025
c324429
style: добавление параметра для использования non-nullable локализации
neekeetuh Jun 1, 2025
92475a8
style: вынесение переменной с апи ключом в класс, где она используется
neekeetuh Jun 1, 2025
e8d8362
refactor: вынесение ассетов в отдельные классы
neekeetuh Jun 1, 2025
fc0cb7b
fix: вынесение добавления listener'a на обновление пользовательской и…
neekeetuh Jun 1, 2025
4218d04
fix: улучшение читабельности методов + добавление обработки ошибки по…
neekeetuh Jun 1, 2025
ac28497
feat: добавление обработки ошибки покупки товара на ui
neekeetuh Jun 1, 2025
baad32b
fix: удаление лишнего consumer'а
neekeetuh Jun 1, 2025
5a94522
fix: изменение логики взаимодействия модулей
neekeetuh Jun 2, 2025
26e1f19
fix: изменение способа инъекции зависимостей для репозитория
neekeetuh Jun 2, 2025
6c882f7
fix: отказ от реактивного спобоса обновления статуса пользователя в п…
neekeetuh Jun 2, 2025
30ba166
Merge branch 'develop' of https://github.com/Student-Labs-2024/aurora…
neekeetuh Jun 2, 2025
30bd629
fix: удаление константных строк, которые должны использоваться через …
neekeetuh Jun 3, 2025
502be15
fix: исправление ошибки, при которой в локальную бд записывались лока…
neekeetuh Jun 5, 2025
9e52e4e
feat + refactor: добавление возможности восстановления покупок пользо…
neekeetuh Jun 8, 2025
c468439
refactor: вынесение данных, хранящихся в провайдере в классы состояни…
neekeetuh Jun 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
REVENUE_CAT_API_KEY=
2 changes: 2 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
*.env

# Miscellaneous
*.class
*.log
Expand Down
17 changes: 17 additions & 0 deletions frontend/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
PODS:
- Flutter (1.0.0)
- purchases_flutter (5.8.2):
- Flutter
- PurchasesHybridCommon (= 6.3.2)
- PurchasesHybridCommon (6.3.2):
- RevenueCat (= 4.26.1)
- RevenueCat (4.26.1)
- sqflite (0.0.3):
- Flutter
- FlutterMacOS

DEPENDENCIES:
- Flutter (from `Flutter`)
- purchases_flutter (from `.symlinks/plugins/purchases_flutter/ios`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)

SPEC REPOS:
trunk:
- PurchasesHybridCommon
- RevenueCat

EXTERNAL SOURCES:
Flutter:
:path: Flutter
purchases_flutter:
:path: ".symlinks/plugins/purchases_flutter/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/darwin"

SPEC CHECKSUMS:
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
purchases_flutter: 32697fa7b2aceb2af2dde25826c5f8b74021fcd1
PurchasesHybridCommon: 98af59169dd9c418eac0725c43f02db92a643b79
RevenueCat: 4e8899a69fd57180ef166237d1eb670023be05de
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec

PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796
Expand Down
18 changes: 9 additions & 9 deletions frontend/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -473,8 +473,8 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = JVX7U43WRX;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = HC3NQ49D3T;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ChessKnock;
Expand All @@ -484,7 +484,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = band.effective.chessknock;
PRODUCT_BUNDLE_IDENTIFIER = dev.effective.chessknock;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
Expand Down Expand Up @@ -664,8 +664,8 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = JVX7U43WRX;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = HC3NQ49D3T;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ChessKnock;
Expand All @@ -675,7 +675,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = band.effective.chessknock;
PRODUCT_BUNDLE_IDENTIFIER = dev.effective.chessknock;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
Expand All @@ -695,8 +695,8 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = JVX7U43WRX;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = HC3NQ49D3T;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ChessKnock;
Expand All @@ -706,7 +706,7 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = band.effective.chessknock;
PRODUCT_BUNDLE_IDENTIFIER = dev.effective.chessknock;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
Expand Down
4 changes: 2 additions & 2 deletions frontend/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
Expand All @@ -38,7 +40,5 @@
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
</dict>
</plist>
1 change: 1 addition & 0 deletions frontend/l10n.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
arb-dir: lib/l10n
template-arb-file: en.arb
output-localization-file: app_localizations.dart
nullable-getter: false
14 changes: 14 additions & 0 deletions frontend/lib/common/shared_functions.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import 'package:flutter/material.dart';
import 'package:frontend/constants/text_styles.dart';

sealed class SharedFunctions {
static bool isTablet(BuildContext context) {
final shortestSide = MediaQuery.of(context).size.shortestSide;
return shortestSide >= 640;
}

static void showSnackBar(BuildContext context, String text) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.black,
content: Text(
text,
style: TextStyles.caption2,
),
duration: const Duration(seconds: 2),
),
);
}
}
28 changes: 28 additions & 0 deletions frontend/lib/constants/assets.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
sealed class Assets {
static const handbook = "assets/images/icons/handbook.svg";
static const loading = "assets/images/icons/loading.svg";
static const board = "assets/images/board.svg";
static const advantage = 'assets/images/icons/advantage.svg';
static const backArrowIcon = "assets/images/icons/back_arrow_icon.svg";
static const pointLight = "assets/images/icons/point_light.svg";
static const darkSwitch = "assets/images/icons/dark_switch.svg";
static const lightSwitch = "assets/images/icons/light_switch.svg";
static const proSparkles = 'assets/images/icons/pro_sparkles.svg';
static const sun = 'assets/images/icons/sun.svg';
static const moon = 'assets/images/icons/moon.svg';
static const explosion = 'assets/images/icons/explosion.svg';
static const lamp = 'assets/images/icons/lamp.svg';
static const book = 'assets/images/icons/book.svg';
static const chessPiece = 'assets/images/icons/chess_piece.svg';
static const leftBigArrowIcon = "assets/images/icons/left_big_arrow_icon.svg";
static const rightBigArrowIcon =
"assets/images/icons/right_big_arrow_icon.svg";
static const questionIcon = "assets/images/icons/question_icon.svg";
}

sealed class AssetsPieces {
static const bishop = 'assets/images/pieces/bishop.svg';
static const rook = 'assets/images/pieces/rook.svg';
static const knight = 'assets/images/pieces/knight.svg';
static const queen = 'assets/images/pieces/queen.svg';
}
1 change: 0 additions & 1 deletion frontend/lib/constants/constants.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export "colors.dart";
export "text_styles.dart";
export "string_constants.dart";
export 'gradient_consts.dart';
7 changes: 0 additions & 7 deletions frontend/lib/constants/string_constants.dart

This file was deleted.

5 changes: 5 additions & 0 deletions frontend/lib/data_sources/in_app_purchase_data_source.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
abstract interface class IInAppPurchaseDataSource {
Future<bool> buyProduct(String productId);
Future<bool> checkStatus(String productId);
Future<void> restorePurchases();
}
23 changes: 23 additions & 0 deletions frontend/lib/data_sources/revenuecat_data_source.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:frontend/data_sources/in_app_purchase_data_source.dart';
import 'package:purchases_flutter/purchases_flutter.dart';

class RevenueCatDataSource implements IInAppPurchaseDataSource {
@override
Future<bool> buyProduct(String productId) async {
final product = (await Purchases.getProducts([productId])).first;
final customerInfo = await Purchases.purchaseStoreProduct(product);
final isPro = customerInfo.entitlements.active[productId] != null;
return isPro;
}

@override
Future<bool> checkStatus(String productId) async {
final customerInfo = await Purchases.getCustomerInfo();
return customerInfo.entitlements.active.containsKey(productId);
}

@override
Future<void> restorePurchases() async {
Purchases.restorePurchases();
Comment on lines +20 to +21

Choose a reason for hiding this comment

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

Эта операция у тебя всегда будет успешной? Что если я выключу интернет прямо во время запроса?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Это обрабатывается уже на уровне провайдера. Если будет какое-то исключение, то установится состояние ошибки, и в связи с этим появится снекбар с сообщением об ошибке

}
}
9 changes: 7 additions & 2 deletions frontend/lib/l10n/en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,10 @@
"moveBackModal": "Ability to return\nthe game board one move back",
"threatsModal": "Display of danger\nof your piece being captured",
"hintsModal": "Display of hints\nfor possible moves",
"description": "Description"
}
"description": "Description",
"price": "1.99 $",
"purchaseError": "Could not buy the product. Try again later",
"restorePurchases": "Restore Purchases",
"restorePurchasesError": "Could not restore purchases. Try again later",
"restorePurchasesCompleted" : "Restore purchases completed. Check your current status"
}
9 changes: 7 additions & 2 deletions frontend/lib/l10n/ru.arb
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,10 @@
"moveBackModal": "Возможность вернуть\nигровое поле на ход назад",
"threatsModal": "Отображение опасности\nвзятия вашей фигуры",
"hintsModal": "Отображение подсказок\nвозможных ходов",
"description": "Описание"
}
"description": "Описание",
"price": "199 ₽",
"purchaseError": "Ошибка покупки товара. Попробуйте позже",
"restorePurchases": "Восстановить покупки",
"restorePurchasesError": "Ошибка восстановления покупок. Попробуйте позже",
"restorePurchasesCompleted" : "Восстановление покупок прошло успешно. Проверьте ваш текущий статус"
}
26 changes: 24 additions & 2 deletions frontend/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
import "dart:io";

import "package:flutter_dotenv/flutter_dotenv.dart";
import "package:frontend/data_sources/revenuecat_data_source.dart";
import "package:frontend/exports.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:frontend/repositories/in_app_purchase_repository.dart";
import "package:provider/provider.dart";
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import "package:purchases_flutter/purchases_flutter.dart";

void main() {
void main() async {
await dotenv.load(fileName: ".env");
WidgetsFlutterBinding.ensureInitialized();
if (Platform.isIOS) {
await configureRevenueCat();
}
runApp(const MyApp());
}

Future<void> configureRevenueCat() async {
Purchases.setLogLevel(LogLevel.debug);
final apiKey = dotenv.env['REVENUE_CAT_API_KEY'];
Purchases.configure(PurchasesConfiguration(apiKey!));
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

Expand All @@ -23,7 +40,12 @@ class MyApp extends StatelessWidget {
create: (context) => ThemeProvider(),
),
ChangeNotifierProvider(
create: (context) => ProVersionProvider(),
create: (context) => ProVersionProvider(
repository: InAppPurchaseRepository(
//TODO: choose data source based off of a platform
dataSource: RevenueCatDataSource(),
),
),
)
],
child: Builder(builder: (context) {
Expand Down
1 change: 1 addition & 0 deletions frontend/lib/providers/pro_version_errors.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
enum ProVersionError { purchaseError, restoreError }
65 changes: 57 additions & 8 deletions frontend/lib/providers/pro_version_provider.dart
Original file line number Diff line number Diff line change
@@ -1,20 +1,69 @@
import 'package:flutter/material.dart';
import 'package:frontend/providers/pro_version_errors.dart';
import 'package:frontend/providers/pro_version_states.dart';
import 'package:frontend/repositories/in_app_purchase_repository.dart';

class ProVersionProvider extends ChangeNotifier {
bool _isProStatus = false;
//The key of a product should be the same as an entitlement name attached to it
static const _proVersionKey = 'chessknock_pro_version';

void _setProStatus(bool status) {
_isProStatus = status;
notifyListeners();
late ProVersionState _state;

final InAppPurchaseRepository _repository;

ProVersionProvider({required InAppPurchaseRepository repository})
: _repository = repository {
_state = const InitialProVersionState();

Choose a reason for hiding this comment

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

У тебя и здесь происходит задание переменной, и в init. Потенциально нельзя здесь не трогать тогда _state? Или ты так ты пытаешься обезопасить от вызова, когда init еще не отработал?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Да, потому что init асинхронный и стейт может из-за этого поздно инициализироваться, когда уже будет отрисован ui (тестил на айпаде)

_init();
}

void upgradeToPro() {
_setProStatus(true);
void _init() async {
final isPro = await _repository.checkStatus(_proVersionKey);
_setState(InitialProVersionState(isProStatus: isPro));
}

Future<void> upgradeToPro() async {
try {
await _repository.buyProduct(_proVersionKey);
final isPro = await _repository.checkStatus(_proVersionKey);
if (isPro) _setState(const SuccessfulPurchaseState());
} catch (_) {
_setState(ErrorProVersionState(
error: ProVersionError.purchaseError,
isProStatus: state.isProStatus,
));
}
}

void downgradeFromPro() {
_setProStatus(false);
_setState(const InitialProVersionState());

Choose a reason for hiding this comment

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

В данном случае, когда есть у нас восстановление покупок это будто бы тоже самое, что downgrade. Только если не подразумевается другая логика в будущем для этого метода. Точно ли он у тебя до сих пор где-то используется или только для дебага?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Да, в текущей версии под ios в этом методе смысла нет, это скорее на будущее оставил, если понадобится функционал отмены покупки/возврата средств по нажатию на кнопку в приложении (если сбп будет подключаться, например)

}

Future<void> restorePurchases() async {
try {
await _repository.restorePurchases();
final isPro = await _repository.checkStatus(_proVersionKey);
_setState(SuccessfulRestorePurchasesState(isProStatus: isPro));
} catch (_) {
_setState(ErrorProVersionState(
error: ProVersionError.restoreError,
isProStatus: state.isProStatus,
));
}
}

void _setState(ProVersionState state) {

Choose a reason for hiding this comment

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

Честно, хотелось бы любое другое название, но не _setState. Возникают сразу же ассоциации с виджетовским методом

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Понимаю, но блин, это буквально сеттер стейта), да и область видимости у него только внутри провайдера, так что наверное сложно будет запутаться. Хотя ладно, придумаю новое название

_state = state;
notifyListeners();
}

bool get isPro => _isProStatus;
ProVersionState get state => _state;

bool get isPro => state.isProStatus;
ProVersionError? get error {
if (state is ErrorProVersionState) {
return (state as ErrorProVersionState).error;
}
return null;
}
}
Loading
Loading