diff --git a/.flutter-plugins b/.flutter-plugins deleted file mode 100644 index 57b74a8..0000000 --- a/.flutter-plugins +++ /dev/null @@ -1,8 +0,0 @@ -# This is a generated file; do not edit or check into version control. -connectivity_plus=/Users/habeshageeks/flutter/.pub-cache/hosted/pub.dartlang.org/connectivity_plus-2.3.6+1/ -connectivity_plus_linux=/Users/habeshageeks/flutter/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_linux-1.3.1/ -connectivity_plus_macos=/Users/habeshageeks/flutter/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_macos-1.2.4/ -connectivity_plus_web=/Users/habeshageeks/flutter/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_web-1.2.3/ -connectivity_plus_windows=/Users/habeshageeks/flutter/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_windows-1.2.2/ -flutter_inappwebview=/Users/habeshageeks/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_inappwebview-5.4.3+7/ -fluttertoast=/Users/habeshageeks/flutter/.pub-cache/hosted/pub.dartlang.org/fluttertoast-8.0.9/ diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies deleted file mode 100644 index 5afb255..0000000 --- a/.flutter-plugins-dependencies +++ /dev/null @@ -1 +0,0 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/Users/habeshageeks/flutter/.pub-cache/hosted/pub.dartlang.org/connectivity_plus-2.3.6+1/","native_build":true,"dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/habeshageeks/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_inappwebview-5.4.3+7/","native_build":true,"dependencies":[]},{"name":"fluttertoast","path":"/Users/habeshageeks/flutter/.pub-cache/hosted/pub.dartlang.org/fluttertoast-8.0.9/","native_build":true,"dependencies":[]}],"android":[{"name":"connectivity_plus","path":"/Users/habeshageeks/flutter/.pub-cache/hosted/pub.dartlang.org/connectivity_plus-2.3.6+1/","native_build":true,"dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/habeshageeks/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_inappwebview-5.4.3+7/","native_build":true,"dependencies":[]},{"name":"fluttertoast","path":"/Users/habeshageeks/flutter/.pub-cache/hosted/pub.dartlang.org/fluttertoast-8.0.9/","native_build":true,"dependencies":[]}],"macos":[{"name":"connectivity_plus_macos","path":"/Users/habeshageeks/flutter/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_macos-1.2.4/","native_build":true,"dependencies":[]}],"linux":[{"name":"connectivity_plus_linux","path":"/Users/habeshageeks/flutter/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_linux-1.3.1/","native_build":false,"dependencies":[]}],"windows":[{"name":"connectivity_plus_windows","path":"/Users/habeshageeks/flutter/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_windows-1.2.2/","native_build":true,"dependencies":[]}],"web":[{"name":"connectivity_plus_web","path":"/Users/habeshageeks/flutter/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_web-1.2.3/","dependencies":[]},{"name":"fluttertoast","path":"/Users/habeshageeks/flutter/.pub-cache/hosted/pub.dartlang.org/fluttertoast-8.0.9/","dependencies":[]}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":["connectivity_plus_linux","connectivity_plus_macos","connectivity_plus_web","connectivity_plus_windows"]},{"name":"connectivity_plus_linux","dependencies":[]},{"name":"connectivity_plus_macos","dependencies":[]},{"name":"connectivity_plus_web","dependencies":[]},{"name":"connectivity_plus_windows","dependencies":[]},{"name":"flutter_inappwebview","dependencies":[]},{"name":"fluttertoast","dependencies":[]}],"date_created":"2023-05-22 08:43:34.602972","version":"3.3.6"} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6528aa8..9b333b1 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ build/ .gradle /example/ios/Podfile.lock /coverage/ +.flutter-plugins +.flutter-plugins-dependencies diff --git a/README.md b/README.md index d7e4473..63cc168 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,171 @@ - # Chapa Flutter SDK -Chapa Flutter sdk for Chapa payment API. It is not official and is not supported by Chapa. It is provided as-is. The main features of this library is it supports connectivity tests, auto reroutes, parameter checks for payment options. +The official **Chapa Flutter SDK** enables Flutter developers to integrate Chapa's Payment API seamlessly into their applications. It supports both native and web checkout, providing a robust and flexible solution for initiating and validating payments. +--- +## **Features** -## API Reference +- šŸŒŸ **Initiate Payment:** Easily facilitate transactions via four supported wallets telebirr,cbebirr,mpesa and ebirr. +- āœ… **Validate Payment Status:** Confirm payment completion and notify users instantly. +- šŸŒ **Web Checkout Support:** Enable users to use the web checkout for additional payment options. -#### Create new transaction from mobile end point +--- +## **Preview** -Base end point -https://api.chapa.co/v1 +
+
+

Payment Methods in Grid View

+ Payment Methods in Grid View +
+
+

Payment Methods in List View with Customized Button Color

+ Payment Methods in List View +
+
+

Error

+ Error +
+
+

Successful Payment Receipt

+ Successful Payment Receipt +
+
-```http - POST /transaction/mobile-initialize -``` -| Parameter | Type | Description | -| :-------- | :------- | :------------------------- | -| `key` | `string` | **Required**. This will be your public key from Chapa. When on test mode use the test key, and when on live mode use the live key. | -| `email` | `string` | A customerā€™s email address. | -| `amount` | `string` | **Required**. The amount you will be charging your customer. | -| `first_name` | `string` | A customerā€™s first name. | -| `last_name` | `string` | A customer's last name. | -| `tx_ref` | `string` | **Required**. A unique reference given to each transaction. | -| `currency` | `string` | **Required**. The currency in which all the charges are made. i.e ETB, USD | -| `phone` | `digit` | A customerā€™s phone number. | -| `callback_url`| `string` | The URL to redirect the customer to after payment is done.| -| `customization[title]`| `string` | The customizations field (optional) allows you to customize the look and feel of the payment modal.| -#### SDK requires additional parameter for fallBack page which is named route which will help you reroute webview after payment completed, on internet disconnected and many more +## **Getting Started** +### **Installation** +Add the following dependency to your `pubspec.yaml` file: -| Parameter | Type | Description | -| :-------- | :------- | :-------------------------------- | -| `namedRouteFallBack` | `string` | **Required by the sdk**. This will accepted route name in String, After successful transaction the app will direct to this page with necessary information for flutter developers to update the backend or to regenerate new transaction reference. | +```yaml +dependencies: + chapasdk: ^latest_version +``` +Then, run the command: +```bash +flutter pub get +``` +--- + +## **Parameters** + +| Parameter | Type | Required | Description | +|----------------------------|-------------------|-----------|---------------------------------------------------------------------------------------------------------------| +| `context` | `BuildContext` | **Yes** | Context of the current widget. | +| `publicKey` | `String` | **Yes** | Your Chapa public key (use test key for testing and live key for production). | +| `currency` | `String` | **Yes** | Transaction currency (ETB for native checkout, ETB or USD for web checkout NB=> You can not use USD for Naive checkout is it is only available for Web checkout.). | +| `amount` | `String` | **Yes** | The amount to be charged. | +| `email` | `String` | **Yes** | Customerā€™s email address. | +| `phone` | `String` | **Yes** | Customerā€™s phone number. | +| `firstName` | `String` | **Yes** | Customerā€™s first name. | +| `lastName` | `String` | **Yes** | Customerā€™s last name. | +| `txRef` | `String` | **Yes** | Unique reference for the transaction. | +| `title` | `String` | **Yes** | Title of the payment modal. | +| `desc` | `String` | **Yes** | Description of the payment. | +| `namedRouteFallBack` | `String` | **Yes** | Named route to redirect users to after payment events (success, failure, or cancellation). | +| `nativeCheckout` | `bool` | No | Whether to use native checkout (`true`) or web checkout (`false`). Default is `true`. | +| `showPaymentMethodsOnGridView` | `bool` | No | Display payment methods in grid (`true`) or horizontal view (`false`). Default is `true`. | +| `availablePaymentMethods` | `List` | No | List of allowed payment methods (`mpesa`, `cbebirr`, `telebirr`, `ebirr`). Defaults to all methods. | +| `buttonColor` | `Color` | No | Button color for native checkout. Defaults to the appā€™s primary theme color. | + +--- + +## **Usage** + +```dart +import 'package:chapasdk/chapasdk.dart'; -## Installation +Chapa.paymentParameters( + context: context, + publicKey: 'CHAPUBK-@@@@', + currency: 'ETB', + amount: '1', + email: 'fetanchapa.co', + phone: '0911223344', + firstName: 'Israel', + lastName: 'Goytom', + txRef: 'txn_12345', + title: 'Order Payment', + desc: 'Payment for order #12345', + nativeCheckout: true, + namedRouteFallBack: '/', + showPaymentMethodsOnGridView: true, + availablePaymentMethods: ['mpesa', 'cbebirr', 'telebirr', 'ebirr'], +); +``` -Installation instructions coming soon its better if you install it from pub dev +--- +## **Payment Responses from** +### For Native Checkout: -## Usage/Example +Transaction Reference Number in Native Checkout context is Chapa's Reference number for the Payment. +```json +{ + "message": "Any Descriptive message regarding the payment status", + "transactionReference": "CHHhvtn7xLEkZ", + "paidAmount": "1.00" +} +``` -```flutter -import 'package:chapasdk/chapasdk.dart'; +### For Web Checkout: +Transaction Reference Number in Web Checkout context is The transaction reference number you generated for the payment. -Chapa.paymentParameters( - context: context, // context - publicKey: 'CHASECK_TEST--------------', - currency: 'ETB', - amount: '200', - email: 'xyz@gmail.com', - phone: '911223344', - firstName: 'fullName', - lastName: 'lastName', - txRef: '34TXTHHgb', - title: 'title', - desc:'desc', - namedRouteFallBack: '/second', // fall back route name - ); +Important Note: +If the web checkout is initiated from the native checkout page, the Transaction Reference for the web checkout will be generated by the app itself. This is because a single transaction reference cannot be used for both native and web checkout processes. Therefore, the app will generate a separate transaction reference for the web checkout. + +#### Payment Canceled: +```json +{ + "message": "paymentCancelled", + "transactionReference": "txn_12345", + "paidAmount": "1.00" +} +``` +#### Payment Successful: +```json +{ + "message": "paymentSuccessful", + "transactionReference": "txn_12345" , + "paidAmount": "1.00" +} ``` +#### Payment Failed: +```json +{ + "message": "paymentFailed", + "transactionReference": "txn_12345", + "paidAmount": "0.00" +} +``` + +--- + +## **FAQ** +### **1. Is the fallback route mandatory?** +Yes, `namedRouteFallBack` is required to handle post-payment events such as success, failure, or cancellation. -## FAQ +### **2. What currencies are supported?** +- Native Checkout: ONLY **ETB** +- Web Checkout: **ETB**, **USD** -#### Should my fallBack route should be named route? +--- -Answer Yes, the fallBackRoute comes with an information such as payment is successful, user cancelled payment and connectivity issue messages. Those information will help you to update your backend, to generate new transaction reference. +## **Support** +For any questions or issues: +- **Email:** [info@chapa.co](mailto:infot@chapa.co) +- **Call Center:** [+251960724272](tel:+251960724272) +- **Short Code:** [8911](tel:8911) +- **Documentation:** [Chapa Developer Docs](https://developer.chapa.co/) +Start building seamless payment experiences today with the **Chapa Flutter SDK**! šŸš€ diff --git a/assets/images/cbebirr.png b/assets/images/cbebirr.png new file mode 100644 index 0000000..a25c528 Binary files /dev/null and b/assets/images/cbebirr.png differ diff --git a/assets/images/chapa-logo.png b/assets/images/chapa-logo.png new file mode 100644 index 0000000..533ee24 Binary files /dev/null and b/assets/images/chapa-logo.png differ diff --git a/assets/images/ebirr.png b/assets/images/ebirr.png new file mode 100644 index 0000000..ceaa5de Binary files /dev/null and b/assets/images/ebirr.png differ diff --git a/assets/images/ethiopia-flag.png b/assets/images/ethiopia-flag.png new file mode 100644 index 0000000..d6d6e07 Binary files /dev/null and b/assets/images/ethiopia-flag.png differ diff --git a/assets/images/mpesa.png b/assets/images/mpesa.png new file mode 100644 index 0000000..d6a9221 Binary files /dev/null and b/assets/images/mpesa.png differ diff --git a/assets/images/success-icon.png b/assets/images/success-icon.png new file mode 100644 index 0000000..1d36615 Binary files /dev/null and b/assets/images/success-icon.png differ diff --git a/assets/images/telebirr.png b/assets/images/telebirr.png new file mode 100644 index 0000000..183110b Binary files /dev/null and b/assets/images/telebirr.png differ diff --git a/doc/error.png b/doc/error.png new file mode 100644 index 0000000..c79f829 Binary files /dev/null and b/doc/error.png differ diff --git a/doc/gridview.png b/doc/gridview.png new file mode 100644 index 0000000..1678263 Binary files /dev/null and b/doc/gridview.png differ diff --git a/doc/listview.png b/doc/listview.png new file mode 100644 index 0000000..fd36ef3 Binary files /dev/null and b/doc/listview.png differ diff --git a/doc/success.png b/doc/success.png new file mode 100644 index 0000000..db9ac79 Binary files /dev/null and b/doc/success.png differ diff --git a/example/.metadata b/example/.metadata index 5a02328..732ba6d 100644 --- a/example/.metadata +++ b/example/.metadata @@ -1,10 +1,30 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled and should not be manually edited. +# This file should be version controlled. version: - revision: 7e9793dee1b85a243edd0e06cb1658e98b077561 + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 channel: stable project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: android + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/android/build.gradle b/example/android/build.gradle index 4256f91..4d649ab 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:8.4.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index bc6a58a..1f45544 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8.0-all.zip diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 8d4492f..7c56964 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 12.0 diff --git a/example/ios/Podfile b/example/ios/Podfile index 1e8c3c9..2c068c4 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 7cdf2aa..99b6483 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,12 +3,13 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 6ABE65F751942FCF67014A78 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 367D95931BDBBD94D4483798 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -31,10 +32,14 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 367D95931BDBBD94D4483798 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 7259F65F242427A551ABCB1C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7E289552D56EF4AE86813DEA /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 826D7A2CF170DE860AAB9389 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -49,12 +54,24 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6ABE65F751942FCF67014A78 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 937E302E8D077A6705BB5022 /* Pods */ = { + isa = PBXGroup; + children = ( + 7E289552D56EF4AE86813DEA /* Pods-Runner.debug.xcconfig */, + 826D7A2CF170DE860AAB9389 /* Pods-Runner.release.xcconfig */, + 7259F65F242427A551ABCB1C /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -72,6 +89,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + 937E302E8D077A6705BB5022 /* Pods */, + C4C1FCD9E3CA04DF32FD7E6F /* Frameworks */, ); sourceTree = ""; }; @@ -98,6 +117,14 @@ path = Runner; sourceTree = ""; }; + C4C1FCD9E3CA04DF32FD7E6F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 367D95931BDBBD94D4483798 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -105,12 +132,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + AC96978B6AAFA251E53C8BD8 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + CBB6A21FE89A520C71A60ED6 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -127,7 +156,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -171,10 +200,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -185,6 +216,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -197,6 +229,45 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + AC96978B6AAFA251E53C8BD8 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + CBB6A21FE89A520C71A60ED6 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -272,7 +343,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -350,7 +421,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -399,7 +470,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c87d15a..5e31d3d 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 70693e4..b636303 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index b369190..b218a27 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -43,5 +43,9 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/example/lib/database/db_helper.dart b/example/lib/database/db_helper.dart index 15bcd9a..581a115 100644 --- a/example/lib/database/db_helper.dart +++ b/example/lib/database/db_helper.dart @@ -1,60 +1,132 @@ +import 'dart:developer'; +import 'dart:io' as io; +import 'package:shopping_cart_app/model/cart_model.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart'; -import 'dart:io' as io; -import 'package:shopping_cart_app/model/cart_model.dart'; class DBHelper { static Database? _database; - Future get database async { + Future get database async { if (_database != null) { return _database!; } _database = await initDatabase(); - return null; + return _database!; } - initDatabase() async { + Future initDatabase() async { io.Directory directory = await getApplicationDocumentsDirectory(); String path = join(directory.path, 'cart.db'); - var db = await openDatabase(path, version: 1, onCreate: _onCreate); - return db; + return await openDatabase( + path, + version: 2, + onCreate: _onCreate, + ); } - _onCreate(Database db, int version) async { + Future _onCreate(Database db, int version) async { await db.execute( - 'CREATE TABLE cart(id INTEGER PRIMARY KEY, productId VARCHAR UNIQUE, productName TEXT, initialPrice INTEGER, productPrice INTEGER, quantity INTEGER, unitTag TEXT, image TEXT)'); + 'CREATE TABLE cart(' + 'id INTEGER PRIMARY KEY AUTOINCREMENT, ' + 'productId VARCHAR UNIQUE, ' + 'productName TEXT, ' + 'initialPrice INTEGER, ' + 'productPrice INTEGER, ' + 'quantity INTEGER, ' + 'unitTag TEXT, ' + 'image TEXT)', + ); } Future insert(Cart cart) async { - var dbClient = await database; - await dbClient!.insert('cart', cart.toMap()); + final dbClient = await database; + try { + await dbClient.insert( + 'cart', + cart.toMap(), + conflictAlgorithm: ConflictAlgorithm.ignore, + ); + } catch (e) { + log('Error inserting cart: $e'); + } + return cart; + } + + Future addToCart(Cart cart) async { + final dbClient = await database; + try { + List> existingProduct = await dbClient.query( + 'cart', + where: 'productId = ?', + whereArgs: [cart.productId], + ); + + if (existingProduct.isEmpty) { + await dbClient.insert('cart', cart.toMap()); + } else { + final newQuantity = + (cart.quantity?.value ?? 0) + existingProduct[0]['quantity']; + await dbClient.update( + 'cart', + {'quantity': newQuantity}, + where: 'productId = ?', + whereArgs: [cart.productId], + ); + } + } catch (e) { + log('Error in addToCart: $e'); + } return cart; } Future> getCartList() async { - var dbClient = await database; - final List> queryResult = - await dbClient!.query('cart'); - return queryResult.map((result) => Cart.fromMap(result)).toList(); + final dbClient = await database; + try { + final queryResult = await dbClient.query('cart'); + return queryResult.map((result) => Cart.fromMap(result)).toList(); + } catch (e) { + log('Error fetching cart list: $e'); + return []; + } } Future deleteCartItem(int id) async { - var dbClient = await database; - return await dbClient!.delete('cart', where: 'id = ?', whereArgs: [id]); + final dbClient = await database; + try { + return await dbClient.delete('cart', where: 'id = ?', whereArgs: [id]); + } catch (e) { + log('Error deleting cart item: $e'); + return 0; + } } Future updateQuantity(Cart cart) async { - var dbClient = await database; - return await dbClient!.update('cart', cart.quantityMap(), - where: "productId = ?", whereArgs: [cart.productId]); + final dbClient = await database; + try { + return await dbClient.update( + 'cart', + cart.quantityMap(), + where: 'productId = ?', + whereArgs: [cart.productId], + ); + } catch (e) { + log('Error updating quantity: $e'); + return 0; + } } + /// Retrieve cart items by their ID Future> getCartId(int id) async { - var dbClient = await database; - final List> queryIdResult = - await dbClient!.query('cart', where: 'id = ?', whereArgs: [id]); - return queryIdResult.map((e) => Cart.fromMap(e)).toList(); + final dbClient = await database; + try { + final queryIdResult = + await dbClient.query('cart', where: 'id = ?', whereArgs: [id]); + return queryIdResult.map((e) => Cart.fromMap(e)).toList(); + } catch (e) { + log('Error fetching cart by ID: $e'); + return []; + } } } diff --git a/example/lib/main.dart b/example/lib/main.dart index 6ec17db..d28dd58 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -9,7 +9,9 @@ void main() { } class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); + const MyApp({ + Key? key, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -20,10 +22,12 @@ class MyApp extends StatelessWidget { debugShowCheckedModeBanner: false, title: 'Flutter Demo', theme: ThemeData( - primarySwatch: Colors.blue, + primarySwatch: Colors.green, ), - routes: {'/checkoutPage': (context) => const CartScreen()}, - home: const ProductList(), + routes: { + '/checkoutPage': (context) => const CartScreen(), + '/': (context) => ProductList() + }, ); }), ); diff --git a/example/lib/model/cart_model.dart b/example/lib/model/cart_model.dart index c8551b3..6f44731 100644 --- a/example/lib/model/cart_model.dart +++ b/example/lib/model/cart_model.dart @@ -32,7 +32,7 @@ class Cart { Map toMap() { return { - 'id': id, + 'productId': productId, 'productName': productName, 'initialPrice': initialPrice, diff --git a/example/lib/model/item_model.dart b/example/lib/model/item_model.dart index d7aafdb..8832e4a 100644 --- a/example/lib/model/item_model.dart +++ b/example/lib/model/item_model.dart @@ -1,17 +1,20 @@ class Item { + final int productId; final String name; final String unit; final int price; final String image; Item( - {required this.name, + {required this.productId, + required this.name, required this.unit, required this.price, required this.image}); Map toJson() { return { + 'product_id': productId, 'name': name, 'unit': unit, 'price': price, diff --git a/example/lib/screens/cart_screen.dart b/example/lib/screens/cart_screen.dart index fd87548..354c9e0 100644 --- a/example/lib/screens/cart_screen.dart +++ b/example/lib/screens/cart_screen.dart @@ -1,4 +1,6 @@ -import 'package:badges/badges.dart'; +import 'dart:math'; + +import 'package:badges/badges.dart' as badge; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shopping_cart_app/database/db_helper.dart'; @@ -45,9 +47,12 @@ class _CartScreenState extends State { return Scaffold( appBar: AppBar( centerTitle: true, - title: const Text('My Shopping Cart'), + title: Text( + 'My Shopping Cart', + style: Theme.of(context).textTheme.titleMedium, + ), actions: [ - Badge( + badge.Badge( badgeContent: Consumer( builder: (context, value, child) { return Text( @@ -57,7 +62,7 @@ class _CartScreenState extends State { ); }, ), - position: const BadgePosition(start: 30, bottom: 30), + position: badge.BadgePosition.custom(start: 30, bottom: 30), child: IconButton( onPressed: () {}, icon: const Icon(Icons.shopping_cart), @@ -246,39 +251,63 @@ class _CartScreenState extends State { ], ); }, - ) - ], - ), - bottomNavigationBar: InkWell( - onTap: () { - /// - Chapa.paymentParameters( - context: context, // context - publicKey: 'CHASECK_TEST-', - currency: 'etb', - amount: '300', - email: 'xyz@gmail.com', - phone: '911223344', - firstName: 'testname', - lastName: 'lastName', - txRef: '55ttyyy', - title: 'title', - desc: 'desc', - namedRouteFallBack: '/checkoutPage', // fall back route name - ); - }, - child: Container( - color: Colors.yellow.shade600, - alignment: Alignment.center, - height: 50.0, - child: const Text( - 'Proceed to Pay', - style: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, + ), + SizedBox( + height: MediaQuery.of(context).size.height * 0.012, + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.9, + child: ElevatedButton( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + Theme.of(context).primaryColor), + ), + onPressed: () { + var r = Random(); + + const _chars = + 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; + String transactionRef = List.generate( + 10, (index) => _chars[r.nextInt(_chars.length)]).join(); + + /// + /// + /// + /// + Chapa.paymentParameters( + context: context, // context + publicKey: 'CHAPUBK-@@@', + currency: 'ETB', + amount: '1', + email: 'fetan@chapa.co', + phone: '0964001822', + firstName: 'Israel', + lastName: 'Goytom', + txRef: transactionRef, + title: 'Test Payment', + desc: 'Text Payment', + nativeCheckout: true, + namedRouteFallBack: '/', + showPaymentMethodsOnGridView: false, + availablePaymentMethods: [ + 'mpesa', + 'cbebirr', + 'telebirr', + 'ebirr', + ], + ); + }, + child: Text( + 'Proceed to Pay', + style: + TextStyle(fontWeight: FontWeight.bold, color: Colors.white), + ), ), ), - ), + SizedBox( + height: MediaQuery.of(context).size.height * 0.04, + ) + ], ), ); } @@ -320,11 +349,11 @@ class ReusableWidget extends StatelessWidget { children: [ Text( title, - style: Theme.of(context).textTheme.subtitle1, + //style: Theme.of(context).textTheme.subtitle1, ), Text( value.toString(), - style: Theme.of(context).textTheme.subtitle2, + //style: Theme.of(context).textTheme.subtitle2, ), ], ), diff --git a/example/lib/screens/product_list.dart b/example/lib/screens/product_list.dart index bb2a36b..a229c51 100644 --- a/example/lib/screens/product_list.dart +++ b/example/lib/screens/product_list.dart @@ -1,4 +1,6 @@ -import 'package:badges/badges.dart'; +import 'dart:developer'; + +import 'package:badges/badges.dart' as badge; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shopping_cart_app/model/item_model.dart'; @@ -8,7 +10,7 @@ import 'package:shopping_cart_app/model/cart_model.dart'; import 'package:shopping_cart_app/screens/cart_screen.dart'; class ProductList extends StatefulWidget { - const ProductList({Key? key}) : super(key: key); + const ProductList({super.key}); @override State createState() => _ProductListState(); @@ -16,59 +18,66 @@ class ProductList extends StatefulWidget { class _ProductListState extends State { DBHelper dbHelper = DBHelper(); - + CartProvider cartProvider = CartProvider(); List products = [ Item( - name: 'Apple', unit: 'Kg', price: 20, image: 'assets/images/apple.png'), + productId: 1, + name: 'Apple', + unit: 'Kg', + price: 20, + image: 'assets/images/apple.png', + ), Item( - name: 'Mango', - unit: 'Doz', - price: 30, - image: 'assets/images/mango.png'), + productId: 2, + name: 'Mango', + unit: 'Doz', + price: 30, + image: 'assets/images/mango.png', + ), Item( - name: 'Banana', - unit: 'Doz', - price: 10, - image: 'assets/images/banana.png'), + productId: 3, + name: 'Banana', + unit: 'Doz', + price: 10, + image: 'assets/images/banana.png', + ), Item( + productId: 4, name: 'Grapes', unit: 'Kg', price: 8, image: 'assets/images/grapes.png'), Item( - name: 'Water Melon', - unit: 'Kg', - price: 25, - image: 'assets/images/watermelon.png'), - Item(name: 'Kiwi', unit: 'Pc', price: 40, image: 'assets/images/kiwi.png'), + productId: 5, + name: 'Water Melon', + unit: 'Kg', + price: 25, + image: 'assets/images/watermelon.png', + ), + Item( + productId: 6, + name: 'Kiwi', + unit: 'Pc', + price: 40, + image: 'assets/images/kiwi.png', + ), Item( + productId: 7, name: 'Orange', unit: 'Doz', price: 15, image: 'assets/images/orange.png'), - Item(name: 'Peach', unit: 'Pc', price: 8, image: 'assets/images/peach.png'), - Item( - name: 'Strawberry', - unit: 'Box', - price: 12, - image: 'assets/images/strawberry.png'), - Item( - name: 'Fruit Basket', - unit: 'Kg', - price: 55, - image: 'assets/images/fruitBasket.png'), ]; - //List clicked = List.generate(10, (index) => false, growable: true); @override Widget build(BuildContext context) { final cart = Provider.of(context); void saveData(int index) { dbHelper - .insert( + .addToCart( Cart( id: index, - productId: index.toString(), + productId: products[index].productId.toString(), productName: products[index].name, initialPrice: products[index].price, productPrice: products[index].price, @@ -80,18 +89,22 @@ class _ProductListState extends State { .then((value) { cart.addTotalPrice(products[index].price.toDouble()); cart.addCounter(); - print('Product Added to cart'); }).onError((error, stackTrace) { - print(error.toString()); + log("Error here"); + log('$error'); + log(error.toString()); }); } return Scaffold( appBar: AppBar( centerTitle: true, - title: const Text('Product List'), + title: Text( + 'Product List', + style: Theme.of(context).textTheme.titleMedium, + ), actions: [ - Badge( + badge.Badge( badgeContent: Consumer( builder: (context, value, child) { return Text( @@ -101,7 +114,7 @@ class _ProductListState extends State { ); }, ), - position: const BadgePosition(start: 30, bottom: 30), + position: badge.BadgePosition.custom(start: 30, bottom: 30), child: IconButton( onPressed: () { Navigator.push( @@ -157,7 +170,8 @@ class _ProductListState extends State { text: '${products[index].name.toString()}\n', style: const TextStyle( - fontWeight: FontWeight.bold)), + fontWeight: FontWeight.bold, + )), ]), ), RichText( @@ -195,7 +209,8 @@ class _ProductListState extends State { ), ElevatedButton( style: ElevatedButton.styleFrom( - primary: Colors.blueGrey.shade900), + // primary: Colors.blueGrey.shade900, + ), onPressed: () { saveData(index); }, diff --git a/example/pubspec.lock b/example/pubspec.lock index 882d183..ec22d77 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,142 +5,177 @@ packages: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.6.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" + url: "https://pub.dev" + source: hosted + version: "1.5.8" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.11.0" badges: dependency: "direct main" description: name: badges - url: "https://pub.dartlang.org" + sha256: a7b6bbd60dce418df0db3058b53f9d083c22cdb5132a052145dc267494df0b84 + url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "3.1.2" + bloc: + dependency: transitive + description: + name: bloc + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + url: "https://pub.dev" + source: hosted + version: "8.1.4" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" chapasdk: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.0.1" + version: "0.0.6" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" + version: "1.3.0" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.19.0" connectivity_plus: dependency: transitive description: name: connectivity_plus - url: "https://pub.dartlang.org" + sha256: e0817759ec6d2d8e57eb234e6e57d2173931367a865850c7acea40d4b4f9c27d + url: "https://pub.dev" source: hosted - version: "2.3.6+1" - connectivity_plus_linux: - dependency: transitive - description: - name: connectivity_plus_linux - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - connectivity_plus_macos: - dependency: transitive - description: - name: connectivity_plus_macos - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.4" + version: "6.1.1" connectivity_plus_platform_interface: dependency: transitive description: name: connectivity_plus_platform_interface - url: "https://pub.dartlang.org" + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + url: "https://pub.dev" source: hosted - version: "1.2.1" - connectivity_plus_web: + version: "2.0.1" + convert: dependency: transitive description: - name: connectivity_plus_web - url: "https://pub.dartlang.org" + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" source: hosted - version: "1.2.3" - connectivity_plus_windows: + version: "3.1.2" + crypto: dependency: transitive description: - name: connectivity_plus_windows - url: "https://pub.dartlang.org" + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "3.0.6" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.8" dbus: dependency: transitive description: name: dbus - url: "https://pub.dartlang.org" + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" + dio: + dependency: transitive + description: + name: dio + sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + url: "https://pub.dev" + source: hosted + version: "5.7.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "2.0.0" + encrypt: + dependency: transitive + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: transitive description: name: ffi - url: "https://pub.dartlang.org" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "2.1.3" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -150,16 +185,74 @@ packages: dependency: transitive description: name: flutter_inappwebview - url: "https://pub.dartlang.org" + sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + flutter_inappwebview_android: + dependency: transitive + description: + name: flutter_inappwebview_android + sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + flutter_inappwebview_ios: + dependency: transitive + description: + name: flutter_inappwebview_ios + sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_macos: + dependency: transitive + description: + name: flutter_inappwebview_macos + sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_platform_interface: + dependency: transitive + description: + name: flutter_inappwebview_platform_interface + sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 + url: "https://pub.dev" + source: hosted + version: "1.3.0+1" + flutter_inappwebview_web: + dependency: transitive + description: + name: flutter_inappwebview_web + sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" + url: "https://pub.dev" source: hosted - version: "5.4.3+7" + version: "1.1.2" + flutter_inappwebview_windows: + dependency: transitive + description: + name: flutter_inappwebview_windows + sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" + url: "https://pub.dev" + source: hosted + version: "0.6.0" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "5.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -174,322 +267,415 @@ packages: dependency: transitive description: name: fluttertoast - url: "https://pub.dartlang.org" + sha256: "24467dc20bbe49fd63e57d8e190798c4d22cbbdac30e54209d153a15273721d1" + url: "https://pub.dev" source: hosted - version: "8.0.9" + version: "8.2.10" http: dependency: transitive description: name: http - url: "https://pub.dartlang.org" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "1.2.2" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + intl: + dependency: transitive + description: + name: intl + sha256: "00f33b908655e606b86d2ade4710a231b802eec6f11e87e4ea3783fd72077a50" + url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "0.20.1" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + url: "https://pub.dev" + source: hosted + version: "10.0.7" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "3.0.8" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "5.1.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" source: hosted - version: "0.12.11" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.11.1" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.15.0" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" source: hosted version: "1.0.0" nm: dependency: transitive description: name: nm - url: "https://pub.dartlang.org" + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" source: hosted version: "0.5.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.9.0" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.dartlang.org" + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + url: "https://pub.dev" source: hosted - version: "2.0.14" - path_provider_ios: + version: "2.2.15" + path_provider_foundation: dependency: transitive description: - name: path_provider_ios - url: "https://pub.dartlang.org" + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.4.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" source: hosted - version: "2.1.6" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.6" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "2.3.0" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.2" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" source: hosted - version: "2.1.2" - process: + version: "2.1.8" + pointycastle: dependency: transitive description: - name: process - url: "https://pub.dartlang.org" + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "3.9.1" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" source: hosted - version: "6.0.3" + version: "6.1.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.dartlang.org" + sha256: "3c7e73920c694a436afaf65ab60ce3453d91f84208d761fbd83fc21182134d93" + url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.3.4" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.dartlang.org" + sha256: "02a7d8a9ef346c9af715811b01fbd8e27845ad2c41148eefd31321471b41863d" + url: "https://pub.dev" source: hosted - version: "2.0.12" - shared_preferences_ios: + version: "2.4.0" + shared_preferences_foundation: dependency: transitive description: - name: shared_preferences_ios - url: "https://pub.dartlang.org" + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.dartlang.org" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" source: hosted - version: "2.1.1" - shared_preferences_macos: - dependency: transitive - description: - name: shared_preferences_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.4" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.dartlang.org" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.dartlang.org" + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.dartlang.org" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.4.1" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.10.0" sqflite: dependency: "direct main" description: name: sqflite - url: "https://pub.dartlang.org" + sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" + url: "https://pub.dev" source: hosted - version: "2.0.2+1" + version: "2.4.0" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.dartlang.org" + sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" + url: "https://pub.dev" + source: hosted + version: "2.5.4+6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "96a698e2bc82bd770a4d6aab00b42396a7c63d9e33513a56945cbccb594c2474" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" source: hosted - version: "2.2.1+1" + version: "2.4.0" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.12.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.3.0" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.dartlang.org" + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + url: "https://pub.dev" source: hosted - version: "3.0.0+2" + version: "3.3.0+3" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + url: "https://pub.dev" source: hosted - version: "0.4.9" + version: "0.7.3" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.0" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" - win32: + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + url: "https://pub.dev" + source: hosted + version: "14.3.0" + web: dependency: transitive description: - name: win32 - url: "https://pub.dartlang.org" + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "1.1.0" xdg_directories: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" source: hosted - version: "0.2.0+1" + version: "1.1.0" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.5.0" sdks: - dart: ">=2.17.0 <3.0.0" - flutter: ">=2.8.1" + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 83d202d..1c51a2c 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.16.1 <3.0.0" + sdk: '>=3.4.1 <4.0.0' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -33,12 +33,12 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.4 - shared_preferences: ^2.0.15 - path_provider: ^2.0.10 - sqflite: ^2.0.2+1 - badges: ^2.0.2 - provider: ^6.0.3 + cupertino_icons: + shared_preferences: + path_provider: + sqflite: + badges: + provider: chapasdk: path: ../ @@ -52,7 +52,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^1.0.0 + flutter_lints: # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/lib/chapa_payment initializer.dart b/lib/chapa_payment initializer.dart index 51c3aa4..ce47add 100644 --- a/lib/chapa_payment initializer.dart +++ b/lib/chapa_payment initializer.dart @@ -1,7 +1,8 @@ +import 'package:chapasdk/features/native-checkout/chapa_native_payment.dart'; import 'package:flutter/material.dart'; -import 'constants/common.dart'; -import 'constants/requests.dart'; -import 'constants/strings.dart'; +import 'package:chapasdk/domain/constants/common.dart'; +import 'package:chapasdk/domain/constants/requests.dart'; +import 'package:chapasdk/domain/constants/strings.dart'; class Chapa { BuildContext context; @@ -16,6 +17,12 @@ class Chapa { String title; String desc; String namedRouteFallBack; + bool nativeCheckout; + + final Color? buttonColor; + + final bool? showPaymentMethodsOnGridView; + List? availablePaymentMethods; Chapa.paymentParameters({ required this.context, @@ -30,11 +37,15 @@ class Chapa { required this.title, required this.desc, required this.namedRouteFallBack, + this.nativeCheckout = false, + this.buttonColor, + this.showPaymentMethodsOnGridView, + this.availablePaymentMethods, }) { _validateKeys(); currency = currency.toUpperCase(); if (_validateKeys()) { - initatePayment(); + initiatePayment(); } } @@ -60,8 +71,44 @@ class Chapa { return true; } - void initatePayment() async { - intilizeMyPayment(context, publicKey, email, phone, amount, currency, - firstName, lastName, txRef, title, desc, namedRouteFallBack); + void initiatePayment() async { + if (nativeCheckout) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChapaNativePayment( + context: context, + publicKey: publicKey, + currency: currency, + firstName: firstName, + lastName: lastName, + amount: amount, + email: email, + phone: phone, + namedRouteFallBack: namedRouteFallBack, + title: title, + desc: desc, + txRef: txRef, + buttonColor: buttonColor, + showPaymentMethodsOnGridView: showPaymentMethodsOnGridView, + availablePaymentMethods: availablePaymentMethods ?? [], + ), + )); + } else { + await initializeMyPayment( + context, + email, + phone, + amount, + currency, + firstName, + lastName, + txRef, + title, + desc, + namedRouteFallBack, + publicKey, + ); + } } } diff --git a/lib/chapawebview.dart b/lib/chapawebview.dart index 7a46b78..d5c7d23 100644 --- a/lib/chapawebview.dart +++ b/lib/chapawebview.dart @@ -1,9 +1,10 @@ import 'dart:async'; -import 'package:chapasdk/constants/strings.dart'; +import 'package:chapasdk/domain/constants/app_colors.dart'; +import 'package:chapasdk/domain/constants/strings.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'constants/common.dart'; +import 'package:chapasdk/domain/constants/common.dart'; class ChapaWebView extends StatefulWidget { final String url; @@ -16,6 +17,7 @@ class ChapaWebView extends StatefulWidget { //description // + // ignore: use_super_parameters const ChapaWebView( {Key? key, required this.url, @@ -42,30 +44,28 @@ class _ChapaWebViewState extends State { super.initState(); } - void checkConnectivity() { + void checkConnectivity() async { connection = Connectivity() .onConnectivityChanged - .listen((ConnectivityResult result) { - if (result == ConnectivityResult.none) { - setState(() { - isOffline = true; - }); - showErrorToast(ChapaStrings.connectionError); - - exitPaymentPage(ChapaStrings.connectionError); - } else if (result == ConnectivityResult.mobile) { + .listen((List result) async { + if (result.contains(ConnectivityResult.none)) { + await Future.delayed(const Duration(microseconds: 5)); + final newConnectivityStatus = await Connectivity().checkConnectivity(); + if (newConnectivityStatus.contains(ConnectivityResult.none)) { + setState(() { + isOffline = true; + }); + showErrorToast(ChapaStrings.connectionError); + exitPaymentPage(ChapaStrings.connectionError); + } + } else if (result.contains(ConnectivityResult.mobile) || + result.contains(ConnectivityResult.wifi) || + result.contains(ConnectivityResult.ethernet) || + result.contains(ConnectivityResult.vpn)) { setState(() { isOffline = false; }); - } else if (result == ConnectivityResult.wifi) { - setState(() { - isOffline = false; - }); - } else if (result == ConnectivityResult.ethernet) { - setState(() { - isOffline = false; - }); - } else if (result == ConnectivityResult.bluetooth) { + } else if (result.contains(ConnectivityResult.bluetooth)) { setState(() { isOffline = false; }); @@ -74,10 +74,11 @@ class _ChapaWebViewState extends State { }); } - void exitPaymentPage(String message) { - Navigator.pushNamed( + exitPaymentPage(String message) { + Navigator.pushNamedAndRemoveUntil( context, widget.fallBackNamedUrl, + (Route route) => false, arguments: { 'message': message, 'transactionReference': widget.transactionReference, @@ -94,67 +95,104 @@ class _ChapaWebViewState extends State { @override Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - body: Column(children: [ - Expanded( - child: InAppWebView( - initialUrlRequest: URLRequest(url: Uri.parse(widget.url)), - onWebViewCreated: (controller) { - setState(() { - webViewController = controller; - }); - controller.addJavaScriptHandler( - handlerName: "buttonState", - callback: (args) async { - webViewController = controller; - - if (args[2][1] == 'CancelbuttonClicked') { - exitPaymentPage('paymentCancelled'); - } - - return args.reduce((curr, next) => curr + next); - }); - }, - onUpdateVisitedHistory: (InAppWebViewController controller, - Uri? uri, androidIsReload) async { - if (uri.toString() == 'https://chapa.co') { - exitPaymentPage('paymentSuccessful'); - } - if (uri.toString().contains('checkout/payment-receipt/')) { - await delay(); - exitPaymentPage('paymentSuccessful'); - } - controller.addJavaScriptHandler( - handlerName: "handlerFooWithArgs", - callback: (args) async { - webViewController = controller; - if (args[2][1] == 'failed') { - await delay(); - exitPaymentPage('paymentFailed'); - } - if (args[2][1] == 'success') { - await delay(); - exitPaymentPage('paymentSuccessful'); - } - return args.reduce((curr, next) => curr + next); - }); - - controller.addJavaScriptHandler( - handlerName: "buttonState", - callback: (args) async { - webViewController = controller; - - if (args[2][1] == 'CancelbuttonClicked') { - exitPaymentPage('paymentCancelled'); - } - - return args.reduce((curr, next) => curr + next); - }); - }, + return PopScope( + canPop: false, + child: MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + appBar: AppBar( + backgroundColor: AppColors.chapaSecondaryColor, + title: Text( + "Checkout", + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(color: Colors.white), ), + actions: [ + TextButton.icon( + onPressed: () { + exitPaymentPage("paymentCancelled"); + }, + icon: Text( + "Cancel", + style: Theme.of(context) + .textTheme + .titleSmall! + .copyWith(color: Colors.white), + ), + label: Icon( + Icons.close, + color: Colors.white, + ), + ) + ], ), - ]), + body: Column(children: [ + Expanded( + child: InAppWebView( + initialUrlRequest: URLRequest( + url: WebUri( + (widget.url), + ), + ), + onWebViewCreated: (controller) { + setState(() { + webViewController = controller; + }); + controller.addJavaScriptHandler( + handlerName: "buttonState", + callback: (args) async { + webViewController = controller; + if (args[2][1] == 'CancelbuttonClicked') { + exitPaymentPage('paymentCancelled'); + } + + return args.reduce((curr, next) => curr + next); + }); + }, + onUpdateVisitedHistory: (InAppWebViewController controller, + Uri? uri, androidIsReload) async { + if (uri.toString() == 'https://chapa.co') { + exitPaymentPage('paymentSuccessful'); + } + if (uri.toString().contains('checkout/payment-receipt/')) { + // await delay(); + await Future.delayed(const Duration(seconds: 5)); + exitPaymentPage('paymentSuccessful'); + } + controller.addJavaScriptHandler( + handlerName: "handlerFooWithArgs", + callback: (args) async { + webViewController = controller; + if (args[2][1] == 'failed') { + await delay(); + + exitPaymentPage('paymentFailed'); + } + if (args[2][1] == 'success') { + await delay(); + exitPaymentPage('paymentSuccessful'); + } + return args.reduce((curr, next) => curr + next); + }); + + controller.addJavaScriptHandler( + handlerName: "buttonState", + callback: (args) async { + webViewController = controller; + + if (args[2][1] == 'CancelbuttonClicked') { + exitPaymentPage('paymentCancelled'); + } + + return args.reduce((curr, next) => curr + next); + }); + }, + ), + ), + ]), + ), ), ); } diff --git a/lib/constants/requests.dart b/lib/constants/requests.dart deleted file mode 100644 index 0789592..0000000 --- a/lib/constants/requests.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:convert'; -import 'package:chapasdk/constants/url.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:http/http.dart' as http; -import '../chapawebview.dart'; -import '../model/data.dart'; - -Future intilizeMyPayment( - BuildContext context, - String authorization, - String email, - String phone, - String amount, - String currency, - String firstName, - String lastName, - String transactionReference, - String customTitle, - String customDescription, - String fallBackNamedRoute, -) async { - final http.Response response = await http.post( - Uri.parse(ChapaUrl.baseUrl), - headers: { - 'Authorization': 'Bearer $authorization', - }, - body: { - 'phone_number': phone, - 'amount': amount, - 'currency': currency.toUpperCase(), - 'first_name': firstName, - 'last_name': lastName, - 'tx_ref': transactionReference, - 'customization[title]': customTitle, - 'customization[description]': customDescription - }, - ); - var jsonResponse = json.decode(response.body); - if (response.statusCode == 400) { - showToast(jsonResponse); - } else if (response.statusCode == 200) { - ResponseData res = ResponseData.fromJson(jsonResponse); - - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ChapaWebView( - url: res.data.checkoutUrl.toString(), - fallBackNamedUrl: fallBackNamedRoute, - transactionReference: transactionReference, - amountPaid: amount, - )), - ); - - return res.data.checkoutUrl.toString(); - } - - return ''; -} - -Future showToast(jsonResponse) { - return Fluttertoast.showToast( - msg: jsonResponse['message'], - toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.CENTER, - timeInSecForIosWeb: 1, - backgroundColor: Colors.red, - textColor: Colors.white, - fontSize: 16.0); -} diff --git a/lib/constants/url.dart b/lib/constants/url.dart deleted file mode 100644 index 04b2a4e..0000000 --- a/lib/constants/url.dart +++ /dev/null @@ -1,13 +0,0 @@ -class ChapaUrl { - static const String baseUrl = - "https://api.chapa.co/v1/transaction/mobile-initialize"; - static const String chargeCardUrl = "charges?type=card"; - static const String validateCharge = "validate-charge"; - static const String defaultRedirectUrl = "https://chapa.co/"; - static const String verifyTransaction = - "https://api.chapa.co/v1/transaction/verify/"; - - static String getBaseUrl(final bool isDebugMode) { - return baseUrl; - } -} diff --git a/lib/data/api_client.dart b/lib/data/api_client.dart new file mode 100644 index 0000000..7ad04f6 --- /dev/null +++ b/lib/data/api_client.dart @@ -0,0 +1,125 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:chapasdk/domain/constants/enums.dart'; +import 'package:chapasdk/data/dio_client.dart'; +import 'package:chapasdk/data/model/response/api_error_response.dart'; +import 'package:chapasdk/data/model/response/api_response.dart'; +import 'package:chapasdk/data/model/network_response.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:dio/dio.dart'; + +class ApiClient { + late DioClient dioClient; + final Dio dio; + final Connectivity connectivity; + Map defaultParams = {}; + + ApiClient({ + required this.dio, + required this.connectivity, + }) { + dioClient = DioClient(dio, connectivity: connectivity); + } + Future request( + {required RequestType requestType, + bool requiresAuth = true, + bool requiresDefaultParams = true, + required String path, + Map? queryParameters, + Map? data, + Map? headers, + bool isBodyJsonToString = false, + String jsonToStringBody = "", + required T Function(Map) fromJsonSuccess, + required U Function(Map, int) fromJsonError, + required String publicKey}) async { + try { + if (requiresAuth) { + await dioClient.addAuthorizationInterceptor(publicKey); + } + if (requiresDefaultParams && data != null) { + data = Map.from(data); + data.addAll(defaultParams); + } + + Options? options; + + dynamic response; + switch (requestType) { + case RequestType.get: + response = await dioClient.get(path, + options: options, queryParameters: queryParameters); + break; + case RequestType.post: + response = await dioClient.post( + path, + options: options, + data: isBodyJsonToString ? jsonToStringBody : data, + queryParameters: queryParameters, + ); + break; + case RequestType.patch: + response = await dioClient.patch(path, options: options, data: data); + break; + case RequestType.delete: + response = await dioClient.delete(path, options: options); + break; + case RequestType.put: + response = await dioClient.put(path, options: options, data: data); + break; + } + try { + if (response == null) { + return Success( + body: ApiResponse( + code: 200, + message: "Success", + )); + } + final successResponse = fromJsonSuccess(response); + return Success(body: successResponse); + } catch (e) { + return Success( + body: ApiResponse( + code: 200, + message: "Success", + )); + } + } on DioException catch (e) { + try { + switch (e.type) { + case DioExceptionType.connectionError: + return NetworkError( + error: SocketException(e.message ?? ""), + ); + + case DioExceptionType.badResponse: + try { + return ApiError( + error: fromJsonError( + e.response!.data, + e.response!.statusCode!, + ), + code: e.response!.statusCode!, + ); + } catch (error) { + return ApiError( + error: ApiErrorResponse.fromJson( + e.response!.data, + e.response!.statusCode!, + ), + code: e.response!.statusCode!, + ); + } + + default: + return UnknownError(error: e); + } + } catch (exeption) { + return ApiError(error: e.response!.data, code: e.response!.statusCode!); + } + } catch (e) { + return UnknownError(error: e); + } + } +} diff --git a/lib/data/dio_client.dart b/lib/data/dio_client.dart new file mode 100644 index 0000000..68ff2c9 --- /dev/null +++ b/lib/data/dio_client.dart @@ -0,0 +1,177 @@ +import 'dart:io'; +import 'package:chapasdk/domain/constants/url.dart'; +import 'package:chapasdk/data/dio_interceptors.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:dio/dio.dart'; +import 'package:dio/io.dart'; + +class DioClient { + late Dio _dio; + final String? baseUrl; + final List? interceptors; + final Connectivity connectivity; + + DioClient( + Dio? dio, { + this.interceptors, + this.baseUrl, + required this.connectivity, + }) { + _dio = dio ?? Dio(); + (_dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () => + HttpClient() + ..badCertificateCallback = + (X509Certificate cert, String host, int port) => true; + + _dio + ..options = BaseOptions( + baseUrl: ChapaUrl.directChargeBaseUrl, + connectTimeout: const Duration(seconds: 30), + receiveTimeout: const Duration(seconds: 30), + followRedirects: false, + ) + ..httpClientAdapter + ..options.headers = {'Content-Type': 'application/json'}; + if (interceptors?.isNotEmpty ?? false) { + _dio.interceptors.addAll(interceptors!); + } + } + + Future addAuthorizationInterceptor(String publicKey) async { + final hasAuthInterceptor = + _dio.interceptors.any((element) => element is AuthorizationInterceptor); + if (!hasAuthInterceptor) { + _dio.interceptors.add(AuthorizationInterceptor(_dio, publicKey)); + } + } + + Future get( + String uri, { + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onReceiveProgress, + }) async { + try { + var response = await _dio.get( + uri, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onReceiveProgress: onReceiveProgress, + ); + + return response.data; + } on SocketException catch (e) { + throw SocketException(e.toString()); + } on FormatException catch (_) { + throw const FormatException("Unable to process the data"); + } catch (e) { + rethrow; + } + } + + Future post( + String uri, { + data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + try { + var response = await _dio.post( + uri, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + return response.data; + } on FormatException catch (_) { + throw const FormatException("Unable to process the data"); + } catch (e) { + rethrow; + } + } + + Future patch( + String uri, { + data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + try { + var response = await _dio.patch( + uri, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + return response.data; + } on FormatException catch (_) { + throw const FormatException("Unable to process the data"); + } catch (e) { + rethrow; + } + } + + Future delete( + String uri, { + data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + }) async { + try { + var response = await _dio.delete( + uri, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + ); + return response.data; + } on FormatException catch (_) { + throw const FormatException("Unable to process the data"); + } catch (e) { + rethrow; + } + } + + Future put( + String uri, { + data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + try { + var response = await _dio.put( + uri, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + return response.data; + } on FormatException catch (_) { + throw const FormatException("Unable to process the data"); + } catch (e) { + rethrow; + } + } +} diff --git a/lib/data/dio_interceptors.dart b/lib/data/dio_interceptors.dart new file mode 100644 index 0000000..5d7b04e --- /dev/null +++ b/lib/data/dio_interceptors.dart @@ -0,0 +1,42 @@ +import 'dart:async'; +import 'dart:developer'; +import 'package:dio/dio.dart'; + +class AuthorizationInterceptor extends InterceptorsWrapper { + final Dio dio; + final String publicKey; + AuthorizationInterceptor(this.dio, this.publicKey); + + @override + Future onRequest( + RequestOptions options, + RequestInterceptorHandler handler, + ) async { + try { + options.headers['Authorization'] = "Bearer $publicKey"; + } catch (e) { + log('auth intercepter catched: $e'); + } + return handler.next(options); + } + + @override + Future onResponse( + Response response, + ResponseInterceptorHandler handler, + ) async { + if (response.statusCode == 401) { + dio.interceptors + .removeWhere((element) => element is AuthorizationInterceptor); + return handler.reject( + DioException( + response: response, + error: 'Bearer Token Expired', + type: DioExceptionType.unknown, + requestOptions: response.requestOptions, + ), + ); + } + return handler.next(response); + } +} diff --git a/lib/data/model/initiate_payment.dart b/lib/data/model/initiate_payment.dart new file mode 100644 index 0000000..f458218 --- /dev/null +++ b/lib/data/model/initiate_payment.dart @@ -0,0 +1,65 @@ +class DirectChargeApiError { + String? message; + String? status; + ValidationErrorData? data; + int? code; + Validate? validate; + String? paymentStatus; + + DirectChargeApiError( + {required this.message, + required this.status, + this.data, + required this.code, + this.validate, + this.paymentStatus}); + + DirectChargeApiError.fromJson(Map json, int statusCode) { + message = json['message']; + status = json['status']; + data = json['data'] != null + ? ValidationErrorData.fromJson(json['data']) + : null; + code = statusCode; + validate = json['validate']; + paymentStatus = json['payment_status']; + } +} + +class Validate { + List? mobile; + String? status; + String? data; + + Validate({ + this.mobile, + this.status, + this.data, + }); + + Validate.fromJson( + Map json, + ) { + mobile = json['mobile']; + } +} + +class ValidationErrorData { + String? message; + String? status; + dynamic data; + String? paymentStatus; + + ValidationErrorData( + {required this.message, + required this.status, + this.data, + this.paymentStatus}); + + ValidationErrorData.fromJson(Map json) { + message = json['message']; + status = json['status']; + data = json['data']; + paymentStatus = json['payment_status']; + } +} diff --git a/lib/data/model/network_response.dart b/lib/data/model/network_response.dart new file mode 100644 index 0000000..3e443b2 --- /dev/null +++ b/lib/data/model/network_response.dart @@ -0,0 +1,38 @@ +import 'dart:io'; + +abstract class NetworkResponse { + const NetworkResponse(); +} + +class Success extends NetworkResponse { + final T body; + + const Success({ + required this.body, + }); +} + +class ApiError extends NetworkResponse { + final U error; + final int code; + + const ApiError({ + required this.error, + required this.code, + }); +} + +class NetworkError extends NetworkResponse { + final IOException error; + + const NetworkError({ + required this.error, + }); +} + +class UnknownError extends NetworkResponse { + final dynamic error; + const UnknownError({ + required this.error, + }); +} diff --git a/lib/data/model/request/direct_charge_request.dart b/lib/data/model/request/direct_charge_request.dart new file mode 100644 index 0000000..6957fdf --- /dev/null +++ b/lib/data/model/request/direct_charge_request.dart @@ -0,0 +1,32 @@ +class DirectChargeRequest { + String firstName; + String amount; + String lastName; + String currency; + String email; + String txRef; + String mobile; + String paymentMethod; + DirectChargeRequest( + {required this.mobile, + required this.firstName, + required this.lastName, + required this.amount, + required this.currency, + required this.email, + required this.txRef, + required this.paymentMethod}); + + Map toJson() { + return { + "mobile": mobile, + 'currency': currency == "ETB" ? currency : "ETB", + 'tx_ref': txRef, + 'amount': amount, + 'first_name': firstName, + 'last_name': lastName, + 'email': email, + 'payment_method': paymentMethod + }; + } +} diff --git a/lib/data/model/request/validate_directCharge_request.dart b/lib/data/model/request/validate_directCharge_request.dart new file mode 100644 index 0000000..1b39559 --- /dev/null +++ b/lib/data/model/request/validate_directCharge_request.dart @@ -0,0 +1,18 @@ +// ignore: file_names +class ValidateDirectChargeRequest { + final String reference; + final String mobile; + final String paymentMethod; + ValidateDirectChargeRequest({ + required this.reference, + required this.mobile, + required this.paymentMethod, + }); + Map toJson() { + return { + "reference": reference, + "mobile": mobile, + "payment_method": paymentMethod + }; + } +} diff --git a/lib/data/model/response/api_error_response.dart b/lib/data/model/response/api_error_response.dart new file mode 100644 index 0000000..341f919 --- /dev/null +++ b/lib/data/model/response/api_error_response.dart @@ -0,0 +1,19 @@ +class ApiErrorResponse { + String? message; + int? statusCode; + String? status; + String? data; + String? validation; + + ApiErrorResponse({ + this.message, + this.statusCode, + }); + ApiErrorResponse.fromJson(Map json, int statusCode) { + message = json['message'] ?? ""; + statusCode = statusCode; + status = json['status'] ?? ""; + data = json['data'] ?? ""; + validation = json["validate"] ?? ""; + } +} diff --git a/lib/data/model/response/api_response.dart b/lib/data/model/response/api_response.dart new file mode 100644 index 0000000..e2d83fd --- /dev/null +++ b/lib/data/model/response/api_response.dart @@ -0,0 +1,114 @@ +import 'dart:convert'; +import 'package:http/http.dart'; + +class ApiResponse { + List get data => body["body"]; + bool get allGood => errors!.isEmpty; + String get hasException => exception!; + bool hasError() => errors!.isNotEmpty; + bool hasMessageError() => messageError!.isNotEmpty; + bool hasData() => data.isNotEmpty; + + int? code; + String? message; + dynamic body; + List? errors; + List? messageError; + String? exception; + + ApiResponse({ + required this.code, + required this.message, + this.body, + this.errors, + }); + + factory ApiResponse.fromResponse( + Response response, + ) { + int code = response.statusCode; + dynamic body = jsonDecode(response.body); + List errors = []; + String message = ""; + List messageError = []; + + switch (code) { + case 200: + try { + message = body is Map ? body['message'] : ""; + } catch (error) { + messageError.add(message); + } + + break; + case 201: + try { + message = body is Map ? body['message'] : ""; + } catch (error) { + messageError.add(message); + } + break; + case 400: + try { + message = body is Map ? body['errors'][0]['message'] ?? "" : ""; + + errors.add(message); + } catch (error) { + message = body is Map ? body['message'] : ""; + + errors.add(message); + } + break; + + case 401: + try { + message = + body is Map ? body['message'] ?? "unauthorized" : "unauthorized"; + errors.add(message); + } catch (error) { + // debugPrint("Message reading error in Error ==> $error"); + errors.add(message); + } + break; + case 408: + try { + message = + "Looks like the server is taking to long to respond, please try again in sometime"; + errors.add(message); + } catch (error) { + errors.add(message); + } + break; + + case 429: + try { + message = body is Map ? body['message'] ?? "" : "too many request"; + errors.add(message); + } catch (error) { + errors.add(message); + } + break; + + default: + try { + message = body["message"] ?? + "Sorry! Something went wrong, please contact support."; + } catch (e) { + message = "Sorry! Something went wrong, please contact support."; + } + + break; + } + + return ApiResponse( + code: code, + message: message, + body: body, + errors: errors, + ); + } + + ApiResponse.fromJson(Map json) { + message = json['message']; + } +} diff --git a/lib/data/model/response/direct_charge_success_response.dart b/lib/data/model/response/direct_charge_success_response.dart new file mode 100644 index 0000000..b905f6a --- /dev/null +++ b/lib/data/model/response/direct_charge_success_response.dart @@ -0,0 +1,45 @@ +import 'package:chapasdk/domain/constants/enums.dart'; +import 'package:chapasdk/domain/constants/extentions.dart'; + +class DirectChargeSuccessResponse { + String? message; + String? status; + Data? data; + DirectChargeSuccessResponse({this.message, this.status, this.data}); + DirectChargeSuccessResponse.fromJson(Map json) { + message = json['message']; + status = json['status']; + data = Data.fromJson(json['data']); + } +} + +class Data { + VerificationType? authDataType; + String? requestID; + MetaData? meta; + String? mode; + + Data({this.authDataType, this.requestID, this.meta, this.mode}); + + Data.fromJson(Map json) { + authDataType = json["auth_type"].toString().parseAuthDataType(); + requestID = json['requestID']; + meta = MetaData.fromJson(json['meta']); + mode = json['mode']; + } +} + +class MetaData { + String? message; + String? status; + String? refId; + PaymentStatus? paymentStatus; + MetaData({this.message, this.status, this.paymentStatus, this.refId}); + + MetaData.fromJson(Map json) { + message = json['message']; + status = json['status']; + refId = json['ref_id']; + paymentStatus = json['payment_status'].toString().parsePaymentStatus(); + } +} diff --git a/lib/data/model/response/verify_direct_charge_response.dart b/lib/data/model/response/verify_direct_charge_response.dart new file mode 100644 index 0000000..1b42729 --- /dev/null +++ b/lib/data/model/response/verify_direct_charge_response.dart @@ -0,0 +1,44 @@ +class ValidateDirectChargeResponse { + String? message; + String? trxRef; + String? processorId; + HistoryData? data; + ValidateDirectChargeResponse( + {this.message, this.trxRef, this.processorId, required this.data}); + + ValidateDirectChargeResponse.fromJson(Map json) { + message = json['message']; + trxRef = json['trx_ref']; + processorId = json['processor_id']; + data = HistoryData.fromJson(json['data']); + } + + DateTime getCreatedAtTime() { + if (data != null) { + if (data?.createdAt != null) { + return DateTime.parse(data!.createdAt!); + } else { + return DateTime.now(); + } + } else { + return DateTime.now(); + } + } +} + +class HistoryData { + String? amount; + String? charge; + String? status; + String? createdAt; + + HistoryData({this.amount, this.charge, this.status, this.createdAt}); + + HistoryData.fromJson(Map json) { + amount = json['amount']; + charge = json['charge']; + status = json['status']; + createdAt = json['created_at']; + } + +} diff --git a/lib/data/services/payment_service.dart b/lib/data/services/payment_service.dart new file mode 100644 index 0000000..10f9345 --- /dev/null +++ b/lib/data/services/payment_service.dart @@ -0,0 +1,44 @@ +import 'package:chapasdk/data/model/initiate_payment.dart'; +import 'package:chapasdk/data/model/response/direct_charge_success_response.dart'; +import 'package:chapasdk/domain/constants/enums.dart'; +import 'package:chapasdk/domain/constants/url.dart'; +import 'package:chapasdk/data/api_client.dart'; +import 'package:chapasdk/data/model/network_response.dart'; +import 'package:chapasdk/data/model/request/direct_charge_request.dart'; +import 'package:chapasdk/data/model/request/validate_directCharge_request.dart'; +import 'package:chapasdk/data/model/response/api_error_response.dart'; +import 'package:chapasdk/data/model/response/verify_direct_charge_response.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:dio/dio.dart'; + +class PaymentService { + ApiClient apiClient = ApiClient(dio: Dio(), connectivity: Connectivity()); + Future initializeDirectPayment({ + required DirectChargeRequest request, + required String publicKey, + }) async { + return apiClient.request( + requestType: RequestType.post, + path: ChapaUrl.directCharge, + requiresAuth: true, + data: request.toJson(), + fromJsonSuccess: DirectChargeSuccessResponse.fromJson, + fromJsonError: DirectChargeApiError.fromJson, + publicKey: publicKey, + ); + } + + Future verifyPayment( + {required ValidateDirectChargeRequest body, + required String publicKey}) async { + return apiClient.request( + requestType: RequestType.post, + requiresAuth: true, + path: ChapaUrl.verifyUrl, + data: body.toJson(), + fromJsonSuccess: ValidateDirectChargeResponse.fromJson, + fromJsonError: ApiErrorResponse.fromJson, + publicKey: publicKey, + ); + } +} diff --git a/lib/domain/constants/app_colors.dart b/lib/domain/constants/app_colors.dart new file mode 100644 index 0000000..6f51cba --- /dev/null +++ b/lib/domain/constants/app_colors.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; + +class AppColors { + static const Color chapaPrimaryColor = Color(0xff7DC400); + static const Color chapaSecondaryColor = Color(0xff0D1B34); + + static const Color shadowColor = Color(0xfff6f6f6); + +} diff --git a/lib/domain/constants/app_images.dart b/lib/domain/constants/app_images.dart new file mode 100644 index 0000000..7f8b90c --- /dev/null +++ b/lib/domain/constants/app_images.dart @@ -0,0 +1,10 @@ +class AppImages { + static const telebirr = "packages/chapasdk/assets/images/telebirr.png"; + static const mpesa = "packages/chapasdk/assets/images/mpesa.png"; + static const ebirr = "packages/chapasdk/assets/images/ebirr.png"; + static const chapaFullLogo = "packages/chapasdk/assets/images/chapa-logo.png"; + static const cbebirr = "packages/chapasdk/assets/images/cbebirr.png"; + static const ethiopiaLogo = + "packages/chapasdk/assets/images/ethiopia-flag.png"; + static const successIcon = "packages/chapasdk/assets/images/success-icon.png"; +} diff --git a/lib/constants/common.dart b/lib/domain/constants/common.dart similarity index 92% rename from lib/constants/common.dart rename to lib/domain/constants/common.dart index ef5a0be..6b24b73 100644 --- a/lib/constants/common.dart +++ b/lib/domain/constants/common.dart @@ -5,7 +5,7 @@ Future showErrorToast(String message) { return Fluttertoast.showToast( msg: message, toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.CENTER, + gravity: ToastGravity.TOP, timeInSecForIosWeb: 1, backgroundColor: Colors.red, textColor: Colors.white, diff --git a/lib/domain/constants/enums.dart b/lib/domain/constants/enums.dart new file mode 100644 index 0000000..fadc804 --- /dev/null +++ b/lib/domain/constants/enums.dart @@ -0,0 +1,29 @@ +enum LocalPaymentMethods { + telebirr, + mpessa, + cbebirr, + ebirr, +} + +enum RequestType { + get, + post, + patch, + put, + delete, +} + +enum PaymentStatus { + pending, + success, +} + +enum Mode { + live, + testing, +} + +enum VerificationType { + ussd, + otp, +} diff --git a/lib/domain/constants/extentions.dart b/lib/domain/constants/extentions.dart new file mode 100644 index 0000000..2f98191 --- /dev/null +++ b/lib/domain/constants/extentions.dart @@ -0,0 +1,137 @@ +import 'dart:math'; + +import 'package:chapasdk/domain/constants/app_images.dart'; +import 'package:chapasdk/domain/constants/enums.dart'; +import 'package:intl/intl.dart'; + +extension PaymentTypeExtention on LocalPaymentMethods { + String displayName() { + switch (this) { + case LocalPaymentMethods.telebirr: + return "Telebirr"; + case LocalPaymentMethods.cbebirr: + return "CBEBirr"; + case LocalPaymentMethods.mpessa: + return "M-Pesa"; + case LocalPaymentMethods.ebirr: + return "Ebirr"; + } + } + + String value() { + switch (this) { + case LocalPaymentMethods.telebirr: + return "telebirr"; + case LocalPaymentMethods.mpessa: + return "mpesa"; + case LocalPaymentMethods.ebirr: + return "ebirr"; + case LocalPaymentMethods.cbebirr: + return "cbebirr"; + } + } + + VerificationType verificationType() { + switch (this) { + case LocalPaymentMethods.telebirr: + return VerificationType.ussd; + case LocalPaymentMethods.mpessa: + return VerificationType.ussd; + case LocalPaymentMethods.ebirr: + return VerificationType.ussd; + case LocalPaymentMethods.cbebirr: + return VerificationType.ussd; + } + } + + String iconPath() { + switch (this) { + case LocalPaymentMethods.telebirr: + return AppImages.telebirr; + case LocalPaymentMethods.mpessa: + return AppImages.mpesa; + case LocalPaymentMethods.ebirr: + return AppImages.ebirr; + case LocalPaymentMethods.cbebirr: + return AppImages.cbebirr; + } + } +} + +extension StringExtention on String { + String formattedBirr() { + var noSymbolInUSFormat = NumberFormat.compactCurrency(locale: "am"); + + String amount = this; + return noSymbolInUSFormat.format(double.parse(amount)); + } + + VerificationType parseAuthDataType() { + switch (this) { + case "ussd": + return VerificationType.ussd; + case "otp": + return VerificationType.otp; + default: + return VerificationType.otp; + } + } + + Mode parseMode() { + switch (this) { + case "live": + return Mode.live; + case "testing": + return Mode.testing; + default: + return Mode.testing; + } + } + + PaymentStatus parsePaymentStatus() { + { + switch (this) { + case "pending": + return PaymentStatus.pending; + + default: + return PaymentStatus.pending; + } + } + } +} + +extension VerificationTypeExtention on VerificationType { + String getVerificationTypeValue() { + switch (this) { + case VerificationType.otp: + return "otp"; + case VerificationType.ussd: + return "ussd"; + } + } +} + +List getFilteredPaymentMethods(List filterValues) { + return LocalPaymentMethods.values.where((paymentMethod) { + return filterValues.any((filter) => + filter.toLowerCase() == paymentMethod.value().toLowerCase()); + }).toList(); +} + +extension DateExtention on DateTime { + String format() { + return DateFormat('EEE, MMM d yyyy, h:mm a').format( + this, + ); + } +} + +String generateTransactionRef() { + var r = Random(); + const _chars = + 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; + String transactionRef = + List.generate(10, (index) => _chars[r.nextInt(_chars.length)]).join(); + return transactionRef; +} diff --git a/lib/domain/constants/requests.dart b/lib/domain/constants/requests.dart new file mode 100644 index 0000000..5a23123 --- /dev/null +++ b/lib/domain/constants/requests.dart @@ -0,0 +1,86 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; +import 'package:chapasdk/chapawebview.dart'; +import 'package:chapasdk/domain/constants/url.dart'; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:http/http.dart' as http; + + +Future initializeMyPayment( + BuildContext context, + String email, + String phone, + String amount, + String currency, + String firstName, + String lastName, + String transactionReference, + String customTitle, + String customDescription, + String fallBackNamedRoute, + String publicKey) async { + try { + final http.Response response = await http.post( + Uri.parse(ChapaUrl.chapaPay), + body: { + 'public_key': publicKey, + 'phone_number': phone, + 'amount': amount, + 'currency': currency.toUpperCase(), + 'first_name': firstName, + 'last_name': lastName, + "email": email, + 'tx_ref': transactionReference, + 'customization[title]': customTitle, + 'customization[description]': customDescription + }, + ); + if (response.statusCode == 400) { + var jsonResponse = json.decode(response.body); + showToast(jsonResponse); + return ''; + } else if (response.statusCode == 302) { + String? redirectUrl = response.headers['location']; + if (redirectUrl != null) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChapaWebView( + url: redirectUrl, + fallBackNamedUrl: fallBackNamedRoute, + transactionReference: transactionReference, + amountPaid: amount, + )), + ); + } + return redirectUrl.toString(); + } else { + log("Http Error"); + log(response.body); + return ''; + } + } on SocketException catch (_) { + showToast({ + 'message': + "There is no Internet Connection \n Please check your Internet Connection and Try it again." + }); + return ''; + } catch (e) { + log(e.toString()); + log("Exception here"); + return ''; + } +} + +Future showToast(jsonResponse) { + return Fluttertoast.showToast( + msg: jsonResponse['message'], + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.TOP, + timeInSecForIosWeb: 1, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0); +} diff --git a/lib/constants/strings.dart b/lib/domain/constants/strings.dart similarity index 100% rename from lib/constants/strings.dart rename to lib/domain/constants/strings.dart diff --git a/lib/domain/constants/url.dart b/lib/domain/constants/url.dart new file mode 100644 index 0000000..4a093d0 --- /dev/null +++ b/lib/domain/constants/url.dart @@ -0,0 +1,6 @@ +class ChapaUrl { + static const String chapaPay = "https://api.chapa.co/v1/hosted/pay"; + static String directChargeBaseUrl = "https://inline.chapaservices.net/v1"; + static String directCharge = "/inline/charge"; + static String verifyUrl = "/inline/validate"; +} diff --git a/lib/domain/custom-widget/contact_us.dart b/lib/domain/custom-widget/contact_us.dart new file mode 100644 index 0000000..2c4a4f7 --- /dev/null +++ b/lib/domain/custom-widget/contact_us.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +class ContactUs extends StatelessWidget { + const ContactUs({super.key}); + + @override + Widget build(BuildContext context) { + Size deviceSize = MediaQuery.of(context).size; + return Align( + alignment: Alignment.topCenter, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error, + size: deviceSize.width * 0.2, + color: Colors.red, + ), + SizedBox( + height: deviceSize.height * 0.02, + ), + Text( + "Oops", + style: TextStyle( + fontSize: deviceSize.width * 0.054, + fontWeight: FontWeight.bold, + color: Theme.of(context).textTheme.bodyLarge!.color), + ), + Padding( + padding: EdgeInsets.only( + left: deviceSize.width * 0.04, + right: deviceSize.width * 0.04, + top: 8), + child: Text( + "Something went wrong Please contact us \n Thank you!", + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.w200, + fontSize: deviceSize.width * 0.032, + wordSpacing: 5, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/domain/custom-widget/custom_button.dart b/lib/domain/custom-widget/custom_button.dart new file mode 100644 index 0000000..03ade60 --- /dev/null +++ b/lib/domain/custom-widget/custom_button.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +// ignore: must_be_immutable +class CustomButton extends StatelessWidget { + Function onPressed; + Color? backgroundColor; + String title; + + CustomButton( + {super.key, + required this.onPressed, + this.backgroundColor, + required this.title}); + + @override + Widget build(BuildContext context) { + Size deviceSize = MediaQuery.of(context).size; + return SizedBox( + width: deviceSize.width, + height: deviceSize.height * 0.048, + child: ElevatedButton( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + backgroundColor ?? Theme.of(context).primaryColor, + )), + onPressed: () { + onPressed(); + }, + child: Text( + title, + style: TextStyle( + color: Colors.white, + ), + ), + ), + ); + } +} diff --git a/lib/domain/custom-widget/custom_textform.dart b/lib/domain/custom-widget/custom_textform.dart new file mode 100644 index 0000000..caa4d72 --- /dev/null +++ b/lib/domain/custom-widget/custom_textform.dart @@ -0,0 +1,163 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +// ignore: must_be_immutable +class CustomTextForm extends StatefulWidget { + CustomTextForm( + {super.key, + required this.controller, + this.enableBorder = true, + this.hintText = "", + this.labelText = "", + this.cursorColor = Colors.green, + this.filled = false, + this.filledColor = Colors.transparent, + this.hintTextStyle = const TextStyle(color: Colors.black), + this.obscuringCharacter = "*", + this.readOnly = false, + this.obscureText = false, + this.labelTextStyle = const TextStyle(color: Colors.black), + this.prefix, + this.suffix, + this.textStyle, + this.textInputAction = TextInputAction.done, + this.keyboardType = TextInputType.text, + this.validator, + this.onTap, + this.onChanged, + this.autovalidateMode = AutovalidateMode.onUserInteraction, + this.inputFormatter, + this.maxLine = 2, + this.minLine = 1}); + final TextEditingController controller; + TextInputType keyboardType; + TextInputAction textInputAction; + + final bool enableBorder; + final bool filled; + final bool readOnly; + final bool obscureText; + final AutovalidateMode? autovalidateMode; + + final String hintText; + final String labelText; + final String obscuringCharacter; + + final TextStyle? hintTextStyle; + final TextStyle labelTextStyle; + final TextStyle? textStyle; + + final Color filledColor; + final Color cursorColor; + + final Widget? suffix; + final Widget? prefix; + + final Function? onTap; + final String? Function(String?)? validator; + Function(String)? onFieldSubmitted; + Function(String)? onChanged; + List? inputFormatter; + final int maxLine; + final int minLine; + + @override + State createState() => _CustomTextFormState(); +} + +class _CustomTextFormState extends State { + bool makePasswordVisible = true; + + @override + Widget build(BuildContext context) { + Size deviceSize = MediaQuery.of(context).size; + return TextFormField( + style: widget.textStyle, + controller: widget.controller, + cursorColor: widget.cursorColor, + decoration: InputDecoration( + floatingLabelBehavior: FloatingLabelBehavior.always, + filled: widget.filled, + fillColor: widget.filledColor, + hintText: widget.hintText, + hintStyle: widget.hintTextStyle, + labelText: widget.labelText, + labelStyle: widget.labelTextStyle, + isDense: true, + prefixIcon: widget.prefix, + focusedErrorBorder: OutlineInputBorder( + borderSide: widget.enableBorder + ? const BorderSide( + width: 0.5, + color: Colors.red, + ) + : BorderSide.none, + borderRadius: BorderRadius.circular(11), + ), + errorBorder: OutlineInputBorder( + borderSide: widget.enableBorder + ? const BorderSide(width: 0.5, color: Colors.red) + : BorderSide.none, + borderRadius: BorderRadius.circular(11), + ), + prefixIconConstraints: const BoxConstraints(minHeight: 0, minWidth: 0), + constraints: const BoxConstraints(minHeight: 0, minWidth: 0), + contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 14), + enabledBorder: OutlineInputBorder( + borderSide: widget.enableBorder + ? BorderSide( + width: 0.1, + color: Theme.of(context).textTheme.bodyMedium!.color!) + : BorderSide.none, + borderRadius: BorderRadius.circular(11), + ), + focusedBorder: OutlineInputBorder( + borderSide: widget.enableBorder + ? BorderSide(width: 0.5, color: Theme.of(context).primaryColor) + : BorderSide.none, + borderRadius: BorderRadius.circular(11), + ), + suffixIcon: widget.suffix ?? _getSuffixWidget(), + ), + onFieldSubmitted: (data) { + if (widget.onFieldSubmitted != null) { + widget.onFieldSubmitted!(data); + FocusScope.of(context).unfocus(); + } else { + // FocusScope.of(context).requestFocus(widget.nextFocusNode); + } + }, + textInputAction: widget.textInputAction, + validator: (value) => widget.validator!(value), + obscureText: widget.obscureText ? makePasswordVisible : false, + obscuringCharacter: widget.obscuringCharacter, + onTap: () => widget.onTap, + readOnly: widget.readOnly, + onChanged: (value) => widget.onChanged, + keyboardType: widget.keyboardType, + inputFormatters: widget.inputFormatter, + autovalidateMode: widget.autovalidateMode, + scrollPadding: EdgeInsets.symmetric( + vertical: deviceSize.height * 0.2, + ), + ); + } + + Widget _getSuffixWidget() { + if (widget.obscureText) { + return TextButton( + onPressed: () { + setState(() { + makePasswordVisible = !makePasswordVisible; + }); + }, + child: Icon( + (!makePasswordVisible) ? Icons.visibility : Icons.visibility_off, + color: Theme.of(context).primaryColor, + ), + ); + } else { + return const SizedBox.shrink(); + } + } +} diff --git a/lib/domain/custom-widget/no_connection.dart b/lib/domain/custom-widget/no_connection.dart new file mode 100644 index 0000000..6cd085e --- /dev/null +++ b/lib/domain/custom-widget/no_connection.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +class NoConnection extends StatelessWidget { + const NoConnection({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + Size deviceSize = MediaQuery.of(context).size; + return PopScope( + canPop: false, + child: Scaffold( + body: Align( + alignment: Alignment.topCenter, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: deviceSize.width * 0.2), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.wifi_off_outlined, + size: deviceSize.width * 0.16, + color: Colors.red, + ), + SizedBox( + height: deviceSize.height * 0.02, + ), + Text( + "Opps", + style: TextStyle( + fontSize: deviceSize.width * 0.04, + fontWeight: FontWeight.bold, + color: Theme.of(context).textTheme.bodyLarge!.color), + ), + Padding( + padding: EdgeInsets.only( + left: deviceSize.width * 0.04, + right: deviceSize.width * 0.04, + top: 8), + child: Text( + "There is no Internet Connection \n Please check your Internet Connection and Try it again.", + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.w200, + color: Theme.of(context).textTheme.bodySmall!.color, + fontSize: deviceSize.width * 0.024, + ), + ), + ), + ], + ), + ), + )), + ); + } +} diff --git a/lib/features/native-checkout/bloc/chapa_native_checkout_bloc.dart b/lib/features/native-checkout/bloc/chapa_native_checkout_bloc.dart new file mode 100644 index 0000000..1a9c0ca --- /dev/null +++ b/lib/features/native-checkout/bloc/chapa_native_checkout_bloc.dart @@ -0,0 +1,108 @@ +import 'package:bloc/bloc.dart'; +import 'package:chapasdk/data/model/initiate_payment.dart'; +import 'package:chapasdk/data/model/response/direct_charge_success_response.dart'; +import 'package:chapasdk/data/model/network_response.dart'; +import 'package:chapasdk/data/model/request/direct_charge_request.dart'; +import 'package:chapasdk/data/model/request/validate_directCharge_request.dart'; +import 'package:chapasdk/data/model/response/api_error_response.dart'; +import 'package:chapasdk/data/model/response/verify_direct_charge_response.dart'; +import 'package:chapasdk/data/services/payment_service.dart'; +import 'package:meta/meta.dart'; + +part 'chapa_native_checkout_event.dart'; +part 'chapa_native_checkout_state.dart'; + +class ChapaNativeCheckoutBloc + extends Bloc { + PaymentService paymentService; + ChapaNativeCheckoutBloc({required this.paymentService}) + : super(ChapaNativeCheckoutInitial()) { + on((event, emit) { + emit(ChapaNativeCheckoutInitial()); + }); + on((event, emit) async { + emit(ChapaNativeCheckoutLoadingState()); + try { + NetworkResponse networkResponse = + await paymentService.initializeDirectPayment( + request: event.directChargeRequest, + publicKey: event.publicKey, + ); + + if (networkResponse is Success) { + DirectChargeSuccessResponse directChargeSuccessResponse = + networkResponse.body; + String reference = + directChargeSuccessResponse.data!.meta!.refId ?? ""; + + if (reference.isNotEmpty) { + add(ValidatePayment( + validateDirectChargeRequest: ValidateDirectChargeRequest( + reference: reference, + mobile: event.directChargeRequest.mobile, + paymentMethod: event.directChargeRequest.paymentMethod), + publicKey: event.publicKey, + )); + } + } else if (networkResponse is ApiError) { + try { + DirectChargeApiError directChargeApiError = networkResponse.error; + emit(ChapaNativeCheckoutPaymentInitiateApiError( + directChargeApiError: directChargeApiError, + )); + } catch (e) { + ApiErrorResponse apiErrorResponse = networkResponse.error; + + emit(ChapaNativeCheckoutPaymentInitiateApiError( + apiErrorResponse: apiErrorResponse, + )); + } + } else if (networkResponse is NetworkError) { + emit(ChapaNativeCheckoutNetworkError()); + } else if (networkResponse is UnknownError) { + emit(ChapaNativeCheckoutUnknownError()); + } else { + emit(ChapaNativeCheckoutUnknownError()); + } + } catch (e) { + emit(ChapaNativeCheckoutUnknownError()); + } + }); + on((event, emit) async { + emit(ChapaNativeCheckoutValidationOngoingState()); + try { + NetworkResponse networkResponse = await paymentService.verifyPayment( + body: event.validateDirectChargeRequest, + publicKey: event.publicKey, + ); + if (networkResponse is Success) { + ValidateDirectChargeResponse verifyResult = networkResponse.body; + if (verifyResult.data?.status == "success") { + emit(ChapaNativeCheckoutPaymentValidateSuccessState( + directChargeValidateResponse: networkResponse.body, + isPaymentFailed: false, + )); + } else if (verifyResult.data?.status == "pending") { + add(ValidatePayment( + validateDirectChargeRequest: event.validateDirectChargeRequest, + publicKey: event.publicKey, + )); + } else { + emit(ChapaNativeCheckoutPaymentValidateSuccessState( + directChargeValidateResponse: networkResponse.body, + isPaymentFailed: true)); + } + } else if (networkResponse is ApiError) { + emit(ChapaNativeCheckoutPaymentValidateApiError( + apiErrorResponse: networkResponse.error)); + } else if (networkResponse is NetworkError) { + emit(ChapaNativeCheckoutNetworkError()); + } else { + emit(ChapaNativeCheckoutUnknownError()); + } + } catch (e) { + emit(ChapaNativeCheckoutUnknownError()); + } + }); + } +} diff --git a/lib/features/native-checkout/bloc/chapa_native_checkout_event.dart b/lib/features/native-checkout/bloc/chapa_native_checkout_event.dart new file mode 100644 index 0000000..d98c19d --- /dev/null +++ b/lib/features/native-checkout/bloc/chapa_native_checkout_event.dart @@ -0,0 +1,28 @@ +part of 'chapa_native_checkout_bloc.dart'; + +@immutable +sealed class ChapaNativeCheckoutEvent {} + +// ignore: must_be_immutable +class InitiatePayment extends ChapaNativeCheckoutEvent { + DirectChargeRequest directChargeRequest; + String publicKey; + + InitiatePayment({ + required this.directChargeRequest, + required this.publicKey, + + }); +} + +// ignore: must_be_immutable +class ValidatePayment extends ChapaNativeCheckoutEvent { + ValidateDirectChargeRequest validateDirectChargeRequest; + String publicKey; + + ValidatePayment({ + required this.validateDirectChargeRequest, + required this.publicKey, + + }); +} diff --git a/lib/features/native-checkout/bloc/chapa_native_checkout_state.dart b/lib/features/native-checkout/bloc/chapa_native_checkout_state.dart new file mode 100644 index 0000000..77f84fc --- /dev/null +++ b/lib/features/native-checkout/bloc/chapa_native_checkout_state.dart @@ -0,0 +1,56 @@ +part of 'chapa_native_checkout_bloc.dart'; + +@immutable +sealed class ChapaNativeCheckoutState {} + +final class ChapaNativeCheckoutInitial extends ChapaNativeCheckoutState {} + +final class ChapaNativeCheckoutLoadingState extends ChapaNativeCheckoutState {} + +final class ChapaNativeCheckoutPaymentInitiateSuccessState + extends ChapaNativeCheckoutState { + final DirectChargeSuccessResponse directChargeSuccessResponse; + + ChapaNativeCheckoutPaymentInitiateSuccessState({ + required this.directChargeSuccessResponse, + }); +} + +// ignore: must_be_immutable +final class ChapaNativeCheckoutPaymentInitiateApiError extends ChapaNativeCheckoutState { + ApiErrorResponse? apiErrorResponse; + DirectChargeApiError? directChargeApiError; + ChapaNativeCheckoutPaymentInitiateApiError({ + this.apiErrorResponse, + this.directChargeApiError, + }); +} + +// Validating + +final class ChapaNativeCheckoutValidationOngoingState + extends ChapaNativeCheckoutState {} + +final class ChapaNativeCheckoutPaymentValidateSuccessState + extends ChapaNativeCheckoutState { + final ValidateDirectChargeResponse directChargeValidateResponse; + final bool isPaymentFailed; + ChapaNativeCheckoutPaymentValidateSuccessState({ + required this.directChargeValidateResponse, + required this.isPaymentFailed, + }); +} + +// ignore: must_be_immutable +final class ChapaNativeCheckoutPaymentValidateApiError + extends ChapaNativeCheckoutState { + ApiErrorResponse? apiErrorResponse; + + ChapaNativeCheckoutPaymentValidateApiError({ + this.apiErrorResponse, + }); +} + +final class ChapaNativeCheckoutUnknownError extends ChapaNativeCheckoutState {} + +final class ChapaNativeCheckoutNetworkError extends ChapaNativeCheckoutState {} diff --git a/lib/features/native-checkout/chapa_native_payment.dart b/lib/features/native-checkout/chapa_native_payment.dart new file mode 100644 index 0000000..41009e9 --- /dev/null +++ b/lib/features/native-checkout/chapa_native_payment.dart @@ -0,0 +1,877 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui'; +import 'package:chapasdk/domain/constants/app_colors.dart'; +import 'package:chapasdk/domain/constants/app_images.dart'; +import 'package:chapasdk/domain/constants/enums.dart'; +import 'package:chapasdk/domain/constants/extentions.dart'; +import 'package:chapasdk/domain/constants/requests.dart'; +import 'package:chapasdk/domain/custom-widget/contact_us.dart'; +import 'package:chapasdk/domain/custom-widget/custom_button.dart'; +import 'package:chapasdk/data/model/request/direct_charge_request.dart'; +import 'package:chapasdk/data/services/payment_service.dart'; +import 'package:chapasdk/domain/custom-widget/custom_textform.dart'; +import 'package:chapasdk/domain/custom-widget/no_connection.dart'; +import 'package:chapasdk/features/native-checkout/bloc/chapa_native_checkout_bloc.dart'; +import 'package:chapasdk/features/network/bloc/network_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +// ignore: must_be_immutable +class ChapaNativePayment extends StatefulWidget { + final BuildContext context; + final String publicKey; + final String email; + final String phone; + final String amount; + final String currency; + final String firstName; + final String lastName; + final String txRef; + final String title; + final String desc; + final String namedRouteFallBack; + final Color? buttonColor; + final bool? showPaymentMethodsOnGridView; + List availablePaymentMethods; + + ChapaNativePayment({ + super.key, + required this.context, + required this.publicKey, + required this.email, + required this.phone, + required this.amount, + required this.firstName, + required this.lastName, + required this.txRef, + required this.title, + required this.desc, + required this.namedRouteFallBack, + required this.currency, + this.buttonColor, + this.showPaymentMethodsOnGridView, + this.availablePaymentMethods = const [ + "telebirr", + "cbebirr", + "ebirr", + "mpesa", + ], + }); + + @override + State createState() => _ChapaNativePaymentState(); +} + +class _ChapaNativePaymentState extends State { + PaymentService paymentService = PaymentService(); + LocalPaymentMethods? selectedLocalPaymentMethods; + final _formKey = GlobalKey(); + TextEditingController phoneNumberController = TextEditingController(); + late ChapaNativeCheckoutBloc _chapaNativeCheckoutBloc; + late NetworkBloc _networkBloc; + bool chapasButtonIsClicked = false; + bool showPaymentMethodError = false; + bool _isDialogShowing = false; + List paymentMethods = []; + + @override + void initState() { + phoneNumberController = TextEditingController( + text: widget.phone, + ); + + _chapaNativeCheckoutBloc = + ChapaNativeCheckoutBloc(paymentService: PaymentService()); + _networkBloc = NetworkBloc(); + + setState(() { + if (widget.availablePaymentMethods.isNotEmpty) { + paymentMethods = getFilteredPaymentMethods( + widget.availablePaymentMethods, + ); + } else { + paymentMethods = LocalPaymentMethods.values; + } + }); + + super.initState(); + } + + @override + void dispose() { + _chapaNativeCheckoutBloc.close(); + phoneNumberController.dispose(); + super.dispose(); + } + + exitPaymentPage(String message, String? chapaTransactionRef) { + Navigator.pushNamedAndRemoveUntil( + context, + widget.namedRouteFallBack, + (Route route) => false, + arguments: { + 'message': message, + 'transactionReference': chapaTransactionRef ?? widget.txRef, + 'paidAmount': widget.amount + }, + ); + } + + Future _showProcessingDialog() async { + if (_isDialogShowing) return; + setState(() => _isDialogShowing = true); + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const Dialog( + child: Padding( + padding: EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Processing Payment'), + SizedBox(height: 12), + Text('Please wait while we process your payment.'), + SizedBox(height: 12), + SizedBox( + width: 15, + height: 15, + child: CircularProgressIndicator(), + ), + ], + ), + ), + ), + ); + } + + _hideDialog() { + if (_isDialogShowing) { + Navigator.of(context, rootNavigator: true).pop(); + setState(() => _isDialogShowing = false); + } + } + + @override + Widget build(BuildContext context) { + Size deviceSize = MediaQuery.of(context).size; + + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: Icon( + Platform.isAndroid ? Icons.arrow_back : Icons.arrow_back_ios, + color: Theme.of(context).iconTheme.color, + )), + title: Text( + "Checkout", + style: Theme.of(context).textTheme.titleMedium, + ), + ), + body: StreamBuilder( + stream: _networkBloc.stream, + initialData: NetworkInitial(), + builder: (context, netSnapshot) { + if (netSnapshot.hasData) { + final netState = netSnapshot.data; + if (netState is OnNetworkNotConnected) { + return const NoConnection(); + } else { + return StreamBuilder( + stream: _chapaNativeCheckoutBloc.stream, + initialData: ChapaNativeCheckoutInitial(), + builder: (context, snapshot) { + if (snapshot.hasData) { + final state = snapshot.data; + return _buildStreamState(state, deviceSize); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + ); + } + } else { + return const NoConnection(); + } + }, + ), + ); + } + + Widget _buildStreamState(ChapaNativeCheckoutState? state, Size deviceSize) { + if (state is ChapaNativeCheckoutLoadingState) { + return const Center( + child: CircularProgressIndicator(), + ); + } + if (state is ChapaNativeCheckoutValidationOngoingState) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => _showProcessingDialog(), + ); + } + if (state is ChapaNativeCheckoutPaymentValidateSuccessState) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => _hideDialog(), + ); + return _buildPaymentValidateSuccessResult(state, deviceSize); + } + + if (state is ChapaNativeCheckoutPaymentInitiateApiError) { + return _buildPaymentInitiateError(state, deviceSize); + } + if (state is ChapaNativeCheckoutPaymentValidateApiError) { + return _buildPaymentValidateError(state); + } + if (state is ChapaNativeCheckoutNetworkError) { + return NoConnection(); + } + + if (state is ChapaNativeCheckoutUnknownError) { + return ContactUs(); + } + + return _buildPaymentForm(state, deviceSize); + } + + Widget _buildPaymentForm(ChapaNativeCheckoutState? state, Size deviceSize) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: deviceSize.width * 0.064), + child: ImageFiltered( + imageFilter: ImageFilter.blur( + sigmaX: state is ChapaNativeCheckoutValidationOngoingState ? 5.0 : 0, + sigmaY: state is ChapaNativeCheckoutValidationOngoingState ? 5.0 : 0, + ), + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Payment Method", + style: Theme.of(context).textTheme.labelMedium, + ), + SizedBox( + height: deviceSize.height * 0.012, + ), + Flexible( + child: PaymentMethodsCustomBuilderView( + showPaymentMethodsOnGridView: + widget.showPaymentMethodsOnGridView, + availablePaymentMethods: paymentMethods, + onPressed: (val) { + setState(() { + selectedLocalPaymentMethods = val; + showPaymentMethodError = false; + }); + }, + selectedPaymentMethod: selectedLocalPaymentMethods, + ), + ), + Visibility( + visible: showPaymentMethodError, + child: Text( + "Please Select Payment Method", + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.red), + ), + ), + SizedBox( + height: deviceSize.height * 0.02, + ), + Text( + "Phone Number", + style: Theme.of(context).textTheme.labelLarge, + ), + SizedBox( + height: deviceSize.height * 0.006, + ), + CustomTextForm( + prefix: Padding( + padding: const EdgeInsets.only(left: 20.0, right: 12), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + AppImages.ethiopiaLogo, + width: deviceSize.width * 0.064, + ), + const SizedBox( + width: 4, + ), + ], + ), + ), + textStyle: Theme.of(context).textTheme.bodyMedium, + controller: phoneNumberController, + hintText: "0911121314", + keyboardType: TextInputType.number, + inputFormatter: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(10) + ], + hintTextStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: const Color.fromARGB(255, 156, 145, 145), + ), + labelText: "", + filled: false, + filledColor: Colors.transparent, + obscureText: false, + onTap: () {}, + validator: (phone) { + if (phone == null || phone.isEmpty) { + return 'Phone number can\'t be empty'; + } + + if (!RegExp(r'^[0-9]{10}$').hasMatch(phone)) { + return 'Phone number must be a 10-digit number'; + } + + if (!RegExp(r'^(09|07|011)').hasMatch(phone)) { + return 'Enter a valid phone no.'; + } + return null; + }, + onChanged: (val) {}, + ), + Align( + alignment: Alignment.topRight, + child: TextButton( + onPressed: () async { + if (!chapasButtonIsClicked) { + String transactionRef = generateTransactionRef(); + await initializeMyPayment( + widget.context, + widget.email, + widget.phone, + widget.amount, + widget.currency, + widget.firstName, + widget.lastName, + transactionRef, + widget.title, + widget.desc, + widget.namedRouteFallBack, + widget.publicKey, + ).then((String result) { + if (result.isNotEmpty) { + setState(() { + chapasButtonIsClicked = true; + }); + } + }); + } + }, + child: Text( + "Pay with Chapa", + style: Theme.of(context).textTheme.labelLarge?.copyWith( + decoration: TextDecoration.underline, + decorationColor: AppColors.chapaPrimaryColor, + color: AppColors.chapaPrimaryColor, + ), + ), + ), + ), + SizedBox( + height: deviceSize.height * 0.064, + ), + CustomButton( + title: "Pay ${widget.amount} ${widget.currency.toUpperCase()}", + backgroundColor: widget.buttonColor, + onPressed: () async { + if (_formKey.currentState!.validate() && + selectedLocalPaymentMethods != null) { + DirectChargeRequest request = DirectChargeRequest( + amount: widget.amount, + mobile: phoneNumberController.text, + currency: widget.currency, + firstName: widget.firstName, + lastName: widget.lastName, + email: widget.email, + txRef: widget.txRef, + paymentMethod: selectedLocalPaymentMethods!.value(), + ); + _chapaNativeCheckoutBloc.add(InitiatePayment( + directChargeRequest: request, + publicKey: widget.publicKey, + )); + } + if (selectedLocalPaymentMethods == null) { + setState(() { + showPaymentMethodError = true; + }); + } + }, + ), + Spacer(), + ], + ), + ), + ), + ); + } + + Widget _buildPaymentValidateSuccessResult( + ChapaNativeCheckoutPaymentValidateSuccessState state, Size deviceSize) { + if (state.isPaymentFailed) { + return Padding( + padding: EdgeInsets.symmetric( + horizontal: deviceSize.width * 0.08, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: deviceSize.height * 0.064, + ), + CircleAvatar( + backgroundColor: Colors.red, + radius: deviceSize.height * 0.028, + child: Icon( + Icons.close, + color: Theme.of(context).scaffoldBackgroundColor, + ), + ), + SizedBox( + height: deviceSize.height * 0.016, + ), + Align( + alignment: Alignment.center, + child: Text( + "Payment Failed", + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: Colors.red, + fontWeight: FontWeight.w500, + ), + ), + ), + SizedBox( + height: deviceSize.height * 0.012, + ), + Text( + "Payment is failed. Please try again. \n The transaction is canceled or third party time is out.", + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + SizedBox( + height: deviceSize.height * 0.04, + ), + SizedBox( + width: deviceSize.width * 0.64, + child: CustomButton( + backgroundColor: Colors.red, + onPressed: () { + exitPaymentPage( + state.directChargeValidateResponse.message ?? + "Payment is Failed", + state.directChargeValidateResponse.trxRef, + ); + }, + title: "Retry Again", + ), + ) + ], + ), + ); + } else { + return Padding( + padding: EdgeInsets.symmetric( + horizontal: deviceSize.width * 0.04, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: deviceSize.height * 0.054, + ), + Image.asset( + AppImages.successIcon, + width: deviceSize.width * 0.2, + ), + SizedBox( + height: deviceSize.height * 0.012, + ), + Text( + "${state.directChargeValidateResponse.data!.amount?.formattedBirr()}", + style: Theme.of(context).textTheme.titleLarge!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 60), + child: Divider(), + ), + Align( + alignment: Alignment.center, + child: Text( + state.directChargeValidateResponse.message ?? "Successful", + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w500, + color: AppColors.chapaPrimaryColor), + ), + ), + SizedBox( + height: deviceSize.height * 0.012, + ), + Row( + children: [ + Text( + "Order ID", + style: Theme.of(context) + .textTheme + .labelSmall! + .copyWith(color: Colors.grey), + ), + Spacer(), + Text( + state.directChargeValidateResponse.processorId ?? "", + style: Theme.of(context).textTheme.labelSmall, + ), + ], + ), + Divider(), + Row( + children: [ + Text( + "Amount: ", + style: Theme.of(context) + .textTheme + .labelSmall! + .copyWith(color: Colors.grey), + ), + Spacer(), + Text( + state.directChargeValidateResponse.data?.amount + ?.formattedBirr() ?? + "", + style: Theme.of(context).textTheme.bodyMedium, + ) + ], + ), + Divider(), + Row( + children: [ + Text( + "Charge: ", + style: Theme.of(context) + .textTheme + .labelSmall! + .copyWith(color: Colors.grey), + ), + Spacer(), + Text( + state.directChargeValidateResponse.data?.charge + ?.formattedBirr() ?? + "", + style: Theme.of(context).textTheme.bodyMedium, + ) + ], + ), + Divider(), + Row( + children: [ + Text( + "Reference ID: ", + style: Theme.of(context) + .textTheme + .labelSmall! + .copyWith(color: Colors.grey), + ), + Spacer(), + Text( + state.directChargeValidateResponse.trxRef ?? "", + style: Theme.of(context).textTheme.bodyMedium, + ) + ], + ), + Divider(), + Row( + children: [ + Text( + "Paid At: ", + style: Theme.of(context) + .textTheme + .labelSmall! + .copyWith(color: Colors.grey), + ), + Spacer(), + Text( + state.directChargeValidateResponse + .getCreatedAtTime() + .format(), + style: Theme.of(context).textTheme.bodyMedium, + ) + ], + ), + Divider(), + SizedBox( + height: deviceSize.height * 0.048, + ), + SizedBox( + width: deviceSize.width * 0.72, + child: CustomButton( + onPressed: () { + exitPaymentPage( + 'paymentSuccessful', + state.directChargeValidateResponse.trxRef, + ); + }, + title: "Finish", + ), + ), + Spacer(), + Image.asset( + AppImages.chapaFullLogo, + width: deviceSize.width * 0.28, + ), + SizedBox( + height: deviceSize.height * 0.008, + ), + Text( + "Thank you for using chapa", + style: Theme.of(context) + .textTheme + .labelMedium! + .copyWith(color: Colors.grey, fontWeight: FontWeight.w400), + ), + SizedBox( + height: deviceSize.height * 0.048, + ), + ], + ), + ); + } + } + + Widget _buildPaymentInitiateError( + ChapaNativeCheckoutPaymentInitiateApiError? state, Size deviceSize) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: deviceSize.width * 0.08), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + height: deviceSize.height * 0.064, + ), + CircleAvatar( + backgroundColor: Colors.red, + radius: deviceSize.height * 0.028, + child: Icon( + Icons.close, + color: Theme.of(context).scaffoldBackgroundColor, + ), + ), + SizedBox( + height: deviceSize.height * 0.016, + ), + Align( + alignment: Alignment.center, + child: Text( + state?.directChargeApiError?.status?.toUpperCase() ?? + "Payment Failed", + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: Colors.red, + fontWeight: FontWeight.w500, + ), + ), + ), + SizedBox( + height: deviceSize.height * 0.012, + ), + Text( + state?.directChargeApiError?.data?.message ?? + state?.directChargeApiError?.message ?? + "Something went wrong", + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + SizedBox( + height: deviceSize.height * 0.04, + ), + SizedBox( + width: deviceSize.width * 0.64, + child: CustomButton( + backgroundColor: Colors.red, + onPressed: () { + exitPaymentPage( + state?.directChargeApiError?.message ?? "Payment is Failed", + null, + ); + }, + title: "Retry Again", + ), + ) + ], + ), + ); + } + + Widget _buildPaymentValidateError( + ChapaNativeCheckoutPaymentValidateApiError state) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(state.apiErrorResponse?.status ?? "Error occurred"), + const SizedBox(height: 8), + Text( + state.apiErrorResponse?.message ?? "Something went wrong", + ), + ], + ), + ); + } +} + +// ignore: must_be_immutable +class PaymentMethodsCustomBuilderView extends StatefulWidget { + final bool? showPaymentMethodsOnGridView; + final List availablePaymentMethods; + LocalPaymentMethods? selectedPaymentMethod; + Function(LocalPaymentMethods) onPressed; + PaymentMethodsCustomBuilderView({ + super.key, + required this.showPaymentMethodsOnGridView, + required this.availablePaymentMethods, + required this.onPressed, + required this.selectedPaymentMethod, + }); + + @override + State createState() => + _PaymentMethodsCustomBuilderViewState(); +} + +class _PaymentMethodsCustomBuilderViewState + extends State { + @override + Widget build(BuildContext context) { + Size deviceSize = MediaQuery.of(context).size; + return widget.showPaymentMethodsOnGridView ?? true + ? Container( + color: AppColors.shadowColor, + // padding: EdgeInsets.symmetric(horizontal: 8, vertical: 12), + child: GridView.count( + scrollDirection: Axis.vertical, + crossAxisCount: 3, + shrinkWrap: true, + childAspectRatio: 1, + crossAxisSpacing: deviceSize.width * 0.02, + children: widget.availablePaymentMethods + .map((method) => InkWell( + onTap: () { + widget.onPressed(method); + }, + child: paymentMethodItem(method, deviceSize, + method == widget.selectedPaymentMethod), + )) + .toList(), + ), + ) + : Container( + color: AppColors.shadowColor, + padding: EdgeInsets.symmetric( + horizontal: 8, + ), + height: deviceSize.height * 0.132, + child: ListView.builder( + shrinkWrap: true, + itemCount: widget.availablePaymentMethods.length, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) { + final paymentMethod = widget.availablePaymentMethods[index]; + return InkWell( + onTap: () { + widget.onPressed(paymentMethod); + }, + child: paymentMethodItem(paymentMethod, deviceSize, + paymentMethod == widget.selectedPaymentMethod), + ); + }, + ), + ); + } + + Widget paymentMethodItem( + LocalPaymentMethods paymentMethod, Size deviceSize, bool isSelected) { + return Container( + margin: const EdgeInsets.only(right: 6, bottom: 6, top: 6, left: 6), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: deviceSize.width * 0.24, + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 16), + decoration: BoxDecoration( + border: Border.all( + color: isSelected ? Colors.green : Colors.transparent, + ), + borderRadius: BorderRadius.circular(16), + color: Theme.of(context).scaffoldBackgroundColor, + boxShadow: [ + BoxShadow( + color: Theme.of(context).shadowColor.withOpacity(0.05), + blurRadius: 2, + spreadRadius: 2.0, + offset: Offset(0, 4), // Controls the position of the shadow + ), + ], + ), + child: Stack( + // crossAxisAlignment: CrossAxisAlignment.end, + // mainAxisAlignment: MainAxisAlignment.start, + // mainAxisSize: MainAxisSize.min, + children: [ + Positioned( + top: 0, + right: 0, + child: CircleAvatar( + radius: 6, + backgroundColor: isSelected + ? Color(0xff7DC400) + : Theme.of(context).scaffoldBackgroundColor, + child: Icon( + Icons.done, + size: 8, + color: isSelected + ? Colors.black + : Theme.of(context).scaffoldBackgroundColor, + ), + ), + ), + Container( + margin: EdgeInsets.only( + top: 6, + ), + width: deviceSize.width * 0.2, + height: deviceSize.width * 0.08, + child: Image.asset( + paymentMethod.iconPath(), + fit: BoxFit.contain, + ), + ) + ], + ), + ), + SizedBox( + height: 8, + ), + Text( + paymentMethod.displayName(), + style: Theme.of(context).textTheme.labelSmall, + textAlign: TextAlign.center, + ), + ], + ), + ); + } +} diff --git a/lib/features/network/bloc/network_bloc.dart b/lib/features/network/bloc/network_bloc.dart new file mode 100644 index 0000000..1c7c36b --- /dev/null +++ b/lib/features/network/bloc/network_bloc.dart @@ -0,0 +1,35 @@ +import 'dart:async'; +import 'package:bloc/bloc.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +part 'network_event.dart'; +part 'network_state.dart'; + +class NetworkBloc extends Bloc { + StreamSubscription? subscription; + NetworkBloc() : super(NetworkInitial()) { + on((event, emit) { + emit(NetworkSuccess()); + }); + on((event, emit) { + emit(NetworkFailure()); + }); + subscription = Connectivity() + .onConnectivityChanged + .listen((List connectivityResult) { + if (connectivityResult.contains(ConnectivityResult.mobile) || + connectivityResult.contains(ConnectivityResult.wifi) || + connectivityResult.contains(ConnectivityResult.ethernet) || + connectivityResult.contains(ConnectivityResult.vpn)) { + add(OnNetworkConnected()); + } else { + add(OnNetworkNotConnected()); + } + }); + } + + @override + Future close() { + subscription?.cancel; + return super.close(); + } +} diff --git a/lib/features/network/bloc/network_event.dart b/lib/features/network/bloc/network_event.dart new file mode 100644 index 0000000..6e9b0b1 --- /dev/null +++ b/lib/features/network/bloc/network_event.dart @@ -0,0 +1,9 @@ +part of 'network_bloc.dart'; + +class NetworkEvent { + const NetworkEvent(); +} + +class OnNetworkConnected extends NetworkEvent {} + +class OnNetworkNotConnected extends NetworkEvent {} diff --git a/lib/features/network/bloc/network_state.dart b/lib/features/network/bloc/network_state.dart new file mode 100644 index 0000000..76f3f46 --- /dev/null +++ b/lib/features/network/bloc/network_state.dart @@ -0,0 +1,14 @@ +part of 'network_bloc.dart'; + +enum ConnectionType { wifi, mobile, none } + +abstract class NetworkState {} + +class NetworkInitial extends NetworkState {} + +// ignore: must_be_immutable +class NetworkLoading extends NetworkState {} + +class NetworkSuccess extends NetworkState {} + +class NetworkFailure extends NetworkState {} diff --git a/lib/model/data.dart b/lib/model/data.dart deleted file mode 100644 index ba51f21..0000000 --- a/lib/model/data.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'dart:convert'; - -ResponseData dataFromJson(String str) => - ResponseData.fromJson(json.decode(str)); - -String dataToJson(ResponseData data) => json.encode(data.toJson()); - -class ResponseData { - ResponseData({ - required this.message, - required this.status, - required this.data, - }); - - String message; - String status; - DataClass data; - - factory ResponseData.fromJson(Map json) => ResponseData( - message: json["message"], - status: json["status"], - data: DataClass.fromJson(json["data"]), - ); - - Map toJson() => { - "message": message, - "status": status, - "data": data.toJson(), - }; -} - -class DataClass { - DataClass({ - required this.checkoutUrl, - }); - - String checkoutUrl; - - factory DataClass.fromJson(Map json) => DataClass( - checkoutUrl: json["checkout_url"], - ); - - Map toJson() => { - "checkout_url": checkoutUrl, - }; -} diff --git a/pubspec.yaml b/pubspec.yaml index 6d63c97..571a05b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,22 +1,38 @@ name: chapasdk -description: Chapa flutter library to accept payment via Card, PayPal, Amole, CBEBirr, AwashBirr and/or telebirr etc. +description: Chapa flutter library to accept payment via telebirr, CBEBirr, E-birr and m-pessa etc. version: 0.0.6 homepage: https://github.com/chapa-et/chapa-flutter environment: - sdk: ">=2.15.0<3.0.0" - flutter: ">=1.17.0" + sdk: '>=3.4.1 <4.0.0' + flutter: ">=3.4.1" dependencies: flutter: sdk: flutter - http: ^0.13.0 - flutter_inappwebview: ^5.4.3+7 - connectivity_plus: ^2.3.6+1 - fluttertoast: ^8.0.9 + http: + flutter_inappwebview: + connectivity_plus: + fluttertoast: + dio: + encrypt: + pointycastle: + bloc: + intl: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^1.0.0 + flutter_lints: + +flutter: + uses-material-design: true + assets: + - assets/images/cbebirr.png + - assets/images/ebirr.png + - assets/images/telebirr.png + - assets/images/ethiopia-flag.png + - assets/images/mpesa.png + - assets/images/success-icon.png + - assets/images/chapa-logo.png