diff --git a/.github/labeler.yml b/.github/labeler.yml index 3e56bda45..59237e5bc 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -27,6 +27,8 @@ - packages/google_sign_in/**/* "p: image_picker": - packages/image_picker/**/* +"p: in_app_purchase": + - packages/in_app_purchase/**/* "p: integration_test": - packages/integration_test/**/* "p: messageport": diff --git a/.github/recipe.yaml b/.github/recipe.yaml index bae29be6d..0161210ce 100644 --- a/.github/recipe.yaml +++ b/.github/recipe.yaml @@ -34,6 +34,7 @@ plugins: camera: [] flutter_webrtc: [] geolocator: [] + in_app_purchase: [] network_info_plus: [] video_player_videohole: [] diff --git a/README.md b/README.md index 37457bb2d..92e146b4e 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ The _"non-endorsed"_ status means that the plugin is not endorsed by the origina | [**google_maps_flutter_tizen**](packages/google_maps_flutter) | [google_maps_flutter](https://pub.dev/packages/google_maps_flutter) (1st-party) | [![pub package](https://img.shields.io/pub/v/google_maps_flutter_tizen.svg)](https://pub.dev/packages/google_maps_flutter_tizen) | No | | [**google_sign_in_tizen**](packages/google_sign_in) | [google_sign_in](https://pub.dev/packages/google_sign_in) (1st-party) | [![pub package](https://img.shields.io/pub/v/google_sign_in_tizen.svg)](https://pub.dev/packages/google_sign_in_tizen) | No | | [**image_picker_tizen**](packages/image_picker) | [image_picker](https://pub.dev/packages/image_picker) (1st-party) | [![pub package](https://img.shields.io/pub/v/image_picker_tizen.svg)](https://pub.dev/packages/image_picker_tizen) | No | +| [**in_app_purchase_tizen**](packages/in_app_purchase) | [in_app_purchase](https://pub.dev/packages/in_app_purchase) (1st-party) | [![pub package](https://img.shields.io/pub/v/in_app_purchase_tizen.svg)](https://pub.dev/packages/in_app_purchase_tizen) | No | | [**integration_test_tizen**](packages/integration_test) | [integration_test](https://github.com/flutter/flutter/tree/main/packages/integration_test) (1st-party) | [![pub package](https://img.shields.io/pub/v/integration_test_tizen.svg)](https://pub.dev/packages/integration_test_tizen) | No | | [**messageport_tizen**](packages/messageport) | (Tizen-only) | [![pub package](https://img.shields.io/pub/v/messageport_tizen.svg)](https://pub.dev/packages/messageport_tizen) | N/A | | [**network_info_plus_tizen**](packages/network_info_plus) | [network_info_plus](https://pub.dev/packages/network_info_plus) (1st-party) | [![pub package](https://img.shields.io/pub/v/network_info_plus_tizen.svg)](https://pub.dev/packages/network_info_plus_tizen) | No | @@ -71,6 +72,7 @@ The _"non-endorsed"_ status means that the plugin is not endorsed by the origina | [**google_maps_flutter_tizen**](packages/google_maps_flutter) | ✔️ | ✔️ | ✔️ | ✔️ | | [**google_sign_in_tizen**](packages/google_sign_in) | ✔️ | ✔️ | ✔️ | ✔️ | | [**image_picker_tizen**](packages/image_picker) | ⚠️ | ❌ | ❌ | ❌ | No camera,
No file manager app | +| [**in_app_purchase_tizen**](packages/in_app_purchase) | ❌ | ❌ | ✔️ | ❌ | Only applicable for TV | | [**integration_test_tizen**](packages/integration_test) | ✔️ | ✔️ | ✔️ | ✔️ | | [**messageport_tizen**](packages/messageport) | ✔️ | ✔️ | ✔️ | ✔️ | | [**network_info_plus_tizen**](packages/network_info_plus) | ✔️ | ❌ | ✔️ | ❌ | API not supported on emulator | diff --git a/packages/in_app_purchase/.gitignore b/packages/in_app_purchase/.gitignore new file mode 100644 index 000000000..e9dc58d3d --- /dev/null +++ b/packages/in_app_purchase/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md new file mode 100644 index 000000000..607323422 --- /dev/null +++ b/packages/in_app_purchase/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +* Initial release. diff --git a/packages/in_app_purchase/LICENSE b/packages/in_app_purchase/LICENSE new file mode 100644 index 000000000..036fb575e --- /dev/null +++ b/packages/in_app_purchase/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2023 Samsung Electronics Co., Ltd. All rights reserved. +Copyright (c) 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the names of the copyright holders nor the names of the + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/in_app_purchase/README.md b/packages/in_app_purchase/README.md new file mode 100644 index 000000000..833297df7 --- /dev/null +++ b/packages/in_app_purchase/README.md @@ -0,0 +1,79 @@ +# in_app_purchase_tizen + +The Tizen implementation of [`in_app_purchase`](https://pub.dev/packages/in_app_purchase) based on the [Samsung Checkout](https://developer.samsung.com/smarttv/develop/guides/samsung-checkout/samsung-checkout.html) API. + +## Supported devices + +This plugin is only supported on Samsung Smart TVs running Tizen 5.5 and above. + +## Required privileges + +To use this plugin in a Tizen application, you need to declare the following privileges in your `tizen-manifest.xml` file. + +```xml + + http://developer.samsung.com/privilege/billing + http://developer.samsung.com/privilege/sso.partner + http://tizen.org/privilege/appmanager.launch + +``` + +The sso.partner privilege is required by the [Sso API](https://developer.samsung.com/smarttv/develop/api-references/samsung-product-api-references/sso-api.html) to internally obtain the user's custom ID (UID). Your app must be signed with a [partner-level certificate](https://docs.tizen.org/application/dotnet/get-started/certificates/creating-certificates) to use this privilege. + +## Preparation + +Follow these steps before setting up in-app purchases for your application: + +1. [Register your application](https://github.com/flutter-tizen/flutter-tizen/blob/master/doc/publish-app.md) at the [Samsung Apps TV Seller Office](https://seller.samsungapps.com/tv) if you haven't registered yet. You do not need to complete the registration process at this point. Go to the **Billing Info** page of the app and set the **Samsung Checkout** checkbox to ON. You can return back to this page and finish the registration process when the final version of your app is ready. + +2. Log in to the [Samsung Checkout DPI Portal](https://dpi.samsungcheckout.com) and register your in-app items. You can find your **App ID** and **Security Key** in the [**App Details Setting**](https://dpi.samsungcheckout.com/settings/appdetails) page. These values will be used as request parameters in your app code. + +## Usage + +This package is not an _endorsed_ implementation of `in_app_purchase`. Therefore, you have to include `in_app_purchase_tizen` alongside `in_app_purchase` as dependencies in your `pubspec.yaml` file. + +```yaml +dependencies: + in_app_purchase: ^3.1.4 + in_app_purchase_tizen: ^0.1.0 +``` + +Then you can import `in_app_purchase` and `in_app_purchase_tizen` in your Dart code: + +```dart +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:in_app_purchase_tizen/in_app_purchase_tizen.dart'; +``` + +You must call `setRequestParameters` to set required parameters before making any plugin API call. + +```dart +final InAppPurchaseTizenPlatformAddition platformAddition = _inAppPurchase + .getPlatformAddition(); +platformAddition.setRequestParameters( + appId: 'your_dpi_app_id', + pageSize: 20, + pageNum: 1, + securityKey: 'your_security_key', +); + +final ProductDetailsResponse response = + await _inAppPurchase.queryProductDetails({}); +``` + +For detailed usage, see https://pub.dev/packages/in_app_purchase#usage and the [example](example/lib) app. + +For more information on the Samsung Checkout API, visit the following pages. + +- [Samsung Developers: Implementing the Purchase Process](https://developer.samsung.com/smarttv/develop/guides/samsung-checkout/implementing-the-purchase-process.html) +- [Samsung Developers: Billing API References](https://developer.samsung.com/smarttv/develop/api-references/samsung-product-api-references/billing-api.html) + +## Supported APIs + +- [x] `InAppPurchase.purchaseStream` +- [x] `InAppPurchase.isAvailable` +- [x] `InAppPurchase.queryProductDetails` +- [x] `InAppPurchase.buyNonConsumable` +- [x] `InAppPurchase.buyConsumable` +- [ ] `InAppPurchase.completePurchase` (Andriod/iOS-only) +- [x] `InAppPurchase.restorePurchases` diff --git a/packages/in_app_purchase/example/.gitignore b/packages/in_app_purchase/example/.gitignore new file mode 100644 index 000000000..2d6916b03 --- /dev/null +++ b/packages/in_app_purchase/example/.gitignore @@ -0,0 +1,41 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# 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 \ No newline at end of file diff --git a/packages/in_app_purchase/example/README.md b/packages/in_app_purchase/example/README.md new file mode 100644 index 000000000..52f12c802 --- /dev/null +++ b/packages/in_app_purchase/example/README.md @@ -0,0 +1,7 @@ +# in_app_purchase_tizen_example + +Demonstrates how to use the in_app_purchase_tizen plugin. + +## Getting Started + +To run this app on your Tizen device, use [flutter-tizen](https://github.com/flutter-tizen/flutter-tizen). diff --git a/packages/in_app_purchase/example/integration_test/in_app_purchase_test.dart b/packages/in_app_purchase/example/integration_test/in_app_purchase_test.dart new file mode 100644 index 000000000..437ee99e9 --- /dev/null +++ b/packages/in_app_purchase/example/integration_test/in_app_purchase_test.dart @@ -0,0 +1,16 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Can create InAppPurchase instance', (WidgetTester tester) async { + final InAppPurchase iapInstance = InAppPurchase.instance; + expect(iapInstance, isNotNull); + }); +} diff --git a/packages/in_app_purchase/example/lib/main.dart b/packages/in_app_purchase/example/lib/main.dart new file mode 100644 index 000000000..177fd0049 --- /dev/null +++ b/packages/in_app_purchase/example/lib/main.dart @@ -0,0 +1,347 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:in_app_purchase_tizen/billing_manager_wrappers.dart'; +import 'package:in_app_purchase_tizen/in_app_purchase_tizen.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + runApp(_MyApp()); +} + +// To try without auto-consume, change `true` to `false` here. +const bool _kAutoConsume = true; + +const String _kAppId = '3201504002021'; +const int _kPageSize = 20; +const int _kPageNum = 1; +// Do not expose your DPI security key. You can use a key management server to retrieve it for greater security. +const String _kSecurityKey = 'YxE757K+aDWHJXa0QMnL5AJmItefoEizvv8L7WPJAMs='; + +class _MyApp extends StatefulWidget { + @override + State<_MyApp> createState() => _MyAppState(); +} + +class _MyAppState extends State<_MyApp> { + final InAppPurchase _inAppPurchase = InAppPurchase.instance; + late StreamSubscription> _subscription; + List _notFoundIds = []; + List _products = []; + List _purchases = []; + bool _isAvailable = false; + bool _purchasePending = false; + bool _loading = true; + String? _queryProductError; + + @override + void initState() { + final Stream> purchaseUpdated = + _inAppPurchase.purchaseStream; + _subscription = + purchaseUpdated.listen((List purchaseDetailsList) { + _listenToPurchaseUpdated(purchaseDetailsList); + }, onDone: () { + _subscription.cancel(); + }, onError: (Object error) { + // handle error here. + }); + initStoreInfo(); + super.initState(); + } + + Future initStoreInfo() async { + // Tizen specific API: + // You need to set necessary parameters before calling any plugin API. + final InAppPurchaseTizenPlatformAddition platformAddition = _inAppPurchase + .getPlatformAddition(); + platformAddition.setRequestParameters( + appId: _kAppId, + pageSize: _kPageSize, + pageNum: _kPageNum, + securityKey: _kSecurityKey, + ); + + final bool isAvailable = await _inAppPurchase.isAvailable(); + if (!isAvailable) { + setState(() { + _isAvailable = isAvailable; + _products = []; + _purchases = []; + _notFoundIds = []; + _purchasePending = false; + _loading = false; + }); + return; + } + + // The `identifiers` argument is not used on Tizen. + // Use `InAppPurchaseTizenPlatformAddition.setRequestParameters` instead. + final ProductDetailsResponse productDetailResponse = + await _inAppPurchase.queryProductDetails({}); + if (productDetailResponse.error != null) { + setState(() { + _queryProductError = productDetailResponse.error!.message; + _isAvailable = isAvailable; + _products = productDetailResponse.productDetails; + _purchases = []; + _notFoundIds = productDetailResponse.notFoundIDs; + _purchasePending = false; + _loading = false; + }); + return; + } + + if (productDetailResponse.productDetails.isEmpty) { + setState(() { + _queryProductError = null; + _isAvailable = isAvailable; + _products = productDetailResponse.productDetails; + _purchases = []; + _notFoundIds = productDetailResponse.notFoundIDs; + _purchasePending = false; + _loading = false; + }); + return; + } + + setState(() { + _isAvailable = isAvailable; + _products = productDetailResponse.productDetails; + _notFoundIds = productDetailResponse.notFoundIDs; + _purchasePending = false; + _loading = false; + }); + } + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final List stack = []; + if (_queryProductError == null) { + stack.add( + ListView( + children: [ + _buildConnectionCheckTile(), + _buildProductList(), + _buildRestoreButton(), + ], + ), + ); + } else { + stack.add(Center( + child: Text(_queryProductError!), + )); + } + if (_purchasePending) { + stack.add( + // TODO(goderbauer): Make this const when that's available on stable. + // ignore: prefer_const_constructors + Stack( + children: const [ + Opacity( + opacity: 0.3, + child: ModalBarrier(dismissible: false, color: Colors.grey), + ), + Center( + child: CircularProgressIndicator(), + ), + ], + ), + ); + } + + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('IAP Example'), + ), + body: Stack( + children: stack, + ), + ), + ); + } + + Card _buildConnectionCheckTile() { + if (_loading) { + return const Card(child: ListTile(title: Text('Trying to connect...'))); + } + final Widget storeHeader = ListTile( + leading: Icon(_isAvailable ? Icons.check : Icons.block, + color: _isAvailable + ? Colors.green + : ThemeData.light().colorScheme.error), + title: + Text('The store is ${_isAvailable ? 'available' : 'unavailable'}.'), + ); + final List children = [storeHeader]; + + if (!_isAvailable) { + children.addAll([ + const Divider(), + ListTile( + title: Text('Not connected', + style: TextStyle(color: ThemeData.light().colorScheme.error)), + subtitle: const Text( + 'Unable to connect to the payments processor. Are you signed in with your Samsung account on this device?'), + ), + ]); + } + return Card(child: Column(children: children)); + } + + Card _buildProductList() { + if (_loading) { + return const Card( + child: ListTile( + leading: CircularProgressIndicator(), + title: Text('Fetching products...'))); + } + if (!_isAvailable) { + return const Card(); + } + const ListTile productHeader = ListTile(title: Text('Products for Sale')); + final List productList = []; + if (_notFoundIds.isNotEmpty) { + productList.add(ListTile( + title: Text('[${_notFoundIds.join(", ")}] not found', + style: TextStyle(color: ThemeData.light().colorScheme.error)), + subtitle: const Text( + 'This app needs special configuration to run. Please see README.md for instructions.'))); + } + + productList.addAll(_products.map( + (ProductDetails productDetails) { + return ListTile( + title: Text( + productDetails.title, + ), + subtitle: Text( + productDetails.description, + ), + trailing: TextButton( + style: TextButton.styleFrom( + backgroundColor: Colors.green[800], + // ignore: deprecated_member_use + primary: Colors.white, + ), + onPressed: () { + final PurchaseParam purchaseParam = PurchaseParam( + productDetails: productDetails, + ); + + if (productDetails is SamsungCheckoutProductDetails) { + if (productDetails.itemDetails.itemType == + ItemType.consumable) { + _inAppPurchase.buyConsumable( + purchaseParam: purchaseParam, + // ignore: avoid_redundant_argument_values + autoConsume: _kAutoConsume); + } else { + _inAppPurchase.buyNonConsumable( + purchaseParam: purchaseParam); + } + } + }, + child: Text(productDetails.price), + )); + }, + )); + + return Card( + child: Column( + children: [productHeader, const Divider()] + productList)); + } + + Widget _buildRestoreButton() { + if (_loading) { + return Container(); + } + + return Padding( + padding: const EdgeInsets.all(4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + style: TextButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + // ignore: deprecated_member_use + primary: Colors.white, + ), + onPressed: () => _inAppPurchase.restorePurchases(), + child: const Text('Restore purchases'), + ), + ], + ), + ); + } + + void showPendingUI() { + setState(() { + _purchasePending = true; + }); + } + + Future deliverProduct(PurchaseDetails purchaseDetails) async { + // IMPORTANT!! Always verify purchase details before delivering the product. + setState(() { + _purchases.add(purchaseDetails); + _purchasePending = false; + }); + } + + void handleError(IAPError error) { + setState(() { + _purchasePending = false; + }); + } + + Future _verifyPurchase(PurchaseDetails purchaseDetails) { + // IMPORTANT!! Always verify a purchase before delivering the product. + + // Tizen specific verify purchase: + // If `PurchaseDetails.status` is `purchased`, need to verify purchase. + final InAppPurchaseTizenPlatformAddition platformAddition = _inAppPurchase + .getPlatformAddition(); + return platformAddition.verifyPurchase(purchaseDetails: purchaseDetails); + } + + void _handleInvalidPurchase(PurchaseDetails purchaseDetails) { + // handle invalid purchase here if _verifyPurchase` failed. + } + + Future _listenToPurchaseUpdated( + List purchaseDetailsList) async { + for (final PurchaseDetails purchaseDetails in purchaseDetailsList) { + if (purchaseDetails.status == PurchaseStatus.pending) { + showPendingUI(); + } else { + if (purchaseDetails.status == PurchaseStatus.error) { + handleError(purchaseDetails.error!); + } else if (purchaseDetails.status == PurchaseStatus.purchased || + purchaseDetails.status == PurchaseStatus.restored) { + final bool valid = await _verifyPurchase(purchaseDetails); + if (valid) { + deliverProduct(purchaseDetails); + } else { + _handleInvalidPurchase(purchaseDetails); + return; + } + } + } + } + } +} diff --git a/packages/in_app_purchase/example/pubspec.yaml b/packages/in_app_purchase/example/pubspec.yaml new file mode 100644 index 000000000..1571b753c --- /dev/null +++ b/packages/in_app_purchase/example/pubspec.yaml @@ -0,0 +1,27 @@ +name: in_app_purchase_tizen_example +description: Demonstrates how to use the in_app_purchase_tizen plugin. +publish_to: "none" + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + in_app_purchase: ^3.1.4 + in_app_purchase_tizen: + path: ../ + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + integration_test_tizen: + path: ../../integration_test/ + +flutter: + uses-material-design: true diff --git a/packages/in_app_purchase/example/test_driver/integration_test.dart b/packages/in_app_purchase/example/test_driver/integration_test.dart new file mode 100644 index 000000000..4f10f2a52 --- /dev/null +++ b/packages/in_app_purchase/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/in_app_purchase/example/tizen/.gitignore b/packages/in_app_purchase/example/tizen/.gitignore new file mode 100644 index 000000000..750f3af1b --- /dev/null +++ b/packages/in_app_purchase/example/tizen/.gitignore @@ -0,0 +1,5 @@ +flutter/ +.vs/ +*.user +bin/ +obj/ diff --git a/packages/in_app_purchase/example/tizen/App.cs b/packages/in_app_purchase/example/tizen/App.cs new file mode 100644 index 000000000..6dd4a6356 --- /dev/null +++ b/packages/in_app_purchase/example/tizen/App.cs @@ -0,0 +1,20 @@ +using Tizen.Flutter.Embedding; + +namespace Runner +{ + public class App : FlutterApplication + { + protected override void OnCreate() + { + base.OnCreate(); + + GeneratedPluginRegistrant.RegisterPlugins(this); + } + + static void Main(string[] args) + { + var app = new App(); + app.Run(args); + } + } +} diff --git a/packages/in_app_purchase/example/tizen/Runner.csproj b/packages/in_app_purchase/example/tizen/Runner.csproj new file mode 100644 index 000000000..f4e369d0c --- /dev/null +++ b/packages/in_app_purchase/example/tizen/Runner.csproj @@ -0,0 +1,19 @@ + + + + Exe + tizen40 + + + + + + + + + + %(RecursiveDir) + + + + diff --git a/packages/in_app_purchase/example/tizen/shared/res/ic_launcher.png b/packages/in_app_purchase/example/tizen/shared/res/ic_launcher.png new file mode 100644 index 000000000..4d6372eeb Binary files /dev/null and b/packages/in_app_purchase/example/tizen/shared/res/ic_launcher.png differ diff --git a/packages/in_app_purchase/example/tizen/tizen-manifest.xml b/packages/in_app_purchase/example/tizen/tizen-manifest.xml new file mode 100644 index 000000000..55bcb3fef --- /dev/null +++ b/packages/in_app_purchase/example/tizen/tizen-manifest.xml @@ -0,0 +1,15 @@ + + + + + + ic_launcher.png + + + + + http://developer.samsung.com/privilege/billing + http://developer.samsung.com/privilege/sso.partner + http://tizen.org/privilege/appmanager.launch + + diff --git a/packages/in_app_purchase/lib/billing_manager_wrappers.dart b/packages/in_app_purchase/lib/billing_manager_wrappers.dart new file mode 100644 index 000000000..5b80f4e42 --- /dev/null +++ b/packages/in_app_purchase/lib/billing_manager_wrappers.dart @@ -0,0 +1,5 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/billing_manager_wrappers/billing_manager_wrapper.dart'; diff --git a/packages/in_app_purchase/lib/in_app_purchase_tizen.dart b/packages/in_app_purchase/lib/in_app_purchase_tizen.dart new file mode 100644 index 000000000..d3b79fccf --- /dev/null +++ b/packages/in_app_purchase/lib/in_app_purchase_tizen.dart @@ -0,0 +1,6 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/in_app_purchase_tizen_platform.dart'; +export 'src/in_app_purchase_tizen_platform_addition.dart'; diff --git a/packages/in_app_purchase/lib/src/billing_manager_wrappers/billing_manager_wrapper.dart b/packages/in_app_purchase/lib/src/billing_manager_wrappers/billing_manager_wrapper.dart new file mode 100644 index 000000000..c2c891111 --- /dev/null +++ b/packages/in_app_purchase/lib/src/billing_manager_wrappers/billing_manager_wrapper.dart @@ -0,0 +1,776 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../channel.dart'; +import '../in_app_purchase_tizen_platform.dart'; + +// WARNING: Changes to `@JsonSerializable` classes need to be reflected in the +// below generated file. Run `flutter-tizen packages pub run build_runner watch` to +// rebuild and watch for further changes. +part 'billing_manager_wrapper.g.dart'; + +/// This class can be used directly to call Billing(Samsung Checkout) APIs. +/// +/// Wraps a +/// [`BillingManager`](https://developer.samsung.com/smarttv/develop/api-references/samsung-product-api-references/billing-api.html#BillingManager) +/// instance. +class BillingManager { + /// Creates a billing manager. + BillingManager(); + + late Map _requestParameters = {}; + + /// Call this to set tizen specific parameters. + // ignore: use_setters_to_change_properties + void setRequestParameters(Map requestParameters) { + _requestParameters = requestParameters; + } + + /// This is different from response [ItemType]. The value `2` means `all items`. + /// + /// [_requestItemType] is only used for [BillingManager.requestPurchases]. + static const String _requestItemType = '2'; + + /// Calls + /// [`BillingManager-isServiceAvailable`](https://developer.samsung.com/smarttv/develop/api-references/samsung-product-api-references/billing-api.html#BillingManager-isServiceAvailable) + /// to check whether the Billing server is available. + Future isAvailable() async { + final String? isAvailableResult = + await channel.invokeMethod('isAvailable'); + if (isAvailableResult == null) { + throw PlatformException( + code: 'no_response', + message: 'Failed to get response from platform.', + ); + } + + final ServiceAvailableAPIResult isAvailable = + ServiceAvailableAPIResult.fromJson( + json.decode(isAvailableResult) as Map); + if (isAvailable.status == '100000') { + return true; + } else { + return false; + } + } + + /// Calls + /// [`BillingManager-getProductsList`](https://developer.samsung.com/smarttv/develop/api-references/samsung-product-api-references/billing-api.html#BillingManager-getProductsList) + /// to retrieves the list of products registered on the Billing (DPI) server. + Future requestProducts( + List requestparameters) async { + final String? countryCode = + await channel.invokeMethod('GetCountryCode'); + final String checkValue = base64.encode(Hmac(sha256, + utf8.encode(_requestParameters['securityKey'] as String? ?? '')) + .convert(utf8.encode((_requestParameters['appId'] as String? ?? '') + + (countryCode ?? ''))) + .bytes); + + final Map arguments = { + 'appId': _requestParameters['appId'], + 'countryCode': countryCode, + 'pageSize': _requestParameters['pageSize'], + 'pageNum': _requestParameters['pageNum'], + 'checkValue': checkValue, + }; + + final String? productResponse = + await channel.invokeMethod('getProductList', arguments); + if (productResponse == null) { + throw PlatformException( + code: 'no_response', + message: 'failed to get response from platform.', + ); + } + return ProductsListApiResult.fromJson( + json.decode(productResponse) as Map); + } + + /// Calls + /// [`BillingManager-getUserPurchaseList`](https://developer.samsung.com/smarttv/develop/api-references/samsung-product-api-references/billing-api.html#BillingManager-getUserPurchaseList) + /// to retrieves the user's purchase list. + Future requestPurchases( + {String? applicationUserName}) async { + final String? customId = await channel.invokeMethod('GetCustomId'); + final String? countryCode = + await channel.invokeMethod('GetCountryCode'); + final String checkValue = base64.encode(Hmac(sha256, + utf8.encode(_requestParameters['securityKey'] as String? ?? '')) + .convert(utf8.encode((_requestParameters['appId'] as String? ?? '') + + (customId ?? '') + + (countryCode ?? '') + + _requestItemType + + (_requestParameters['pageNum'] as int? ?? -1).toString())) + .bytes); + + final Map arguments = { + 'appId': _requestParameters['appId'], + 'customId': customId, + 'countryCode': countryCode, + 'pageNum': _requestParameters['pageNum'], + 'checkValue': checkValue, + }; + + final String? purchaseResponse = + await channel.invokeMethod('getPurchaseList', arguments); + if (purchaseResponse == null) { + throw PlatformException( + code: 'no_response', + message: 'failed to get response from platform.', + ); + } + return GetUserPurchaseListAPIResult.fromJson( + json.decode(purchaseResponse) as Map); + } + + /// Calls + /// [`BillingManager-buyItem`](https://developer.samsung.com/smarttv/develop/api-references/samsung-product-api-references/billing-api.html#BillingManager-buyItem) + /// to enables implementing the Samsung Checkout Client module within the application. + /// After authenticating the purchase information through the application, the user can proceed to purchase payment. + Future buyItem({ + required String orderItemId, + required String orderTitle, + required String orderTotal, + required String orderCurrencyId, + }) async { + final Map orderDetails = { + 'OrderItemID': orderItemId, + 'OrderTitle': orderTitle, + 'OrderTotal': orderTotal, + 'OrderCurrencyID': orderCurrencyId + }; + final Map arguments = { + 'appId': _requestParameters['appId'], + 'payDetails': json.encode(orderDetails) + }; + + final Map? buyResult = + await channel.invokeMapMethod('buyItem', arguments); + if (buyResult == null) { + throw PlatformException( + code: 'request parameters null', + message: 'failed to get response from platform.', + ); + } + return BillingBuyData.fromJson(buyResult); + } + + /// Calls + /// [`BillingManager-verifyInvoice`](https://developer.samsung.com/smarttv/develop/api-references/samsung-product-api-references/billing-api.html#BillingManager-verifyInvoice) + /// to enables implementing the Samsung Checkout Client module within the application. + /// Checks whether a purchase, corresponding to a specific "InvoiceID", was successful. + Future verifyInvoice( + {required String invoiceId}) async { + final String? customId = await channel.invokeMethod('GetCustomId'); + final String? countryCode = + await channel.invokeMethod('GetCountryCode'); + final Map arguments = { + 'invoiceId': invoiceId, + 'appId': _requestParameters['appId'], + 'customId': customId, + 'countryCode': countryCode, + }; + + final String? verifyInvoiceResult = + await channel.invokeMethod('verifyInvoice', arguments); + if (verifyInvoiceResult == null) { + throw PlatformException( + code: 'no_response', + message: 'failed to get response from platform.', + ); + } + return VerifyInvoiceAPIResult.fromJson( + json.decode(verifyInvoiceResult) as Map); + } +} + +/// Dart wrapper around [`ServiceAvailableAPIResult`] in (https://developer.samsung.com/smarttv/develop/api-references/samsung-product-api-references/billing-api.html). +/// +/// Defines a dictionary for data returned by the IsServiceAvailable API. +/// This only can be used in [BillingManager.isAvailable]. +@JsonSerializable() +@immutable +class ServiceAvailableAPIResult { + /// Creates a [ServiceAvailableAPIResult] with the given purchase details. + const ServiceAvailableAPIResult({ + required this.status, + required this.result, + this.serviceYn, + }); + + /// Constructs an instance of this from a json string. + /// + /// The json needs to have named string keys with values matching the names and + /// types of all of the members on this class. + factory ServiceAvailableAPIResult.fromJson(Map json) => + _$ServiceAvailableAPIResultFromJson(json); + + /// Constructs an instance of this to a json string. + Map toJson() => _$ServiceAvailableAPIResultToJson(this); + + /// The result code of connecting to billing server. + /// Returns "100000" on success and other codes on failure. + @JsonKey(defaultValue: '') + final String status; + + /// The result message of connecting to billing server. + /// Returns "Success" on success. + @JsonKey(defaultValue: '') + final String result; + + /// Returns "Y" if the service is available. + /// It will be null, if disconnect to billing server. + @JsonKey(defaultValue: '') + final String? serviceYn; +} + +/// Dart wrapper around [`ProductsListApiResult`] in (https://developer.samsung.com/smarttv/develop/api-references/samsung-product-api-references/billing-api.html). +/// +/// Defines a dictionary for product list data returned by the getProductsList API. +/// This only can be used in [BillingManager.requestProducts]. +@JsonSerializable() +@immutable +class ProductsListApiResult { + /// Creates a [ProductsListApiResult] with the given purchase details. + const ProductsListApiResult({ + required this.cpStatus, + this.cpResult, + required this.checkValue, + required this.totalCount, + required this.itemDetails, + }); + + /// Constructs an instance of this from a json string. + /// + /// The json needs to have named string keys with values matching the names and + /// types of all of the members on this class. + factory ProductsListApiResult.fromJson(Map json) => + _$ProductsListApiResultFromJson(json); + + /// Constructs an instance of this to a json string. + Map toJson() => _$ProductsListApiResultToJson(this); + + /// DPI result code. + /// Returns "100000" on success and other codes on failure. + @JsonKey(defaultValue: '', name: 'CPStatus') + final String cpStatus; + + /// The result message. + /// "EOF":Last page of the product list. + /// "hasNext:TRUE" Product list has further pages. + /// Other error message, depending on the DPI result code. + @JsonKey(defaultValue: '', name: 'CPResult') + final String? cpResult; + + /// Total number of invoices. + @JsonKey(defaultValue: 0, name: 'TotalCount') + final int totalCount; + + /// Security check value. + @JsonKey(defaultValue: '', name: 'CheckValue') + final String checkValue; + + /// ItemDetails in JSON format + @JsonKey(defaultValue: [], name: 'ItemDetails') + final List itemDetails; +} + +/// Dart wrapper around [`ItemDetails`] in (https://developer.samsung.com/smarttv/develop/api-references/samsung-product-api-references/billing-api.html). +/// +/// Defines a dictionary for the ProductsListAPIResult dictionary 'ItemDetails' parameter. +/// This only can be used in [ProductsListApiResult]. +@JsonSerializable() +@immutable +class ItemDetails { + /// Creates a [ItemDetails] with the given purchase details. + const ItemDetails({ + required this.seq, + required this.itemId, + required this.itemTitle, + required this.itemDesc, + required this.itemType, + required this.price, + required this.currencyId, + }); + + /// Constructs an instance of this from a json string. + /// + /// The json needs to have named string keys with values matching the names and + /// types of all of the members on this class. + factory ItemDetails.fromJson(Map json) => + _$ItemDetailsFromJson(json); + + /// Constructs an instance of this to a json string. + Map toJson() => _$ItemDetailsToJson(this); + + /// Sequence number (1 ~ TotalCount). + @JsonKey(defaultValue: 0, name: 'Seq') + final int seq; + + /// The ID of Product. + @JsonKey(defaultValue: '', name: 'ItemID') + final String itemId; + + /// The name of product. + @JsonKey(defaultValue: '', name: 'ItemTitle') + final String itemTitle; + + /// The description of product. + @JsonKey(defaultValue: '', name: 'ItemDesc') + final String itemDesc; + + /// The type of product. + @JsonKey(defaultValue: ItemType.none, name: 'ItemType') + final ItemType itemType; + + /// The price of product, in "xxxx.yy" format. + @JsonKey(defaultValue: 0, name: 'Price') + final num price; + + /// The currency code + @JsonKey(defaultValue: '', name: 'CurrencyID') + final String currencyId; +} + +/// Dart wrapper around [`ProductSubscriptionInfo`] in (https://developer.samsung.com/smarttv/develop/api-references/samsung-product-api-references/billing-api.html). +/// +/// Defines a dictionary for the ItemDetails dictionary 'SubscriptionInfo' parameter. +/// This only can be used in [ItemDetails]. +@JsonSerializable() +@immutable +class ProductSubscriptionInfo { + /// Creates a [ProductSubscriptionInfo] with the given purchase details. + const ProductSubscriptionInfo({ + required this.paymentCycle, + required this.paymentCycleFrq, + required this.paymentCyclePeriod, + }); + + /// Constructs an instance of this from a json string. + /// + /// The json needs to have named string keys with values matching the names and + /// types of all of the members on this class. + factory ProductSubscriptionInfo.fromJson(Map json) => + _$ProductSubscriptionInfoFromJson(json); + + /// Constructs an instance of this to a json string. + Map toJson() => _$ProductSubscriptionInfoToJson(this); + + /// Subscription payment period: + /// "D": Days + /// "W": Weeks + /// "M": Months + @JsonKey(defaultValue: '', name: 'PaymentCyclePeriod') + final String paymentCyclePeriod; + + /// Payment cycle frequency. + @JsonKey(defaultValue: 0, name: 'PaymentCycleFrq') + final int paymentCycleFrq; + + /// Number of payment cycles. + @JsonKey(defaultValue: 0, name: 'PaymentCycle') + final int paymentCycle; +} + +/// The class represents the information of a product as registered in at +/// Samsung Checkout DPI portal. +class SamsungCheckoutProductDetails extends ProductDetails { + /// Creates a new Samsung Checkout specific product details object with the + /// provided details. + SamsungCheckoutProductDetails({ + required super.id, + required super.title, + required super.description, + required super.price, + required super.currencyCode, + required this.itemDetails, + super.rawPrice = 0.0, + super.currencySymbol, + }); + + /// Generate a [SamsungCheckoutProductDetails] object based on [ItemDetails] object. + factory SamsungCheckoutProductDetails.fromProduct(ItemDetails itemDetails) { + return SamsungCheckoutProductDetails( + id: itemDetails.itemId, + title: itemDetails.itemTitle, + description: itemDetails.itemDesc, + price: itemDetails.price.toString(), + currencyCode: itemDetails.currencyId, + itemDetails: itemDetails, + ); + } + + /// Points back to the [ItemDetails] object that was used to generate + /// this [SamsungCheckoutProductDetails] object. + final ItemDetails itemDetails; +} + +/// Dart wrapper around [`BillingBuyData`](https://developer.samsung.com/smarttv/develop/api-references/samsung-product-api-references/billing-api.html#BillingBuyData). +/// +/// Defines the payment result and information. +@JsonSerializable() +@immutable +class BillingBuyData { + /// Creates a [BillingBuyData] with the given purchase details. + const BillingBuyData({ + required this.payResult, + required this.payDetails, + }); + + /// Constructs an instance of this from a json string. + /// + /// The json needs to have named string keys with values matching the names and + /// types of all of the members on this class. + factory BillingBuyData.fromJson(Map json) => + _$BillingBuyDataFromJson(json); + + /// Constructs an instance of this to a json string. + Map toJson() => _$BillingBuyDataToJson(this); + + /// The payment result + @JsonKey(defaultValue: '') + final String payResult; + + /// The payment information. It is same with paymentDetails param of buyItem. + @JsonKey(defaultValue: {}) + final Map payDetails; +} + +/// Dart wrapper around [`GetUserPurchaseListAPIResult`] in (https://developer.samsung.com/smarttv/develop/api-references/samsung-product-api-references/billing-api.html). +/// +/// Defines a dictionary for data returned by the getUserPurchaseList API. +/// This only can be used in [BillingManager.requestPurchases] +@JsonSerializable() +@immutable +class GetUserPurchaseListAPIResult { + /// Creates a [GetUserPurchaseListAPIResult] with the given purchase details. + const GetUserPurchaseListAPIResult({ + required this.cpStatus, + this.cpResult, + required this.invoiceDetails, + required this.totalCount, + required this.checkValue, + }); + + /// Constructs an instance of this from a json string. + /// + /// The json needs to have named string keys with values matching the names and + /// types of all of the members on this class. + factory GetUserPurchaseListAPIResult.fromJson(Map json) => + _$GetUserPurchaseListAPIResultFromJson(json); + + /// Constructs an instance of this to a json string. + Map toJson() => _$GetUserPurchaseListAPIResultToJson(this); + + /// It returns "100000" in success and other codes in failure. Refer to DPI Error Code. + @JsonKey(defaultValue: '', name: 'CPStatus') + final String cpStatus; + + /// The result message: + /// "EOF":Last page of the product list + /// "hasNext:TRUE" Product list has further pages + /// Other error message, depending on the DPI result code + @JsonKey(defaultValue: '', name: 'CPResult') + final String? cpResult; + + /// Total number of invoices. + @JsonKey(defaultValue: 0, name: 'TotalCount') + final int? totalCount; + + /// Security check value. + @JsonKey(defaultValue: '', name: 'CheckValue') + final String? checkValue; + + /// InvoiceDetailsin JSON format. + @JsonKey(defaultValue: [], name: 'InvoiceDetails') + final List invoiceDetails; +} + +/// Dart wrapper around [`InvoiceDetails`] in (https://developer.samsung.com/smarttv/develop/api-references/samsung-product-api-references/billing-api.html). +/// +/// Defines a dictionary for the GetUserPurchaseListAPIResult dictionary 'InvoiceDetails' parameter. +/// This only can be used in [GetUserPurchaseListAPIResult]. +@JsonSerializable() +@immutable +class InvoiceDetails { + /// Creates a [InvoiceDetails] with the given purchase details. + const InvoiceDetails({ + required this.seq, + required this.invoiceId, + required this.itemId, + required this.itemTitle, + required this.itemType, + required this.orderTime, + required this.price, + required this.orderCurrencyId, + required this.appliedStatus, + required this.cancelStatus, + this.appliedTime, + this.period, + this.limitEndTime, + this.remainTime, + }); + + /// Constructs an instance of this from a json string. + /// + /// The json needs to have named string keys with values matching the names and + /// types of all of the members on this class. + factory InvoiceDetails.fromJson(Map json) => + _$InvoiceDetailsFromJson(json); + + /// Constructs an instance of this to a json string. + Map toJson() => _$InvoiceDetailsToJson(this); + + /// Sequence number (1 ~ TotalCount). + @JsonKey(defaultValue: 0, name: 'Seq') + final int seq; + + /// Invoice ID of this purchase history. + @JsonKey(defaultValue: '', name: 'InvoiceID') + final String invoiceId; + + /// The ID of product. + @JsonKey(defaultValue: '', name: 'ItemID') + final String itemId; + + /// The name of product. + @JsonKey(defaultValue: '', name: 'ItemTitle') + final String itemTitle; + + /// The type of product. + @JsonKey(defaultValue: ItemType.none, name: 'ItemType') + final ItemType itemType; + + /// Payment time, in 14-digit UTC time. + @JsonKey(defaultValue: '', name: 'OrderTime') + final String orderTime; + + /// Limited period product duration, in minutes. + @JsonKey(defaultValue: 0, name: 'Period') + final int? period; + + /// Product price, in "xxxx.yy" format. + @JsonKey(defaultValue: 0, name: 'Price') + final num price; + + /// Currency code. + @JsonKey(defaultValue: '', name: 'OrderCurrencyID') + final String orderCurrencyId; + + /// Cancellation status: + /// "true": Sale canceled + /// "false" : Sale ongoing + @JsonKey(defaultValue: false, name: 'CancelStatus') + final bool cancelStatus; + + /// Product application status: + /// "true": Applied + /// "false": Not applied + @JsonKey(defaultValue: false, name: 'AppliedStatus') + final bool appliedStatus; + + /// Time product applied, in 14-digit UTC time + @JsonKey(defaultValue: '', name: 'AppliedTime') + final String? appliedTime; + + /// Limited period product end time, in 14-digit UTC time + @JsonKey(defaultValue: '', name: 'LimitEndTime') + final String? limitEndTime; + + /// Limited period product time remaining, in seconds + @JsonKey(defaultValue: '', name: 'RemainTime') + final String? remainTime; +} + +/// Dart wrapper around [`PurchaseSubscriptionInfo`] in (https://developer.samsung.com/smarttv/develop/api-references/samsung-product-api-references/billing-api.html). +/// +/// Defines a dictionary for the InvoiceDetails dictionary 'SubscriptionInfo' parameter. +/// This only can be used in [InvoiceDetails]. +@JsonSerializable() +@immutable +class PurchaseSubscriptionInfo { + /// Creates a [PurchaseSubscriptionInfo] with the given purchase details. + const PurchaseSubscriptionInfo({ + required this.subscriptionId, + required this.subsStartTime, + required this.subsEndTime, + required this.subsStatus, + }); + + /// Constructs an instance of this from a json string. + /// + /// The json needs to have named string keys with values matching the names and + /// types of all of the members on this class. + factory PurchaseSubscriptionInfo.fromJson(Map json) => + _$PurchaseSubscriptionInfoFromJson(json); + + /// Constructs an instance of this to a json string. + Map toJson() => _$PurchaseSubscriptionInfoToJson(this); + + /// ID of subscription history. + @JsonKey(defaultValue: '', name: 'SubscriptionId') + final String subscriptionId; + + /// Subscription start time, in 14-digit UTC time. + @JsonKey(defaultValue: '', name: 'SubsStartTime') + final String subsStartTime; + + /// Subscription expiry time, in 14-digit UTC time. + @JsonKey(defaultValue: '', name: 'SubsEndTime') + final String subsEndTime; + + /// Subscription status: + /// "00": Active + /// "01": Subscription expired + /// "02": Canceled by buyer + /// "03": Canceled for payment failure + /// "04": Canceled by CP + /// "05": Canceled by admin + @JsonKey(defaultValue: '', name: 'SubsStatus') + final String subsStatus; +} + +/// The class represents the information of a purchase made using Samsung Checkout. +class SamsungCheckoutPurchaseDetails extends PurchaseDetails { + /// Creates a new Samsung Checkout specific purchase details object with the + /// provided details. + SamsungCheckoutPurchaseDetails({ + required super.productID, + required super.purchaseID, + required super.status, + required super.transactionDate, + required super.verificationData, + required this.invoiceDetails, + }); + + /// Generate a [SamsungCheckoutPurchaseDetails] object based on [PurchaseDetails] object. + factory SamsungCheckoutPurchaseDetails.fromPurchase( + InvoiceDetails invoiceDetails) { + final SamsungCheckoutPurchaseDetails purchaseDetails = + SamsungCheckoutPurchaseDetails( + purchaseID: invoiceDetails.invoiceId, + productID: invoiceDetails.itemId, + verificationData: PurchaseVerificationData( + localVerificationData: invoiceDetails.invoiceId, + serverVerificationData: invoiceDetails.invoiceId, + source: kIAPSource), + transactionDate: invoiceDetails.orderTime, + status: const PurchaseStateConverter() + .toPurchaseStatus(invoiceDetails.cancelStatus), + invoiceDetails: invoiceDetails, + ); + + if (purchaseDetails.status == PurchaseStatus.error) { + purchaseDetails.error = IAPError( + source: kIAPSource, + code: kPurchaseErrorCode, + message: '', + ); + } + + return purchaseDetails; + } + + /// Points back to the [InvoiceDetails] which was used to generate this + /// [SamsungCheckoutPurchaseDetails] object. + final InvoiceDetails invoiceDetails; +} + +/// Dart wrapper around [`VerifyInvoiceAPIResult`] in (https://developer.samsung.com/smarttv/develop/api-references/samsung-product-api-references/billing-api.html). +/// +/// This only can be used in [BillingManager.verifyInvoice]. +@JsonSerializable() +@immutable +class VerifyInvoiceAPIResult { + /// Creates a [VerifyInvoiceAPIResult] with the given purchase details. + const VerifyInvoiceAPIResult({ + required this.cpStatus, + this.cpResult, + required this.appId, + required this.invoiceId, + }); + + /// Constructs an instance of this from a json string. + /// + /// The json needs to have named string keys with values matching the names and + /// types of all of the members on this class. + factory VerifyInvoiceAPIResult.fromJson(Map json) => + _$VerifyInvoiceAPIResultFromJson(json); + + /// Constructs an instance of this to a json string. + Map toJson() => _$VerifyInvoiceAPIResultToJson(this); + + /// DPI result code. Returns "100000" on success and other codes on failure. + @JsonKey(defaultValue: '', name: 'CPStatus') + final String cpStatus; + + /// The result message: + /// "SUCCESS" and Other error message, depending on the DPI result code. + @JsonKey(defaultValue: '', name: 'CPResult') + final String? cpResult; + + /// The application ID. + @JsonKey(defaultValue: '', name: 'AppID') + final String appId; + + /// Invoice ID that you want to verify whether a purchase was successful. + @JsonKey(defaultValue: '', name: 'InvoiceID') + final String invoiceId; +} + +/// Convert bool value to purchase status.ll +class PurchaseStateConverter { + /// Default const constructor. + const PurchaseStateConverter(); + + /// Converts the purchase state stored in `object` to a [PurchaseStatus]. + PurchaseStatus toPurchaseStatus(bool object) { + switch (object) { + case false: + return PurchaseStatus.purchased; + case true: + return PurchaseStatus.canceled; + default: + return PurchaseStatus.error; + } + } +} + +/// The type of product. +/// Enum representing potential [ItemDetails.itemType]s and [InvoiceDetails.itemType]s. +/// Wraps +/// [`Product`](https://developer.samsung.com/smarttv/develop/guides/samsung-checkout/samsung-checkout-dpi-portal.html#Product) +/// See the linked documentation for an explanation of the different constants. +@JsonEnum(alwaysCreate: true) +enum ItemType { + /// None type. + @JsonValue(0) + none, + + /// Consumers can purchase this type of product anytime. + @JsonValue(1) + consumable, + + /// Consumers can purchase this type of product only once. + @JsonValue(2) + nonComsumabel, + + /// Once this type of product is purchased, repurchase cannot be made during the time when the product effect set by CP lasts. + @JsonValue(3) + limitedPeriod, + + /// DPI system processes automatic payment on a certain designated cycle. + @JsonValue(4) + subscription +} diff --git a/packages/in_app_purchase/lib/src/billing_manager_wrappers/billing_manager_wrapper.g.dart b/packages/in_app_purchase/lib/src/billing_manager_wrappers/billing_manager_wrapper.g.dart new file mode 100644 index 000000000..deb9ed791 --- /dev/null +++ b/packages/in_app_purchase/lib/src/billing_manager_wrappers/billing_manager_wrapper.g.dart @@ -0,0 +1,203 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'billing_manager_wrapper.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ServiceAvailableAPIResult _$ServiceAvailableAPIResultFromJson( + Map json) => + ServiceAvailableAPIResult( + status: json['status'] as String? ?? '', + result: json['result'] as String? ?? '', + serviceYn: json['serviceYn'] as String? ?? '', + ); + +Map _$ServiceAvailableAPIResultToJson( + ServiceAvailableAPIResult instance) => + { + 'status': instance.status, + 'result': instance.result, + 'serviceYn': instance.serviceYn, + }; + +ProductsListApiResult _$ProductsListApiResultFromJson( + Map json) => + ProductsListApiResult( + cpStatus: json['CPStatus'] as String? ?? '', + cpResult: json['CPResult'] as String? ?? '', + checkValue: json['CheckValue'] as String? ?? '', + totalCount: json['TotalCount'] as int? ?? 0, + itemDetails: (json['ItemDetails'] as List?) + ?.map((e) => ItemDetails.fromJson(e as Map)) + .toList() ?? + [], + ); + +Map _$ProductsListApiResultToJson( + ProductsListApiResult instance) => + { + 'CPStatus': instance.cpStatus, + 'CPResult': instance.cpResult, + 'TotalCount': instance.totalCount, + 'CheckValue': instance.checkValue, + 'ItemDetails': instance.itemDetails, + }; + +ItemDetails _$ItemDetailsFromJson(Map json) => ItemDetails( + seq: json['Seq'] as int? ?? 0, + itemId: json['ItemID'] as String? ?? '', + itemTitle: json['ItemTitle'] as String? ?? '', + itemDesc: json['ItemDesc'] as String? ?? '', + itemType: $enumDecodeNullable(_$ItemTypeEnumMap, json['ItemType']) ?? + ItemType.none, + price: json['Price'] as num? ?? 0, + currencyId: json['CurrencyID'] as String? ?? '', + ); + +Map _$ItemDetailsToJson(ItemDetails instance) => + { + 'Seq': instance.seq, + 'ItemID': instance.itemId, + 'ItemTitle': instance.itemTitle, + 'ItemDesc': instance.itemDesc, + 'ItemType': _$ItemTypeEnumMap[instance.itemType]!, + 'Price': instance.price, + 'CurrencyID': instance.currencyId, + }; + +const _$ItemTypeEnumMap = { + ItemType.none: 0, + ItemType.consumable: 1, + ItemType.nonComsumabel: 2, + ItemType.limitedPeriod: 3, + ItemType.subscription: 4, +}; + +ProductSubscriptionInfo _$ProductSubscriptionInfoFromJson( + Map json) => + ProductSubscriptionInfo( + paymentCycle: json['PaymentCycle'] as int? ?? 0, + paymentCycleFrq: json['PaymentCycleFrq'] as int? ?? 0, + paymentCyclePeriod: json['PaymentCyclePeriod'] as String? ?? '', + ); + +Map _$ProductSubscriptionInfoToJson( + ProductSubscriptionInfo instance) => + { + 'PaymentCyclePeriod': instance.paymentCyclePeriod, + 'PaymentCycleFrq': instance.paymentCycleFrq, + 'PaymentCycle': instance.paymentCycle, + }; + +BillingBuyData _$BillingBuyDataFromJson(Map json) => + BillingBuyData( + payResult: json['payResult'] as String? ?? '', + payDetails: (json['payDetails'] as Map?)?.map( + (k, e) => MapEntry(k, e as String), + ) ?? + {}, + ); + +Map _$BillingBuyDataToJson(BillingBuyData instance) => + { + 'payResult': instance.payResult, + 'payDetails': instance.payDetails, + }; + +GetUserPurchaseListAPIResult _$GetUserPurchaseListAPIResultFromJson( + Map json) => + GetUserPurchaseListAPIResult( + cpStatus: json['CPStatus'] as String? ?? '', + cpResult: json['CPResult'] as String? ?? '', + invoiceDetails: (json['InvoiceDetails'] as List?) + ?.map((e) => InvoiceDetails.fromJson(e as Map)) + .toList() ?? + [], + totalCount: json['TotalCount'] as int? ?? 0, + checkValue: json['CheckValue'] as String? ?? '', + ); + +Map _$GetUserPurchaseListAPIResultToJson( + GetUserPurchaseListAPIResult instance) => + { + 'CPStatus': instance.cpStatus, + 'CPResult': instance.cpResult, + 'TotalCount': instance.totalCount, + 'CheckValue': instance.checkValue, + 'InvoiceDetails': instance.invoiceDetails, + }; + +InvoiceDetails _$InvoiceDetailsFromJson(Map json) => + InvoiceDetails( + seq: json['Seq'] as int? ?? 0, + invoiceId: json['InvoiceID'] as String? ?? '', + itemId: json['ItemID'] as String? ?? '', + itemTitle: json['ItemTitle'] as String? ?? '', + itemType: $enumDecodeNullable(_$ItemTypeEnumMap, json['ItemType']) ?? + ItemType.none, + orderTime: json['OrderTime'] as String? ?? '', + price: json['Price'] as num? ?? 0, + orderCurrencyId: json['OrderCurrencyID'] as String? ?? '', + appliedStatus: json['AppliedStatus'] as bool? ?? false, + cancelStatus: json['CancelStatus'] as bool? ?? false, + appliedTime: json['AppliedTime'] as String? ?? '', + period: json['Period'] as int? ?? 0, + limitEndTime: json['LimitEndTime'] as String? ?? '', + remainTime: json['RemainTime'] as String? ?? '', + ); + +Map _$InvoiceDetailsToJson(InvoiceDetails instance) => + { + 'Seq': instance.seq, + 'InvoiceID': instance.invoiceId, + 'ItemID': instance.itemId, + 'ItemTitle': instance.itemTitle, + 'ItemType': _$ItemTypeEnumMap[instance.itemType]!, + 'OrderTime': instance.orderTime, + 'Period': instance.period, + 'Price': instance.price, + 'OrderCurrencyID': instance.orderCurrencyId, + 'CancelStatus': instance.cancelStatus, + 'AppliedStatus': instance.appliedStatus, + 'AppliedTime': instance.appliedTime, + 'LimitEndTime': instance.limitEndTime, + 'RemainTime': instance.remainTime, + }; + +PurchaseSubscriptionInfo _$PurchaseSubscriptionInfoFromJson( + Map json) => + PurchaseSubscriptionInfo( + subscriptionId: json['SubscriptionId'] as String? ?? '', + subsStartTime: json['SubsStartTime'] as String? ?? '', + subsEndTime: json['SubsEndTime'] as String? ?? '', + subsStatus: json['SubsStatus'] as String? ?? '', + ); + +Map _$PurchaseSubscriptionInfoToJson( + PurchaseSubscriptionInfo instance) => + { + 'SubscriptionId': instance.subscriptionId, + 'SubsStartTime': instance.subsStartTime, + 'SubsEndTime': instance.subsEndTime, + 'SubsStatus': instance.subsStatus, + }; + +VerifyInvoiceAPIResult _$VerifyInvoiceAPIResultFromJson( + Map json) => + VerifyInvoiceAPIResult( + cpStatus: json['CPStatus'] as String? ?? '', + cpResult: json['CPResult'] as String? ?? '', + appId: json['AppID'] as String? ?? '', + invoiceId: json['InvoiceID'] as String? ?? '', + ); + +Map _$VerifyInvoiceAPIResultToJson( + VerifyInvoiceAPIResult instance) => + { + 'CPStatus': instance.cpStatus, + 'CPResult': instance.cpResult, + 'AppID': instance.appId, + 'InvoiceID': instance.invoiceId, + }; diff --git a/packages/in_app_purchase/lib/src/channel.dart b/packages/in_app_purchase/lib/src/channel.dart new file mode 100644 index 000000000..c08790c68 --- /dev/null +++ b/packages/in_app_purchase/lib/src/channel.dart @@ -0,0 +1,9 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; + +/// Method channel for the plugin's platform<-->Dart calls. +const MethodChannel channel = + MethodChannel('plugins.flutter.tizen.io/in_app_purchase'); diff --git a/packages/in_app_purchase/lib/src/in_app_purchase_tizen_platform.dart b/packages/in_app_purchase/lib/src/in_app_purchase_tizen_platform.dart new file mode 100644 index 000000000..246c6da7e --- /dev/null +++ b/packages/in_app_purchase/lib/src/in_app_purchase_tizen_platform.dart @@ -0,0 +1,180 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +import '../billing_manager_wrappers.dart'; +import '../in_app_purchase_tizen.dart'; + +/// [IAPError.code] code for failed purchases. +const String kPurchaseErrorCode = 'purchase_error'; + +/// Indicates store front is Samsung Checkout +const String kIAPSource = 'samsung_checkout'; + +/// An [InAppPurchasePlatform] that wraps BillingManager. +/// +/// This translates various `BillingManager` calls and responses into the +/// generic plugin API. +class InAppPurchaseTizenPlatform extends InAppPurchasePlatform { + /// Creates an [InAppPurchaseTizenPlatform] instance. + InAppPurchaseTizenPlatform(); + + /// Registers this class as the default instance of [InAppPurchasePlatform]. + static void register() { + // Register the [InAppPurchaseTizenPlatformAddition] containing + // Samsung Checkout-specific functionality. + InAppPurchasePlatformAddition.instance = + InAppPurchaseTizenPlatformAddition(billingManager); + + // Register the platform-specific implementation of the idiomatic + // InAppPurchase API. + InAppPurchasePlatform.instance = InAppPurchaseTizenPlatform(); + } + + static final StreamController> + _purchaseUpdatedController = + StreamController>.broadcast(); + + @override + Stream> get purchaseStream => + _purchaseUpdatedController.stream; + + /// The [BillingManager] that's abstracted by Samsung Checkout. + /// + /// This field should not be used out of test code. + @visibleForTesting + static final BillingManager billingManager = BillingManager(); + + @override + Future isAvailable() async { + return billingManager.isAvailable(); + } + + @override + Future queryProductDetails( + Set identifiers) async { + ProductsListApiResult response; + PlatformException? exception; + try { + response = await billingManager.requestProducts(identifiers.toList()); + } on PlatformException catch (e) { + exception = e; + response = const ProductsListApiResult( + cpStatus: 'fail to receive response', + cpResult: 'fail to receive response', + checkValue: 'fail to receive response', + totalCount: 0, + itemDetails: []); + } + + List productDetailsList = + []; + final List invalidMessage = []; + + if (response.cpStatus == '100000') { + productDetailsList = response.itemDetails + .map((ItemDetails productWrapper) => + SamsungCheckoutProductDetails.fromProduct(productWrapper)) + .toList(); + } else { + invalidMessage.add(response.toJson().toString()); + } + + final ProductDetailsResponse productDetailsResponse = + ProductDetailsResponse( + productDetails: productDetailsList, + notFoundIDs: invalidMessage, + error: exception == null + ? null + : IAPError( + source: kIAPSource, + code: exception.code, + message: exception.message ?? '', + details: exception.details), + ); + return productDetailsResponse; + } + + @override + Future restorePurchases({ + String? applicationUserName, + }) async { + List responses; + + responses = await Future.wait(>[ + billingManager.requestPurchases() + ]); + + final List pastPurchases = + responses.expand((GetUserPurchaseListAPIResult response) { + if (response.cpStatus == '100000') { + return response.invoiceDetails; + } else { + return []; + } + }).map((InvoiceDetails purchaseWrapper) { + final SamsungCheckoutPurchaseDetails purchaseDetails = + SamsungCheckoutPurchaseDetails.fromPurchase(purchaseWrapper); + + purchaseDetails.status = PurchaseStatus.restored; + return purchaseDetails; + }).toList(); + _purchaseUpdatedController.add(pastPurchases); + } + + @override + Future buyNonConsumable({required PurchaseParam purchaseParam}) async { + final BillingBuyData billingResultWrapper = await billingManager.buyItem( + orderItemId: purchaseParam.productDetails.id, + orderTitle: purchaseParam.productDetails.title, + orderTotal: purchaseParam.productDetails.price, + orderCurrencyId: purchaseParam.productDetails.currencyCode); + + if (billingResultWrapper.payResult == 'SUCCESS') { + final String invoiceId = + billingResultWrapper.payDetails['InvoiceID'] ?? ''; + + billingManager + .requestPurchases() + .then((GetUserPurchaseListAPIResult responses) { + for (int i = 0; i < responses.invoiceDetails.length; i++) { + if (responses.invoiceDetails[i].invoiceId == invoiceId) { + final List purchases = []; + purchases.add(PurchaseDetails( + purchaseID: responses.invoiceDetails[i].invoiceId, + productID: responses.invoiceDetails[i].itemId, + verificationData: PurchaseVerificationData( + localVerificationData: responses.invoiceDetails[i].invoiceId, + serverVerificationData: responses.invoiceDetails[i].invoiceId, + source: kIAPSource), + transactionDate: responses.invoiceDetails[i].orderTime, + status: const PurchaseStateConverter() + .toPurchaseStatus(responses.invoiceDetails[i].cancelStatus), + )); + + _purchaseUpdatedController.add(purchases); + } + } + }).catchError((Object error) { + _purchaseUpdatedController.addError(error); + }); + + return true; + } else { + return false; + } + } + + @override + Future buyConsumable( + {required PurchaseParam purchaseParam, bool autoConsume = true}) { + assert(autoConsume == true, 'On Tizen, we should always auto consume'); + return buyNonConsumable(purchaseParam: purchaseParam); + } +} diff --git a/packages/in_app_purchase/lib/src/in_app_purchase_tizen_platform_addition.dart b/packages/in_app_purchase/lib/src/in_app_purchase_tizen_platform_addition.dart new file mode 100644 index 000000000..d4ae44a8e --- /dev/null +++ b/packages/in_app_purchase/lib/src/in_app_purchase_tizen_platform_addition.dart @@ -0,0 +1,67 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +import '../billing_manager_wrappers.dart'; + +/// Contains InApp Purchase features that are only available on SamsungCheckout. +class InAppPurchaseTizenPlatformAddition extends InAppPurchasePlatformAddition { + /// Creates a [InAppPurchaseTizenPlatformAddition] which uses the supplied + /// `BillingManager` to provide Tizen specific features. + InAppPurchaseTizenPlatformAddition(this._billingManager); + + final BillingManager _billingManager; + + /// Set all request parameters that SamsungCheckout DPI Portal needed. + /// + /// The `appId` is your application id, it is required. + /// + /// The `pageSize` is the number of products retrieved per page.(>=1,<=100) + /// Use it when call `queryProductDetails`. + /// + /// The `pageNum` is the requested page number.(>=1) + /// Use it when call `queryProductDetails` and `restorePurchases`. + /// + /// The `securityKey` is DPI security key. + /// Use it when call `queryProductDetails` and `restorePurchases`. + /// + /// See README.md file to find how to get these values. + void setRequestParameters({ + required String appId, + int? pageSize, + int? pageNum, + String? securityKey, + }) { + final Map requestParameters = { + 'appId': appId, + 'pageSize': pageSize, + 'pageNum': pageNum, + 'securityKey': securityKey, + }; + _billingManager.setRequestParameters(requestParameters); + } + + /// Check whether a purchase, corresponding to the requested "InvoiceID", was successful. + Future verifyPurchase( + {required PurchaseDetails purchaseDetails}) async { + VerifyInvoiceAPIResult verifyPurchaseResult; + try { + verifyPurchaseResult = await _billingManager.verifyInvoice( + invoiceId: purchaseDetails.verificationData.serverVerificationData); + } on PlatformException { + verifyPurchaseResult = const VerifyInvoiceAPIResult( + appId: 'error appId', + cpStatus: 'error cpStatus', + invoiceId: 'error invoiceId'); + } + + if (verifyPurchaseResult.cpStatus == '100000') { + return true; + } else { + return false; + } + } +} diff --git a/packages/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/pubspec.yaml new file mode 100644 index 000000000..10b7549fb --- /dev/null +++ b/packages/in_app_purchase/pubspec.yaml @@ -0,0 +1,28 @@ +name: in_app_purchase_tizen +description: Tizen implementation of the in_app_purchase plugin for Samsung Smart TV. +homepage: https://github.com/flutter-tizen/plugins +repository: https://github.com/flutter-tizen/plugins/tree/master/packages/in_app_purchase +version: 0.1.0 + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +flutter: + plugin: + platforms: + tizen: + pluginClass: InAppPurchaseTizenPlugin + fileName: in_app_purchase_tizen_plugin.h + dartPluginClass: InAppPurchaseTizenPlatform + +dependencies: + crypto: ^3.0.2 + flutter: + sdk: flutter + in_app_purchase_platform_interface: ^1.3.3 + json_annotation: ^4.6.0 + +dev_dependencies: + build_runner: ^2.0.0 + json_serializable: ^6.3.1 diff --git a/packages/in_app_purchase/tizen/.gitignore b/packages/in_app_purchase/tizen/.gitignore new file mode 100644 index 000000000..a2a7d62b1 --- /dev/null +++ b/packages/in_app_purchase/tizen/.gitignore @@ -0,0 +1,5 @@ +.cproject +.sign +crash-info/ +Debug/ +Release/ diff --git a/packages/in_app_purchase/tizen/inc/in_app_purchase_tizen_plugin.h b/packages/in_app_purchase/tizen/inc/in_app_purchase_tizen_plugin.h new file mode 100644 index 000000000..87b8babde --- /dev/null +++ b/packages/in_app_purchase/tizen/inc/in_app_purchase_tizen_plugin.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_IN_APP_PURCHASE_TIZEN_PLUGIN_H_ +#define FLUTTER_PLUGIN_IN_APP_PURCHASE_TIZEN_PLUGIN_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default"))) +#else +#define FLUTTER_PLUGIN_EXPORT +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void InAppPurchaseTizenPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_IN_APP_PURCHASE_TIZEN_PLUGIN_H_ diff --git a/packages/in_app_purchase/tizen/project_def.prop b/packages/in_app_purchase/tizen/project_def.prop new file mode 100644 index 000000000..1c2901b34 --- /dev/null +++ b/packages/in_app_purchase/tizen/project_def.prop @@ -0,0 +1,20 @@ +# See https://docs.tizen.org/application/tizen-studio/native-tools/project-conversion +# for details. + +APPNAME = in_app_purchase_tizen_plugin +type = staticLib +profile = common-5.5 + +# Source files +USER_SRCS += src/*.cc + +# User defines +USER_DEFS = +USER_UNDEFS = +USER_CPP_DEFS = FLUTTER_PLUGIN_IMPL +USER_CPP_UNDEFS = + +# User includes +USER_INC_DIRS = inc src +USER_INC_FILES = +USER_CPP_INC_FILES = diff --git a/packages/in_app_purchase/tizen/src/billing_manager.cc b/packages/in_app_purchase/tizen/src/billing_manager.cc new file mode 100644 index 000000000..43579cb74 --- /dev/null +++ b/packages/in_app_purchase/tizen/src/billing_manager.cc @@ -0,0 +1,298 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "billing_manager.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include "log.h" + +const char *kInvalidArgument = "Invalid argument"; + +static std::string ServerTypeToString(billing_server_type server_type) { + switch (server_type) { + case SERVERTYPE_OPERATE: + return "PRD"; + break; + case SERVERTYPE_DEV: + return "DEV"; + break; + default: + return "NONE"; + break; + } +} + +template +static bool GetValueFromEncodableMap(const flutter::EncodableMap *map, + const char *key, T &out) { + auto iter = map->find(flutter::EncodableValue(key)); + if (iter != map->end() && !iter->second.IsNull()) { + if (auto *value = std::get_if(&iter->second)) { + out = *value; + return true; + } + } + return false; +} + +template +static T GetRequiredArg(const flutter::EncodableMap *arguments, + const char *key) { + T value; + if (GetValueFromEncodableMap(arguments, key, value)) { + return value; + } + std::string message = + "No " + std::string(key) + " provided or has invalid type or value."; + throw std::invalid_argument(message); +} + +bool BillingManager::Init() { + LOG_INFO("[BillingManager] Init billing api."); + + if (!BillingWrapper::GetInstance().Initialize()) { + LOG_ERROR("[BillingManager] Fail to initialize billing APIs."); + return false; + } + return true; +} + +std::string BillingManager::GetCustomId() { + void *handle = dlopen("libsso_api.so", RTLD_LAZY); + std::string custom_id = ""; + if (!handle) { + LOG_ERROR("[BillingManager] Fail to open sso APIs."); + } else { + FuncSsoGetLoginInfo sso_get_login_info = + reinterpret_cast( + dlsym(handle, "sso_get_login_info")); + if (sso_get_login_info) { + sso_login_info_s login_info; + if (!sso_get_login_info(&login_info)) { + custom_id = login_info.uid; + } + } + dlclose(handle); + } + return custom_id; +} + +std::string BillingManager::GetCountryCode() { + void *handle = dlopen("libvconf.so.0.3.1", RTLD_LAZY); + char *country_code = ""; + if (!handle) { + LOG_ERROR("[BillingManager] Fail to open vconf APIs."); + } else { + FuncVconfGetStr vconf_get_str = + reinterpret_cast(dlsym(handle, "vconf_get_str")); + if (vconf_get_str) { + country_code = vconf_get_str("db/comss/countrycode"); + } + dlclose(handle); + } + return country_code; +} + +bool BillingManager::BillingIsAvailable( + std::unique_ptr> result) { + LOG_INFO("[BillingManager] Check billing server is available."); + + void *handle = dlopen("libcapi-system-info.so.0.2.1", RTLD_LAZY); + if (!handle) { + LOG_ERROR("[BillingManager] Fail to open system APIs."); + } else { + FuncSystemInfGetValueInt system_info_get_value_int = + reinterpret_cast( + dlsym(handle, "system_info_get_value_int")); + if (system_info_get_value_int) { + int tv_server_type = 0; + int ret = system_info_get_value_int(SYSTEM_INFO_KEY_INFO_LINK_SERVER_TYPE, + &tv_server_type); + if (ret == SYSTEM_INFO_ERROR_NONE) { + switch (tv_server_type) { + case PRD: + billing_server_type_ = SERVERTYPE_OPERATE; + break; + case DEV: + billing_server_type_ = SERVERTYPE_DEV; + break; + default: + billing_server_type_ = SERVERTYPE_NONE; + break; + } + LOG_INFO("[BillingManager] Billing Server Type is %d", + billing_server_type_); + } else { + LOG_ERROR("[BillingManager] Fail to get TV server type."); + dlclose(handle); + return false; + } + } + dlclose(handle); + } + + bool ret = BillingWrapper::GetInstance().service_billing_is_service_available( + billing_server_type_, OnAvailable, result.release()); + if (!ret) { + LOG_ERROR("[BillingManager] service_billing_is_service_available failed."); + return false; + } + return true; +} + +bool BillingManager::GetProductList( + const char *app_id, const char *country_code, int page_size, + int page_number, const char *check_value, + std::unique_ptr> result) { + LOG_INFO("[BillingManager] Start get product list."); + + bool ret = BillingWrapper::GetInstance().service_billing_get_products_list( + app_id, country_code, page_size, page_number, check_value, + billing_server_type_, OnProducts, result.release()); + if (!ret) { + LOG_ERROR("[BillingManager] service_billing_get_products_list failed."); + return false; + } + return true; +} + +bool BillingManager::GetPurchaseList( + const char *app_id, const char *custom_id, const char *country_code, + int page_number, const char *check_value, + std::unique_ptr> result) { + LOG_INFO("[BillingManager] Start get purchase list."); + + bool ret = BillingWrapper::GetInstance().service_billing_get_purchase_list( + app_id, custom_id, country_code, page_number, check_value, + billing_server_type_, OnPurchase, result.release()); + if (!ret) { + LOG_ERROR("[BillingManager] service_billing_get_purchase_list failed."); + return false; + } + return true; +} + +bool BillingManager::BuyItem( + const char *app_id, const char *detail_info, + std::unique_ptr> result) { + LOG_INFO("[BillingManager] Start buy item"); + + bool ret = BillingWrapper::GetInstance().service_billing_buyitem( + app_id, ServerTypeToString(billing_server_type_).c_str(), detail_info); + BillingWrapper::GetInstance().service_billing_set_buyitem_cb( + OnBuyItem, result.release()); + if (!ret) { + LOG_ERROR("[BillingManager] service_billing_buyitem failed."); + return false; + } + return true; +} + +bool BillingManager::VerifyInvoice( + const char *app_id, const char *custom_id, const char *invoice_id, + const char *country_code, + std::unique_ptr> result) { + LOG_INFO("[BillingManager] Start verify invoice"); + + bool ret = BillingWrapper::GetInstance().service_billing_verify_invoice( + app_id, custom_id, invoice_id, country_code, billing_server_type_, + OnVerify, result.release()); + if (!ret) { + LOG_ERROR("[BillingManager] service_billing_verify_invoice failed."); + return false; + } + return true; +} + +void BillingManager::OnAvailable(const char *detail_result, void *user_data) { + LOG_INFO("[BillingManager] Billing server detail_result: %s", detail_result); + + flutter::MethodResult *result = + reinterpret_cast *>( + user_data); + if (result) { + result->Success(flutter::EncodableValue(std::string(detail_result))); + } else { + result->Error("OnAvailable Failed", "method result is null !"); + } + delete (result); +} + +void BillingManager::OnProducts(const char *detail_result, void *user_data) { + LOG_INFO("[BillingManager] Productlist: %s", detail_result); + + flutter::MethodResult *result = + reinterpret_cast *>( + user_data); + if (result) { + result->Success(flutter::EncodableValue(std::string(detail_result))); + } else { + result->Error("OnProducts Failed", "method result is null !"); + } + delete (result); +} + +void BillingManager::OnPurchase(const char *detail_result, void *user_data) { + LOG_INFO("[BillingManager] Purchaselist: %s", detail_result); + + flutter::MethodResult *result = + reinterpret_cast *>( + user_data); + if (result) { + result->Success(flutter::EncodableValue(std::string(detail_result))); + } else { + result->Error("OnPurchase Failed", "method result is null !"); + } + delete (result); +} + +bool BillingManager::OnBuyItem(const char *pay_result, const char *detail_info, + void *user_data) { + LOG_INFO("[BillingManager] Buy items result: %s, result details: %s", + pay_result, detail_info); + + flutter::EncodableMap result_map = { + {flutter::EncodableValue("PayResult"), + flutter::EncodableValue(pay_result)}, + }; + + flutter::MethodResult *result = + reinterpret_cast *>( + user_data); + if (result) { + result->Success(flutter::EncodableValue(result_map)); + } else { + result->Error("OnBuyItem Failed", "method result is null !"); + } + delete (result); + return true; +} + +void BillingManager::OnVerify(const char *detail_result, void *user_data) { + LOG_INFO("[BillingManager] Verify details: %s", detail_result); + + flutter::MethodResult *result = + reinterpret_cast *>( + user_data); + if (result) { + result->Success(flutter::EncodableValue(std::string(detail_result))); + } else { + result->Error("OnVerify Failed", "method result is null !"); + } + delete (result); +} + +void BillingManager::Dispose() { + LOG_INFO("[BillingManager] Dispose billing."); + + method_channel_->SetMethodCallHandler(nullptr); +} diff --git a/packages/in_app_purchase/tizen/src/billing_manager.h b/packages/in_app_purchase/tizen/src/billing_manager.h new file mode 100644 index 000000000..7383d0770 --- /dev/null +++ b/packages/in_app_purchase/tizen/src/billing_manager.h @@ -0,0 +1,187 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_BILLING_MANAGER_H +#define FLUTTER_PLUGIN_BILLING_MANAGER_H + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "billing_service_proxy.h" + +#define SSO_API_MAX_STRING_LEN 128 + +typedef struct sso_login_info { + char login_id[SSO_API_MAX_STRING_LEN]; + char login_pwd[SSO_API_MAX_STRING_LEN]; + char login_guid[SSO_API_MAX_STRING_LEN]; + char uid[SSO_API_MAX_STRING_LEN]; + char user_icon[SSO_API_MAX_STRING_LEN * 8]; +} sso_login_info_s; + +typedef enum { + PRD = 0, + DEV, +} tv_server_type; + +typedef enum { + SYSTEM_INFO_ERROR_NONE = TIZEN_ERROR_NONE, /**< Successful */ + SYSTEM_INFO_ERROR_INVALID_PARAMETER = + TIZEN_ERROR_INVALID_PARAMETER, /**< Invalid parameter */ + SYSTEM_INFO_ERROR_OUT_OF_MEMORY = + TIZEN_ERROR_OUT_OF_MEMORY, /**< Out of memory */ + SYSTEM_INFO_ERROR_IO_ERROR = + TIZEN_ERROR_IO_ERROR, /**< An input/output error occurred when reading + value from system */ + SYSTEM_INFO_ERROR_PERMISSION_DENIED = + TIZEN_ERROR_PERMISSION_DENIED, /**< No permission to use the API */ + SYSTEM_INFO_ERROR_NOT_SUPPORTED = + TIZEN_ERROR_NOT_SUPPORTED, /**< Not supported parameter (Since 3.0) */ +} system_info_error_e; + +typedef enum { + SYSTEM_INFO_KEY_NUM_OF_TUNER = 18, + SYSTEM_INFO_KEY_STAMRT_HUB_HBBTV_SUPPORTED = 41, + SYSTEM_INFO_KEY_SMART_LED_SUPPORTED = 56, + SYSTEM_INFO_KEY_KR_CABLE_QAM_SUPPORTED = 59, + SYSTEM_INFO_KEY_SMART_LED_DEMO_POSITION = 63, + SYSTEM_INFO_KEY_PANEL_SIZE = 69, + SYSTEM_INFO_KEY_PANEL_TYPE = 73, + SYSTEM_INFO_KEY_PANEL_TYPE_STRING = 74, + SYSTEM_INFO_KEY_LOCAL_SET = 90, + SYSTEM_INFO_KEY_WIFI_REGION = 109, + SYSTEM_INFO_KEY_NUM_OF_DTV = 110, + SYSTEM_INFO_KEY_NUM_OF_ATV = 112, + SYSTEM_INFO_KEY_NUM_OF_RVU = 121, + SYSTEM_INFO_KEY_NUM_OF_HDMI = 122, + SYSTEM_INFO_KEY_INFO_LINK_SERVER_TYPE = 126, + SYSTEM_INFO_KEY_REGION_KIND = 127, + SYSTEM_INFO_KEY_SW_VERSION = 128, + SYSTEM_INFO_KEY_TUNER_TYPE = 131, + SYSTEM_INFO_KEY_VERSION_MICOM = 133, + SYSTEM_INFO_KEY_VERSION_EMANUAL = 138, + SYSTEM_INFO_KEY_AUTOSTORE = 174, + SYSTEM_INFO_KEY_SERIAL_NUMBER = 187, + SYSTEM_INFO_KEY_SW_VERSION_MODEL = 199, + SYSTEM_INFO_KEY_TARGET_LOCATION = 201, + SYSTEM_INFO_KEY_PANEL_VFREQ = 204, + SYSTEM_INFO_KEY_PANEL_ASPECT_RATIO = 205, + SYSTEM_INFO_KEY_VERSION_EPOP_APP = 209, + SYSTEM_INFO_KEY_VERSION_WIFI = 216, + SYSTEM_INFO_KEY_SATELLITE_MASK = 220, + SYSTEM_INFO_KEY_LANGUAGE_LIST = 227, + SYSTEM_INFO_KEY_USB_COPY_FORMAT_SUPPORTED = 236, + SYSTEM_INFO_KEY_NUM_OF_PVR_RECORD = 238, + SYSTEM_INFO_KEY_CERT_OPTION = 240, + SYSTEM_INFO_KEY_GET_JP_MIGRATION_BACKUP_SOURCE_PATH = 247, + SYSTEM_INFO_KEY_GET_JP_MIGRATION_RESTORE_SOURCE_PATH = 248, + SYSTEM_INFO_KEY_AUTOMOTIONPLUS_CLEAR_BLUR_SUPPORTED = 258, + SYSTEM_INFO_KEY_CHECK_SKIP_LOCALSET = 280, + SYSTEM_INFO_KEY_CHECK_WIFI_VENDOR = 281, + SYSTEM_INFO_KEY_LOCAL_SET_ENUM = 284, + SYSTEM_INFO_KEY_REGION_KIND_ENUM = 285, + SYSTEM_INFO_KEY_PRODUCT_CODE_SW = 291, + SYSTEM_INFO_KEY_PRODUCT_CODE_BOM = 292, + SYSTEM_INFO_KEY_ONTV_SUPPORTED = 304, + SYSTEM_INFO_KEY_HOTEL_TV_SUPPORTED = 315, + SYSTEM_INFO_KEY_ERROR_POPUP_ON_OFF = 320, + SYSTEM_INFO_KEY_TUNER_SHAPE = 325, + SYSTEM_INFO_KEY_PANEL_DEFAULT_VFREQ = 326, + SYSTIM_INFO_KEY_DTV_TYPE = 333, + SYSTEM_INFO_KEY_TUNER_SUP_EPOP = 342, + SYSTEM_INFO_KEY_CES_OPTION = 343, + SYSTEM_INFO_KEY_UPDATE_SUP_EMANUAL = 352, + SYSTEM_INFO_KEY_ENERGY_STAR_LOGO_SUPPORTED = 360, + SYSTEM_INFO_KEY_FHD_EVK_TV_YEAR = 362, + SYSTEM_INFO_KEY_TV_CURRENT_YEAR = 367, + SYSTEM_INFO_KEY_PC_DIMMING_SUPPORT = 370, + SYSTEM_INFO_KEY_EVK_SW_VERSION = 378, + SYSTEM_INFO_KEY_PNP_COUNTRY_LIST = 388, + SYSTEM_INFO_KEY_DIGITAL_COUNTRY_LIST = 389, + SYSTEM_INFO_KEY_ANALOG_COUNTRY_LIST = 390, + SYSTEM_INFO_KEY_VERSION_CAMERA = 457, + SYSTEM_INFO_KEY_VERSION_MIC = 458, + SYSTEM_INFO_KEY_ALWAYS_INSTANT_ON_SUPPORT = 466, + SYSTEM_INFO_KEY_APP_BOOTING_SUPPORT = 488, + SYSTEM_INFO_KEY_PNP_LANGUAGE_LIST = 498, + SYSTEM_INFO_KEY_MODEL_SERIES_INFO = 505, + SYSTEM_INFO_KEY_IOT_HUB_SUPPORTED = 510, + SYSTEM_INFO_KEY_DEFAULT_DIGITAL_COUNTRY = 511, + SYSTEM_INFO_KEY_DEFAULT_ANALOG_COUNTRY = 512, + SYSTEM_INFO_KEY_CH_MAP = 517, + SYSTEM_INFO_KEY_A_PICTURE_DIRECT = 518, + SYSTEM_INFO_KEY_MIN_BACKLIGHT = 532, + SYSTEM_INFO_KEY_CLOUD_SCAN_UPLOAD = 537, + SYSTEM_INFO_KEY_DEFAULT_HDMI_1_BOOTING = 538, + SYSTEM_INFO_KEY_PLATFORM_TYPE = 564, + SYSTEM_INFO_KEY_FRAME_TV = 583, + SYSTEM_INFO_KEY_360VR_SUPPORT = 589, + SYSTEM_INFO_KEY_DYNAMIC_CONTRAST = 590, + SYSTEM_INFO_KEY_RUN_EW = 600, + SYSTEM_INFO_KEY_EXHIBITION_MODE = 601, + SYSTEM_INFO_KEY_ATSC3_SUPPORTED = 604, + SYSTEM_INFO_KEY_PANEL_TIME = 605, + SYSTEM_INFO_KEY_CN_WEB_MODEL = 620, + SYSTEM_INFO_KEY_HOTEL_MIN_VOLUME = 645, + SYSTEM_INFO_KEY_HOTEL_MAX_VOLUME = 646, + SYSTEM_INFO_KEY_HOTEL_MODE = 647, + SYSTEM_INFO_KEY_HOTEL_POWER_ON_VOLUME = 648, + SYSTEM_INFO_KEY_NUM_OF_DISPLAY = 656, + SYSTEM_INFO_KEY_STD_HDR_BL = 668, + SYSTEM_INFO_KEY_HARDWARE_VERSION = 669, + SYSTEM_INFO_KEY_PANEL_TYPE_82INCH_SDC = 670, +} system_info_key_e; + +typedef bool (*FuncSsoGetLoginInfo)(sso_login_info_s *login_info); +typedef char *(*FuncVconfGetStr)(const char *in_key); +typedef int (*FuncSystemInfGetValueInt)(system_info_key_e key, int *value); + +class BillingManager { + public: + explicit BillingManager() {} + ~BillingManager(){}; + + bool Init(); + void Dispose(); + bool BillingIsAvailable( + std::unique_ptr> result); + bool BuyItem( + const char *app_id, const char *detail_info, + std::unique_ptr> result); + bool GetProductList( + const char *app_id, const char *country_code, int page_size, + int page_number, const char *check_value, + std::unique_ptr> result); + bool GetPurchaseList( + const char *app_id, const char *custom_id, const char *country_code, + int page_number, const char *check_value, + std::unique_ptr> result); + bool VerifyInvoice( + const char *app_id, const char *custom_id, const char *invoice_id, + const char *country_code, + std::unique_ptr> result); + std::string GetCustomId(); + std::string GetCountryCode(); + + private: + static void OnProducts(const char *detail_result, void *user_data); + static void OnPurchase(const char *detail_result, void *user_data); + static bool OnBuyItem(const char *pay_result, const char *detail_info, + void *user_data); + static void OnAvailable(const char *detail_result, void *user_data); + static void OnVerify(const char *detail_result, void *user_data); + + std::unique_ptr> + method_channel_ = nullptr; + billing_server_type billing_server_type_; +}; + +#endif // FLUTTER_PLUGIN_BILLING_MANAGER_H diff --git a/packages/in_app_purchase/tizen/src/billing_service_proxy.cc b/packages/in_app_purchase/tizen/src/billing_service_proxy.cc new file mode 100644 index 000000000..9d2c12ffa --- /dev/null +++ b/packages/in_app_purchase/tizen/src/billing_service_proxy.cc @@ -0,0 +1,97 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "billing_service_proxy.h" + +#include + +BillingWrapper::BillingWrapper() { + handle_ = dlopen("libbilling_api.so", RTLD_LAZY); +} + +BillingWrapper::~BillingWrapper() { + if (handle_) { + dlclose(handle_); + } +} + +bool BillingWrapper::Initialize() { + if (!handle_) { + return false; + } + + get_products_list = reinterpret_cast( + dlsym(handle_, "service_billing_get_products_list")); + get_purchase_list = reinterpret_cast( + dlsym(handle_, "service_billing_get_purchase_list")); + is_service_available = reinterpret_cast( + dlsym(handle_, "service_billing_is_service_available")); + buyitem = + reinterpret_cast(dlsym(handle_, "service_billing_buyitem")); + set_buyitem_cb = reinterpret_cast( + dlsym(handle_, "service_billing_set_buyitem_cb")); + verify_invoice = reinterpret_cast( + dlsym(handle_, "service_billing_verify_invoice")); + return get_products_list && get_purchase_list && is_service_available && + buyitem && set_buyitem_cb && verify_invoice; +} + +bool BillingWrapper::service_billing_get_products_list( + const char *app_id, const char *country_code, int page_size, + int page_number, const char *check_value, billing_server_type server_type, + billing_payment_api_cb callback, void *user_data) { + if (get_products_list) { + return get_products_list(app_id, country_code, page_size, page_number, + check_value, server_type, callback, user_data); + } + return false; +} + +bool BillingWrapper::service_billing_get_purchase_list( + const char *app_id, const char *custom_id, const char *country_code, + int page_number, const char *check_value, billing_server_type server_type, + billing_payment_api_cb callback, void *user_data) { + if (get_purchase_list) { + return get_purchase_list(app_id, custom_id, country_code, page_number, + check_value, server_type, callback, user_data); + } + return false; +} + +bool BillingWrapper::service_billing_buyitem(const char *app_id, + const char *server_type, + const char *detail_info) { + if (buyitem) { + return buyitem(app_id, server_type, detail_info); + } + return false; +} + +void BillingWrapper::service_billing_set_buyitem_cb(billing_buyitem_cb callback, + void *user_data) { + if (set_buyitem_cb) { + return set_buyitem_cb(callback, user_data); + } + return; +} + +bool BillingWrapper::service_billing_is_service_available( + billing_server_type server_type, billing_payment_api_cb callback, + void *user_data) { + if (is_service_available) { + return is_service_available(server_type, callback, user_data); + } + return false; +} + +bool BillingWrapper::service_billing_verify_invoice( + const char *app_id, const char *custom_id, const char *invoice_id, + const char *country_code, billing_server_type server_type, + billing_payment_api_cb callback, void *user_data) { + if (verify_invoice) { + return verify_invoice(app_id, custom_id, invoice_id, country_code, + server_type, callback, user_data); + } + return false; +} diff --git a/packages/in_app_purchase/tizen/src/billing_service_proxy.h b/packages/in_app_purchase/tizen/src/billing_service_proxy.h new file mode 100644 index 000000000..86ec90a75 --- /dev/null +++ b/packages/in_app_purchase/tizen/src/billing_service_proxy.h @@ -0,0 +1,94 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_PLUGIN_BILLING_SERVICE_PROXY_H_ +#define FLUTTER_PLUGIN_BILLING_SERVICE_PROXY_H_ + +typedef enum { + SERVERTYPE_OPERATE = 10005, + SERVERTYPE_DEV, + SERVERTYPE_WORKING, + SERVERTYPE_DUMMY, + SERVERTYPE_NONE +} billing_server_type; + +typedef void (*billing_payment_api_cb)(const char *detail_result, + void *user_data); +typedef bool (*billing_buyitem_cb)(const char *pay_result, + const char *detail_info, void *user_data); +typedef bool (*FuncGetProductslist)(const char *app_id, + const char *country_code, int page_size, + int page_number, const char *check_value, + billing_server_type server_type, + billing_payment_api_cb callback, + void *user_data); +typedef bool (*FuncGetpurchaselist)(const char *app_id, const char *custom_id, + const char *country_code, int page_number, + const char *check_value, + billing_server_type server_type, + billing_payment_api_cb callback, + void *user_data); +typedef bool (*FuncBuyItem)(const char *app_id, const char *server_type, + const char *detail_info); +typedef void (*FuncSetBuyItemCb)(billing_buyitem_cb callback, void *user_data); +typedef bool (*FuncIsServiceAvailable)(billing_server_type server_type, + billing_payment_api_cb callback, + void *user_data); +typedef bool (*FuncVerifyInvoice)(const char *app_id, const char *custom_id, + const char *invoice_id, + const char *country_code, + billing_server_type server_type, + billing_payment_api_cb callback, + void *user_data); + +class BillingWrapper { + public: + static BillingWrapper &GetInstance() { + static BillingWrapper instance = BillingWrapper(); + return instance; + } + + ~BillingWrapper(); + + BillingWrapper(const BillingWrapper &) = delete; + BillingWrapper &operator=(const BillingWrapper &) = delete; + + bool Initialize(); + + bool service_billing_get_products_list( + const char *app_id, const char *country_code, int page_size, + int page_number, const char *check_value, billing_server_type server_type, + billing_payment_api_cb callback, void *user_data); + bool service_billing_get_purchase_list( + const char *app_id, const char *custom_id, const char *country_code, + int page_number, const char *check_value, billing_server_type server_type, + billing_payment_api_cb callback, void *user_data); + bool service_billing_buyitem(const char *app_id, const char *server_type, + const char *detail_info); + void service_billing_set_buyitem_cb(billing_buyitem_cb callback, + void *user_data); + bool service_billing_is_service_available(billing_server_type server_type, + billing_payment_api_cb callback, + void *user_data); + bool service_billing_verify_invoice(const char *app_id, const char *custom_id, + const char *invoice_id, + const char *country_code, + billing_server_type server_type, + billing_payment_api_cb callback, + void *user_data); + + private: + BillingWrapper(); + + FuncGetProductslist get_products_list = nullptr; + FuncGetpurchaselist get_purchase_list = nullptr; + FuncBuyItem buyitem = nullptr; + FuncSetBuyItemCb set_buyitem_cb = nullptr; + FuncIsServiceAvailable is_service_available = nullptr; + FuncVerifyInvoice verify_invoice = nullptr; + + void *handle_ = nullptr; +}; + +#endif // FLUTTER_PLUGIN_BILLING_SERVICE_PROXY_H_ diff --git a/packages/in_app_purchase/tizen/src/in_app_purchase_tizen_plugin.cc b/packages/in_app_purchase/tizen/src/in_app_purchase_tizen_plugin.cc new file mode 100644 index 000000000..87fe186bd --- /dev/null +++ b/packages/in_app_purchase/tizen/src/in_app_purchase_tizen_plugin.cc @@ -0,0 +1,228 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "in_app_purchase_tizen_plugin.h" + +#include +#include +#include + +#include +#include + +#include "billing_manager.h" +#include "log.h" + +namespace { + +const char *kInvalidArgument = "Invalid argument"; + +template +static bool GetValueFromEncodableMap(const flutter::EncodableMap *map, + const char *key, T &out) { + auto iter = map->find(flutter::EncodableValue(key)); + if (iter != map->end() && !iter->second.IsNull()) { + if (auto *value = std::get_if(&iter->second)) { + out = *value; + return true; + } + } + return false; +} + +template +static T GetRequiredArg(const flutter::EncodableMap *arguments, + const char *key) { + T value; + if (GetValueFromEncodableMap(arguments, key, value)) { + return value; + } + std::string message = + "No " + std::string(key) + " provided or has invalid type or value."; + throw std::invalid_argument(message); +} + +class InAppPurchaseTizenPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrar *plugin_registrar); + + InAppPurchaseTizenPlugin(); + virtual ~InAppPurchaseTizenPlugin() { Dispose(); } + + private: + void Dispose(); + void HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr> result); + void GetProductList( + const flutter::EncodableMap *encodables, + std::unique_ptr> result); + void GetPurchaseList( + const flutter::EncodableMap *encodables, + std::unique_ptr> result); + void BuyItem( + const flutter::EncodableMap *encodables, + std::unique_ptr> result); + void VerifyInvoice( + const flutter::EncodableMap *encodables, + std::unique_ptr> result); + + std::unique_ptr billing_ = nullptr; +}; + +InAppPurchaseTizenPlugin::InAppPurchaseTizenPlugin() { + billing_ = std::make_unique(); + if (!billing_->Init()) { + Dispose(); + } +} + +void InAppPurchaseTizenPlugin::Dispose() { + if (billing_) { + billing_->Dispose(); + } + billing_ = nullptr; +} + +void InAppPurchaseTizenPlugin::RegisterWithRegistrar( + flutter::PluginRegistrar *plugin_registrar) { + auto channel = + std::make_unique>( + plugin_registrar->messenger(), + "plugins.flutter.tizen.io/in_app_purchase", + &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)); + }); + + plugin_registrar->AddPlugin(std::move(plugin)); +} + +void InAppPurchaseTizenPlugin::HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr> result) { + const auto *encodables = + std::get_if(method_call.arguments()); + const auto &method_name = method_call.method_name(); + + try { + if (method_name == "getProductList") { + if (!encodables) { + result->Error(kInvalidArgument, "No arguments provided"); + return; + } + GetProductList(encodables, std::move(result)); + } else if (method_name == "getPurchaseList") { + if (!encodables) { + result->Error(kInvalidArgument, "No arguments provided"); + return; + } + GetPurchaseList(encodables, std::move(result)); + } else if (method_name == "buyItem") { + if (!encodables) { + result->Error(kInvalidArgument, "No arguments provided"); + return; + } + BuyItem(encodables, std::move(result)); + } else if (method_name == "verifyInvoice") { + if (!encodables) { + result->Error(kInvalidArgument, "No arguments provided"); + return; + } + VerifyInvoice(encodables, std::move(result)); + } else if (method_name == "isAvailable") { + if (!billing_->BillingIsAvailable(std::move(result))) { + return; + } + } else if (method_name == "GetCustomId") { + result->Success( + flutter::EncodableValue(std::string(billing_->GetCustomId()))); + } else if (method_name == "GetCountryCode") { + result->Success( + flutter::EncodableValue(std::string(billing_->GetCountryCode()))); + } else { + result->NotImplemented(); + } + } catch (const std::invalid_argument &error) { + result->Error(kInvalidArgument, error.what()); + } +} + +void InAppPurchaseTizenPlugin::GetProductList( + const flutter::EncodableMap *encodables, + std::unique_ptr> result) { + std::string app_id = GetRequiredArg(encodables, "appId"); + std::string country_code = + GetRequiredArg(encodables, "countryCode"); + int64_t page_size = GetRequiredArg(encodables, "pageSize"); + int64_t page_num = GetRequiredArg(encodables, "pageNum"); + std::string check_value = + GetRequiredArg(encodables, "checkValue"); + + if (!billing_->GetProductList(app_id.c_str(), country_code.c_str(), page_size, + page_num, check_value.c_str(), + std::move(result))) { + return; + } +} + +void InAppPurchaseTizenPlugin::GetPurchaseList( + const flutter::EncodableMap *encodables, + std::unique_ptr> result) { + std::string app_id = GetRequiredArg(encodables, "appId"); + std::string custom_id = GetRequiredArg(encodables, "customId"); + std::string country_code = + GetRequiredArg(encodables, "countryCode"); + int64_t page_num = GetRequiredArg(encodables, "pageNum"); + std::string check_value = + GetRequiredArg(encodables, "checkValue"); + + if (!billing_->GetPurchaseList(app_id.c_str(), custom_id.c_str(), + country_code.c_str(), page_num, + check_value.c_str(), std::move(result))) { + return; + } +} + +void InAppPurchaseTizenPlugin::BuyItem( + const flutter::EncodableMap *encodables, + std::unique_ptr> result) { + std::string pay_details = + GetRequiredArg(encodables, "payDetails"); + std::string app_id = GetRequiredArg(encodables, "appId"); + + if (!billing_->BuyItem(app_id.c_str(), pay_details.c_str(), + std::move(result))) { + return; + } +} + +void InAppPurchaseTizenPlugin::VerifyInvoice( + const flutter::EncodableMap *encodables, + std::unique_ptr> result) { + std::string app_id = GetRequiredArg(encodables, "appId"); + std::string custom_id = GetRequiredArg(encodables, "customId"); + std::string invoice_id = GetRequiredArg(encodables, "invoiceId"); + std::string country_code = + GetRequiredArg(encodables, "countryCode"); + + if (!billing_->VerifyInvoice(app_id.c_str(), custom_id.c_str(), + invoice_id.c_str(), country_code.c_str(), + std::move(result))) { + return; + } +} + +} // namespace + +void InAppPurchaseTizenPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + InAppPurchaseTizenPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/packages/in_app_purchase/tizen/src/log.h b/packages/in_app_purchase/tizen/src/log.h new file mode 100644 index 000000000..5c209d1e9 --- /dev/null +++ b/packages/in_app_purchase/tizen/src/log.h @@ -0,0 +1,24 @@ +#ifndef __LOG_H__ +#define __LOG_H__ + +#include + +#ifdef LOG_TAG +#undef LOG_TAG +#endif +#define LOG_TAG "InAppPurchaseTizenPlugin" + +#ifndef __MODULE__ +#define __MODULE__ strrchr("/" __FILE__, '/') + 1 +#endif + +#define LOG(prio, fmt, arg...) \ + dlog_print(prio, LOG_TAG, "%s: %s(%d) > " fmt, __MODULE__, __func__, \ + __LINE__, ##arg) + +#define LOG_DEBUG(fmt, args...) LOG(DLOG_DEBUG, fmt, ##args) +#define LOG_INFO(fmt, args...) LOG(DLOG_INFO, fmt, ##args) +#define LOG_WARN(fmt, args...) LOG(DLOG_WARN, fmt, ##args) +#define LOG_ERROR(fmt, args...) LOG(DLOG_ERROR, fmt, ##args) + +#endif // __LOG_H__