Compare commits

..

8 Commits

Author SHA1 Message Date
Jonas Bark
661d72fa8c windows adjustments 2025-12-21 10:29:53 +01:00
jonas.bark@gmx.de
38df962e43 windows trial changes 2025-12-21 10:25:52 +01:00
Jonas Bark
e02563733f fix missing entitlements on release builds for macOS 2025-12-20 20:40:53 +01:00
Jonas Bark
c246d2d1fe fix missing entitlements on release builds for macOS 2025-12-20 20:40:12 +01:00
Jonas Bark
8dbed9a8b5 wrong translation 2025-12-20 18:08:13 +01:00
Jonas Bark
6ea4fa82a7 version++ 2025-12-20 11:39:02 +01:00
Jonas Bark
0d78ca6352 version++ 2025-12-20 11:20:39 +01:00
Jonas Bark
b82ad80d1c fix windows build version 2025-12-20 10:56:19 +01:00
10 changed files with 160 additions and 90 deletions

View File

@@ -302,7 +302,7 @@ abstract class BluetoothDevice extends BaseDevice {
),
if (firmwareVersion != null)
DeviceInfo(
title: context.i18n.signal,
title: context.i18n.firmware,
icon: this is ZwiftDevice && firmwareVersion != (this as ZwiftDevice).latestFirmwareVersion
? Icons.warning
: Icons.text_fields_sharp,

View File

@@ -52,7 +52,7 @@ class OpenBikeControlMdnsEmulator extends TrainerConnection {
throw 'Could not find network interface';
}
_createTcpServer();
await _createTcpServer();
if (kDebugMode) {
enableLogging(LogTopic.calls);

View File

@@ -63,7 +63,7 @@ class FtmsMdnsEmulator extends TrainerConnection {
throw 'Could not find network interface';
}
_createTcpServer();
await _createTcpServer();
if (kDebugMode) {
enableLogging(LogTopic.calls);

View File

@@ -1,6 +1,8 @@
import 'dart:async';
import 'package:bike_control/bluetooth/messages/notification.dart';
import 'package:bike_control/main.dart';
import 'package:bike_control/utils/core.dart';
import 'package:bike_control/utils/iap/iap_manager.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
@@ -14,7 +16,7 @@ class WindowsIAPService {
static const int trialDays = 7;
static const int dailyCommandLimit = 15;
static const String _purchaseStatusKey = 'iap_purchase_status';
static const String _purchaseStatusKey = 'iap_purchase_status_2';
static const String _dailyCommandCountKey = 'iap_daily_command_count';
static const String _lastCommandDateKey = 'iap_last_command_date';
@@ -51,13 +53,15 @@ class WindowsIAPService {
Future<void> _checkExistingPurchase() async {
// First check if we have a stored purchase status
final storedStatus = await _prefs.read(key: _purchaseStatusKey);
core.connection.signalNotification(LogNotification('Is purchased status: $storedStatus'));
if (storedStatus == "true") {
IAPManager.instance.isPurchased.value = true;
return;
}
final trial = await _windowsIapPlugin.getTrialStatusAndRemainingDays();
core.connection.signalNotification(LogNotification('Trial status: $trial'));
trialDaysRemaining = trial.remainingDays;
if (!trial.isTrial && trial.remainingDays <= 0) {
if (trial.isActive && !trial.isTrial && trial.remainingDays <= 0) {
IAPManager.instance.isPurchased.value = true;
await _prefs.write(key: _purchaseStatusKey, value: "true");
} else {
@@ -71,6 +75,7 @@ class WindowsIAPService {
try {
final status = await _windowsIapPlugin.makePurchase(productId);
if (status == StorePurchaseStatus.succeeded || status == StorePurchaseStatus.alreadyPurchased) {
IAPManager.instance.isPurchased.value = true;
/*buildToast(
navigatorKey.currentContext!,
title: 'Purchase Successful',

View File

@@ -10,6 +10,8 @@
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>keychain-access-groups</key>

View File

@@ -1,7 +1,7 @@
name: bike_control
description: "BikeControl - Control your virtual riding"
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 4.2.0+58
version: 4.2.2+62
environment:
sdk: ^3.9.0

View File

@@ -123,7 +123,7 @@ Future<void> main() async {
required Widget child,
}) => CustomFrame(
platform: size.type,
title: 'BikeControl connects to any controller',
title: 'Control your favorite trainer app using ANY controller',
device: device,
child: child,
),

View File

@@ -1,6 +1,18 @@
class Trial {
final bool isTrial;
final int remainingDays;
final bool isActive;
final bool isTrialOwnedByThisUser;
Trial({required this.isTrial, required this.remainingDays});
Trial({
required this.isTrial,
required this.remainingDays,
required this.isActive,
required this.isTrialOwnedByThisUser,
});
@override
String toString() {
return 'Trial{isTrial: $isTrial, remainingDays: $remainingDays, isActive: $isActive, isTrialOwnedByThisUser: $isTrialOwnedByThisUser}';
}
}

View File

@@ -22,7 +22,8 @@ const _escapeMap = {
};
/// A [RegExp] that matches whitespace characters that should be escaped.
var _escapeRegExp = RegExp('[\\x00-\\x07\\x0E-\\x1F${_escapeMap.keys.map(_getHexLiteral).join()}]');
var _escapeRegExp = RegExp(
'[\\x00-\\x07\\x0E-\\x1F${_escapeMap.keys.map(_getHexLiteral).join()}]');
/// Returns [str] with all whitespace characters represented as their escape
/// sequences.
@@ -51,7 +52,8 @@ class MethodChannelWindowsIap extends WindowsIapPlatform {
@override
Future<StorePurchaseStatus?> makePurchase(String storeId) async {
final result = await methodChannel.invokeMethod<int>('makePurchase', {'storeId': storeId});
final result = await methodChannel
.invokeMethod<int>('makePurchase', {'storeId': storeId});
if (result == null) {
return null;
}
@@ -72,9 +74,12 @@ class MethodChannelWindowsIap extends WindowsIapPlatform {
@override
Stream<List<Product>> productsStream() {
return const EventChannel('windows_iap_event_products').receiveBroadcastStream().map((event) {
return const EventChannel('windows_iap_event_products')
.receiveBroadcastStream()
.map((event) {
if (event is String) {
return parseListNotNull(json: jsonDecode(escape(event)), fromJson: Product.fromJson);
return parseListNotNull(
json: jsonDecode(escape(event)), fromJson: Product.fromJson);
} else {
return [];
}
@@ -87,20 +92,26 @@ class MethodChannelWindowsIap extends WindowsIapPlatform {
if (result == null) {
return [];
}
return parseListNotNull(json: jsonDecode(escape(result)), fromJson: Product.fromJson);
return parseListNotNull(
json: jsonDecode(escape(result)), fromJson: Product.fromJson);
}
@override
Future<bool> checkPurchase({required String storeId}) async {
final result = await methodChannel.invokeMethod<bool>('checkPurchase', {'storeId': storeId});
final result = await methodChannel
.invokeMethod<bool>('checkPurchase', {'storeId': storeId});
return result ?? false;
}
@override
Future<Trial> getTrialStatusAndRemainingDays() async {
final result = await methodChannel.invokeMethod<Map>('getTrialStatusAndRemainingDays');
final result =
await methodChannel.invokeMethod<Map>('getTrialStatusAndRemainingDays');
return Trial(
isTrial: result?['isTrial'] as bool? ?? false,
isActive: result?['isActive'] as bool? ?? false,
isTrialOwnedByThisUser:
result?['isTrialOwnedByThisUser'] as bool? ?? false,
remainingDays: result?['remainingDays'] as int? ?? 0,
);
}
@@ -111,6 +122,7 @@ class MethodChannelWindowsIap extends WindowsIapPlatform {
if (result == null) {
return {};
}
return result.map((key, value) => MapEntry(key.toString(), StoreLicense.fromJson(jsonDecode(value))));
return result.map((key, value) =>
MapEntry(key.toString(), StoreLicense.fromJson(jsonDecode(value))));
}
}

View File

@@ -27,54 +27,63 @@ using namespace Windows::Services::Store;
using namespace Windows::Foundation::Collections;
namespace foundation = Windows::Foundation;
namespace windows_iap {
namespace windows_iap
{
//////////////////////////////////////////////////////////////////////// BEGIN OF MY CODE //////////////////////////////////////////////////////////////
flutter::PluginRegistrarWindows* _registrar;
flutter::PluginRegistrarWindows *_registrar;
HWND GetRootWindow(flutter::FlutterView* view) {
HWND GetRootWindow(flutter::FlutterView *view)
{
return ::GetAncestor(view->GetNativeWindow(), GA_ROOT);
}
StoreContext getStore() {
StoreContext getStore()
{
StoreContext store = StoreContext::GetDefault();
auto initWindow = store.try_as<IInitializeWithWindow>();
if (initWindow != nullptr) {
if (initWindow != nullptr)
{
initWindow->Initialize(GetRootWindow(_registrar->GetView()));
}
return store;
}
std::wstring s2ws(const std::string& s)
std::wstring s2ws(const std::string &s)
{
int len;
int slength = (int)s.length() + 1;
len = MultiByteToWideChar(CP_ACP, 0, s.c_str(), slength, 0, 0);
wchar_t* buf = new wchar_t[len];
wchar_t *buf = new wchar_t[len];
MultiByteToWideChar(CP_ACP, 0, s.c_str(), slength, buf, len);
std::wstring r(buf);
delete[] buf;
return r;
}
std::string debugString(std::vector<std::string> vt) {
std::string debugString(std::vector<std::string> vt)
{
std::stringstream ss;
ss << "( ";
for (auto t : vt) {
for (auto t : vt)
{
ss << t << ", ";
}
ss << " )\n";
return ss.str();
}
std::string getExtendedErrorString(winrt::hresult error) {
std::string getExtendedErrorString(winrt::hresult error)
{
const HRESULT IAP_E_UNEXPECTED = 0x803f6107L;
std::string message;
if (error.value == IAP_E_UNEXPECTED) {
if (error.value == IAP_E_UNEXPECTED)
{
message = "This Product has not been properly configured.";
}
else {
else
{
message = "ExtendedError: " + std::to_string(error.value);
}
return message;
@@ -84,12 +93,14 @@ namespace windows_iap {
{
StorePurchaseResult result = co_await getStore().RequestPurchaseAsync(storeId);
if (result.ExtendedError().value != S_OK) {
if (result.ExtendedError().value != S_OK)
{
resultCallback->Error(std::to_string(result.ExtendedError().value), getExtendedErrorString(result.ExtendedError().value));
co_return;
}
int32_t returnCode;
switch (result.Status()) {
switch (result.Status())
{
case StorePurchaseStatus::AlreadyPurchased:
returnCode = 1;
break;
@@ -111,7 +122,7 @@ namespace windows_iap {
break;
default:
auto status = reinterpret_cast<int32_t*>(result.Status());
auto status = reinterpret_cast<int32_t *>(result.Status());
resultCallback->Error(std::to_string(*status), "Product was not purchased due to an unknown error.");
co_return;
break;
@@ -120,10 +131,12 @@ namespace windows_iap {
resultCallback->Success(flutter::EncodableValue(returnCode));
}
std::string productsToString(std::vector<StoreProduct> products) {
std::string productsToString(std::vector<StoreProduct> products)
{
std::stringstream ss;
ss << "[";
for (int i = 0; i < products.size(); i++) {
for (int i = 0; i < products.size(); i++)
{
auto product = products.at(i);
ss << "{";
ss << "\"title\":\"" << to_string(product.Title()) << "\",";
@@ -133,7 +146,8 @@ namespace windows_iap {
ss << "\"productKind\":\"" << to_string(product.ProductKind()) << "\",";
ss << "\"storeId\":\"" << to_string(product.StoreId()) << "\"";
ss << "}";
if (i != products.size() - 1) {
if (i != products.size() - 1)
{
ss << ",";
}
}
@@ -142,16 +156,19 @@ namespace windows_iap {
return ss.str();
}
foundation::IAsyncAction getProducts(std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> resultCallback) {
auto result = co_await getStore().GetAssociatedStoreProductsAsync({ L"Consumable", L"Durable", L"UnmanagedConsumable" });
if (result.ExtendedError().value != S_OK) {
foundation::IAsyncAction getProducts(std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> resultCallback)
{
auto result = co_await getStore().GetAssociatedStoreProductsAsync({L"Consumable", L"Durable", L"UnmanagedConsumable"});
if (result.ExtendedError().value != S_OK)
{
resultCallback->Error(std::to_string(result.ExtendedError().value), getExtendedErrorString(result.ExtendedError()));
}
else if (result.Products().Size() == 0) {
else if (result.Products().Size() == 0)
{
resultCallback->Success(flutter::EncodableValue("[]"));
}
else {
else
{
std::vector<StoreProduct> products;
for (IKeyValuePair<hstring, StoreProduct> addOn : result.Products())
{
@@ -163,7 +180,8 @@ namespace windows_iap {
}
}
std::string getStoreLicenseString(StoreLicense license) {
std::string getStoreLicenseString(StoreLicense license)
{
std::stringstream ss;
ss << "{";
ss << "\"isActive\":" << (license.IsActive() ? "true" : "false") << ",";
@@ -175,7 +193,8 @@ namespace windows_iap {
return ss.str();
}
foundation::IAsyncAction getAddonLicenses(std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> resultCallback) {
foundation::IAsyncAction getAddonLicenses(std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> resultCallback)
{
auto result = co_await getStore().GetAppLicenseAsync();
auto addonLicenses = result.AddOnLicenses();
@@ -192,10 +211,12 @@ namespace windows_iap {
/// <summary>
/// need to test in real app on store
/// </summary>
foundation::IAsyncAction checkPurchase(std::string storeId, std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> resultCallback) {
foundation::IAsyncAction checkPurchase(std::string storeId, std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> resultCallback)
{
auto result = co_await getStore().GetAppLicenseAsync();
if (result.IsActive()) {
if (result.IsActive())
{
auto addonLicenses = result.AddOnLicenses();
@@ -203,69 +224,79 @@ namespace windows_iap {
{
StoreLicense license = addonLicense.Value();
if (storeId.compare("") == 0) {
if (storeId.compare("") == 0)
{
// Truong hop storeId empty => bat ky Add-on nao co IsActive = true deu return true
if (license.IsActive()) {
if (license.IsActive())
{
resultCallback->Success(flutter::EncodableValue(true));
co_return;
}
}
else {
else
{
// Truong hop storeId not empty => check key = storeId
auto key = to_string(addonLicense.Key());
if (key.compare(storeId) == 0) {
if (key.compare(storeId) == 0)
{
resultCallback->Success(flutter::EncodableValue(license.IsActive()));
co_return;
}
}
}
// truong hop duyet het add-on license nhung vang khong tim thay IsActive = true thi return false
resultCallback->Success(flutter::EncodableValue(false));
}
else {
else
{
resultCallback->Success(flutter::EncodableValue(false));
}
}
/// <summary>
/// need to test in real app on store
/// </summary>
/// <summary>
/// need to test in real app on store
/// </summary>
foundation::IAsyncAction getTrialStatusAndRemainingDays(
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> resultCallback)
{
auto license = co_await getStore().GetAppLicenseAsync();
/// <summary>
/// need to test in real app on store
/// </summary>
/// <summary>
/// need to test in real app on store
/// </summary>
foundation::IAsyncAction getTrialStatusAndRemainingDays(
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> resultCallback)
{
auto store = getStore();
auto license = co_await store.GetAppLicenseAsync();
flutter::EncodableMap result;
result[flutter::EncodableValue("isTrial")] = flutter::EncodableValue(false);
result[flutter::EncodableValue("remainingDays")] = flutter::EncodableValue(0);
flutter::EncodableMap result;
result[flutter::EncodableValue("isTrial")] = flutter::EncodableValue(true);
result[flutter::EncodableValue("remainingDays")] = flutter::EncodableValue(0);
result[flutter::EncodableValue("isActive")] = flutter::EncodableValue(license.IsActive());
result[flutter::EncodableValue("isTrialOwnedByThisUser")] = flutter::EncodableValue(license.IsTrialOwnedByThisUser());
if (!license.IsActive()) {
resultCallback->Success(flutter::EncodableValue(result));
co_return;
}
if (!license.IsActive())
{
resultCallback->Success(flutter::EncodableValue(result));
co_return;
}
if (license.IsTrial()) {
result[flutter::EncodableValue("isTrial")] = flutter::EncodableValue(true);
if (license.IsTrial())
{
result[flutter::EncodableValue("isTrial")] = flutter::EncodableValue(true);
winrt::Windows::Foundation::TimeSpan expiration = license.TrialTimeRemaining();
const auto inDays = std::chrono::duration_cast<std::chrono::hours>(expiration).count() / 24.0;
result[flutter::EncodableValue("remainingDays")] = flutter::EncodableValue(inDays);
}
resultCallback->Success(flutter::EncodableValue(result));
}
result[flutter::EncodableValue("remainingDays")] = flutter::EncodableValue(inDays);
}
resultCallback->Success(flutter::EncodableValue(result));
}
//////////////////////////////////////////////////////////////////////// END OF MY CODE //////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////// END OF MY CODE //////////////////////////////////////////////////////////////
// static
// static
void WindowsIapPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarWindows* registrar) {
flutter::PluginRegistrarWindows *registrar)
{
_registrar = registrar;
auto channel =
@@ -276,9 +307,10 @@ namespace windows_iap {
auto plugin = std::make_unique<WindowsIapPlugin>();
channel->SetMethodCallHandler(
[plugin_pointer = plugin.get()](const auto& call, auto result) {
plugin_pointer->HandleMethodCall(call, std::move(result));
});
[plugin_pointer = plugin.get()](const auto &call, auto result)
{
plugin_pointer->HandleMethodCall(call, std::move(result));
});
registrar->AddPlugin(std::move(plugin));
}
@@ -288,30 +320,37 @@ namespace windows_iap {
WindowsIapPlugin::~WindowsIapPlugin() {}
void WindowsIapPlugin::HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
if (method_call.method_name().compare("makePurchase") == 0) {
const flutter::MethodCall<flutter::EncodableValue> &method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
{
if (method_call.method_name().compare("makePurchase") == 0)
{
auto args = std::get<flutter::EncodableMap>(*method_call.arguments());
auto storeId = std::get<std::string>(args[flutter::EncodableValue("storeId")]);
makePurchase(to_hstring(storeId), std::move(result));
}
else if (method_call.method_name().compare("getProducts") == 0) {
else if (method_call.method_name().compare("getProducts") == 0)
{
getProducts(std::move(result));
}
else if (method_call.method_name().compare("checkPurchase") == 0) {
else if (method_call.method_name().compare("checkPurchase") == 0)
{
auto args = std::get<flutter::EncodableMap>(*method_call.arguments());
auto storeId = std::get<std::string>(args[flutter::EncodableValue("storeId")]);
checkPurchase(storeId, std::move(result));
}
else if (method_call.method_name().compare("getAddonLicenses") == 0) {
else if (method_call.method_name().compare("getAddonLicenses") == 0)
{
getAddonLicenses(std::move(result));
}
else if (method_call.method_name().compare("getTrialStatusAndRemainingDays") == 0) {
getTrialStatusAndRemainingDays(std::move(result));
else if (method_call.method_name().compare("getTrialStatusAndRemainingDays") == 0)
{
getTrialStatusAndRemainingDays(std::move(result));
}
else {
else
{
result->NotImplemented();
}
}
} // namespace windows_iap
} // namespace windows_iap