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) | [](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) | [](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) | [](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) | [](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) | [](https://pub.dev/packages/integration_test_tizen) | No |
| [**messageport_tizen**](packages/messageport) | (Tizen-only) | [](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) | [](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