diff --git a/ios/Podfile b/ios/Podfile index 620e46e..40359ae 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,6 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '13.0' + +platform :ios, '15.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f62d54a..c57865b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -7,14 +7,22 @@ PODS: - Flutter (1.0.0) - flutter_local_notifications (0.0.1): - Flutter + - flutter_secure_storage_darwin (10.0.0): + - Flutter + - FlutterMacOS - gamepads_ios (0.1.1): - Flutter - image_picker_ios (0.0.1): - Flutter + - in_app_purchase_storekit (0.0.1): + - Flutter + - FlutterMacOS - in_app_review (2.0.0): - Flutter - integration_test (0.0.1): - Flutter + - ios_receipt (0.0.1): + - Flutter - media_key_detector_ios (0.0.1): - Flutter - nsd_ios (0.0.1): @@ -44,10 +52,13 @@ DEPENDENCIES: - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Flutter (from `Flutter`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) + - flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`) - gamepads_ios (from `.symlinks/plugins/gamepads_ios/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) + - ios_receipt (from `.symlinks/plugins/ios_receipt/ios`) - media_key_detector_ios (from `.symlinks/plugins/media_key_detector_ios/ios`) - nsd_ios (from `.symlinks/plugins/nsd_ios/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) @@ -68,14 +79,20 @@ EXTERNAL SOURCES: :path: Flutter flutter_local_notifications: :path: ".symlinks/plugins/flutter_local_notifications/ios" + flutter_secure_storage_darwin: + :path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin" gamepads_ios: :path: ".symlinks/plugins/gamepads_ios/ios" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" + in_app_purchase_storekit: + :path: ".symlinks/plugins/in_app_purchase_storekit/darwin" in_app_review: :path: ".symlinks/plugins/in_app_review/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" + ios_receipt: + :path: ".symlinks/plugins/ios_receipt/ios" media_key_detector_ios: :path: ".symlinks/plugins/media_key_detector_ios/ios" nsd_ios: @@ -102,10 +119,13 @@ SPEC CHECKSUMS: device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f + flutter_secure_storage_darwin: 557817588b80e60213cbecb573c45c76b788018d gamepads_ios: 1d2930c7a4450a9a1b57444ebf305a6a6cbeea0b image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 + in_app_purchase_storekit: 2342c0a5da86593124d08dd13d920f39a52b273a in_app_review: 436034b18594851a7328d7f1c2ed5ec235b79cfc integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + ios_receipt: c2d5b4c36953c377a024992393976214ce6951e6 media_key_detector_ios: 7ff9aefdfea00bb7b71e184132381b7d0e7e1269 nsd_ios: 8c37babdc6538e3350dbed3a52674d2edde98173 package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 @@ -117,6 +137,6 @@ SPEC CHECKSUMS: url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e -PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e +PODFILE CHECKSUM: 7ebd5c9b932b3af79d5c67e3af873118b74e970f COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 088919a..be41f95 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -496,6 +496,7 @@ DEVELOPMENT_TEAM = UZRHKPVWN9; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -683,6 +684,7 @@ DEVELOPMENT_TEAM = UZRHKPVWN9; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -710,6 +712,7 @@ DEVELOPMENT_TEAM = UZRHKPVWN9; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/lib/utils/iap/windows_iap_service.dart b/lib/utils/iap/windows_iap_service.dart index fc0bf2e..5226631 100644 --- a/lib/utils/iap/windows_iap_service.dart +++ b/lib/utils/iap/windows_iap_service.dart @@ -1,7 +1,10 @@ import 'dart:async'; +import 'package:bike_control/utils/core.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:version/version.dart'; +import 'package:windows_iap/windows_iap.dart'; /// Windows-specific IAP service /// Note: This is a stub implementation. For actual Windows Store integration, @@ -25,6 +28,8 @@ class WindowsIAPService { String? _lastCommandDate; int? _dailyCommandCount; + final _windowsIapPlugin = WindowsIap(); + WindowsIAPService(this._prefs); /// Initialize the Windows IAP service @@ -53,7 +58,7 @@ class WindowsIAPService { _isPurchased = true; return; } - + _windowsIapPlugin. // TODO: Add Windows Store API integration // Check if the app was purchased from the Windows Store // This would require platform channel implementation to call Windows Store APIs @@ -68,11 +73,15 @@ class WindowsIAPService { // IMPORTANT: This assumes the app is currently paid and this update will be released // while the app is still paid. Only users who downloaded the paid version will have // a last_seen_version. After changing the app to free, new users won't have this set. - final lastSeenVersion = await _prefs.read(key: 'last_seen_version'); + final lastSeenVersion = core.settings.getLastSeenVersion(); if (lastSeenVersion != null && lastSeenVersion.isNotEmpty) { - _isPurchased = true; - await _prefs.write(key: _purchaseStatusKey, value: "true"); - debugPrint('Existing Windows user detected - granting full access'); + Version lastVersion = Version.parse(lastSeenVersion); + // If they had a previous version, they're an existing paid user + _isPurchased = lastVersion < Version(4, 2, 0); + if (_isPurchased) { + await _prefs.write(key: _purchaseStatusKey, value: "true"); + } + debugPrint('Existing Android user detected - granting full access'); } } catch (e) { debugPrint('Error checking Windows previous version: $e'); diff --git a/pubspec.lock b/pubspec.lock index 1ac2018..63526f3 100755 --- a/pubspec.lock +++ b/pubspec.lock @@ -1657,6 +1657,13 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" + windows_iap: + dependency: "direct main" + description: + path: windows_iap + relative: true + source: path + version: "0.0.1" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 952f037..02aff1a 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,8 @@ dependencies: path: ios_receipt flutter_secure_storage: ^10.0.0 in_app_purchase: ^3.2.1 + windows_iap: + path: windows_iap window_manager: ^0.5.1 device_info_plus: ^12.1.0 keypress_simulator: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index e72f280..d795d4f 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -18,6 +18,7 @@ #include #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { BluetoothLowEnergyWindowsPluginCApiRegisterWithRegistrar( @@ -44,4 +45,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("UrlLauncherWindows")); WindowManagerPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("WindowManagerPlugin")); + WindowsIapPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowsIapPluginCApi")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 5c3e9e6..3ac321f 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -15,6 +15,7 @@ list(APPEND FLUTTER_PLUGIN_LIST universal_ble url_launcher_windows window_manager + windows_iap ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/windows_iap/.gitignore b/windows_iap/.gitignore new file mode 100644 index 0000000..96486fd --- /dev/null +++ b/windows_iap/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/windows_iap/.metadata b/windows_iap/.metadata new file mode 100644 index 0000000..ea33959 --- /dev/null +++ b/windows_iap/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + channel: stable + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + - platform: windows + create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/windows_iap/CHANGELOG.md b/windows_iap/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/windows_iap/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/windows_iap/LICENSE b/windows_iap/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/windows_iap/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/windows_iap/README.md b/windows_iap/README.md new file mode 100644 index 0000000..910303d --- /dev/null +++ b/windows_iap/README.md @@ -0,0 +1,15 @@ +# windows_iap + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/windows_iap/analysis_options.yaml b/windows_iap/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/windows_iap/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/windows_iap/example/.gitignore b/windows_iap/example/.gitignore new file mode 100644 index 0000000..a8e938c --- /dev/null +++ b/windows_iap/example/.gitignore @@ -0,0 +1,47 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/windows_iap/example/README.md b/windows_iap/example/README.md new file mode 100644 index 0000000..1d67fc9 --- /dev/null +++ b/windows_iap/example/README.md @@ -0,0 +1,16 @@ +# windows_iap_example + +Demonstrates how to use the windows_iap plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/windows_iap/example/analysis_options.yaml b/windows_iap/example/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/windows_iap/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/windows_iap/example/lib/main.dart b/windows_iap/example/lib/main.dart new file mode 100644 index 0000000..8041060 --- /dev/null +++ b/windows_iap/example/lib/main.dart @@ -0,0 +1,75 @@ +// ignore_for_file: use_build_context_synchronously, avoid_print + +import 'package:andesgroup_common/common.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:windows_iap/windows_iap.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + final _windowsIapPlugin = WindowsIap(); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Builder(builder: (context) { + return Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton( + onPressed: () async { + final result = await _windowsIapPlugin.checkPurchase(); + showAlertDialog(context, + content: 'checkPurchase: $result'); + }, + child: const Text('checkPurchase')), + const Gap(16), + ElevatedButton( + onPressed: () async { + final result = + await _windowsIapPlugin.makePurchase('hihi'); + print('result is $result'); + }, + child: const Text('makePurchase')), + const Gap(16), + ElevatedButton( + onPressed: () async { + try { + final products = await _windowsIapPlugin.getProducts(); + print('products: $products'); + } on PlatformException catch (e, s) { + print('error'); + print(e.toString()); + } + }, + child: const Text('getProducts')), + const Gap(16), + ElevatedButton( + onPressed: () async { + final result = await _windowsIapPlugin.getAddonLicenses(); + print('licenses: $result'); + }, + child: const Text('getAddonLicenses')), + ], + ), + ), + ); + }), + ); + } +} diff --git a/windows_iap/example/pubspec.lock b/windows_iap/example/pubspec.lock new file mode 100644 index 0000000..3d91380 --- /dev/null +++ b/windows_iap/example/pubspec.lock @@ -0,0 +1,529 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + andesgroup_common: + dependency: "direct main" + description: + name: andesgroup_common + sha256: e560b64c55e3a8605fc5d2ad789cf625c3124348d48f2549de7bf0aa565df65e + url: "https://pub.dev" + source: hosted + version: "1.0.19" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + cached_network_image: + dependency: transitive + description: + name: cached_network_image + sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15 + url: "https://pub.dev" + source: hosted + version: "3.2.3" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" + source: hosted + version: "3.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.dev" + source: hosted + version: "1.0.5" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_blurhash: + dependency: transitive + description: + name: flutter_blurhash + sha256: "05001537bd3fac7644fa6558b09ec8c0a3f2eba78c0765f88912882b1331a5c6" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "32cd900555219333326a2d0653aaaf8671264c29befa65bbd9856d204a4c9fb3" + url: "https://pub.dev" + source: hosted + version: "3.3.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fluttertoast: + dependency: transitive + description: + name: fluttertoast + sha256: "7cc92eabe01e3f1babe1571c5560b135dfc762a34e41e9056881e2196b178ec1" + url: "https://pub.dev" + source: hosted + version: "8.1.2" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338 + url: "https://pub.dev" + source: hosted + version: "2.2.0" + http: + dependency: transitive + description: + name: http + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" + source: hosted + version: "0.13.5" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + intl: + dependency: transitive + description: + name: intl + sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + url: "https://pub.dev" + source: hosted + version: "0.18.0" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + url: "https://pub.dev" + source: hosted + version: "4.8.0" + keyboard_dismisser: + dependency: transitive + description: + name: keyboard_dismisser + sha256: f67e032581fc3dd1f77e1cb54c421b089e015d122aeba2490ba001cfcc42a181 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + logger: + dependency: transitive + description: + name: logger + sha256: "5076f09225f91dc49289a4ccb92df2eeea9ea01cf7c26d49b3a1f04c6a49eec1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "107f3ed1330006a3bea63615e81cf637433f5135a52466c7caa0e7152bca9143" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + url: "https://pub.dev" + source: hosted + version: "2.0.12" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + url: "https://pub.dev" + source: hosted + version: "2.0.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + url: "https://pub.dev" + source: hosted + version: "2.1.7" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + url: "https://pub.dev" + source: hosted + version: "2.0.5" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c + url: "https://pub.dev" + source: hosted + version: "2.1.3" + pedantic: + dependency: transitive + description: + name: pedantic + sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + platform: + dependency: transitive + description: + name: platform + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + process: + dependency: transitive + description: + name: process + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" + source: hosted + version: "4.2.4" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" + shimmer: + dependency: transitive + description: + name: shimmer + sha256: "1f1009b5845a1f88f1c5630212279540486f97409e9fc3f63883e71070d107bf" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: "78324387dc81df14f78df06019175a86a2ee0437624166c382e145d0a7fd9a4f" + url: "https://pub.dev" + source: hosted + version: "2.2.4+1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: bfd6973aaeeb93475bc0d875ac9aefddf7965ef22ce09790eb963992ffc5183f + url: "https://pub.dev" + source: hosted + version: "2.4.2+2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + uuid: + dependency: transitive + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" + validators: + dependency: transitive + description: + name: validators + sha256: "884515951f831a9c669a41ed6c4d3c61c2a0e8ec6bca761a4480b28e99cecf5d" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + url: "https://pub.dev" + source: hosted + version: "14.3.0" + win32: + dependency: transitive + description: + name: win32 + sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + url: "https://pub.dev" + source: hosted + version: "3.1.3" + windows_iap: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + url: "https://pub.dev" + source: hosted + version: "0.2.0+3" +sdks: + dart: ">=3.8.0-0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/windows_iap/example/pubspec.yaml b/windows_iap/example/pubspec.yaml new file mode 100644 index 0000000..ef80010 --- /dev/null +++ b/windows_iap/example/pubspec.yaml @@ -0,0 +1,85 @@ +name: windows_iap_example +description: Demonstrates how to use the windows_iap plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ">=2.19.0 <3.0.0" + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + windows_iap: + # When depending on this package from a real application you should use: + # windows_iap: x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: 1.0.5 + andesgroup_common: ^1.0.19 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: 2.0.1 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/windows_iap/example/test/widget_test.dart b/windows_iap/example/test/widget_test.dart new file mode 100644 index 0000000..f1367b0 --- /dev/null +++ b/windows_iap/example/test/widget_test.dart @@ -0,0 +1,16 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + + // Verify that platform version is retrieved. + }); +} diff --git a/windows_iap/example/windows/.gitignore b/windows_iap/example/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/windows_iap/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows_iap/example/windows/CMakeLists.txt b/windows_iap/example/windows/CMakeLists.txt new file mode 100644 index 0000000..16bfc00 --- /dev/null +++ b/windows_iap/example/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(windows_iap_example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "windows_iap_example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows_iap/example/windows/flutter/CMakeLists.txt b/windows_iap/example/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..930d207 --- /dev/null +++ b/windows_iap/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows_iap/example/windows/flutter/generated_plugin_registrant.cc b/windows_iap/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..c630a16 --- /dev/null +++ b/windows_iap/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + WindowsIapPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowsIapPluginCApi")); +} diff --git a/windows_iap/example/windows/flutter/generated_plugin_registrant.h b/windows_iap/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/windows_iap/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows_iap/example/windows/flutter/generated_plugins.cmake b/windows_iap/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..82ec794 --- /dev/null +++ b/windows_iap/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + windows_iap +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows_iap/example/windows/runner/CMakeLists.txt b/windows_iap/example/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..b9e550f --- /dev/null +++ b/windows_iap/example/windows/runner/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows_iap/example/windows/runner/Runner.rc b/windows_iap/example/windows/runner/Runner.rc new file mode 100644 index 0000000..f568142 --- /dev/null +++ b/windows_iap/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.andesgroup" "\0" + VALUE "FileDescription", "windows_iap_example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "windows_iap_example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 com.andesgroup. All rights reserved." "\0" + VALUE "OriginalFilename", "windows_iap_example.exe" "\0" + VALUE "ProductName", "windows_iap_example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows_iap/example/windows/runner/flutter_window.cpp b/windows_iap/example/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..b43b909 --- /dev/null +++ b/windows_iap/example/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows_iap/example/windows/runner/flutter_window.h b/windows_iap/example/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/windows_iap/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows_iap/example/windows/runner/main.cpp b/windows_iap/example/windows/runner/main.cpp new file mode 100644 index 0000000..302c33a --- /dev/null +++ b/windows_iap/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"windows_iap_example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows_iap/example/windows/runner/resource.h b/windows_iap/example/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/windows_iap/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows_iap/example/windows/runner/resources/app_icon.ico b/windows_iap/example/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/windows_iap/example/windows/runner/resources/app_icon.ico differ diff --git a/windows_iap/example/windows/runner/runner.exe.manifest b/windows_iap/example/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..c977c4a --- /dev/null +++ b/windows_iap/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/windows_iap/example/windows/runner/utils.cpp b/windows_iap/example/windows/runner/utils.cpp new file mode 100644 index 0000000..f5bf9fa --- /dev/null +++ b/windows_iap/example/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows_iap/example/windows/runner/utils.h b/windows_iap/example/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/windows_iap/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows_iap/example/windows/runner/win32_window.cpp b/windows_iap/example/windows/runner/win32_window.cpp new file mode 100644 index 0000000..c10f08d --- /dev/null +++ b/windows_iap/example/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/windows_iap/example/windows/runner/win32_window.h b/windows_iap/example/windows/runner/win32_window.h new file mode 100644 index 0000000..17ba431 --- /dev/null +++ b/windows_iap/example/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/windows_iap/lib/models/product.dart b/windows_iap/lib/models/product.dart new file mode 100644 index 0000000..c4ba5d8 --- /dev/null +++ b/windows_iap/lib/models/product.dart @@ -0,0 +1,41 @@ +class Product { + final String? title; + final String? description; + final String? price; + final bool? inCollection; + final String? productKind; + final String? storeId; + + const Product({ + required this.title, + required this.description, + required this.price, + required this.inCollection, + required this.productKind, + required this.storeId, + }); + + String? get formattedTitle { + return "$title ($productKind) $price, InUserCollection: $inCollection"; + } + + factory Product.fromJson(Map json) { + return Product( + title: json['title'] as String?, + description: json['description'] as String?, + price: json['price'] as String?, + inCollection: json['inCollection'] as bool?, + productKind: json['productKind'] as String?, + storeId: json['storeId'] as String?); + } + + Map toJson() { + return { + 'title': title, + 'price': price, + 'inCellection': inCollection, + 'productKind': productKind, + 'storeId': storeId, + }; + } +} diff --git a/windows_iap/lib/models/store_license.dart b/windows_iap/lib/models/store_license.dart new file mode 100644 index 0000000..35d4e26 --- /dev/null +++ b/windows_iap/lib/models/store_license.dart @@ -0,0 +1,48 @@ +class StoreLicense { + final bool? isActive; + final String? skuStoreId; + final String? inAppOfferToken; + final num? expirationDate; + + const StoreLicense({ + this.isActive, + this.skuStoreId, + this.inAppOfferToken, + this.expirationDate, + }); + + /// get Expiration date in DateTime format. + DateTime? getExpirationDate() { + if (expirationDate == null) { + return null; + } + + var magic = num.tryParse(expirationDate.toString().substring(0, 14)); + if (magic == null) { + return null; + } + // 11644499200000 is the count of millis from DateTime(1601,1,1) -> DateTime(1970,1,1) + // more docs: https://docs.microsoft.com/en-us/uwp/cpp-ref-for-winrt/clock + // https://docs.microsoft.com/en-us/uwp/api/windows.foundation.datetime?view=winrt-22621#remarks + // https://docs.microsoft.com/en-us/uwp/api/windows.services.store.storelicense.expirationdate?view=winrt-22621 + magic = magic - 11644499200000; + return DateTime.fromMillisecondsSinceEpoch(magic.toInt()); + } + + factory StoreLicense.fromJson(Map json) { + return StoreLicense( + isActive: json['isActive'], + skuStoreId: json['skuStoreId'], + inAppOfferToken: json['inAppOfferToken'], + expirationDate: json['expirationDate']); + } + + Map toJson() { + return { + 'isActive': isActive, + 'skuStoreId': skuStoreId, + 'inAppOfferToken': inAppOfferToken, + 'expirationDate': expirationDate, + }; + } +} diff --git a/windows_iap/lib/utils.dart b/windows_iap/lib/utils.dart new file mode 100644 index 0000000..813ce08 --- /dev/null +++ b/windows_iap/lib/utils.dart @@ -0,0 +1,6 @@ +List parseListNotNull({ + required List json, + required T Function(Map json) fromJson, +}) { + return (json).map((e) => fromJson(e as Map)).toList(); +} diff --git a/windows_iap/lib/windows_iap.dart b/windows_iap/lib/windows_iap.dart new file mode 100644 index 0000000..5fbe02f --- /dev/null +++ b/windows_iap/lib/windows_iap.dart @@ -0,0 +1,67 @@ +library windows_iap; + +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:windows_iap/models/product.dart'; + +import 'models/store_license.dart'; +import 'windows_iap_platform_interface.dart'; + + +enum StorePurchaseStatus { + succeeded, + alreadyPurchased, + notPurchased, + networkError, + serverError, +} + +class WindowsIap { + Future makePurchase(String storeId) { + return WindowsIapPlatform.instance.makePurchase(storeId); + } + + /// throw PlatformException if error + Future> getProducts() { + if (Platform.isMacOS) { + return Future.delayed(const Duration(seconds: 2), () { + throw PlatformException(code: '123123123', message: 'Products can not loaded now.'); + }); + } + return WindowsIapPlatform.instance.getProducts(); + } + + /// Check when user has current valid purchase + /// + /// - Add-On type: Subscription, Durable + /// + /// - Always return false if AppLicense has IsActive status = false. + /// + /// - if storeId is Not Empty: + /// + /// -- it will return true if Product(storeId) has IsActive status = true. + /// + /// -- return false if not. + /// + /// - if storeId is Empty: + /// + /// -- it will return true if any Add-On have IsActive status = true. + /// + /// -- return false if all Add-On have IsActive status = false. + Future checkPurchase({String storeId = ''}) { + if (Platform.isMacOS) { + return Future.value(false); + } + return WindowsIapPlatform.instance.checkPurchase(storeId: storeId); + } + + /// return the map of StoreLicense + /// + /// A map of key and value pairs, where each key is the Store ID of an add-on SKU from the + /// Microsoft Store catalog and each value is a StoreLicense object that contains license + /// info for the add-on. + Future> getAddonLicenses() { + return WindowsIapPlatform.instance.getAddonLicenses(); + } +} diff --git a/windows_iap/lib/windows_iap_method_channel.dart b/windows_iap/lib/windows_iap_method_channel.dart new file mode 100644 index 0000000..cdf5e37 --- /dev/null +++ b/windows_iap/lib/windows_iap_method_channel.dart @@ -0,0 +1,114 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'models/product.dart'; +import 'models/store_license.dart'; +import 'utils.dart'; +import 'windows_iap.dart'; +import 'windows_iap_platform_interface.dart'; + +/// A [Map] between whitespace characters and their escape sequences. +const _escapeMap = { + '\n': '', + '\r': '', + '\f': '', + '\b': '', + '\t': '', + '\v': '', + '\x7F': '', // delete +}; + +/// A [RegExp] that matches whitespace characters that should be escaped. +var _escapeRegExp = RegExp( + '[\\x00-\\x07\\x0E-\\x1F${_escapeMap.keys.map(_getHexLiteral).join()}]'); + +/// Returns [str] with all whitespace characters represented as their escape +/// sequences. +/// +/// Backslash characters are escaped as `\\` +String escape(String str) { + str = str.replaceAll('\\', r'\\'); + return str.replaceAllMapped(_escapeRegExp, (match) { + var mapped = _escapeMap[match[0]]; + if (mapped != null) return mapped; + return _getHexLiteral(match[0]!); + }); +} + +/// Given single-character string, return the hex-escaped equivalent. +String _getHexLiteral(String input) { + var rune = input.runes.single; + return r'\x' + rune.toRadixString(16).toUpperCase().padLeft(2, '0'); +} + +/// An implementation of [WindowsIapPlatform] that uses method channels. +class MethodChannelWindowsIap extends WindowsIapPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('windows_iap'); + + @override + Future makePurchase(String storeId) async { + final result = await methodChannel + .invokeMethod('makePurchase', {'storeId': storeId}); + if (result == null) { + return null; + } + switch (result) { + case 0: + return StorePurchaseStatus.succeeded; + case 1: + return StorePurchaseStatus.alreadyPurchased; + case 2: + return StorePurchaseStatus.notPurchased; + case 3: + return StorePurchaseStatus.networkError; + case 4: + return StorePurchaseStatus.serverError; + } + return null; + } + + @override + Stream> productsStream() { + return const EventChannel('windows_iap_event_products') + .receiveBroadcastStream() + .map((event) { + if (event is String) { + return parseListNotNull( + json: jsonDecode(escape(event)), fromJson: Product.fromJson); + } else { + return []; + } + }); + } + + @override + Future> getProducts() async { + final result = await methodChannel.invokeMethod('getProducts'); + if (result == null) { + return []; + } + return parseListNotNull( + json: jsonDecode(escape(result)), fromJson: Product.fromJson); + } + + @override + Future checkPurchase({required String storeId}) async { + final result = await methodChannel + .invokeMethod('checkPurchase', {'storeId': storeId}); + return result ?? false; + } + + @override + Future> getAddonLicenses() async { + final result = await methodChannel.invokeMethod('getAddonLicenses'); + if (result == null) { + return {}; + } + return result.map((key, value) => + MapEntry(key.toString(), StoreLicense.fromJson(jsonDecode(value)))); + } +} diff --git a/windows_iap/lib/windows_iap_platform_interface.dart b/windows_iap/lib/windows_iap_platform_interface.dart new file mode 100644 index 0000000..c97d81e --- /dev/null +++ b/windows_iap/lib/windows_iap_platform_interface.dart @@ -0,0 +1,44 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:windows_iap/models/product.dart'; +import 'package:windows_iap/models/store_license.dart'; + +import 'windows_iap.dart'; +import 'windows_iap_method_channel.dart'; + +abstract class WindowsIapPlatform extends PlatformInterface { + /// Constructs a WindowsIapPlatform. + WindowsIapPlatform() : super(token: _token); + + static final Object _token = Object(); + + static WindowsIapPlatform _instance = MethodChannelWindowsIap(); + + /// The default instance of [WindowsIapPlatform] to use. + /// + /// Defaults to [MethodChannelWindowsIap]. + static WindowsIapPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [WindowsIapPlatform] when + /// they register themselves. + static set instance(WindowsIapPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future makePurchase(String storeId) { + throw UnimplementedError('makePurchase() has not been implemented.'); + } + + Future> getProducts() { + throw UnimplementedError('getProducts() has not been implemented.'); + } + + Future checkPurchase({required String storeId}) { + throw UnimplementedError('checkPurchase() has not been implemented.'); + } + + Future> getAddonLicenses() { + throw UnimplementedError('getAddonLicenses() has not been implemented.'); + } +} diff --git a/windows_iap/pubspec.yaml b/windows_iap/pubspec.yaml new file mode 100644 index 0000000..158193a --- /dev/null +++ b/windows_iap/pubspec.yaml @@ -0,0 +1,24 @@ +name: windows_iap +description: A new Flutter project. +version: 0.0.1 +homepage: https://github.com/opmgit/windows_iap + +environment: + sdk: ">=2.19.0 <3.0.0" + flutter: ">=3.7.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.1.4 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + + plugin: + platforms: + windows: + pluginClass: WindowsIapPluginCApi diff --git a/windows_iap/test/windows_iap_method_channel_test.dart b/windows_iap/test/windows_iap_method_channel_test.dart new file mode 100644 index 0000000..04f493f --- /dev/null +++ b/windows_iap/test/windows_iap_method_channel_test.dart @@ -0,0 +1,24 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:windows_iap/windows_iap_method_channel.dart'; + +void main() { + MethodChannelWindowsIap platform = MethodChannelWindowsIap(); + const MethodChannel channel = MethodChannel('windows_iap'); + + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return '42'; + }); + }); + + tearDown(() { + channel.setMockMethodCallHandler(null); + }); + + test('getPlatformVersion', () async { + // expect(await platform.getPlatformVersion(), '42'); + }); +} diff --git a/windows_iap/test/windows_iap_test.dart b/windows_iap/test/windows_iap_test.dart new file mode 100644 index 0000000..0d34f4b --- /dev/null +++ b/windows_iap/test/windows_iap_test.dart @@ -0,0 +1,7 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('1=1', () async { + expect(1, 1); + }); +} diff --git a/windows_iap/watch.bat b/windows_iap/watch.bat new file mode 100644 index 0000000..ecdb2ec --- /dev/null +++ b/windows_iap/watch.bat @@ -0,0 +1 @@ +flutter pub run build_runner watch --delete-conflicting-outputs --use-polling-watcher \ No newline at end of file diff --git a/windows_iap/windows/.gitignore b/windows_iap/windows/.gitignore new file mode 100644 index 0000000..b3eb2be --- /dev/null +++ b/windows_iap/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows_iap/windows/CMakeLists.txt b/windows_iap/windows/CMakeLists.txt new file mode 100644 index 0000000..3d388b4 --- /dev/null +++ b/windows_iap/windows/CMakeLists.txt @@ -0,0 +1,89 @@ +# The Flutter tooling requires that developers have a version of Visual Studio +# installed that includes CMake 3.14 or later. You should not increase this +# version, as doing so will cause the plugin to fail to compile for some +# customers of the plugin. +cmake_minimum_required(VERSION 3.14) + +# Project-level configuration. +set(PROJECT_NAME "windows_iap") +project(${PROJECT_NAME} LANGUAGES CXX) + +include(FetchContent) +set(CPPWINRT_VERSION "2.0.221121.5") + +# This value is used when generating builds using this plugin, so it must +# not be changed +set(PLUGIN_NAME "windows_iap_plugin") + +################ NuGet intall begin ################ +FetchContent_Declare(nuget + URL "https://dist.nuget.org/win-x86-commandline/v6.4.0/nuget.exe" + URL_HASH SHA256=26730829b240581a3e6a4e276b9ace088930032df0c680d5591beccf6452374e + DOWNLOAD_NO_EXTRACT true +) + +find_program(NUGET nuget) +if (NOT NUGET) + message("Nuget.exe not found, trying to download or use cached version.") + FetchContent_MakeAvailable(nuget) + set(NUGET ${nuget_SOURCE_DIR}/nuget.exe) +endif() + +execute_process(COMMAND + ${NUGET} install "Microsoft.Windows.CppWinRT" -Version ${CPPWINRT_VERSION} -OutputDirectory packages + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + RESULT_VARIABLE ret) +if (NOT ret EQUAL 0) + message(FATAL_ERROR "Failed to install nuget package Microsoft.Windows.CppWinRT.${CPPWINRT_VERSION}") +endif() +################ NuGet install end ################ + +# Any new source files that you add to the plugin should be added here. +list(APPEND PLUGIN_SOURCES + "windows_iap_plugin.cpp" + "windows_iap_plugin.h" +) + +# Define the plugin library target. Its name must not be changed (see comment +# on PLUGIN_NAME above). +add_library(${PLUGIN_NAME} SHARED + "include/windows_iap/windows_iap_plugin_c_api.h" + "windows_iap_plugin_c_api.cpp" + ${PLUGIN_SOURCES} +) + +# Apply a standard set of build settings that are configured in the +# application-level CMakeLists.txt. This can be removed for plugins that want +# full control over build settings. +apply_standard_settings(${PLUGIN_NAME}) + +# Symbols are hidden by default to reduce the chance of accidental conflicts +# between plugins. This should not be removed; any symbols that should be +# exported should be explicitly exported with the FLUTTER_PLUGIN_EXPORT macro. +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) + +################ NuGet import begin ################ +set_target_properties(${PLUGIN_NAME} PROPERTIES VS_PROJECT_IMPORT + ${CMAKE_BINARY_DIR}/packages/Microsoft.Windows.CppWinRT.${CPPWINRT_VERSION}/build/native/Microsoft.Windows.CppWinRT.props +) + +target_link_libraries(${PLUGIN_NAME} PRIVATE + ${CMAKE_BINARY_DIR}/packages/Microsoft.Windows.CppWinRT.${CPPWINRT_VERSION}/build/native/Microsoft.Windows.CppWinRT.targets +) +################ NuGet import end ################ + +# Source include directories and library dependencies. Add any plugin-specific +# dependencies here. +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(windows_iap_bundled_libraries + "" + PARENT_SCOPE +) diff --git a/windows_iap/windows/include/windows_iap/windows_iap_plugin_c_api.h b/windows_iap/windows/include/windows_iap/windows_iap_plugin_c_api.h new file mode 100644 index 0000000..af5b6f4 --- /dev/null +++ b/windows_iap/windows/include/windows_iap/windows_iap_plugin_c_api.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_WINDOWS_IAP_PLUGIN_C_API_H_ +#define FLUTTER_PLUGIN_WINDOWS_IAP_PLUGIN_C_API_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void WindowsIapPluginCApiRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_WINDOWS_IAP_PLUGIN_C_API_H_ diff --git a/windows_iap/windows/windows_iap_plugin.cpp b/windows_iap/windows/windows_iap_plugin.cpp new file mode 100644 index 0000000..4693e63 --- /dev/null +++ b/windows_iap/windows/windows_iap_plugin.cpp @@ -0,0 +1,280 @@ +#include "windows_iap_plugin.h" + +// This must be included before many other Windows headers. +#include + +#include +#include +#include + +#include +#include + +#pragma once +#include +#include +#include + +#include +#include +#include +#include + +using namespace winrt; +using namespace Windows::Services::Store; +using namespace Windows::Foundation::Collections; +namespace foundation = Windows::Foundation; + +namespace windows_iap { + + //////////////////////////////////////////////////////////////////////// BEGIN OF MY CODE ////////////////////////////////////////////////////////////// + flutter::PluginRegistrarWindows* _registrar; + + HWND GetRootWindow(flutter::FlutterView* view) { + return ::GetAncestor(view->GetNativeWindow(), GA_ROOT); + } + + StoreContext getStore() { + StoreContext store = StoreContext::GetDefault(); + auto initWindow = store.try_as(); + if (initWindow != nullptr) { + initWindow->Initialize(GetRootWindow(_registrar->GetView())); + } + return store; + } + + 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]; + MultiByteToWideChar(CP_ACP, 0, s.c_str(), slength, buf, len); + std::wstring r(buf); + delete[] buf; + return r; + } + + std::string debugString(std::vector vt) { + + std::stringstream ss; + ss << "( "; + for (auto t : vt) { + ss << t << ", "; + } + ss << " )\n"; + return ss.str(); + } + + std::string getExtendedErrorString(winrt::hresult error) { + const HRESULT IAP_E_UNEXPECTED = 0x803f6107L; + std::string message; + if (error.value == IAP_E_UNEXPECTED) { + message = "This Product has not been properly configured."; + } + else { + message = "ExtendedError: " + std::to_string(error.value); + } + return message; + } + + foundation::IAsyncAction makePurchase(hstring storeId, std::unique_ptr> resultCallback) + { + StorePurchaseResult result = co_await getStore().RequestPurchaseAsync(storeId); + + 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()) { + case StorePurchaseStatus::AlreadyPurchased: + returnCode = 1; + break; + + case StorePurchaseStatus::Succeeded: + returnCode = 0; + break; + + case StorePurchaseStatus::NotPurchased: + returnCode = 2; + break; + + case StorePurchaseStatus::NetworkError: + returnCode = 3; + break; + + case StorePurchaseStatus::ServerError: + returnCode = 4; + break; + + default: + auto status = reinterpret_cast(result.Status()); + resultCallback->Error(std::to_string(*status), "Product was not purchased due to an unknown error."); + co_return; + break; + } + + resultCallback->Success(flutter::EncodableValue(returnCode)); + } + + std::string productsToString(std::vector products) { + std::stringstream ss; + ss << "["; + for (int i = 0; i < products.size(); i++) { + auto product = products.at(i); + ss << "{"; + ss << "\"title\":\"" << to_string(product.Title()) << "\","; + ss << "\"description\":\"" << to_string(product.Description()) << "\","; + ss << "\"price\":\"" << to_string(product.Price().FormattedPrice()) << "\","; + ss << "\"inCollection\":" << (product.IsInUserCollection() ? "true" : "false") << ","; + ss << "\"productKind\":\"" << to_string(product.ProductKind()) << "\","; + ss << "\"storeId\":\"" << to_string(product.StoreId()) << "\""; + ss << "}"; + if (i != products.size() - 1) { + ss << ","; + } + } + ss << "]"; + + return ss.str(); + } + + foundation::IAsyncAction getProducts(std::unique_ptr> 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) { + resultCallback->Success(flutter::EncodableValue("[]")); + + } + else { + std::vector products; + for (IKeyValuePair addOn : result.Products()) + { + StoreProduct product = addOn.Value(); + products.push_back(product); + } + std::string productsString = productsToString(products); + resultCallback->Success(flutter::EncodableValue(productsString)); + } + } + + std::string getStoreLicenseString(StoreLicense license) { + std::stringstream ss; + ss << "{"; + ss << "\"isActive\":" << (license.IsActive() ? "true" : "false") << ","; + ss << "\"skuStoreId\":\"" << to_string(license.SkuStoreId()) << "\","; + ss << "\"inAppOfferToken\":\"" << to_string(license.InAppOfferToken()) << "\","; + ss << "\"expirationDate\":" << license.ExpirationDate().time_since_epoch().count() << ""; + ss << "}"; + + return ss.str(); + } + + foundation::IAsyncAction getAddonLicenses(std::unique_ptr> resultCallback) { + auto result = co_await getStore().GetAppLicenseAsync(); + auto addonLicenses = result.AddOnLicenses(); + + std::map mapLicenses; + + for (IKeyValuePair addonLicense : addonLicenses) + { + mapLicenses[flutter::EncodableValue(to_string(addonLicense.Key()))] = flutter::EncodableValue(getStoreLicenseString(addonLicense.Value())); + } + + resultCallback->Success(flutter::EncodableValue(mapLicenses)); + } + + /// + /// need to test in real app on store + /// + foundation::IAsyncAction checkPurchase(std::string storeId, std::unique_ptr> resultCallback) { + auto result = co_await getStore().GetAppLicenseAsync(); + + if (result.IsActive()) { + + auto addonLicenses = result.AddOnLicenses(); + + for (IKeyValuePair addonLicense : addonLicenses) + { + StoreLicense license = addonLicense.Value(); + + if (storeId.compare("") == 0) { + // Truong hop storeId empty => bat ky Add-on nao co IsActive = true deu return true + if (license.IsActive()) { + resultCallback->Success(flutter::EncodableValue(true)); + co_return; + } + } + else { + // Truong hop storeId not empty => check key = storeId + auto key = to_string(addonLicense.Key()); + 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 { + resultCallback->Success(flutter::EncodableValue(false)); + } + } + + + //////////////////////////////////////////////////////////////////////// END OF MY CODE ////////////////////////////////////////////////////////////// + +// static + void WindowsIapPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarWindows* registrar) { + _registrar = registrar; + + auto channel = + std::make_unique>( + registrar->messenger(), "windows_iap", + &flutter::StandardMethodCodec::GetInstance()); + + auto plugin = std::make_unique(); + + channel->SetMethodCallHandler( + [plugin_pointer = plugin.get()](const auto& call, auto result) { + plugin_pointer->HandleMethodCall(call, std::move(result)); + }); + + registrar->AddPlugin(std::move(plugin)); + } + + WindowsIapPlugin::WindowsIapPlugin() {} + + WindowsIapPlugin::~WindowsIapPlugin() {} + + void WindowsIapPlugin::HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result) { + if (method_call.method_name().compare("makePurchase") == 0) { + auto args = std::get(*method_call.arguments()); + auto storeId = std::get(args[flutter::EncodableValue("storeId")]); + makePurchase(to_hstring(storeId), std::move(result)); + } + else if (method_call.method_name().compare("getProducts") == 0) { + getProducts(std::move(result)); + } + else if (method_call.method_name().compare("checkPurchase") == 0) { + auto args = std::get(*method_call.arguments()); + auto storeId = std::get(args[flutter::EncodableValue("storeId")]); + checkPurchase(storeId, std::move(result)); + } + else if (method_call.method_name().compare("getAddonLicenses") == 0) { + getAddonLicenses(std::move(result)); + } + else { + result->NotImplemented(); + } + } + +} // namespace windows_iap diff --git a/windows_iap/windows/windows_iap_plugin.h b/windows_iap/windows/windows_iap_plugin.h new file mode 100644 index 0000000..018910d --- /dev/null +++ b/windows_iap/windows/windows_iap_plugin.h @@ -0,0 +1,32 @@ +#ifndef FLUTTER_PLUGIN_WINDOWS_IAP_PLUGIN_H_ +#define FLUTTER_PLUGIN_WINDOWS_IAP_PLUGIN_H_ + +#include +#include + +#include + +namespace windows_iap { + +class WindowsIapPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); + + WindowsIapPlugin(); + + virtual ~WindowsIapPlugin(); + + // Disallow copy and assign. + WindowsIapPlugin(const WindowsIapPlugin&) = delete; + WindowsIapPlugin& operator=(const WindowsIapPlugin&) = delete; + + private: + // Called when a method is called on this plugin's channel from Dart. + void HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr> result); +}; + +} // namespace windows_iap + +#endif // FLUTTER_PLUGIN_WINDOWS_IAP_PLUGIN_H_ diff --git a/windows_iap/windows/windows_iap_plugin_c_api.cpp b/windows_iap/windows/windows_iap_plugin_c_api.cpp new file mode 100644 index 0000000..29a2d47 --- /dev/null +++ b/windows_iap/windows/windows_iap_plugin_c_api.cpp @@ -0,0 +1,12 @@ +#include "include/windows_iap/windows_iap_plugin_c_api.h" + +#include + +#include "windows_iap_plugin.h" + +void WindowsIapPluginCApiRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + windows_iap::WindowsIapPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +}