From d7233d9e1deeacf82207ad7bcf218b5cb9a9da16 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Wed, 10 Jan 2024 12:08:40 +0545 Subject: [PATCH 001/131] Sync with remote --- .idea/.name | 1 - .idea/compiler.xml | 2 +- .idea/deploymentTargetDropDown.xml | 10 ++++++++++ .idea/gradle.xml | 6 ++---- .idea/kotlinc.xml | 6 ++++++ .idea/migrations.xml | 10 ++++++++++ .idea/misc.xml | 6 ++++-- .idea/vcs.xml | 2 +- 8 files changed, 34 insertions(+), 9 deletions(-) delete mode 100644 .idea/.name create mode 100644 .idea/deploymentTargetDropDown.xml create mode 100644 .idea/kotlinc.xml create mode 100644 .idea/migrations.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 079ff189..00000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -Khalti Android SDK \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml index fb7f4a8a..b589d56e 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 00000000..0c0c3383 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 30b621c0..8b7c88d3 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -1,13 +1,10 @@ - diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 00000000..7e340a77 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 00000000..f8051a6f --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index cb1a6338..8978d23d 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,9 @@ - - + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 94a25f7f..35eb1ddf 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file From 3736dd36d6c2a17771b357a5d491c461d045722f Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 18 Jan 2024 14:57:15 +0545 Subject: [PATCH 002/131] Add new config class for sdk operation --- .../main/java/com/khalti/android/Khalti.kt | 67 +++++++++++++++++++ .../com/khalti/android/PaymentActivity.kt | 5 +- 2 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 khalti-android/src/main/java/com/khalti/android/Khalti.kt diff --git a/khalti-android/src/main/java/com/khalti/android/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/Khalti.kt new file mode 100644 index 00000000..eacc377b --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/Khalti.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android + +class Khalti private constructor( + val paymentUrl: String, + val returnUrl: String, + val openInKhalti: Boolean, + val publicKey: String?, + val merchantAppDeeplink: String?, +) { + class Builder { + private var publicKey: String? = null + private var paymentUrl: String? = null + private var returnUrl: String? = null + private var openInKhalti: Boolean? = null + private var merchantAppDeeplink: String? = null + + fun publicKey(publicKey: String): Builder { + this.publicKey = publicKey + return this + } + + fun paymentUrl(paymentUrl: String): Builder { + this.paymentUrl = paymentUrl + return this + } + + fun returnUrl(returnUrl: String): Builder { + this.returnUrl = returnUrl + return this + } + + fun openInKhalti(openInKhalti: Boolean): Builder { + this.openInKhalti = openInKhalti + return this + } + + fun merchantAppDeeplink(merchantAppDeeplink: String): Builder { + this.merchantAppDeeplink = merchantAppDeeplink + return this + } + + fun build(): Khalti { + assert(paymentUrl != null) { "Payment url is required" } + assert(returnUrl != null) { "Return url is required" } + + val openInKhalti: Boolean = if (this.openInKhalti == null) { + true + } else { + this.openInKhalti!! + } + + assert(openInKhalti || merchantAppDeeplink != null) { "Merchant app's deeplink is required when [openInKhalti] is true" } + + return Khalti( + paymentUrl = this.paymentUrl!!, + returnUrl = returnUrl!!, + openInKhalti = openInKhalti, + merchantAppDeeplink = this.merchantAppDeeplink, + publicKey = this.publicKey + ) + } + } +} \ No newline at end of file diff --git a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt index b82e67fa..2b59307b 100644 --- a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt +++ b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt @@ -27,7 +27,7 @@ internal class PaymentActivity : Activity() { val appBar = AppBarLayout(this) val toolbar = MaterialToolbar(this) - + val progressBar = ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal) progressBar.isIndeterminate = true @@ -53,7 +53,8 @@ internal class PaymentActivity : Activity() { webView.webViewClient = EPaymentWebClient(this, it.returnUrl) webView.webChromeClient = object : WebChromeClient() { override fun onProgressChanged(view: WebView?, newProgress: Int) { - progressBar.visibility = if (newProgress == 100) ProgressBar.GONE else ProgressBar.VISIBLE + progressBar.visibility = + if (newProgress == 100) ProgressBar.GONE else ProgressBar.VISIBLE } } From 367a5625c7269595741f445ac1b0d2beee2cfc21 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 19 Jan 2024 12:31:49 +0545 Subject: [PATCH 003/131] Update gradle --- build.gradle | 6 ++-- gradle.properties | 4 ++- gradle/wrapper/gradle-wrapper.properties | 2 +- .../main/java/com/khalti/android/Khalti.kt | 32 ++++++++++++------- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/build.gradle b/build.gradle index bd9dade1..e85e75fb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ plugins { - id 'com.android.application' version '7.3.1' apply false - id 'com.android.library' version '7.3.1' apply false - id 'org.jetbrains.kotlin.android' version '1.6.10' apply false + id 'com.android.application' version '8.2.0' apply false + id 'com.android.library' version '8.2.0' apply false + id 'org.jetbrains.kotlin.android' version '1.6.21' apply false id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' } diff --git a/gradle.properties b/gradle.properties index 3c5031eb..a2e90d87 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,4 +20,6 @@ kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file +android.nonTransitiveRClass=true +android.defaults.buildfeatures.buildconfig=true +android.nonFinalResIds=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index aaf5e0bc..ef105d4e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Oct 31 20:08:12 NPT 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/khalti-android/src/main/java/com/khalti/android/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/Khalti.kt index eacc377b..c0182123 100644 --- a/khalti-android/src/main/java/com/khalti/android/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/Khalti.kt @@ -4,19 +4,23 @@ package com.khalti.android -class Khalti private constructor( +class KhaltiPayConfig private constructor( val paymentUrl: String, val returnUrl: String, val openInKhalti: Boolean, + val autoVerify: Boolean, val publicKey: String?, val merchantAppDeeplink: String?, + val onComplete: OnComplete?, ) { class Builder { private var publicKey: String? = null private var paymentUrl: String? = null private var returnUrl: String? = null - private var openInKhalti: Boolean? = null + private var openInKhalti: Boolean = true + private var autoVerify: Boolean = true private var merchantAppDeeplink: String? = null + private var onComplete: OnComplete? = null fun publicKey(publicKey: String): Builder { this.publicKey = publicKey @@ -38,29 +42,35 @@ class Khalti private constructor( return this } + fun autoVerify(autoVerify: Boolean): Builder { + this.autoVerify = autoVerify + return this + } + fun merchantAppDeeplink(merchantAppDeeplink: String): Builder { this.merchantAppDeeplink = merchantAppDeeplink return this } - fun build(): Khalti { + fun onPaymentComplete(onComplete: OnComplete): Builder { + this.onComplete = onComplete + return this + } + + fun build(): KhaltiPayConfig { assert(paymentUrl != null) { "Payment url is required" } assert(returnUrl != null) { "Return url is required" } - val openInKhalti: Boolean = if (this.openInKhalti == null) { - true - } else { - this.openInKhalti!! - } - assert(openInKhalti || merchantAppDeeplink != null) { "Merchant app's deeplink is required when [openInKhalti] is true" } - return Khalti( + return KhaltiPayConfig( paymentUrl = this.paymentUrl!!, returnUrl = returnUrl!!, openInKhalti = openInKhalti, + autoVerify = true, merchantAppDeeplink = this.merchantAppDeeplink, - publicKey = this.publicKey + publicKey = this.publicKey, + onComplete = this.onComplete ) } } From 78effa7eeeaa95eb65653442f090c5b585e1ff6b Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 19 Jan 2024 12:33:28 +0545 Subject: [PATCH 004/131] Add .idea to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0bd772cf..7146a664 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ local.properties /app/release/ /khalti-android/build/ +/.idea \ No newline at end of file From 1989c72dc7a3e2395c6658c3ace3da9283a6e0a3 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 19 Jan 2024 12:34:38 +0545 Subject: [PATCH 005/131] Add new Khalti to control sdk actions --- .idea/compiler.xml | 6 ------ .idea/copyright/Khalti_Authors.xml | 6 ------ .idea/copyright/profiles_settings.xml | 12 ------------ .idea/deploymentTargetDropDown.xml | 10 ---------- .idea/gradle.xml | 19 ------------------- .idea/kotlinc.xml | 6 ------ .idea/migrations.xml | 10 ---------- .idea/misc.xml | 9 --------- .idea/vcs.xml | 6 ------ .../main/java/com/khalti/android/Khalti.kt | 14 +++++++++++--- .../java/com/khalti/android/OnComplete.kt | 9 +++++++++ 11 files changed, 20 insertions(+), 87 deletions(-) delete mode 100644 .idea/compiler.xml delete mode 100644 .idea/copyright/Khalti_Authors.xml delete mode 100644 .idea/copyright/profiles_settings.xml delete mode 100644 .idea/deploymentTargetDropDown.xml delete mode 100644 .idea/gradle.xml delete mode 100644 .idea/kotlinc.xml delete mode 100644 .idea/migrations.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/vcs.xml create mode 100644 khalti-android/src/main/java/com/khalti/android/OnComplete.kt diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index b589d56e..00000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/copyright/Khalti_Authors.xml b/.idea/copyright/Khalti_Authors.xml deleted file mode 100644 index 1c028f15..00000000 --- a/.idea/copyright/Khalti_Authors.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml deleted file mode 100644 index 37eabbdd..00000000 --- a/.idea/copyright/profiles_settings.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml deleted file mode 100644 index 0c0c3383..00000000 --- a/.idea/deploymentTargetDropDown.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 8b7c88d3..00000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index 7e340a77..00000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml deleted file mode 100644 index f8051a6f..00000000 --- a/.idea/migrations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 8978d23d..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddf..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/khalti-android/src/main/java/com/khalti/android/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/Khalti.kt index c0182123..58d742ee 100644 --- a/khalti-android/src/main/java/com/khalti/android/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/Khalti.kt @@ -4,7 +4,7 @@ package com.khalti.android -class KhaltiPayConfig private constructor( +class Khalti private constructor( val paymentUrl: String, val returnUrl: String, val openInKhalti: Boolean, @@ -57,13 +57,13 @@ class KhaltiPayConfig private constructor( return this } - fun build(): KhaltiPayConfig { + fun build(): Khalti { assert(paymentUrl != null) { "Payment url is required" } assert(returnUrl != null) { "Return url is required" } assert(openInKhalti || merchantAppDeeplink != null) { "Merchant app's deeplink is required when [openInKhalti] is true" } - return KhaltiPayConfig( + return Khalti( paymentUrl = this.paymentUrl!!, returnUrl = returnUrl!!, openInKhalti = openInKhalti, @@ -74,4 +74,12 @@ class KhaltiPayConfig private constructor( ) } } + + fun makePayment() { + + } + + fun verifyPayment() { + + } } \ No newline at end of file diff --git a/khalti-android/src/main/java/com/khalti/android/OnComplete.kt b/khalti-android/src/main/java/com/khalti/android/OnComplete.kt new file mode 100644 index 00000000..ce391233 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/OnComplete.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android + +fun interface OnComplete { + fun invoke() +} \ No newline at end of file From e7c30b10e97df8412b20650155df071ccfa3f5b4 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 19 Jan 2024 13:50:51 +0545 Subject: [PATCH 006/131] Upgrade gradle dependencies --- dependencies.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index 5ca6a668..f2552099 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,10 +1,10 @@ ext { - appcompat_version = '1.5.1' + appcompat_version = '1.6.1' compose_version = '1.3.0' - core_ktx_version = '1.9.0' - material_version = '1.7.0' + core_ktx_version = '1.12.0' + material_version = '1.11.0' junit_version = '4.13.2' - junit_ext_version = '1.1.3' - espresso_version = '3.4.0' + junit_ext_version = '1.1.5' + espresso_version = '3.5.1' } \ No newline at end of file From dd68f65e969254875d594dc6b240d7aa327d4120 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 19 Jan 2024 18:04:05 +0545 Subject: [PATCH 007/131] Update assertion for khalti parameters --- .../main/java/com/khalti/android/Khalti.kt | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/Khalti.kt index 58d742ee..17747779 100644 --- a/khalti-android/src/main/java/com/khalti/android/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/Khalti.kt @@ -4,14 +4,16 @@ package com.khalti.android +import android.util.Log + class Khalti private constructor( val paymentUrl: String, val returnUrl: String, val openInKhalti: Boolean, val autoVerify: Boolean, - val publicKey: String?, - val merchantAppDeeplink: String?, - val onComplete: OnComplete?, + val publicKey: String, + val merchantAppDeeplink: String, + val onComplete: OnComplete, ) { class Builder { private var publicKey: String? = null @@ -58,19 +60,29 @@ class Khalti private constructor( } fun build(): Khalti { - assert(paymentUrl != null) { "Payment url is required" } - assert(returnUrl != null) { "Return url is required" } - - assert(openInKhalti || merchantAppDeeplink != null) { "Merchant app's deeplink is required when [openInKhalti] is true" } + val assertionConditions: Map = mapOf( + "onComplete" to (onComplete != null), + "paymentUrl" to (paymentUrl != null), + "returnUrl" to (returnUrl != null), + "publicKey" to (publicKey != null), + "merchantAppDeeplink" to (merchantAppDeeplink != null), + ).filter { !it.value } + + assert(assertionConditions.isEmpty()) + { + val requiredParams = assertionConditions.keys.joinToString(separator = ", ") + val requiredParamsSize = assertionConditions.size + "$requiredParams ${if (requiredParamsSize > 1) "are" else "is"} required" + } return Khalti( - paymentUrl = this.paymentUrl!!, + paymentUrl = paymentUrl!!, returnUrl = returnUrl!!, openInKhalti = openInKhalti, - autoVerify = true, - merchantAppDeeplink = this.merchantAppDeeplink, - publicKey = this.publicKey, - onComplete = this.onComplete + autoVerify = autoVerify, + merchantAppDeeplink = merchantAppDeeplink!!, + publicKey = publicKey!!, + onComplete = onComplete!! ) } } From d0194a1a138976a497786e3a82ea8e3b9082e399 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 19 Jan 2024 18:05:37 +0545 Subject: [PATCH 008/131] Update kotlin compiler version for compose --- app/build.gradle | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2bda88c7..a947aac4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' @@ -37,7 +39,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion '1.1.1' + kotlinCompilerExtensionVersion '1.4.1' } packagingOptions { resources { @@ -45,7 +47,7 @@ android { } } - tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + tasks.withType(KotlinCompile).configureEach { kotlinOptions { freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" } @@ -54,11 +56,11 @@ android { dependencies { implementation "androidx.core:core-ktx:$core_ktx_version" - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' - implementation 'androidx.activity:activity-compose:1.6.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' + implementation 'androidx.activity:activity-compose:1.8.2' implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" - implementation 'androidx.compose.material3:material3:1.1.0-alpha01' + implementation 'androidx.compose.material3:material3:1.1.2' //implementation "com.khalti:khalti-android:$khaltiVersionName" implementation project(path: ':khalti-android') From 0680c065d1908f0275236a36e4ea0886e8afaac1 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 19 Jan 2024 18:06:32 +0545 Subject: [PATCH 009/131] Accept recommended suggestion by android studio --- gradle.properties | 1 - 1 file changed, 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a2e90d87..f19c7b9b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,5 +21,4 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true -android.defaults.buildfeatures.buildconfig=true android.nonFinalResIds=false \ No newline at end of file From 80ddb9bbc4610b16bc7f3b643c13a13d111ba4d1 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 19 Jan 2024 18:06:46 +0545 Subject: [PATCH 010/131] Accept recommended suggestion by android studio --- scripts/publish-module.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/publish-module.gradle b/scripts/publish-module.gradle index ce576f81..18bf705b 100644 --- a/scripts/publish-module.gradle +++ b/scripts/publish-module.gradle @@ -1,7 +1,7 @@ apply plugin: 'maven-publish' apply plugin: 'signing' -task androidSourcesJar(type: Jar) { +tasks.register('androidSourcesJar', Jar) { archiveClassifier.set('sources') if (project.plugins.findPlugin("com.android.library")) { from android.sourceSets.main.java.srcDirs From e5e9a28240aa239d20d870144d074ae41d679f65 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 19 Jan 2024 18:07:13 +0545 Subject: [PATCH 011/131] Update target and compile sdk version to 34 --- metadata.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metadata.gradle b/metadata.gradle index 83f17d31..0f9c92ae 100644 --- a/metadata.gradle +++ b/metadata.gradle @@ -3,6 +3,6 @@ ext { khaltiVersionName = '3.00.00' libraryMinSdk = 16 - libraryCompileSdk = 33 - libraryTargetSdk = 33 + libraryCompileSdk = 34 + libraryTargetSdk = 34 } \ No newline at end of file From cb288dc4d5542207b59463b6929a88de7d54f730 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 19 Jan 2024 18:07:54 +0545 Subject: [PATCH 012/131] Update compose version --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index f2552099..b3d06e1a 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,6 +1,6 @@ ext { appcompat_version = '1.6.1' - compose_version = '1.3.0' + compose_version = '1.5.4' core_ktx_version = '1.12.0' material_version = '1.11.0' From bea2df4ab80c4293e639742af177b9c643e1645f Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 19 Jan 2024 18:08:11 +0545 Subject: [PATCH 013/131] Update kotlin binary version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e85e75fb..a35f2fee 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ plugins { id 'com.android.application' version '8.2.0' apply false id 'com.android.library' version '8.2.0' apply false - id 'org.jetbrains.kotlin.android' version '1.6.21' apply false + id 'org.jetbrains.kotlin.android' version '1.8.0' apply false id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' } From fcabb68c54551112699eaf59c5d8558300358a73 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 19 Jan 2024 18:08:22 +0545 Subject: [PATCH 014/131] Add test code for khalti test --- .../com/khalti/android/demo/composable/DemoScreen.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt index 467a0105..afd218e2 100644 --- a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt +++ b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt @@ -27,6 +27,14 @@ fun DemoScreen() { val (result, setResult) = remember { mutableStateOf(null) } + val khalti = Khalti.Builder() + .publicKey("123") + .paymentUrl("yo") + .returnUrl("hello") + .merchantAppDeeplink("deeplink") + .onPaymentComplete {} + .build() + val khaltiPay = rememberLauncherForActivityResult(OpenKhaltiPay()) { setResult(it) @@ -34,9 +42,11 @@ fun DemoScreen() { is PaymentSuccess -> { Log.i(RESULT_TAG, "Payment Success") } + is PaymentError -> { Log.i(RESULT_TAG, "Payment Error") } + is PaymentCancelled -> { Log.i(RESULT_TAG, "Payment Cancelled") } @@ -109,6 +119,7 @@ fun DemoScreen() { is IllegalArgumentException, is IllegalStateException -> { urlErrorMessage = message } + else -> Log.e(RESULT_TAG, message) } } From aa3a09522eb8f4e74303ddf66e65e75e7c3535c3 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 19 Feb 2024 11:08:22 +0545 Subject: [PATCH 015/131] Rename onPayment to onPayment result --- .../src/main/java/com/khalti/android/OnComplete.kt | 9 --------- .../src/main/java/com/khalti/android/{ => v3}/Khalti.kt | 4 ++-- .../main/java/com/khalti/android/v3/OnPaymentResult.kt | 9 +++++++++ 3 files changed, 11 insertions(+), 11 deletions(-) delete mode 100644 khalti-android/src/main/java/com/khalti/android/OnComplete.kt rename khalti-android/src/main/java/com/khalti/android/{ => v3}/Khalti.kt (96%) create mode 100644 khalti-android/src/main/java/com/khalti/android/v3/OnPaymentResult.kt diff --git a/khalti-android/src/main/java/com/khalti/android/OnComplete.kt b/khalti-android/src/main/java/com/khalti/android/OnComplete.kt deleted file mode 100644 index ce391233..00000000 --- a/khalti-android/src/main/java/com/khalti/android/OnComplete.kt +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) 2024. The Khalti Authors. All rights reserved. - */ - -package com.khalti.android - -fun interface OnComplete { - fun invoke() -} \ No newline at end of file diff --git a/khalti-android/src/main/java/com/khalti/android/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt similarity index 96% rename from khalti-android/src/main/java/com/khalti/android/Khalti.kt rename to khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt index 17747779..df707cb7 100644 --- a/khalti-android/src/main/java/com/khalti/android/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt @@ -4,8 +4,8 @@ package com.khalti.android -import android.util.Log - +// Though kotlin provides named and optional parameters +// builder pattern was required for Java developers class Khalti private constructor( val paymentUrl: String, val returnUrl: String, diff --git a/khalti-android/src/main/java/com/khalti/android/v3/OnPaymentResult.kt b/khalti-android/src/main/java/com/khalti/android/v3/OnPaymentResult.kt new file mode 100644 index 00000000..742ec437 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/v3/OnPaymentResult.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.v3 + +fun interface OnPayment { + fun invoke(result: PaymentResult) +} \ No newline at end of file From 1ec5d2d0980187c7ea83ee8025dd19fdf49aaeac Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 19 Feb 2024 11:09:22 +0545 Subject: [PATCH 016/131] Add new interface OnMessage --- .../src/main/java/com/khalti/android/v3/OnMessage.kt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/v3/OnMessage.kt diff --git a/khalti-android/src/main/java/com/khalti/android/v3/OnMessage.kt b/khalti-android/src/main/java/com/khalti/android/v3/OnMessage.kt new file mode 100644 index 00000000..09334450 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/v3/OnMessage.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.v3 + +fun interface OnMessage { + fun invoke(message: String) +} \ No newline at end of file From f7ad51cb7183d12a0fcf6345d8a6b66950874c55 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 19 Feb 2024 11:09:35 +0545 Subject: [PATCH 017/131] Add new interface OnReturn --- .../src/main/java/com/khalti/android/v3/OnReturn.kt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/v3/OnReturn.kt diff --git a/khalti-android/src/main/java/com/khalti/android/v3/OnReturn.kt b/khalti-android/src/main/java/com/khalti/android/v3/OnReturn.kt new file mode 100644 index 00000000..1f764b41 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/v3/OnReturn.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.v3 + +fun interface OnReturn { + fun invoke() +} \ No newline at end of file From 324edb77d37b870f4034104d0ab98d3ffc268f24 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 19 Feb 2024 11:10:18 +0545 Subject: [PATCH 018/131] Remove unwanted fields from PaymentResult --- .../com/khalti/android/v3/PaymentResult.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/v3/PaymentResult.kt diff --git a/khalti-android/src/main/java/com/khalti/android/v3/PaymentResult.kt b/khalti-android/src/main/java/com/khalti/android/v3/PaymentResult.kt new file mode 100644 index 00000000..1002793c --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/v3/PaymentResult.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.v3 + +/// Status +/// statusCode [1] -> status [Payment Submitted] +/// statusCode [36] -> status [Cancelled] (Cancelled by user) + +data class PaymentResult( + val statusCode: Int, + val status: String, + val payload: PaymentPayload?, + val message: String? +) + +data class PaymentPayload( + val pidx: String, + val amount: Long, + val mobile: String, + val purchaseOrderId: String, + val purchaseOrderName: String, + val transactionId: String +) \ No newline at end of file From dcad2920b83cda62e9a04d45c1a62903f5550f21 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 19 Feb 2024 11:11:38 +0545 Subject: [PATCH 019/131] Replace builder pattern with overloaded functions init --- .../main/java/com/khalti/android/v3/Khalti.kt | 154 ++++++++++-------- 1 file changed, 83 insertions(+), 71 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt index df707cb7..8ec0c9b4 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt @@ -2,96 +2,108 @@ * Copyright (c) 2024. The Khalti Authors. All rights reserved. */ -package com.khalti.android +package com.khalti.android.v3 + +import android.net.Uri // Though kotlin provides named and optional parameters -// builder pattern was required for Java developers +// method overloading was required for Java developers class Khalti private constructor( - val paymentUrl: String, - val returnUrl: String, - val openInKhalti: Boolean, - val autoVerify: Boolean, val publicKey: String, - val merchantAppDeeplink: String, - val onComplete: OnComplete, + val paymentUrl: Uri, + val returnUrl: Uri, + val openInKhalti: Boolean, + val onPaymentResult: OnPaymentResult, + val onMessage: OnMessage, + val onReturn: OnReturn?, ) { - class Builder { - private var publicKey: String? = null - private var paymentUrl: String? = null - private var returnUrl: String? = null - private var openInKhalti: Boolean = true - private var autoVerify: Boolean = true - private var merchantAppDeeplink: String? = null - private var onComplete: OnComplete? = null - - fun publicKey(publicKey: String): Builder { - this.publicKey = publicKey - return this - } - - fun paymentUrl(paymentUrl: String): Builder { - this.paymentUrl = paymentUrl - return this - } - - fun returnUrl(returnUrl: String): Builder { - this.returnUrl = returnUrl - return this - } - - fun openInKhalti(openInKhalti: Boolean): Builder { - this.openInKhalti = openInKhalti - return this - } - - fun autoVerify(autoVerify: Boolean): Builder { - this.autoVerify = autoVerify - return this + companion object { + fun init( + publicKey: String, + paymentUrl: Uri, + returnUrl: Uri, + openInKhalti: Boolean, + onPaymentResult: OnPaymentResult, + onMessage: OnMessage, + onReturn: OnReturn, + ): Khalti { + return Khalti( + publicKey, + paymentUrl, + returnUrl, + openInKhalti, + onPaymentResult, + onMessage, + onReturn, + ) } - fun merchantAppDeeplink(merchantAppDeeplink: String): Builder { - this.merchantAppDeeplink = merchantAppDeeplink - return this + fun init( + publicKey: String, + paymentUrl: Uri, + returnUrl: Uri, + onPaymentResult: OnPaymentResult, + onMessage: OnMessage, + onReturn: OnReturn, + ): Khalti { + return Khalti( + publicKey, + paymentUrl, + returnUrl, + true, + onPaymentResult, + onMessage, + onReturn, + ) } - fun onPaymentComplete(onComplete: OnComplete): Builder { - this.onComplete = onComplete - return this + fun init( + publicKey: String, + paymentUrl: Uri, + returnUrl: Uri, + openInKhalti: Boolean, + onPaymentResult: OnPaymentResult, + onMessage: OnMessage, + ): Khalti { + return Khalti( + publicKey, + paymentUrl, + returnUrl, + openInKhalti, + onPaymentResult, + onMessage, + null, + ) } - fun build(): Khalti { - val assertionConditions: Map = mapOf( - "onComplete" to (onComplete != null), - "paymentUrl" to (paymentUrl != null), - "returnUrl" to (returnUrl != null), - "publicKey" to (publicKey != null), - "merchantAppDeeplink" to (merchantAppDeeplink != null), - ).filter { !it.value } - - assert(assertionConditions.isEmpty()) - { - val requiredParams = assertionConditions.keys.joinToString(separator = ", ") - val requiredParamsSize = assertionConditions.size - "$requiredParams ${if (requiredParamsSize > 1) "are" else "is"} required" - } - + fun init( + publicKey: String, + paymentUrl: Uri, + returnUrl: Uri, + onPaymentResult: OnPaymentResult, + onMessage: OnMessage, + ): Khalti { return Khalti( - paymentUrl = paymentUrl!!, - returnUrl = returnUrl!!, - openInKhalti = openInKhalti, - autoVerify = autoVerify, - merchantAppDeeplink = merchantAppDeeplink!!, - publicKey = publicKey!!, - onComplete = onComplete!! + publicKey, + paymentUrl, + returnUrl, + true, + onPaymentResult, + onMessage, + null, ) } } - fun makePayment() { + fun open() { + + } + + fun verify() { } - fun verifyPayment() { + fun close() { } } \ No newline at end of file From 5650ca9b31b580c98e039f6f46bd9b7480518671 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 19 Feb 2024 11:12:19 +0545 Subject: [PATCH 020/131] Rename OnPayment to OnPaymentResult --- .../src/main/java/com/khalti/android/v3/OnPaymentResult.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/OnPaymentResult.kt b/khalti-android/src/main/java/com/khalti/android/v3/OnPaymentResult.kt index 742ec437..20642f75 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/OnPaymentResult.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/OnPaymentResult.kt @@ -4,6 +4,6 @@ package com.khalti.android.v3 -fun interface OnPayment { +fun interface OnPaymentResult { fun invoke(result: PaymentResult) } \ No newline at end of file From d811dc6cb4f28f379af262cd8b09b24410728d3a Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 19 Feb 2024 11:42:49 +0545 Subject: [PATCH 021/131] Add Environment enum --- .../src/main/java/com/khalti/android/v3/Environment.kt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/v3/Environment.kt diff --git a/khalti-android/src/main/java/com/khalti/android/v3/Environment.kt b/khalti-android/src/main/java/com/khalti/android/v3/Environment.kt new file mode 100644 index 00000000..d9f38a4b --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/v3/Environment.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.v3 + +enum class Environment { + PROD, TEST +} \ No newline at end of file From bdaf039393f7f026bfbc9e76d470a458656706ba Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 19 Feb 2024 11:43:18 +0545 Subject: [PATCH 022/131] Remove all function overloading for parameters with default values --- .../com/khalti/android/demo/MainActivity.kt | 4 +- .../android/demo/composable/DemoScreen.kt | 60 ++++++++++++++++--- .../com/khalti/android/EPaymentWebClient.kt | 39 ++++++++---- .../main/java/com/khalti/android/v3/Khalti.kt | 46 +++----------- 4 files changed, 90 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/com/khalti/android/demo/MainActivity.kt b/app/src/main/java/com/khalti/android/demo/MainActivity.kt index 082a919f..cabcf3a1 100644 --- a/app/src/main/java/com/khalti/android/demo/MainActivity.kt +++ b/app/src/main/java/com/khalti/android/demo/MainActivity.kt @@ -8,7 +8,7 @@ import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.ui.Modifier -import com.khalti.android.demo.composable.DemoScreen +import com.khalti.android.demo.composable.DemoScreenV3 import com.khalti.android.demo.theme.KhaltiTheme class MainActivity : ComponentActivity() { @@ -20,7 +20,7 @@ class MainActivity : ComponentActivity() { Surface( Modifier.fillMaxSize(), ) { - DemoScreen() + DemoScreenV3() } } } diff --git a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt index afd218e2..f6c2126c 100644 --- a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt +++ b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt @@ -2,6 +2,7 @@ package com.khalti.android.demo.composable +import android.net.Uri import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.foundation.Image @@ -14,6 +15,11 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import com.khalti.android.* import com.khalti.android.demo.R +import com.khalti.android.v3.Khalti +import com.khalti.android.v3.OnMessage +import com.khalti.android.v3.OnPaymentResult +import com.khalti.android.v3.OnReturn +import java.net.URI const val RESULT_TAG = "KHALTI_PAY_RESULT" @@ -27,14 +33,6 @@ fun DemoScreen() { val (result, setResult) = remember { mutableStateOf(null) } - val khalti = Khalti.Builder() - .publicKey("123") - .paymentUrl("yo") - .returnUrl("hello") - .merchantAppDeeplink("deeplink") - .onPaymentComplete {} - .build() - val khaltiPay = rememberLauncherForActivityResult(OpenKhaltiPay()) { setResult(it) @@ -130,4 +128,50 @@ fun DemoScreen() { } }, ) +} + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DemoScreenV3() { + + val khalti = Khalti.init( + "", + Uri.parse(""), + Uri.parse(""), + onPaymentResult = {}, + onMessage = {}) + + Scaffold( + topBar = { + CenterAlignedTopAppBar( + title = { + Text("Khalti Android SDK Demo V3") + } + + ) + }, + content = { padding -> + Column( + Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(Modifier.padding(padding)) + Image( + painterResource(R.drawable.khalti_logo_color), + contentDescription = "Khalti Logo", + modifier = Modifier.height(200.dp) + ) + FilledTonalButton( + { + khalti.open() + khalti.verify() + khalti.close() + } + ) { + Text("Pay with Khalti") + } + } + }, + ) } \ No newline at end of file diff --git a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt index 65a69fc2..b55399d2 100644 --- a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt @@ -17,12 +17,12 @@ internal class EPaymentWebClient( @RequiresApi(Build.VERSION_CODES.LOLLIPOP) override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): - Boolean = handleUri(view, request!!.url) + Boolean = handleUriV3(view, request!!.url) @SuppressWarnings("deprecation") @Deprecated("") override fun shouldOverrideUrlLoading(view: WebView?, url: String?): - Boolean = handleUri(view, Uri.parse(url)) + Boolean = handleUriV3(view, Uri.parse(url)) @RequiresApi(Build.VERSION_CODES.M) override fun onReceivedError( @@ -40,6 +40,33 @@ internal class EPaymentWebClient( failingUrl: String? ) = handleError(description) + private fun handleUriV3(view: WebView?, uri: Uri) : Boolean { + val url = uri.toString() + val path = uri.path + val fragment = uri.fragment + + val mPinPath = "/account/transaction_pin" + + if (url.startsWith(returnUrl)) { + // callback + } else if (path.equals(mPinPath) || fragment.equals(mPinPath)) { + val deeplink = "https://khalti.com/go/?t=mpin" + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(deeplink)) + + activity.startActivity(browserIntent) + } + + return false + } + + private fun handleError(description: String?) { + val intent = Intent() + intent.putExtra(OpenKhaltiPay.PAYMENT_URL_LOAD_ERROR_RESULT, description ?: "") + + activity.setResult(OpenKhaltiPay.PAYMENT_URL_LOAD_ERROR, intent) + activity.finish() + } + private fun handleUri(view: WebView?, uri: Uri): Boolean { val url = uri.toString() val path = uri.path @@ -73,12 +100,4 @@ internal class EPaymentWebClient( return true } - - private fun handleError(description: String?) { - val intent = Intent() - intent.putExtra(OpenKhaltiPay.PAYMENT_URL_LOAD_ERROR_RESULT, description ?: "") - - activity.setResult(OpenKhaltiPay.PAYMENT_URL_LOAD_ERROR, intent) - activity.finish() - } } diff --git a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt index 8ec0c9b4..0c0bc231 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt @@ -13,6 +13,7 @@ class Khalti private constructor( val paymentUrl: Uri, val returnUrl: Uri, val openInKhalti: Boolean, + val environment: Environment, val onPaymentResult: OnPaymentResult, val onMessage: OnMessage, val onReturn: OnReturn?, @@ -22,7 +23,8 @@ class Khalti private constructor( publicKey: String, paymentUrl: Uri, returnUrl: Uri, - openInKhalti: Boolean, + openInKhalti: Boolean = true, + environment: Environment = Environment.PROD, onPaymentResult: OnPaymentResult, onMessage: OnMessage, onReturn: OnReturn, @@ -32,6 +34,7 @@ class Khalti private constructor( paymentUrl, returnUrl, openInKhalti, + environment, onPaymentResult, onMessage, onReturn, @@ -42,26 +45,8 @@ class Khalti private constructor( publicKey: String, paymentUrl: Uri, returnUrl: Uri, - onPaymentResult: OnPaymentResult, - onMessage: OnMessage, - onReturn: OnReturn, - ): Khalti { - return Khalti( - publicKey, - paymentUrl, - returnUrl, - true, - onPaymentResult, - onMessage, - onReturn, - ) - } - - fun init( - publicKey: String, - paymentUrl: Uri, - returnUrl: Uri, - openInKhalti: Boolean, + openInKhalti: Boolean = true, + environment: Environment = Environment.PROD, onPaymentResult: OnPaymentResult, onMessage: OnMessage, ): Khalti { @@ -70,24 +55,7 @@ class Khalti private constructor( paymentUrl, returnUrl, openInKhalti, - onPaymentResult, - onMessage, - null, - ) - } - - fun init( - publicKey: String, - paymentUrl: Uri, - returnUrl: Uri, - onPaymentResult: OnPaymentResult, - onMessage: OnMessage, - ): Khalti { - return Khalti( - publicKey, - paymentUrl, - returnUrl, - true, + environment, onPaymentResult, onMessage, null, From 5e01c023661fbd0e51ca0cc32935b0b4e008d408 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 19 Feb 2024 11:53:18 +0545 Subject: [PATCH 023/131] Replace payment url with pidx --- .../src/main/java/com/khalti/android/v3/Khalti.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt index 0c0bc231..0067afe6 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt @@ -10,7 +10,7 @@ import android.net.Uri // method overloading was required for Java developers class Khalti private constructor( val publicKey: String, - val paymentUrl: Uri, + val pidx: String, val returnUrl: Uri, val openInKhalti: Boolean, val environment: Environment, @@ -21,7 +21,7 @@ class Khalti private constructor( companion object { fun init( publicKey: String, - paymentUrl: Uri, + pidx: String, returnUrl: Uri, openInKhalti: Boolean = true, environment: Environment = Environment.PROD, @@ -31,7 +31,7 @@ class Khalti private constructor( ): Khalti { return Khalti( publicKey, - paymentUrl, + pidx, returnUrl, openInKhalti, environment, @@ -43,7 +43,7 @@ class Khalti private constructor( fun init( publicKey: String, - paymentUrl: Uri, + pidx: String, returnUrl: Uri, openInKhalti: Boolean = true, environment: Environment = Environment.PROD, @@ -52,7 +52,7 @@ class Khalti private constructor( ): Khalti { return Khalti( publicKey, - paymentUrl, + pidx, returnUrl, openInKhalti, environment, From e0fbc24a0c1621b68878f7d6c05c73e9ca9f474a Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 11:04:43 +0545 Subject: [PATCH 024/131] Add parcelize plugin --- khalti-android/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/khalti-android/build.gradle b/khalti-android/build.gradle index d81c9301..4f29c8d6 100644 --- a/khalti-android/build.gradle +++ b/khalti-android/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.android.library' id 'org.jetbrains.kotlin.android' + id 'kotlin-parcelize' } android { From b2ab2b5ba63510845c0102f0121ab207d5dc327e Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 11:05:52 +0545 Subject: [PATCH 025/131] Add retrofit for api calls --- khalti-android/build.gradle | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/khalti-android/build.gradle b/khalti-android/build.gradle index 4f29c8d6..9d03025f 100644 --- a/khalti-android/build.gradle +++ b/khalti-android/build.gradle @@ -32,10 +32,18 @@ android { } dependencies { + // Core implementation "androidx.core:core-ktx:$core_ktx_version" implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "com.google.android.material:material:$material_version" + + // Api + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + implementation 'com.squareup.okhttp3:logging-interceptor:4.11.0' + testImplementation "junit:junit:$junit_version" + androidTestImplementation "androidx.test.ext:junit:$junit_ext_version" androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version" } From 655c954d3271bdc83044ed2be5df7a4d55f32bff Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 11:15:06 +0545 Subject: [PATCH 026/131] Remove activity and returnUrl as parameter to EPaymentWebClient --- .../src/main/java/com/khalti/android/EPaymentWebClient.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt index b55399d2..9e2b1d7a 100644 --- a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt @@ -10,10 +10,7 @@ import android.webkit.* import android.widget.Toast import androidx.annotation.RequiresApi -internal class EPaymentWebClient( - private val activity: Activity, - private val returnUrl: String -) : WebViewClient() { +internal class EPaymentWebClient : WebViewClient() { @RequiresApi(Build.VERSION_CODES.LOLLIPOP) override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): From 82aeb74f44349c1aa1acc92d1494c702a2f067f3 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 11:15:55 +0545 Subject: [PATCH 027/131] Remove old handleUri function --- .../com/khalti/android/EPaymentWebClient.kt | 42 ++----------------- 1 file changed, 3 insertions(+), 39 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt index 9e2b1d7a..79624cbe 100644 --- a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt @@ -2,24 +2,22 @@ package com.khalti.android -import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Build import android.webkit.* -import android.widget.Toast import androidx.annotation.RequiresApi internal class EPaymentWebClient : WebViewClient() { @RequiresApi(Build.VERSION_CODES.LOLLIPOP) override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): - Boolean = handleUriV3(view, request!!.url) + Boolean = handleUri(view, request!!.url) @SuppressWarnings("deprecation") @Deprecated("") override fun shouldOverrideUrlLoading(view: WebView?, url: String?): - Boolean = handleUriV3(view, Uri.parse(url)) + Boolean = handleUri(view, Uri.parse(url)) @RequiresApi(Build.VERSION_CODES.M) override fun onReceivedError( @@ -37,7 +35,7 @@ internal class EPaymentWebClient : WebViewClient() { failingUrl: String? ) = handleError(description) - private fun handleUriV3(view: WebView?, uri: Uri) : Boolean { + private fun handleUri(view: WebView?, uri: Uri) : Boolean { val url = uri.toString() val path = uri.path val fragment = uri.fragment @@ -63,38 +61,4 @@ internal class EPaymentWebClient : WebViewClient() { activity.setResult(OpenKhaltiPay.PAYMENT_URL_LOAD_ERROR, intent) activity.finish() } - - private fun handleUri(view: WebView?, uri: Uri): Boolean { - val url = uri.toString() - val path = uri.path - val fragment = uri.fragment - - val eBankingPath = "/ebanking/initiate/" - val mPinPath = "/account/transaction_pin" - - if (url == OpenKhaltiPay.DEFAULT_HOME) { - activity.finish() - } else if (url.startsWith(returnUrl)) { - val isSuccess = uri.getQueryParameter("pidx") != null - val intent = Intent() - intent.putExtra(OpenKhaltiPay.RESULT, url) - - activity.setResult( - if (isSuccess) Activity.RESULT_OK else OpenKhaltiPay.ERROR, - intent - ) - activity.finish() - } else if (path.equals(eBankingPath)) { - view?.loadUrl(url) - } else if (path.equals(mPinPath) || fragment.equals(mPinPath)) { - val deeplink = "https://khalti.com/go/?t=mpin" - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(deeplink)) - - activity.startActivity(browserIntent) - } else { - Toast.makeText(activity, "Action not permitted", Toast.LENGTH_SHORT).show() - } - - return true - } } From 1c7e5bc6bf812a8de6380640d86ae8d46c8489db Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 11:17:34 +0545 Subject: [PATCH 028/131] Access khalti config from cache --- .../com/khalti/android/EPaymentWebClient.kt | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt index 79624cbe..d3ab17be 100644 --- a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt @@ -5,8 +5,11 @@ package com.khalti.android import android.content.Intent import android.net.Uri import android.os.Build +import android.util.Log import android.webkit.* import androidx.annotation.RequiresApi +import com.khalti.android.v3.CacheManager +import com.khalti.android.v3.Khalti internal class EPaymentWebClient : WebViewClient() { @@ -37,28 +40,28 @@ internal class EPaymentWebClient : WebViewClient() { private fun handleUri(view: WebView?, uri: Uri) : Boolean { val url = uri.toString() - val path = uri.path - val fragment = uri.fragment + val khalti = CacheManager.instance().get("khalti") + val returnUrl = khalti?.config?.returnUrl?.toString() ?: "" - val mPinPath = "/account/transaction_pin" + Log.i("Url", url) - if (url.startsWith(returnUrl)) { - // callback - } else if (path.equals(mPinPath) || fragment.equals(mPinPath)) { - val deeplink = "https://khalti.com/go/?t=mpin" - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(deeplink)) + // TODO (Ishwor) Handle redirection to Khalti app for setting MPIN + // MPIN url : /account/transaction_pin - activity.startActivity(browserIntent) + // TODO (Ishwor) Invoke this callback after the page has successfully loaded + if (url.startsWith(returnUrl)) { + khalti?.onReturn?.invoke() } + khalti?.verify() + return false } private fun handleError(description: String?) { - val intent = Intent() - intent.putExtra(OpenKhaltiPay.PAYMENT_URL_LOAD_ERROR_RESULT, description ?: "") - - activity.setResult(OpenKhaltiPay.PAYMENT_URL_LOAD_ERROR, intent) - activity.finish() + val khalti = CacheManager.instance().get("khalti") + if (description != null) { + khalti?.onMessage?.invoke(description) + } } } From 35a2802360cb9b9a1103787dda3c73bfef8b9aaf Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 11:18:25 +0545 Subject: [PATCH 029/131] Cleanup import --- .../src/main/java/com/khalti/android/EPaymentWebClient.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt index d3ab17be..3246c377 100644 --- a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt @@ -2,7 +2,6 @@ package com.khalti.android -import android.content.Intent import android.net.Uri import android.os.Build import android.util.Log From c9b4cf0834084f68d1cd0b2510460b534e1b5e75 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 11:30:34 +0545 Subject: [PATCH 030/131] Fetch khalti config from cache --- .../com/khalti/android/PaymentActivity.kt | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt index 2b59307b..d2c9e71b 100644 --- a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt +++ b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt @@ -7,6 +7,7 @@ import android.app.Activity import android.net.Uri import android.os.Build import android.os.Bundle +import android.util.Log import android.view.Gravity import android.webkit.* import android.widget.LinearLayout @@ -14,6 +15,9 @@ import android.widget.LinearLayout.LayoutParams import android.widget.ProgressBar import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.MaterialToolbar +import com.khalti.android.v3.CacheManager +import com.khalti.android.v3.Environment +import com.khalti.android.v3.Khalti internal class PaymentActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -44,32 +48,37 @@ internal class PaymentActivity : Activity() { webSettings.javaScriptEnabled = true webSettings.domStorageEnabled = true - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableExtra(OpenKhaltiPay.CONFIG, KhaltiPayConfiguration::class.java) - } else { - @Suppress("DEPRECATION") - intent.getParcelableExtra(OpenKhaltiPay.CONFIG) - }?.let { it -> - webView.webViewClient = EPaymentWebClient(this, it.returnUrl) - webView.webChromeClient = object : WebChromeClient() { - override fun onProgressChanged(view: WebView?, newProgress: Int) { - progressBar.visibility = - if (newProgress == 100) ProgressBar.GONE else ProgressBar.VISIBLE - } + webView.webViewClient = EPaymentWebClient() + webView.webChromeClient = object : WebChromeClient() { + override fun onProgressChanged(view: WebView?, newProgress: Int) { + progressBar.visibility = + if (newProgress == 100) ProgressBar.GONE else ProgressBar.VISIBLE } + } + + val khalti = CacheManager.instance().get("khalti") + if (khalti != null) { + val config = khalti.config + val baseUrl = if (config.environment == Environment.PROD) { + "https://pay.khalti.com/" + } else { + "https://test-pay.khalti.com/" + } + + val paymentUri = + Uri.parse(baseUrl).buildUpon().appendQueryParameter("pidx", config.pidx) + + Log.i("Payment Uri", paymentUri.toString()) - val paymentUri = Uri.parse(it.paymentUrl).buildUpon() - .appendQueryParameter("home", OpenKhaltiPay.DEFAULT_HOME) - .build() webView.loadUrl(paymentUri.toString()) - } - appBar.addView(toolbar) + appBar.addView(toolbar) - layout.addView(appBar) - layout.addView(progressBar) - layout.addView(webView, params) + layout.addView(appBar) + layout.addView(progressBar) + layout.addView(webView, params) - setContentView(layout, params) + setContentView(layout, params) + } } } From ebd7839159d525dca92efd874932aa9f0dbc8a56 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 11:31:45 +0545 Subject: [PATCH 031/131] Add broadcast registration and de-registration to handle closing of payment portal --- .../com/khalti/android/PaymentActivity.kt | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt index d2c9e71b..ca2fffda 100644 --- a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt +++ b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt @@ -4,6 +4,10 @@ package com.khalti.android import android.annotation.SuppressLint import android.app.Activity +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.net.Uri import android.os.Build import android.os.Bundle @@ -20,6 +24,7 @@ import com.khalti.android.v3.Environment import com.khalti.android.v3.Khalti internal class PaymentActivity : Activity() { + private var receiver: BroadcastReceiver? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -79,6 +84,42 @@ internal class PaymentActivity : Activity() { layout.addView(webView, params) setContentView(layout, params) + registerBroadcast() } } + + override fun onDestroy() { + unregisterBroadcast() + super.onDestroy() + } + + // TODO (Ishwor) unregister broadcast + @SuppressLint("UnspecifiedRegisterReceiverFlag") + private fun registerBroadcast() { + // TODO (Ishwor) Remove hardcoded + receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent != null && intent.action.equals("close_khalti_payment_portal")) { + finish() + } + } + } + if (Build.VERSION.SDK_INT >= 26) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver( + receiver, IntentFilter("close_khalti_payment_portal"), + RECEIVER_NOT_EXPORTED + ) + } else { + registerReceiver( + receiver, IntentFilter("close_khalti_payment_portal"), + ) + } + } + } + + private fun unregisterBroadcast() { + unregisterReceiver(receiver) + } } + From 1faa03b1078f89b0fad6875ea88ad0e2d34f0c53 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 11:33:13 +0545 Subject: [PATCH 032/131] Remove unnecessary fields --- .../src/main/java/com/khalti/android/v3/PaymentResult.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/PaymentResult.kt b/khalti-android/src/main/java/com/khalti/android/v3/PaymentResult.kt index 1002793c..403ff92a 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/PaymentResult.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/PaymentResult.kt @@ -9,7 +9,6 @@ package com.khalti.android.v3 /// statusCode [36] -> status [Cancelled] (Cancelled by user) data class PaymentResult( - val statusCode: Int, val status: String, val payload: PaymentPayload?, val message: String? @@ -18,8 +17,5 @@ data class PaymentResult( data class PaymentPayload( val pidx: String, val amount: Long, - val mobile: String, - val purchaseOrderId: String, - val purchaseOrderName: String, val transactionId: String ) \ No newline at end of file From 609e2439ced373f24ea57183f16c34e22c2bd657 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 11:34:20 +0545 Subject: [PATCH 033/131] Separate config fields into separate data class for separation of concerns and easy parcelization --- .../com/khalti/android/v3/KhaltiPayConfig.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/v3/KhaltiPayConfig.kt diff --git a/khalti-android/src/main/java/com/khalti/android/v3/KhaltiPayConfig.kt b/khalti-android/src/main/java/com/khalti/android/v3/KhaltiPayConfig.kt new file mode 100644 index 00000000..21f632f3 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/v3/KhaltiPayConfig.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.v3 + +import android.net.Uri +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class KhaltiPayConfig( + val publicKey: String, + val pidx: String, + val returnUrl: Uri, + val openInKhalti: Boolean = true, + val environment: Environment = Environment.PROD, +) : Parcelable From f9b79a74c66bebf8cbabebfd32182518e5c86548 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 11:34:53 +0545 Subject: [PATCH 034/131] Add support for in-memory caching --- .../com/khalti/android/v3/CacheManager.kt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/v3/CacheManager.kt diff --git a/khalti-android/src/main/java/com/khalti/android/v3/CacheManager.kt b/khalti-android/src/main/java/com/khalti/android/v3/CacheManager.kt new file mode 100644 index 00000000..9cf2602b --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/v3/CacheManager.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.v3 + +@Suppress("UNCHECKED_CAST") +class CacheManager private constructor() { + + companion object { + @Volatile + private var instance: CacheManager? = null + + fun instance(): CacheManager { + return instance ?: synchronized(this) { + instance ?: CacheManager().also { instance = it } + } + } + } + + private val cache = HashMap() + + fun put(key: String, value: Any) { + cache[key] = value + } + + fun get(key: String): T? { + return cache[key] as T? + } +} \ No newline at end of file From 3d61288c1adcdcbd7384adaad3112dde71f9c22d Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 11:35:50 +0545 Subject: [PATCH 035/131] Add early implementatin of api service --- .../java/com/khalti/android/api/ApiClient.kt | 69 +++++++++++++++++++ .../java/com/khalti/android/api/ApiService.kt | 16 +++++ 2 files changed, 85 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt create mode 100644 khalti-android/src/main/java/com/khalti/android/api/ApiService.kt diff --git a/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt new file mode 100644 index 00000000..0f4e8978 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.api + +import android.os.Build +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.security.PublicKey + +internal class RetrofitClient( + private val baseUrl: String, + private val publicKey: String, + private val packageName: String, + private val packageVersion: String, + private val moduleVersion: String +) { + + val retrofit: Retrofit by lazy { + val interceptor = HttpLoggingInterceptor() + interceptor.level = HttpLoggingInterceptor.Level.BODY + + val client: OkHttpClient = OkHttpClient.Builder() + .addInterceptor(interceptor) + /*.addInterceptor { + val requestBuilder: Request.Builder = it.request().newBuilder() + requestBuilder.header("Authorization", "Key $publicKey") + requestBuilder.header("checkout-version", moduleVersion) + requestBuilder.header("checkout-platform", "android") + requestBuilder.header("checkout-os-version", Build.VERSION.RELEASE) + requestBuilder.header("checkout-device-model", Build.MODEL) + requestBuilder.header("checkout-device-manufacturer", Build.MANUFACTURER) + requestBuilder.header("merchant-package-name", packageName) + requestBuilder.header("merchant-package-version", packageVersion) + requestBuilder.method(it.request().method, it.request().body) + + it.proceed(requestBuilder.build()) + }*/ + .build() + + Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .client(client) + .build() + } +} + +class ApiClient() { + private var apiService: ApiService? = null + + fun build(baseUrl: String, publicKey: String): ApiService { + apiService = + apiService ?: RetrofitClient( + baseUrl, + publicKey, + "com.apple", + "1.0", + "3.00.00" + ).retrofit.create( + ApiService::class.java + ) + return apiService!! + } +} \ No newline at end of file diff --git a/khalti-android/src/main/java/com/khalti/android/api/ApiService.kt b/khalti-android/src/main/java/com/khalti/android/api/ApiService.kt new file mode 100644 index 00000000..e23d7ad7 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/api/ApiService.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.api + +import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.Headers +import retrofit2.http.POST + +interface ApiService { + @Headers("Authorization: Key live_secret_key_68791341fdd94846a146f0457ff7b455") + @POST("epayment/lookup/") + fun verify(@Body body: Map): Call +} \ No newline at end of file From cd6b3f9fb4ed9ddaab176ce7d0104f13a61b900b Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 12:09:29 +0545 Subject: [PATCH 036/131] Add android context to Khalti; required to open PaymentActivity --- .../android/demo/composable/DemoScreen.kt | 58 +++++++++++++------ .../main/java/com/khalti/android/v3/Khalti.kt | 2 + 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt index f6c2126c..65325eb2 100644 --- a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt +++ b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt @@ -6,20 +6,38 @@ import android.net.Uri import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp -import com.khalti.android.* +import com.khalti.android.KhaltiPayConfiguration +import com.khalti.android.OpenKhaltiPay +import com.khalti.android.PaymentCancelled +import com.khalti.android.PaymentError +import com.khalti.android.PaymentResult +import com.khalti.android.PaymentSuccess import com.khalti.android.demo.R +import com.khalti.android.v3.Environment import com.khalti.android.v3.Khalti -import com.khalti.android.v3.OnMessage -import com.khalti.android.v3.OnPaymentResult -import com.khalti.android.v3.OnReturn -import java.net.URI +import com.khalti.android.v3.KhaltiPayConfig const val RESULT_TAG = "KHALTI_PAY_RESULT" @@ -136,11 +154,19 @@ fun DemoScreen() { fun DemoScreenV3() { val khalti = Khalti.init( - "", - Uri.parse(""), - Uri.parse(""), - onPaymentResult = {}, - onMessage = {}) + LocalContext.current, + KhaltiPayConfig( + "b476aa4b21864b54ab96e430c9192be1", + "2fpAfWxK3S6coXpQkeXVRb", + Uri.parse("https://khalti.com"), + environment = Environment.TEST + ), + {}, + {}, + onReturn = { + Log.i("Demo", "OnReturn") + } + ) Scaffold( topBar = { @@ -165,8 +191,6 @@ fun DemoScreenV3() { FilledTonalButton( { khalti.open() - khalti.verify() - khalti.close() } ) { Text("Pay with Khalti") @@ -174,4 +198,4 @@ fun DemoScreenV3() { } }, ) -} \ No newline at end of file +} diff --git a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt index 0067afe6..b6b50aab 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt @@ -4,11 +4,13 @@ package com.khalti.android.v3 +import android.content.Context import android.net.Uri // Though kotlin provides named and optional parameters // method overloading was required for Java developers class Khalti private constructor( + private val context: Context, val publicKey: String, val pidx: String, val returnUrl: Uri, From 8f43b368e4614ce29e6978183b90ba2bed14285c Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 12:11:33 +0545 Subject: [PATCH 037/131] Move config fields into KhaltiPayConfig --- .../main/java/com/khalti/android/v3/Khalti.kt | 34 +++++-------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt index b6b50aab..e090bd70 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt @@ -11,32 +11,22 @@ import android.net.Uri // method overloading was required for Java developers class Khalti private constructor( private val context: Context, - val publicKey: String, - val pidx: String, - val returnUrl: Uri, - val openInKhalti: Boolean, - val environment: Environment, + val config: KhaltiPayConfig, val onPaymentResult: OnPaymentResult, val onMessage: OnMessage, val onReturn: OnReturn?, ) { companion object { fun init( - publicKey: String, - pidx: String, - returnUrl: Uri, - openInKhalti: Boolean = true, - environment: Environment = Environment.PROD, + context: Context, + config: KhaltiPayConfig, onPaymentResult: OnPaymentResult, onMessage: OnMessage, onReturn: OnReturn, ): Khalti { return Khalti( - publicKey, - pidx, - returnUrl, - openInKhalti, - environment, + context, + config, onPaymentResult, onMessage, onReturn, @@ -44,20 +34,14 @@ class Khalti private constructor( } fun init( - publicKey: String, - pidx: String, - returnUrl: Uri, - openInKhalti: Boolean = true, - environment: Environment = Environment.PROD, + context: Context, + config: KhaltiPayConfig, onPaymentResult: OnPaymentResult, onMessage: OnMessage, ): Khalti { return Khalti( - publicKey, - pidx, - returnUrl, - openInKhalti, - environment, + context, + config, onPaymentResult, onMessage, null, From a7128898031ee2e25d12e0949be1302748b059b2 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 12:12:15 +0545 Subject: [PATCH 038/131] Cache khalti object in cachemanager --- .../src/main/java/com/khalti/android/v3/Khalti.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt index e090bd70..e13cd959 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt @@ -24,13 +24,16 @@ class Khalti private constructor( onMessage: OnMessage, onReturn: OnReturn, ): Khalti { - return Khalti( + val khalti = Khalti( context, config, onPaymentResult, onMessage, onReturn, ) + + CacheManager.instance().put("khalti", khalti) + return khalti } fun init( @@ -39,13 +42,17 @@ class Khalti private constructor( onPaymentResult: OnPaymentResult, onMessage: OnMessage, ): Khalti { - return Khalti( + val khalti = Khalti( context, config, onPaymentResult, onMessage, null, ) + + CacheManager.instance().put("khalti", khalti) + + return khalti } } From 428f3ed369900cb67c4dd8914620d78ae1da3015 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 12:13:21 +0545 Subject: [PATCH 039/131] Add support to open PaymentActivity from Khalti object --- .../src/main/java/com/khalti/android/v3/Khalti.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt index e13cd959..d89d3a2b 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt @@ -5,7 +5,8 @@ package com.khalti.android.v3 import android.content.Context -import android.net.Uri +import android.content.Intent +import com.khalti.android.PaymentActivity // Though kotlin provides named and optional parameters // method overloading was required for Java developers @@ -57,7 +58,8 @@ class Khalti private constructor( } fun open() { - + val intent = Intent(context, PaymentActivity::class.java) + context.startActivity(intent) } fun verify() { From 6c9862764f8e023d65c7fdf81128a0212bb198a2 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 12:13:43 +0545 Subject: [PATCH 040/131] Add support to close PaymentActivity from Khalti object --- khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt index d89d3a2b..86ed7773 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt @@ -67,6 +67,7 @@ class Khalti private constructor( } fun close() { - + val intent = Intent("close_khalti_payment_portal") + context.sendBroadcast(intent) } } \ No newline at end of file From f7906b70496f1a218c8ce96f61a29a57faecd23c Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 12:14:29 +0545 Subject: [PATCH 041/131] Add support to verify payment --- .../main/java/com/khalti/android/v3/Khalti.kt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt index 86ed7773..b9b3f355 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt @@ -4,9 +4,15 @@ package com.khalti.android.v3 +import android.app.Activity import android.content.Context import android.content.Intent +import android.util.Log import com.khalti.android.PaymentActivity +import com.khalti.android.api.ApiClient +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response // Though kotlin provides named and optional parameters // method overloading was required for Java developers @@ -17,6 +23,8 @@ class Khalti private constructor( val onMessage: OnMessage, val onReturn: OnReturn?, ) { + var activity: Activity? = null + companion object { fun init( context: Context, @@ -63,7 +71,29 @@ class Khalti private constructor( } fun verify() { + val apiClient = ApiClient() + val baseUrl = if (config.environment == Environment.PROD) { + "https://khalti.com/api/v2/" + } else { + "https://dev.khalti.com/api/v2/" + } + + val call = apiClient.build(baseUrl, config.publicKey).verify(mapOf("pidx" to config.pidx)) + + call.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + Log.i("Response", response.body().toString()) + } else { + // TODO (Ishwor) Handle error + } + } + override fun onFailure(call: Call, t: Throwable) { + Log.e("Error", t.printStackTrace().toString()) + // TODO (Ishwor) Handle error + } + }) } fun close() { From fa4bf6ad35e4886ddbd8b1eed7e140f68d608475 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 14:10:12 +0545 Subject: [PATCH 042/131] Trigger onReturn callback after page has finished loading --- .../com/khalti/android/EPaymentWebClient.kt | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt index 3246c377..03124699 100644 --- a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt @@ -14,12 +14,12 @@ internal class EPaymentWebClient : WebViewClient() { @RequiresApi(Build.VERSION_CODES.LOLLIPOP) override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): - Boolean = handleUri(view, request!!.url) + Boolean = handleUri(request!!.url) @SuppressWarnings("deprecation") @Deprecated("") override fun shouldOverrideUrlLoading(view: WebView?, url: String?): - Boolean = handleUri(view, Uri.parse(url)) + Boolean = handleUri(Uri.parse(url)) @RequiresApi(Build.VERSION_CODES.M) override fun onReceivedError( @@ -37,23 +37,28 @@ internal class EPaymentWebClient : WebViewClient() { failingUrl: String? ) = handleError(description) - private fun handleUri(view: WebView?, uri: Uri) : Boolean { - val url = uri.toString() + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + val khalti = CacheManager.instance().get("khalti") val returnUrl = khalti?.config?.returnUrl?.toString() ?: "" - Log.i("Url", url) + Log.i("Url", url ?: "") - // TODO (Ishwor) Handle redirection to Khalti app for setting MPIN - // MPIN url : /account/transaction_pin - - // TODO (Ishwor) Invoke this callback after the page has successfully loaded - if (url.startsWith(returnUrl)) { + if (url?.startsWith(returnUrl) != false) { khalti?.onReturn?.invoke() } khalti?.verify() + } + private fun handleUri(uri: Uri): Boolean { + val url = uri.toString() + + Log.i("Url", url) + + // TODO (Ishwor) Handle redirection to Khalti app for setting MPIN + // MPIN url : /account/transaction_pin return false } From e401a1bbf580a1331a015b36b291f106944b591e Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 14:34:28 +0545 Subject: [PATCH 043/131] Add resource directory for urls and string constants --- .../main/java/com/khalti/android/resource/Strings.kt | 9 +++++++++ .../src/main/java/com/khalti/android/resource/Url.kt | 12 ++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/resource/Strings.kt create mode 100644 khalti-android/src/main/java/com/khalti/android/resource/Url.kt diff --git a/khalti-android/src/main/java/com/khalti/android/resource/Strings.kt b/khalti-android/src/main/java/com/khalti/android/resource/Strings.kt new file mode 100644 index 00000000..f73f5113 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/resource/Strings.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.resource + +enum class Strings(val value: String) { + +} \ No newline at end of file diff --git a/khalti-android/src/main/java/com/khalti/android/resource/Url.kt b/khalti-android/src/main/java/com/khalti/android/resource/Url.kt new file mode 100644 index 00000000..738a7fb5 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/resource/Url.kt @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.resource + +enum class Url(val value: String) { + BASE_KHALTI_URL_PROD("https://khalti.com/api/v2/"), + BASE_KHALTI_URL_STAGING("https://dev.khalti.com/api/v2/"), + BASE_PAYMENT_URL_PROD("https://pay.khalti.com/"), + BASE_PAYMENT_URL_STAGING("https://test-pay.khalti.com/"), +} \ No newline at end of file From 8f46a0b16d52578035f287a33ffbcccbde9902e4 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 22 Feb 2024 14:34:56 +0545 Subject: [PATCH 044/131] Replace hardcoded strings with enums for urls --- .../java/com/khalti/android/EPaymentWebClient.kt | 2 -- .../java/com/khalti/android/PaymentActivity.kt | 14 ++++++++------ .../src/main/java/com/khalti/android/v3/Khalti.kt | 11 ++++++----- .../java/com/khalti/android/v3/KhaltiPayConfig.kt | 5 ++++- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt index 03124699..324904c8 100644 --- a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt @@ -43,8 +43,6 @@ internal class EPaymentWebClient : WebViewClient() { val khalti = CacheManager.instance().get("khalti") val returnUrl = khalti?.config?.returnUrl?.toString() ?: "" - Log.i("Url", url ?: "") - if (url?.startsWith(returnUrl) != false) { khalti?.onReturn?.invoke() } diff --git a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt index ca2fffda..304a3bb2 100644 --- a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt +++ b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt @@ -19,6 +19,7 @@ import android.widget.LinearLayout.LayoutParams import android.widget.ProgressBar import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.MaterialToolbar +import com.khalti.android.resource.Url import com.khalti.android.v3.CacheManager import com.khalti.android.v3.Environment import com.khalti.android.v3.Khalti @@ -64,14 +65,16 @@ internal class PaymentActivity : Activity() { val khalti = CacheManager.instance().get("khalti") if (khalti != null) { val config = khalti.config - val baseUrl = if (config.environment == Environment.PROD) { - "https://pay.khalti.com/" + val baseUrl = if (config.isProd()) { + Url.BASE_PAYMENT_URL_PROD } else { - "https://test-pay.khalti.com/" + Url.BASE_PAYMENT_URL_STAGING } - val paymentUri = - Uri.parse(baseUrl).buildUpon().appendQueryParameter("pidx", config.pidx) + val paymentUri = Uri + .parse(baseUrl.value) + .buildUpon() + .appendQueryParameter("pidx", config.pidx) Log.i("Payment Uri", paymentUri.toString()) @@ -93,7 +96,6 @@ internal class PaymentActivity : Activity() { super.onDestroy() } - // TODO (Ishwor) unregister broadcast @SuppressLint("UnspecifiedRegisterReceiverFlag") private fun registerBroadcast() { // TODO (Ishwor) Remove hardcoded diff --git a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt index b9b3f355..6134e79c 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt @@ -10,6 +10,7 @@ import android.content.Intent import android.util.Log import com.khalti.android.PaymentActivity import com.khalti.android.api.ApiClient +import com.khalti.android.resource.Url import retrofit2.Call import retrofit2.Callback import retrofit2.Response @@ -72,13 +73,13 @@ class Khalti private constructor( fun verify() { val apiClient = ApiClient() - val baseUrl = if (config.environment == Environment.PROD) { - "https://khalti.com/api/v2/" + val baseUrl = if (config.isProd()) { + Url.BASE_KHALTI_URL_PROD } else { - "https://dev.khalti.com/api/v2/" + Url.BASE_KHALTI_URL_STAGING } - - val call = apiClient.build(baseUrl, config.publicKey).verify(mapOf("pidx" to config.pidx)) + val call = + apiClient.build(baseUrl.value, config.publicKey).verify(mapOf("pidx" to config.pidx)) call.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { diff --git a/khalti-android/src/main/java/com/khalti/android/v3/KhaltiPayConfig.kt b/khalti-android/src/main/java/com/khalti/android/v3/KhaltiPayConfig.kt index 21f632f3..c3bd4da3 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/KhaltiPayConfig.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/KhaltiPayConfig.kt @@ -15,4 +15,7 @@ data class KhaltiPayConfig( val returnUrl: Uri, val openInKhalti: Boolean = true, val environment: Environment = Environment.PROD, -) : Parcelable +) : Parcelable { + + fun isProd(): Boolean = environment == Environment.PROD +} From d517033816bc3cc632baa594763e686c6d63c7b7 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 23 Feb 2024 15:32:26 +0545 Subject: [PATCH 045/131] Add service directory --- .../android/demo/composable/DemoScreen.kt | 6 +-- .../java/com/khalti/android/api/ApiClient.kt | 17 ++++++--- .../java/com/khalti/android/api/ApiService.kt | 3 +- .../android/servicce/VerificationService.kt | 32 ++++++++++++++++ .../main/java/com/khalti/android/v3/Khalti.kt | 37 +++++++------------ 5 files changed, 62 insertions(+), 33 deletions(-) create mode 100644 khalti-android/src/main/java/com/khalti/android/servicce/VerificationService.kt diff --git a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt index 65325eb2..fe756de2 100644 --- a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt +++ b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt @@ -152,12 +152,11 @@ fun DemoScreen() { @OptIn(ExperimentalMaterial3Api::class) @Composable fun DemoScreenV3() { - val khalti = Khalti.init( LocalContext.current, KhaltiPayConfig( "b476aa4b21864b54ab96e430c9192be1", - "2fpAfWxK3S6coXpQkeXVRb", + "guuVD2quUhbreFfzfwrhw6", Uri.parse("https://khalti.com"), environment = Environment.TEST ), @@ -190,7 +189,8 @@ fun DemoScreenV3() { ) FilledTonalButton( { - khalti.open() +// khalti.open() + khalti.verify() } ) { Text("Pay with Khalti") diff --git a/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt index 0f4e8978..c48d5d7e 100644 --- a/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt @@ -4,13 +4,12 @@ package com.khalti.android.api -import android.os.Build +import com.khalti.android.resource.Url +import com.khalti.android.v3.Environment import okhttp3.OkHttpClient -import okhttp3.Request import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory -import java.security.PublicKey internal class RetrofitClient( private val baseUrl: String, @@ -50,13 +49,19 @@ internal class RetrofitClient( } } -class ApiClient() { +class ApiClient(private val environment: Environment = Environment.PROD) { private var apiService: ApiService? = null - fun build(baseUrl: String, publicKey: String): ApiService { + fun build(publicKey: String): ApiService { + val baseUrl = if (environment == Environment.PROD) { + Url.BASE_KHALTI_URL_PROD + } else { + Url.BASE_KHALTI_URL_STAGING + } + apiService = apiService ?: RetrofitClient( - baseUrl, + baseUrl.value, publicKey, "com.apple", "1.0", diff --git a/khalti-android/src/main/java/com/khalti/android/api/ApiService.kt b/khalti-android/src/main/java/com/khalti/android/api/ApiService.kt index e23d7ad7..6e2d1a2b 100644 --- a/khalti-android/src/main/java/com/khalti/android/api/ApiService.kt +++ b/khalti-android/src/main/java/com/khalti/android/api/ApiService.kt @@ -5,6 +5,7 @@ package com.khalti.android.api import retrofit2.Call +import retrofit2.Response import retrofit2.http.Body import retrofit2.http.Headers import retrofit2.http.POST @@ -12,5 +13,5 @@ import retrofit2.http.POST interface ApiService { @Headers("Authorization: Key live_secret_key_68791341fdd94846a146f0457ff7b455") @POST("epayment/lookup/") - fun verify(@Body body: Map): Call + suspend fun verify(@Body body: Map):Response } \ No newline at end of file diff --git a/khalti-android/src/main/java/com/khalti/android/servicce/VerificationService.kt b/khalti-android/src/main/java/com/khalti/android/servicce/VerificationService.kt new file mode 100644 index 00000000..45ab2486 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/servicce/VerificationService.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.servicce + +import com.khalti.android.api.ApiClient +import com.khalti.android.api.ApiService +import com.khalti.android.v3.KhaltiPayConfig + +class VerificationService(config: KhaltiPayConfig) { + private val apiService: ApiService by lazy { + ApiClient(config.environment).build(config.publicKey) + } + + suspend fun verify(pidx: String): Any? { + try { + val response = apiService.verify(mapOf("pidx" to pidx)) + val error = response.errorBody() + + if (response.isSuccessful) { + return response.body()!! + } else if (error != null) { + throw Exception(error.toString()) + } + } catch (_: Exception) { + + } + + return null + } +} \ No newline at end of file diff --git a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt index 6134e79c..093685f2 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt @@ -11,6 +11,11 @@ import android.util.Log import com.khalti.android.PaymentActivity import com.khalti.android.api.ApiClient import com.khalti.android.resource.Url +import com.khalti.android.servicce.VerificationService +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch import retrofit2.Call import retrofit2.Callback import retrofit2.Response @@ -24,8 +29,6 @@ class Khalti private constructor( val onMessage: OnMessage, val onReturn: OnReturn?, ) { - var activity: Activity? = null - companion object { fun init( context: Context, @@ -71,30 +74,18 @@ class Khalti private constructor( context.startActivity(intent) } + @OptIn(DelicateCoroutinesApi::class) fun verify() { - val apiClient = ApiClient() - val baseUrl = if (config.isProd()) { - Url.BASE_KHALTI_URL_PROD - } else { - Url.BASE_KHALTI_URL_STAGING - } - val call = - apiClient.build(baseUrl.value, config.publicKey).verify(mapOf("pidx" to config.pidx)) + val verificationService = VerificationService(config) - call.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - Log.i("Response", response.body().toString()) - } else { - // TODO (Ishwor) Handle error - } + GlobalScope.launch { + try { + val result = verificationService.verify(config.pidx) + Log.i("Payment Result", result?.toString() ?: "") + } catch (e: Exception) { + Log.e("Payment Result Error", e.toString()) } - - override fun onFailure(call: Call, t: Throwable) { - Log.e("Error", t.printStackTrace().toString()) - // TODO (Ishwor) Handle error - } - }) + } } fun close() { From 865c235977b44279fb239c3084054eaecdcb3fc0 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 14:44:15 +0545 Subject: [PATCH 046/131] Fix typo --- .../khalti/android/{servicce => service}/VerificationService.kt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename khalti-android/src/main/java/com/khalti/android/{servicce => service}/VerificationService.kt (100%) diff --git a/khalti-android/src/main/java/com/khalti/android/servicce/VerificationService.kt b/khalti-android/src/main/java/com/khalti/android/service/VerificationService.kt similarity index 100% rename from khalti-android/src/main/java/com/khalti/android/servicce/VerificationService.kt rename to khalti-android/src/main/java/com/khalti/android/service/VerificationService.kt From 3b4d5922d46fd02ea1cf4bb46021c2aeb630341b Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 14:45:56 +0545 Subject: [PATCH 047/131] Add custom throwable KFailure --- .../com/khalti/android/resource/KFailure.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/resource/KFailure.kt diff --git a/khalti-android/src/main/java/com/khalti/android/resource/KFailure.kt b/khalti-android/src/main/java/com/khalti/android/resource/KFailure.kt new file mode 100644 index 00000000..c5c35e36 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/resource/KFailure.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.resource + +abstract class KFailure(message: String?, cause: Throwable?, val code: Number? = null) : + Exception(message, cause) { + + class NoNetwork(message: String? = null, cause: Throwable? = null) : KFailure(message, cause) + + class ServerUnreachable(message: String? = null, cause: Throwable? = null) : + KFailure(message, cause) + + class HttpCall(message: String? = null, cause: Throwable? = null, code: Number?) : + KFailure(message, cause, code) + + class Payment(message: String? = null, cause: Throwable? = null) : KFailure(message, cause) + + class Generic(message: String? = null, cause: Throwable? = null) : KFailure(message, cause) +} \ No newline at end of file From e710c7aecab81a0b1fc1201f7421f09cefbf1833 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 14:46:21 +0545 Subject: [PATCH 048/131] Add Result for easier error handling --- .../com/khalti/android/resource/Result.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/resource/Result.kt diff --git a/khalti-android/src/main/java/com/khalti/android/resource/Result.kt b/khalti-android/src/main/java/com/khalti/android/resource/Result.kt new file mode 100644 index 00000000..9cbf2736 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/resource/Result.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.resource + +sealed class Result(private val t: T?, private val e: E?) { + + val isOk: Boolean by lazy { + this is Ok + } + + fun match(ok: (T) -> Unit, err: (E) -> Unit) { + if (isOk) { + ok(t!!) + } else { + err(e!!) + } + } +} + +class Ok(t: T) : Result(t, null) + +class Err(e: E) : Result(null, e) From 43f5ed486232c715b4c0a7f1c0bc0eadcab2f504 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 14:47:01 +0545 Subject: [PATCH 049/131] Add ErrorUtil for parsing api errors --- .../com/khalti/android/utils/ErrorUtil.kt | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/utils/ErrorUtil.kt diff --git a/khalti-android/src/main/java/com/khalti/android/utils/ErrorUtil.kt b/khalti-android/src/main/java/com/khalti/android/utils/ErrorUtil.kt new file mode 100644 index 00000000..1ce74622 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/utils/ErrorUtil.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.utils + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import org.json.JSONException +import org.json.JSONObject +import java.lang.reflect.Type +import java.util.Locale + + +class ErrorUtil { + companion object { + const val GENERIC_ERROR = "An error occurred, please try again later" + private const val UNAVAILABLE_ERROR = "Service temporarily unavailable" + private const val GENERIC_JSON_ERROR = + "{\"detail\":\"An error occurred, please try again later\"}" + + + fun parseError(json: String, statusCode: String? = null): String { + var errorJson = "" + if (errorJson.isEmpty()) { + errorJson = GENERIC_JSON_ERROR + } + val map = HashMap() + if (!statusCode.isNullOrEmpty() && statusCode == "503") { + map["detail"] = UNAVAILABLE_ERROR + } else { + try { + val root = JSONObject(errorJson) + if (root.has("detail")) { + val type: Type = object : TypeToken?>() {}.type + if (root.has("meta") && root["meta"].toString() + "" != "{}") { + map.putAll( + Gson().fromJson( + JsonUtil.convertToJsonString(root["meta"]), + type + ) + ) + root.remove("meta") + } + root.remove("error_data") //Remove this if error_data is needed + root.remove("meta") + map.putAll(Gson().fromJson(root.toString() + "", type)) + } else { + if (root.has("non_field_error")) { + map["detail"] = + JsonUtil.parseJsonArray(json = root.getString("non_field_error")) + root.remove("non_field_error") + } + if (root.has("error_key")) { + map["error_key"] = root.getString("error_key") + } + val keys: Iterator<*> = root.keys() + while (keys.hasNext()) { + val currentKey = keys.next() as String + if (!currentKey.lowercase(Locale.getDefault()) + .contains("status") && !currentKey.lowercase( + Locale.getDefault() + ).contains("error_key") + ) { + map[currentKey] = JsonUtil.parseJsonArray(currentKey, errorJson) + } + } + } + } catch (e: JSONException) { + e.printStackTrace() + } + if (map.isEmpty()) { + map["detail"] = Companion.GENERIC_ERROR + } + } + if (!statusCode.isNullOrEmpty()) { + map["status"] = statusCode + } + return JsonUtil.convertToJsonString(map) + } + + fun parseThrowableError(throwable: String?, statusCode: String?): String { + val t = (throwable ?: "").lowercase(Locale.getDefault()) + val map = HashMap() + var error = GENERIC_ERROR + + if (t.contains("timed out")) { + error = + "Looks like you have an unstable network at the moment, please try again when network stabilizes" + } else if (t.contains("cannot connect") || t.contains("failed to connect")) { + error = + "Looks like the server is taking too long to respond, please try again later" + } + + if (statusCode != null) { + map["status"] = statusCode + } + + map["detail"] = error + + return JsonUtil.convertToJsonString(map) + } + + } +} \ No newline at end of file From 0206dd2ea9a8f7d617b9fe3f395326f2f1cb67ae Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 14:47:18 +0545 Subject: [PATCH 050/131] Add helper functions for json --- .../java/com/khalti/android/api/ApiClient.kt | 53 ++++++++++++++++++ .../java/com/khalti/android/api/ApiService.kt | 3 +- .../android/service/VerificationService.kt | 23 +++----- .../java/com/khalti/android/utils/JsonUtil.kt | 56 +++++++++++++++++++ .../main/java/com/khalti/android/v3/Khalti.kt | 24 ++++---- 5 files changed, 128 insertions(+), 31 deletions(-) create mode 100644 khalti-android/src/main/java/com/khalti/android/utils/JsonUtil.kt diff --git a/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt index c48d5d7e..047b6eed 100644 --- a/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt @@ -4,12 +4,24 @@ package com.khalti.android.api +import com.khalti.android.resource.Err +import com.khalti.android.resource.KFailure +import com.khalti.android.resource.Ok +import com.khalti.android.resource.Result import com.khalti.android.resource.Url +import com.khalti.android.utils.ErrorUtil import com.khalti.android.v3.Environment +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor +import okio.IOException +import retrofit2.HttpException +import retrofit2.Response import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import java.net.SocketTimeoutException +import java.net.UnknownHostException internal class RetrofitClient( private val baseUrl: String, @@ -71,4 +83,45 @@ class ApiClient(private val environment: Environment = Environment.PROD) { ) return apiService!! } +} + +suspend fun safeApiCall( + dispatcher: CoroutineDispatcher, apiCall: suspend () -> Response +): Result { + return withContext(dispatcher) { + try { + val response = apiCall.invoke() + if (response.isSuccessful && response.body() != null) { + return@withContext Ok(response.body()!!) + } + return@withContext Err( + KFailure.Payment( + "Error", Throwable( + ErrorUtil.parseError( + if (response.errorBody() != null) String( + response.errorBody()!!.bytes() + ) else "", response.code().toString() + ) + ) + ) + ) + } catch (t: Throwable) { + val processedThrowable = Throwable( + ErrorUtil.parseThrowableError(t.message, "600") + ) + val failure: KFailure = when (t) { + is UnknownHostException -> KFailure.ServerUnreachable(t.message, processedThrowable) + is SocketTimeoutException -> KFailure.NoNetwork(t.message, processedThrowable) + is IOException -> KFailure.NoNetwork(t.message, processedThrowable) + is HttpException -> { + val code = t.code() + + KFailure.HttpCall(t.message, processedThrowable, code) + } + + else -> KFailure.Generic(t.message, processedThrowable) + } + return@withContext Err(failure) + } + } } \ No newline at end of file diff --git a/khalti-android/src/main/java/com/khalti/android/api/ApiService.kt b/khalti-android/src/main/java/com/khalti/android/api/ApiService.kt index 6e2d1a2b..b8f84a81 100644 --- a/khalti-android/src/main/java/com/khalti/android/api/ApiService.kt +++ b/khalti-android/src/main/java/com/khalti/android/api/ApiService.kt @@ -4,7 +4,6 @@ package com.khalti.android.api -import retrofit2.Call import retrofit2.Response import retrofit2.http.Body import retrofit2.http.Headers @@ -13,5 +12,5 @@ import retrofit2.http.POST interface ApiService { @Headers("Authorization: Key live_secret_key_68791341fdd94846a146f0457ff7b455") @POST("epayment/lookup/") - suspend fun verify(@Body body: Map):Response + suspend fun verify(@Body body: Map): Response } \ No newline at end of file diff --git a/khalti-android/src/main/java/com/khalti/android/service/VerificationService.kt b/khalti-android/src/main/java/com/khalti/android/service/VerificationService.kt index 45ab2486..07a18511 100644 --- a/khalti-android/src/main/java/com/khalti/android/service/VerificationService.kt +++ b/khalti-android/src/main/java/com/khalti/android/service/VerificationService.kt @@ -2,31 +2,24 @@ * Copyright (c) 2024. The Khalti Authors. All rights reserved. */ -package com.khalti.android.servicce +package com.khalti.android.service import com.khalti.android.api.ApiClient import com.khalti.android.api.ApiService +import com.khalti.android.api.safeApiCall +import com.khalti.android.resource.KFailure +import com.khalti.android.resource.Result import com.khalti.android.v3.KhaltiPayConfig +import kotlinx.coroutines.Dispatchers class VerificationService(config: KhaltiPayConfig) { private val apiService: ApiService by lazy { ApiClient(config.environment).build(config.publicKey) } - suspend fun verify(pidx: String): Any? { - try { - val response = apiService.verify(mapOf("pidx" to pidx)) - val error = response.errorBody() - - if (response.isSuccessful) { - return response.body()!! - } else if (error != null) { - throw Exception(error.toString()) - } - } catch (_: Exception) { - + suspend fun verify(pidx: String): Result { + return safeApiCall(Dispatchers.IO) { + apiService.verify(mapOf("pidx" to pidx)) } - - return null } } \ No newline at end of file diff --git a/khalti-android/src/main/java/com/khalti/android/utils/JsonUtil.kt b/khalti-android/src/main/java/com/khalti/android/utils/JsonUtil.kt new file mode 100644 index 00000000..79b0a932 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/utils/JsonUtil.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.utils + +import com.google.gson.GsonBuilder +import com.khalti.android.utils.ErrorUtil.Companion.GENERIC_ERROR +import org.json.JSONException +import org.json.JSONObject + + +class JsonUtil { + companion object { + fun convertToJsonString(o: Any): String { + val gson = GsonBuilder().serializeNulls().create() + return gson.toJson(o) + } + + + fun parseJsonArray(key: String? = null, json: String): String { + val stringBuilder = StringBuilder() + try { + val jsonObject = JSONObject(json) + if (key != null) { + return parseArray(key, jsonObject) + } + for (k in jsonObject.keys()) { + stringBuilder.append(parseArray(k, jsonObject)) + } + } catch (e: JSONException) { + e.printStackTrace() + return GENERIC_ERROR + } + return stringBuilder.toString() + } + + private fun parseArray(key: String, jsonObject: JSONObject): String { + val stringBuilder = StringBuilder() + val jsonArray = jsonObject.getJSONArray(key) + val responseArray = ArrayList() + val sep = "\n" + + for (i in 0 until jsonArray.length()) { + val response = jsonArray.getString(i) + responseArray.add(response) + } + + for (i in responseArray.indices) { + stringBuilder.append(responseArray[i]).append(sep) + } + + return stringBuilder.toString() + } + } +} \ No newline at end of file diff --git a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt index 093685f2..01021c1e 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt @@ -4,21 +4,14 @@ package com.khalti.android.v3 -import android.app.Activity import android.content.Context import android.content.Intent import android.util.Log import com.khalti.android.PaymentActivity -import com.khalti.android.api.ApiClient -import com.khalti.android.resource.Url -import com.khalti.android.servicce.VerificationService +import com.khalti.android.service.VerificationService import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response // Though kotlin provides named and optional parameters // method overloading was required for Java developers @@ -79,12 +72,15 @@ class Khalti private constructor( val verificationService = VerificationService(config) GlobalScope.launch { - try { - val result = verificationService.verify(config.pidx) - Log.i("Payment Result", result?.toString() ?: "") - } catch (e: Exception) { - Log.e("Payment Result Error", e.toString()) - } + val result = verificationService.verify(config.pidx) + result.match( + ok = { + Log.i("Payment Result", it.toString()) + }, + err = { + Log.e("Payment Result Error", it.toString()) + } + ) } } From 8e8d48abffb45bd6e7e951eea1fd1852f4389736 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 15:36:43 +0545 Subject: [PATCH 051/131] Mark fields in payment result as serializable --- .../com/khalti/android/v3/PaymentResult.kt | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/PaymentResult.kt b/khalti-android/src/main/java/com/khalti/android/v3/PaymentResult.kt index 403ff92a..53192ba7 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/PaymentResult.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/PaymentResult.kt @@ -4,9 +4,7 @@ package com.khalti.android.v3 -/// Status -/// statusCode [1] -> status [Payment Submitted] -/// statusCode [36] -> status [Cancelled] (Cancelled by user) +import com.google.gson.annotations.SerializedName data class PaymentResult( val status: String, @@ -15,7 +13,21 @@ data class PaymentResult( ) data class PaymentPayload( - val pidx: String, - val amount: Long, - val transactionId: String -) \ No newline at end of file + @SerializedName("pidx") val pidx: String?, + @SerializedName("total_amount") val totalAmount: Long = 0, + @SerializedName("status") val status: String?, + @SerializedName("transaction_id") val transactionId: String?, + @SerializedName("fee") val fee: Long = 0, + @SerializedName("refunded") val refunded: Boolean = false +) { + override fun toString(): String { + return StringBuilder() + .append("pidx: $pidx\n") + .append("totalAmount: $totalAmount\n") + .append("status: $status\n") + .append("transactionId: $transactionId\n") + .append("fee: $fee\n") + .append("refunded: $refunded\n") + .toString() + } +} \ No newline at end of file From 497ede0ccfe26285650d4bd9e7a2f5defcd5d208 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 15:37:14 +0545 Subject: [PATCH 052/131] Remove passing config as parameter when verifying --- khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt index 01021c1e..72abee13 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt @@ -69,7 +69,7 @@ class Khalti private constructor( @OptIn(DelicateCoroutinesApi::class) fun verify() { - val verificationService = VerificationService(config) + val verificationService = VerificationService() GlobalScope.launch { val result = verificationService.verify(config.pidx) From 062adfdc8d24c71ad1e6ac50cab9b39bb8992fa7 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 15:37:47 +0545 Subject: [PATCH 053/131] Simplify creation of ApiClient --- .../java/com/khalti/android/api/ApiClient.kt | 92 ++++++++----------- 1 file changed, 39 insertions(+), 53 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt index 047b6eed..77bb385d 100644 --- a/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt @@ -4,13 +4,16 @@ package com.khalti.android.api +import android.os.Build import com.khalti.android.resource.Err import com.khalti.android.resource.KFailure import com.khalti.android.resource.Ok import com.khalti.android.resource.Result import com.khalti.android.resource.Url import com.khalti.android.utils.ErrorUtil +import com.khalti.android.v3.CacheManager import com.khalti.android.v3.Environment +import com.khalti.android.v3.Khalti import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import okhttp3.OkHttpClient @@ -22,66 +25,49 @@ import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.net.SocketTimeoutException import java.net.UnknownHostException +import java.util.concurrent.TimeUnit -internal class RetrofitClient( - private val baseUrl: String, - private val publicKey: String, - private val packageName: String, - private val packageVersion: String, - private val moduleVersion: String -) { +class ApiClient { + companion object { + private const val TIME_OUT = 30L - val retrofit: Retrofit by lazy { - val interceptor = HttpLoggingInterceptor() - interceptor.level = HttpLoggingInterceptor.Level.BODY - - val client: OkHttpClient = OkHttpClient.Builder() - .addInterceptor(interceptor) - /*.addInterceptor { - val requestBuilder: Request.Builder = it.request().newBuilder() - requestBuilder.header("Authorization", "Key $publicKey") - requestBuilder.header("checkout-version", moduleVersion) - requestBuilder.header("checkout-platform", "android") - requestBuilder.header("checkout-os-version", Build.VERSION.RELEASE) - requestBuilder.header("checkout-device-model", Build.MODEL) - requestBuilder.header("checkout-device-manufacturer", Build.MANUFACTURER) - requestBuilder.header("merchant-package-name", packageName) - requestBuilder.header("merchant-package-version", packageVersion) - requestBuilder.method(it.request().method, it.request().body) + fun build(): ApiService { + val khalti = CacheManager.instance().get("khalti") + assert(khalti != null) { + "Khalti object has not been cached. There probably an issue in internal logic in the sdk. Please contact the developer" + } + val url = if (khalti!!.config.environment == Environment.PROD) { + Url.BASE_KHALTI_URL_PROD + } else { + Url.BASE_KHALTI_URL_STAGING + }.value - it.proceed(requestBuilder.build()) - }*/ - .build() + val loggingInterceptor = HttpLoggingInterceptor() + loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY - Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(GsonConverterFactory.create()) - .client(client) - .build() - } -} + val okHttpClient = OkHttpClient.Builder().readTimeout(TIME_OUT, TimeUnit.SECONDS) + .connectTimeout(TIME_OUT, TimeUnit.SECONDS).addInterceptor { + // TODO (Ishwor) Remove temp variables + val moduleVersion = "" + val packageName = "" + val packageVersion = "" + val request = it.request().newBuilder() + .addHeader("Authorization", "Key ${khalti.config.publicKey}") + .addHeader("checkout-version", moduleVersion) + .addHeader("checkout-platform", "android") + .addHeader("checkout-os-version", Build.VERSION.RELEASE) + .addHeader("checkout-device-model", Build.MODEL) + .addHeader("checkout-device-manufacturer", Build.MANUFACTURER) + .addHeader("merchant-package-name", packageName) + .addHeader("merchant-package-version", packageVersion).build() -class ApiClient(private val environment: Environment = Environment.PROD) { - private var apiService: ApiService? = null + it.proceed(request) + }.addInterceptor(loggingInterceptor).build() - fun build(publicKey: String): ApiService { - val baseUrl = if (environment == Environment.PROD) { - Url.BASE_KHALTI_URL_PROD - } else { - Url.BASE_KHALTI_URL_STAGING + return Retrofit.Builder().baseUrl(url) + .addConverterFactory(GsonConverterFactory.create()).client(okHttpClient).build() + .create(ApiService::class.java) } - - apiService = - apiService ?: RetrofitClient( - baseUrl.value, - publicKey, - "com.apple", - "1.0", - "3.00.00" - ).retrofit.create( - ApiService::class.java - ) - return apiService!! } } From f1bde6274bb46c0386e123f4225aed06536c2b9d Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 15:39:01 +0545 Subject: [PATCH 054/131] Remove hardcoded api headers; add return type --- .../src/main/java/com/khalti/android/api/ApiService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/api/ApiService.kt b/khalti-android/src/main/java/com/khalti/android/api/ApiService.kt index b8f84a81..03b7feb2 100644 --- a/khalti-android/src/main/java/com/khalti/android/api/ApiService.kt +++ b/khalti-android/src/main/java/com/khalti/android/api/ApiService.kt @@ -4,13 +4,13 @@ package com.khalti.android.api +import com.khalti.android.v3.PaymentPayload import retrofit2.Response import retrofit2.http.Body import retrofit2.http.Headers import retrofit2.http.POST interface ApiService { - @Headers("Authorization: Key live_secret_key_68791341fdd94846a146f0457ff7b455") @POST("epayment/lookup/") - suspend fun verify(@Body body: Map): Response + suspend fun verify(@Body body: Map): Response } \ No newline at end of file From e329b503e50109112b0b9bca564eaaf8219bf70d Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 15:39:41 +0545 Subject: [PATCH 055/131] Reformat --- .../src/main/java/com/khalti/android/utils/JsonUtil.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/khalti-android/src/main/java/com/khalti/android/utils/JsonUtil.kt b/khalti-android/src/main/java/com/khalti/android/utils/JsonUtil.kt index 79b0a932..ba9f4f28 100644 --- a/khalti-android/src/main/java/com/khalti/android/utils/JsonUtil.kt +++ b/khalti-android/src/main/java/com/khalti/android/utils/JsonUtil.kt @@ -13,7 +13,9 @@ import org.json.JSONObject class JsonUtil { companion object { fun convertToJsonString(o: Any): String { - val gson = GsonBuilder().serializeNulls().create() + val gson = GsonBuilder() + .serializeNulls() + .create() return gson.toJson(o) } From 4ed1d451f8548f71f6df6e4ec3ef2142c7d8bc98 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 15:40:31 +0545 Subject: [PATCH 056/131] Add strong return type --- .../com/khalti/android/demo/composable/DemoScreen.kt | 2 +- .../com/khalti/android/service/VerificationService.kt | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt index fe756de2..ec8c689b 100644 --- a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt +++ b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt @@ -155,7 +155,7 @@ fun DemoScreenV3() { val khalti = Khalti.init( LocalContext.current, KhaltiPayConfig( - "b476aa4b21864b54ab96e430c9192be1", + "live_secret_key_68791341fdd94846a146f0457ff7b455", "guuVD2quUhbreFfzfwrhw6", Uri.parse("https://khalti.com"), environment = Environment.TEST diff --git a/khalti-android/src/main/java/com/khalti/android/service/VerificationService.kt b/khalti-android/src/main/java/com/khalti/android/service/VerificationService.kt index 07a18511..880eb3b5 100644 --- a/khalti-android/src/main/java/com/khalti/android/service/VerificationService.kt +++ b/khalti-android/src/main/java/com/khalti/android/service/VerificationService.kt @@ -10,15 +10,16 @@ import com.khalti.android.api.safeApiCall import com.khalti.android.resource.KFailure import com.khalti.android.resource.Result import com.khalti.android.v3.KhaltiPayConfig +import com.khalti.android.v3.PaymentPayload import kotlinx.coroutines.Dispatchers -class VerificationService(config: KhaltiPayConfig) { +class VerificationService { private val apiService: ApiService by lazy { - ApiClient(config.environment).build(config.publicKey) + ApiClient.build() } - suspend fun verify(pidx: String): Result { - return safeApiCall(Dispatchers.IO) { + suspend fun verify(pidx: String): Result { + return safeApiCall(Dispatchers.IO) { apiService.verify(mapOf("pidx" to pidx)) } } From 97bf558edafb1f0260bb2892b96d8289a1e76a68 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 17:09:44 +0545 Subject: [PATCH 057/131] Add proper callback invocations --- .../main/java/com/khalti/android/v3/Khalti.kt | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt index 72abee13..fcea04dd 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt @@ -8,6 +8,7 @@ import android.content.Context import android.content.Intent import android.util.Log import com.khalti.android.PaymentActivity +import com.khalti.android.resource.KFailure import com.khalti.android.service.VerificationService import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope @@ -75,10 +76,31 @@ class Khalti private constructor( val result = verificationService.verify(config.pidx) result.match( ok = { - Log.i("Payment Result", it.toString()) + onPaymentResult.invoke( + PaymentResult( + status = it.status ?: "Payment successful", + payload = it + ) + ) }, err = { - Log.e("Payment Result Error", it.toString()) + when (it) { + is KFailure.NoNetwork, is KFailure.ServerUnreachable, is KFailure.Generic -> + onMessage.invoke( + it.message ?: "", it.cause, null + ) + + is KFailure.HttpCall -> onMessage.invoke( + it.message ?: "", it.cause, it.code + ) + + is KFailure.Payment -> onPaymentResult.invoke( + PaymentResult( + status = "Payment failed", + message = it.message ?: "", + ) + ) + } } ) } From bb3ed255a9080f461599aa4b0e91d21d43a348d8 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 17:10:12 +0545 Subject: [PATCH 058/131] Add throwable and code as callback parameters --- khalti-android/src/main/java/com/khalti/android/v3/OnMessage.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/OnMessage.kt b/khalti-android/src/main/java/com/khalti/android/v3/OnMessage.kt index 09334450..da38ff14 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/OnMessage.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/OnMessage.kt @@ -5,5 +5,5 @@ package com.khalti.android.v3 fun interface OnMessage { - fun invoke(message: String) + fun invoke(message: String, throwable: Throwable?, code: Number?) } \ No newline at end of file From 2e8325723994c111fa933f906f1b748d102c450e Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 17:10:34 +0545 Subject: [PATCH 059/131] Add default null value --- .../src/main/java/com/khalti/android/v3/PaymentResult.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/PaymentResult.kt b/khalti-android/src/main/java/com/khalti/android/v3/PaymentResult.kt index 53192ba7..f3833ed8 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/PaymentResult.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/PaymentResult.kt @@ -8,8 +8,8 @@ import com.google.gson.annotations.SerializedName data class PaymentResult( val status: String, - val payload: PaymentPayload?, - val message: String? + val payload: PaymentPayload? = null, + val message: String? = null ) data class PaymentPayload( From a5dd3416dd9fd3704ee82c0f535411dd127fafe6 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 17:11:14 +0545 Subject: [PATCH 060/131] Remove nullability --- .../java/com/khalti/android/resource/KFailure.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/resource/KFailure.kt b/khalti-android/src/main/java/com/khalti/android/resource/KFailure.kt index c5c35e36..c3837b7d 100644 --- a/khalti-android/src/main/java/com/khalti/android/resource/KFailure.kt +++ b/khalti-android/src/main/java/com/khalti/android/resource/KFailure.kt @@ -4,18 +4,18 @@ package com.khalti.android.resource -abstract class KFailure(message: String?, cause: Throwable?, val code: Number? = null) : - Exception(message, cause) { +abstract class KFailure(failureMessage: String, throwable: Throwable? = null, val code: Number? = null) : + Exception(failureMessage, throwable) { - class NoNetwork(message: String? = null, cause: Throwable? = null) : KFailure(message, cause) + class NoNetwork(message: String, cause: Throwable? = null) : KFailure(message, cause) - class ServerUnreachable(message: String? = null, cause: Throwable? = null) : + class ServerUnreachable(message: String, cause: Throwable? = null) : KFailure(message, cause) - class HttpCall(message: String? = null, cause: Throwable? = null, code: Number?) : + class HttpCall(message: String, cause: Throwable? = null, code: Number?) : KFailure(message, cause, code) - class Payment(message: String? = null, cause: Throwable? = null) : KFailure(message, cause) + class Payment(message: String, cause: Throwable? = null) : KFailure(message, cause) - class Generic(message: String? = null, cause: Throwable? = null) : KFailure(message, cause) + class Generic(message: String, cause: Throwable? = null) : KFailure(message, cause) } \ No newline at end of file From c52d4dc45c308d36a2c44d1222568f993e45cc8d Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 17:12:19 +0545 Subject: [PATCH 061/131] Add default value to message --- .../main/java/com/khalti/android/api/ApiClient.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt index 77bb385d..6d4346f6 100644 --- a/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt @@ -96,16 +96,20 @@ suspend fun safeApiCall( ErrorUtil.parseThrowableError(t.message, "600") ) val failure: KFailure = when (t) { - is UnknownHostException -> KFailure.ServerUnreachable(t.message, processedThrowable) - is SocketTimeoutException -> KFailure.NoNetwork(t.message, processedThrowable) - is IOException -> KFailure.NoNetwork(t.message, processedThrowable) + is UnknownHostException -> KFailure.ServerUnreachable( + t.message ?: "", + processedThrowable + ) + + is SocketTimeoutException -> KFailure.NoNetwork(t.message ?: "", processedThrowable) + is IOException -> KFailure.NoNetwork(t.message ?: "", processedThrowable) is HttpException -> { val code = t.code() - KFailure.HttpCall(t.message, processedThrowable, code) + KFailure.HttpCall(t.message ?: "", processedThrowable, code) } - else -> KFailure.Generic(t.message, processedThrowable) + else -> KFailure.Generic(t.message ?: "", processedThrowable) } return@withContext Err(failure) } From 92e684fa5e345f1b7ecd1b99a3be2d1f975bda46 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 17:12:46 +0545 Subject: [PATCH 062/131] Reflect changes to OnMessage --- .../src/main/java/com/khalti/android/EPaymentWebClient.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt index 324904c8..41156518 100644 --- a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt @@ -51,11 +51,8 @@ internal class EPaymentWebClient : WebViewClient() { } private fun handleUri(uri: Uri): Boolean { - val url = uri.toString() - - Log.i("Url", url) - // TODO (Ishwor) Handle redirection to Khalti app for setting MPIN + val url = uri.toString() // MPIN url : /account/transaction_pin return false } @@ -63,7 +60,7 @@ internal class EPaymentWebClient : WebViewClient() { private fun handleError(description: String?) { val khalti = CacheManager.instance().get("khalti") if (description != null) { - khalti?.onMessage?.invoke(description) + khalti?.onMessage?.invoke(description, null, null) } } } From 7c447bb0991d45a06f5c4a06f79d4cb4ffda8a8d Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 17:13:10 +0545 Subject: [PATCH 063/131] Add proper logs to callbacks --- .../com/khalti/android/demo/composable/DemoScreen.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt index ec8c689b..53f56a08 100644 --- a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt +++ b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt @@ -38,6 +38,7 @@ import com.khalti.android.demo.R import com.khalti.android.v3.Environment import com.khalti.android.v3.Khalti import com.khalti.android.v3.KhaltiPayConfig +import com.khalti.android.v3.OnMessage const val RESULT_TAG = "KHALTI_PAY_RESULT" @@ -160,10 +161,14 @@ fun DemoScreenV3() { Uri.parse("https://khalti.com"), environment = Environment.TEST ), - {}, - {}, + { + Log.i("Demo | onPaymentResult", it.toString()) + }, + OnMessage { message: String, _, _ -> + Log.i("Demo | onMessage", message) + }, onReturn = { - Log.i("Demo", "OnReturn") + Log.i("Demo | onReturn", "OnReturn") } ) From dbadc201799476a2ee13b082e7d4e3ade54adae9 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 18:27:21 +0545 Subject: [PATCH 064/131] Add verification repo --- .../android/service/VerificationRepository.kt | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt diff --git a/khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt b/khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt new file mode 100644 index 00000000..e67b2fd6 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.service + +import com.khalti.android.resource.KFailure +import com.khalti.android.v3.Khalti +import com.khalti.android.v3.PaymentResult +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class VerificationRepository { + private val verificationService: VerificationService by lazy { + VerificationService() + } + + @OptIn(DelicateCoroutinesApi::class) + fun verify(pidx: String, khalti: Khalti, onComplete: (() -> Unit)? = null) { + GlobalScope.launch { + val result = verificationService.verify(pidx) + onComplete?.invoke() + result.match( + ok = { + khalti.onPaymentResult.invoke( + PaymentResult( + status = it.status ?: "Payment successful", + payload = it + ) + ) + }, + err = { + when (it) { + is KFailure.NoNetwork, is KFailure.ServerUnreachable, is KFailure.Generic -> + khalti.onMessage.invoke( + it.message ?: "", it.cause, null + ) + + is KFailure.HttpCall -> khalti.onMessage.invoke( + it.message ?: "", it.cause, it.code + ) + + is KFailure.Payment -> khalti.onPaymentResult.invoke( + PaymentResult( + status = "Payment failed", + message = it.message ?: "", + ) + ) + } + } + ) + } + } +} \ No newline at end of file From fd65d5cd69de70fcafe1b189cc0a3abed9572b68 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 18:27:51 +0545 Subject: [PATCH 065/131] Rename CacheManager to Store --- .../com/khalti/android/v3/{CacheManager.kt => Store.kt} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename khalti-android/src/main/java/com/khalti/android/v3/{CacheManager.kt => Store.kt} (69%) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/CacheManager.kt b/khalti-android/src/main/java/com/khalti/android/v3/Store.kt similarity index 69% rename from khalti-android/src/main/java/com/khalti/android/v3/CacheManager.kt rename to khalti-android/src/main/java/com/khalti/android/v3/Store.kt index 9cf2602b..1c5f2fd9 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/CacheManager.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/Store.kt @@ -5,15 +5,15 @@ package com.khalti.android.v3 @Suppress("UNCHECKED_CAST") -class CacheManager private constructor() { +class Store private constructor() { companion object { @Volatile - private var instance: CacheManager? = null + private var instance: Store? = null - fun instance(): CacheManager { + fun instance(): Store { return instance ?: synchronized(this) { - instance ?: CacheManager().also { instance = it } + instance ?: Store().also { instance = it } } } } From c36834754b27d924c29ffe5b38410129012d22f0 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 18:28:27 +0545 Subject: [PATCH 066/131] Remove callback invocations --- .../main/java/com/khalti/android/v3/Khalti.kt | 48 ++----------------- 1 file changed, 5 insertions(+), 43 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt index fcea04dd..8f8b6dfc 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt @@ -6,13 +6,8 @@ package com.khalti.android.v3 import android.content.Context import android.content.Intent -import android.util.Log import com.khalti.android.PaymentActivity -import com.khalti.android.resource.KFailure -import com.khalti.android.service.VerificationService -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import com.khalti.android.service.VerificationRepository // Though kotlin provides named and optional parameters // method overloading was required for Java developers @@ -39,7 +34,7 @@ class Khalti private constructor( onReturn, ) - CacheManager.instance().put("khalti", khalti) + Store.instance().put("khalti", khalti) return khalti } @@ -57,7 +52,7 @@ class Khalti private constructor( null, ) - CacheManager.instance().put("khalti", khalti) + Store.instance().put("khalti", khalti) return khalti } @@ -68,42 +63,9 @@ class Khalti private constructor( context.startActivity(intent) } - @OptIn(DelicateCoroutinesApi::class) fun verify() { - val verificationService = VerificationService() - - GlobalScope.launch { - val result = verificationService.verify(config.pidx) - result.match( - ok = { - onPaymentResult.invoke( - PaymentResult( - status = it.status ?: "Payment successful", - payload = it - ) - ) - }, - err = { - when (it) { - is KFailure.NoNetwork, is KFailure.ServerUnreachable, is KFailure.Generic -> - onMessage.invoke( - it.message ?: "", it.cause, null - ) - - is KFailure.HttpCall -> onMessage.invoke( - it.message ?: "", it.cause, it.code - ) - - is KFailure.Payment -> onPaymentResult.invoke( - PaymentResult( - status = "Payment failed", - message = it.message ?: "", - ) - ) - } - } - ) - } + val verificationRepo = VerificationRepository() + verificationRepo.verify(config.pidx, this) } fun close() { From c2bd6ede29813147c7c7a3b1ad0ab1c7fa4de62d Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 18:28:45 +0545 Subject: [PATCH 067/131] Cleanup import --- .../main/java/com/khalti/android/service/VerificationService.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/khalti-android/src/main/java/com/khalti/android/service/VerificationService.kt b/khalti-android/src/main/java/com/khalti/android/service/VerificationService.kt index 880eb3b5..90c0af6f 100644 --- a/khalti-android/src/main/java/com/khalti/android/service/VerificationService.kt +++ b/khalti-android/src/main/java/com/khalti/android/service/VerificationService.kt @@ -9,7 +9,6 @@ import com.khalti.android.api.ApiService import com.khalti.android.api.safeApiCall import com.khalti.android.resource.KFailure import com.khalti.android.resource.Result -import com.khalti.android.v3.KhaltiPayConfig import com.khalti.android.v3.PaymentPayload import kotlinx.coroutines.Dispatchers From 861e79b90ec8a1eb8741271813f68db59bc90a23 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 18:29:13 +0545 Subject: [PATCH 068/131] Reflect Store rename --- .../src/main/java/com/khalti/android/api/ApiClient.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt index 6d4346f6..8d69fae2 100644 --- a/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt @@ -11,7 +11,7 @@ import com.khalti.android.resource.Ok import com.khalti.android.resource.Result import com.khalti.android.resource.Url import com.khalti.android.utils.ErrorUtil -import com.khalti.android.v3.CacheManager +import com.khalti.android.v3.Store import com.khalti.android.v3.Environment import com.khalti.android.v3.Khalti import kotlinx.coroutines.CoroutineDispatcher @@ -32,7 +32,7 @@ class ApiClient { private const val TIME_OUT = 30L fun build(): ApiService { - val khalti = CacheManager.instance().get("khalti") + val khalti = Store.instance().get("khalti") assert(khalti != null) { "Khalti object has not been cached. There probably an issue in internal logic in the sdk. Please contact the developer" } From d0373cb07288ef0fad56d071f589240efed1f7ab Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 29 Feb 2024 18:29:39 +0545 Subject: [PATCH 069/131] Show progress bar during status lookup --- .../android/demo/composable/DemoScreen.kt | 6 ++-- .../com/khalti/android/EPaymentWebClient.kt | 13 ++++----- .../com/khalti/android/PaymentActivity.kt | 29 ++++++++++++------- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt index 53f56a08..5d02da8c 100644 --- a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt +++ b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt @@ -157,7 +157,7 @@ fun DemoScreenV3() { LocalContext.current, KhaltiPayConfig( "live_secret_key_68791341fdd94846a146f0457ff7b455", - "guuVD2quUhbreFfzfwrhw6", + "nnw9DsPP5beopJxvW7ALRn", Uri.parse("https://khalti.com"), environment = Environment.TEST ), @@ -194,8 +194,8 @@ fun DemoScreenV3() { ) FilledTonalButton( { -// khalti.open() - khalti.verify() + khalti.open() +// khalti.verify() } ) { Text("Pay with Khalti") diff --git a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt index 41156518..4f141c3f 100644 --- a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt @@ -4,13 +4,13 @@ package com.khalti.android import android.net.Uri import android.os.Build -import android.util.Log import android.webkit.* import androidx.annotation.RequiresApi -import com.khalti.android.v3.CacheManager +import com.khalti.android.service.VerificationRepository +import com.khalti.android.v3.Store import com.khalti.android.v3.Khalti -internal class EPaymentWebClient : WebViewClient() { +internal class EPaymentWebClient(val onReturn: () -> Unit) : WebViewClient() { @RequiresApi(Build.VERSION_CODES.LOLLIPOP) override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): @@ -40,14 +40,13 @@ internal class EPaymentWebClient : WebViewClient() { override fun onPageFinished(view: WebView?, url: String?) { super.onPageFinished(view, url) - val khalti = CacheManager.instance().get("khalti") + val khalti = Store.instance().get("khalti") val returnUrl = khalti?.config?.returnUrl?.toString() ?: "" if (url?.startsWith(returnUrl) != false) { khalti?.onReturn?.invoke() + onReturn() } - - khalti?.verify() } private fun handleUri(uri: Uri): Boolean { @@ -58,7 +57,7 @@ internal class EPaymentWebClient : WebViewClient() { } private fun handleError(description: String?) { - val khalti = CacheManager.instance().get("khalti") + val khalti = Store.instance().get("khalti") if (description != null) { khalti?.onMessage?.invoke(description, null, null) } diff --git a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt index 304a3bb2..9defdfeb 100644 --- a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt +++ b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt @@ -20,8 +20,8 @@ import android.widget.ProgressBar import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.MaterialToolbar import com.khalti.android.resource.Url -import com.khalti.android.v3.CacheManager -import com.khalti.android.v3.Environment +import com.khalti.android.service.VerificationRepository +import com.khalti.android.v3.Store import com.khalti.android.v3.Khalti internal class PaymentActivity : Activity() { @@ -54,17 +54,24 @@ internal class PaymentActivity : Activity() { webSettings.javaScriptEnabled = true webSettings.domStorageEnabled = true - webView.webViewClient = EPaymentWebClient() - webView.webChromeClient = object : WebChromeClient() { - override fun onProgressChanged(view: WebView?, newProgress: Int) { - progressBar.visibility = - if (newProgress == 100) ProgressBar.GONE else ProgressBar.VISIBLE - } - } - - val khalti = CacheManager.instance().get("khalti") + val khalti = Store.instance().get("khalti") if (khalti != null) { val config = khalti.config + + webView.webViewClient = EPaymentWebClient { + val verificationRepo = VerificationRepository() + progressBar.visibility = ProgressBar.VISIBLE + verificationRepo.verify(config.pidx, khalti) { + progressBar.visibility = ProgressBar.GONE + } + } + webView.webChromeClient = object : WebChromeClient() { + override fun onProgressChanged(view: WebView?, newProgress: Int) { + progressBar.visibility = + if (newProgress == 100) ProgressBar.GONE else ProgressBar.VISIBLE + } + } + val baseUrl = if (config.isProd()) { Url.BASE_PAYMENT_URL_PROD } else { From ba037a4f08d6f6971ac26c5df5eefe497092a310 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 1 Mar 2024 11:54:35 +0545 Subject: [PATCH 070/131] Add khalti object as callback parameters --- khalti-android/src/main/java/com/khalti/android/v3/OnMessage.kt | 2 +- .../src/main/java/com/khalti/android/v3/OnPaymentResult.kt | 2 +- khalti-android/src/main/java/com/khalti/android/v3/OnReturn.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/OnMessage.kt b/khalti-android/src/main/java/com/khalti/android/v3/OnMessage.kt index da38ff14..566adb46 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/OnMessage.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/OnMessage.kt @@ -5,5 +5,5 @@ package com.khalti.android.v3 fun interface OnMessage { - fun invoke(message: String, throwable: Throwable?, code: Number?) + fun invoke(message: String, khalti: Khalti, throwable: Throwable?, code: Number?) } \ No newline at end of file diff --git a/khalti-android/src/main/java/com/khalti/android/v3/OnPaymentResult.kt b/khalti-android/src/main/java/com/khalti/android/v3/OnPaymentResult.kt index 20642f75..ea8efb0a 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/OnPaymentResult.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/OnPaymentResult.kt @@ -5,5 +5,5 @@ package com.khalti.android.v3 fun interface OnPaymentResult { - fun invoke(result: PaymentResult) + fun invoke(result: PaymentResult, khalti: Khalti) } \ No newline at end of file diff --git a/khalti-android/src/main/java/com/khalti/android/v3/OnReturn.kt b/khalti-android/src/main/java/com/khalti/android/v3/OnReturn.kt index 1f764b41..8255a46b 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/OnReturn.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/OnReturn.kt @@ -5,5 +5,5 @@ package com.khalti.android.v3 fun interface OnReturn { - fun invoke() + fun invoke(khalti: Khalti) } \ No newline at end of file From d604d5d55be5d04443b91a9ec13074382aed41b4 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 1 Mar 2024 11:55:27 +0545 Subject: [PATCH 071/131] Pass khalti object through callbacks --- .../android/service/VerificationRepository.kt | 45 +++++++++---------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt b/khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt index e67b2fd6..bf550bfb 100644 --- a/khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt +++ b/khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt @@ -22,35 +22,30 @@ class VerificationRepository { GlobalScope.launch { val result = verificationService.verify(pidx) onComplete?.invoke() - result.match( - ok = { - khalti.onPaymentResult.invoke( - PaymentResult( - status = it.status ?: "Payment successful", - payload = it - ) + result.match(ok = { + khalti.onPaymentResult.invoke( + PaymentResult( + status = it.status ?: "Payment successful", payload = it + ), khalti + ) + }, err = { + when (it) { + is KFailure.NoNetwork, is KFailure.ServerUnreachable, is KFailure.Generic -> khalti.onMessage.invoke( + it.message ?: "", khalti, it.cause, null ) - }, - err = { - when (it) { - is KFailure.NoNetwork, is KFailure.ServerUnreachable, is KFailure.Generic -> - khalti.onMessage.invoke( - it.message ?: "", it.cause, null - ) - is KFailure.HttpCall -> khalti.onMessage.invoke( - it.message ?: "", it.cause, it.code - ) + is KFailure.HttpCall -> khalti.onMessage.invoke( + it.message ?: "", khalti, it.cause, it.code + ) - is KFailure.Payment -> khalti.onPaymentResult.invoke( - PaymentResult( - status = "Payment failed", - message = it.message ?: "", - ) - ) - } + is KFailure.Payment -> khalti.onPaymentResult.invoke( + PaymentResult( + status = "Payment failed", + message = it.message ?: "", + ), khalti + ) } - ) + }) } } } \ No newline at end of file From 895d3c5450af0e1626e7c2f6a9b7fff1b5b5bb37 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 1 Mar 2024 11:55:54 +0545 Subject: [PATCH 072/131] Use named parameters --- .../src/main/java/com/khalti/android/api/ApiClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt index 8d69fae2..519d9b3b 100644 --- a/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt @@ -82,7 +82,7 @@ suspend fun safeApiCall( } return@withContext Err( KFailure.Payment( - "Error", Throwable( + message = "Error", cause = Throwable( ErrorUtil.parseError( if (response.errorBody() != null) String( response.errorBody()!!.bytes() From 476454829631cd4d8974a1d68bd6aae53d28bfa6 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 1 Mar 2024 11:57:22 +0545 Subject: [PATCH 073/131] Call ui functions in ui thread --- .../src/main/java/com/khalti/android/PaymentActivity.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt index 9defdfeb..361eacfb 100644 --- a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt +++ b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt @@ -62,7 +62,9 @@ internal class PaymentActivity : Activity() { val verificationRepo = VerificationRepository() progressBar.visibility = ProgressBar.VISIBLE verificationRepo.verify(config.pidx, khalti) { - progressBar.visibility = ProgressBar.GONE + runOnUiThread { + progressBar.visibility = ProgressBar.GONE + } } } webView.webChromeClient = object : WebChromeClient() { @@ -85,6 +87,7 @@ internal class PaymentActivity : Activity() { Log.i("Payment Uri", paymentUri.toString()) + webView.clearCache(true) webView.loadUrl(paymentUri.toString()) appBar.addView(toolbar) From ae6b2503bbac7ead887411e24bef96427dc64362 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 1 Mar 2024 11:57:41 +0545 Subject: [PATCH 074/131] Pass khalti object through callbacks --- .../src/main/java/com/khalti/android/EPaymentWebClient.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt index 4f141c3f..f56ae4b5 100644 --- a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt @@ -44,7 +44,7 @@ internal class EPaymentWebClient(val onReturn: () -> Unit) : WebViewClient() { val returnUrl = khalti?.config?.returnUrl?.toString() ?: "" if (url?.startsWith(returnUrl) != false) { - khalti?.onReturn?.invoke() + khalti?.onReturn?.invoke(khalti) onReturn() } } @@ -59,7 +59,7 @@ internal class EPaymentWebClient(val onReturn: () -> Unit) : WebViewClient() { private fun handleError(description: String?) { val khalti = Store.instance().get("khalti") if (description != null) { - khalti?.onMessage?.invoke(description, null, null) + khalti?.onMessage?.invoke(description, khalti, null, null) } } } From d3d469c75aa041001216363c36dbbed2125a6bc5 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 1 Mar 2024 11:58:13 +0545 Subject: [PATCH 075/131] Use named parameters for callbacks to reduce ambiguity --- .../android/demo/composable/DemoScreen.kt | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt index 5d02da8c..fbec6d61 100644 --- a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt +++ b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt @@ -38,7 +38,6 @@ import com.khalti.android.demo.R import com.khalti.android.v3.Environment import com.khalti.android.v3.Khalti import com.khalti.android.v3.KhaltiPayConfig -import com.khalti.android.v3.OnMessage const val RESULT_TAG = "KHALTI_PAY_RESULT" @@ -157,17 +156,20 @@ fun DemoScreenV3() { LocalContext.current, KhaltiPayConfig( "live_secret_key_68791341fdd94846a146f0457ff7b455", - "nnw9DsPP5beopJxvW7ALRn", - Uri.parse("https://khalti.com"), + "4MNRZPhuY8ZvvyRyXqG2fF", + Uri.parse("https://webhook.site/ed508278-3ce3-4f6d-98f1-0b6084c5c5cd"), environment = Environment.TEST ), - { - Log.i("Demo | onPaymentResult", it.toString()) + onPaymentResult = { paymentResult, khalti -> + Log.i("Demo | onPaymentResult", paymentResult.toString()) + khalti.close() }, - OnMessage { message: String, _, _ -> - Log.i("Demo | onMessage", message) + onMessage = { message, khalti, throwable, code -> + Log.i("Demo | onMessage ${if (code != null) "($code)" else ""}", message) + khalti.close() + throwable?.printStackTrace() }, - onReturn = { + onReturn = { _ -> Log.i("Demo | onReturn", "OnReturn") } ) From 78a9bbc341c5abbd1a4e483d0a04772c1b42b092 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 1 Mar 2024 14:23:24 +0545 Subject: [PATCH 076/131] Add merchange package name and package version name to store --- .../src/main/java/com/khalti/android/v3/Khalti.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt index 8f8b6dfc..7029f426 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt @@ -6,6 +6,8 @@ package com.khalti.android.v3 import android.content.Context import android.content.Intent +import android.content.pm.PackageManager.NameNotFoundException +import android.util.Log import com.khalti.android.PaymentActivity import com.khalti.android.service.VerificationRepository @@ -59,6 +61,17 @@ class Khalti private constructor( } fun open() { + val packageName = context.packageName + val store = Store.instance() + store.put("merchant_package_name", packageName) + + try { + val packageInfo = context.packageManager.getPackageInfo(packageName, 0) + store.put("merchant_package_version", packageInfo.versionName) + } catch (e: NameNotFoundException) { + //no-op + } + val intent = Intent(context, PaymentActivity::class.java) context.startActivity(intent) } From 0a756096b5056d387f350a57273838d093b73a0d Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 1 Mar 2024 14:23:52 +0545 Subject: [PATCH 077/131] Remove log and todo --- .../src/main/java/com/khalti/android/PaymentActivity.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt index 361eacfb..777d0e59 100644 --- a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt +++ b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt @@ -21,8 +21,8 @@ import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.MaterialToolbar import com.khalti.android.resource.Url import com.khalti.android.service.VerificationRepository -import com.khalti.android.v3.Store import com.khalti.android.v3.Khalti +import com.khalti.android.v3.Store internal class PaymentActivity : Activity() { private var receiver: BroadcastReceiver? = null @@ -85,8 +85,6 @@ internal class PaymentActivity : Activity() { .buildUpon() .appendQueryParameter("pidx", config.pidx) - Log.i("Payment Uri", paymentUri.toString()) - webView.clearCache(true) webView.loadUrl(paymentUri.toString()) @@ -108,7 +106,6 @@ internal class PaymentActivity : Activity() { @SuppressLint("UnspecifiedRegisterReceiverFlag") private fun registerBroadcast() { - // TODO (Ishwor) Remove hardcoded receiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent != null && intent.action.equals("close_khalti_payment_portal")) { From e0b969838742260d3bc6a4afbe16e98986ba2fa0 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 1 Mar 2024 14:24:18 +0545 Subject: [PATCH 078/131] Use merchant's package name and package version name from store --- .../src/main/java/com/khalti/android/api/ApiClient.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt index 519d9b3b..a5c2d7ba 100644 --- a/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt @@ -5,6 +5,7 @@ package com.khalti.android.api import android.os.Build +import com.khalti.android.BuildConfig import com.khalti.android.resource.Err import com.khalti.android.resource.KFailure import com.khalti.android.resource.Ok @@ -47,13 +48,12 @@ class ApiClient { val okHttpClient = OkHttpClient.Builder().readTimeout(TIME_OUT, TimeUnit.SECONDS) .connectTimeout(TIME_OUT, TimeUnit.SECONDS).addInterceptor { - // TODO (Ishwor) Remove temp variables - val moduleVersion = "" - val packageName = "" - val packageVersion = "" + val store = Store.instance() + val packageName = store.get("merchant_package_name") ?: "" + val packageVersion = store.get("merchant_package_version") ?: "" val request = it.request().newBuilder() .addHeader("Authorization", "Key ${khalti.config.publicKey}") - .addHeader("checkout-version", moduleVersion) + .addHeader("checkout-version", BuildConfig.VERSION_NAME) .addHeader("checkout-platform", "android") .addHeader("checkout-os-version", Build.VERSION.RELEASE) .addHeader("checkout-device-model", Build.MODEL) From 3900824c6b4d7d01fd4db0802ef516a1f31c1305 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 1 Mar 2024 14:24:39 +0545 Subject: [PATCH 079/131] Generate BuildConfig for khalti-android --- khalti-android/build.gradle | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/khalti-android/build.gradle b/khalti-android/build.gradle index 9d03025f..ec9c614d 100644 --- a/khalti-android/build.gradle +++ b/khalti-android/build.gradle @@ -8,18 +8,27 @@ android { namespace 'com.khalti.android' compileSdk libraryCompileSdk + buildFeatures { + buildConfig true + } + defaultConfig { minSdk libraryMinSdk targetSdk libraryTargetSdk + versionName khaltiVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" } buildTypes { + debug { + buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}\"") + } release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}\"") } } compileOptions { @@ -41,7 +50,7 @@ dependencies { implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.squareup.okhttp3:logging-interceptor:4.11.0' - + testImplementation "junit:junit:$junit_version" androidTestImplementation "androidx.test.ext:junit:$junit_ext_version" From 9357e55ff20a9cc088750d131e1bd13bf435baae Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 1 Mar 2024 15:09:03 +0545 Subject: [PATCH 080/131] Add http status code to KFailure.Payment --- .../src/main/java/com/khalti/android/api/ApiClient.kt | 2 +- .../src/main/java/com/khalti/android/resource/KFailure.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt index a5c2d7ba..6fe0a2cb 100644 --- a/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt @@ -88,7 +88,7 @@ suspend fun safeApiCall( response.errorBody()!!.bytes() ) else "", response.code().toString() ) - ) + ), code = response.code() ) ) } catch (t: Throwable) { diff --git a/khalti-android/src/main/java/com/khalti/android/resource/KFailure.kt b/khalti-android/src/main/java/com/khalti/android/resource/KFailure.kt index c3837b7d..ad29df5a 100644 --- a/khalti-android/src/main/java/com/khalti/android/resource/KFailure.kt +++ b/khalti-android/src/main/java/com/khalti/android/resource/KFailure.kt @@ -15,7 +15,7 @@ abstract class KFailure(failureMessage: String, throwable: Throwable? = null, va class HttpCall(message: String, cause: Throwable? = null, code: Number?) : KFailure(message, cause, code) - class Payment(message: String, cause: Throwable? = null) : KFailure(message, cause) + class Payment(message: String, cause: Throwable? = null, code: Number?) : KFailure(message, cause, code) class Generic(message: String, cause: Throwable? = null) : KFailure(message, cause) } \ No newline at end of file From 3d45cdc38bd637e5ff10d677167a10464e8cfac6 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 1 Mar 2024 15:09:25 +0545 Subject: [PATCH 081/131] Route all failure cases to onMessage --- .../android/service/VerificationRepository.kt | 34 ++++++------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt b/khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt index bf550bfb..dfb0d0a7 100644 --- a/khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt +++ b/khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt @@ -4,12 +4,10 @@ package com.khalti.android.service -import com.khalti.android.resource.KFailure import com.khalti.android.v3.Khalti import com.khalti.android.v3.PaymentResult import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay import kotlinx.coroutines.launch class VerificationRepository { @@ -22,30 +20,20 @@ class VerificationRepository { GlobalScope.launch { val result = verificationService.verify(pidx) onComplete?.invoke() - result.match(ok = { - khalti.onPaymentResult.invoke( - PaymentResult( - status = it.status ?: "Payment successful", payload = it - ), khalti - ) - }, err = { - when (it) { - is KFailure.NoNetwork, is KFailure.ServerUnreachable, is KFailure.Generic -> khalti.onMessage.invoke( - it.message ?: "", khalti, it.cause, null - ) - - is KFailure.HttpCall -> khalti.onMessage.invoke( - it.message ?: "", khalti, it.cause, it.code - ) - - is KFailure.Payment -> khalti.onPaymentResult.invoke( + result.match( + ok = { + khalti.onPaymentResult.invoke( PaymentResult( - status = "Payment failed", - message = it.message ?: "", + status = it.status ?: "Payment successful", payload = it ), khalti ) - } - }) + }, + err = { + khalti.onMessage.invoke( + it.message ?: "", khalti, it.cause, it.code + ) + }, + ) } } } \ No newline at end of file From be24a8ae6f4eea5adce0c6d60518c2ba13fb1b7f Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 1 Mar 2024 15:12:56 +0545 Subject: [PATCH 082/131] Remove old files --- .../com/khalti/android/demo/MainActivity.kt | 4 +- .../android/demo/composable/DemoScreen.kt | 123 ------------------ .../khalti/android/KhaltiPayConfiguration.kt | 41 ------ .../java/com/khalti/android/OpenKhaltiPay.kt | 57 -------- .../java/com/khalti/android/PaymentResult.kt | 20 --- 5 files changed, 2 insertions(+), 243 deletions(-) delete mode 100644 khalti-android/src/main/java/com/khalti/android/KhaltiPayConfiguration.kt delete mode 100644 khalti-android/src/main/java/com/khalti/android/OpenKhaltiPay.kt delete mode 100644 khalti-android/src/main/java/com/khalti/android/PaymentResult.kt diff --git a/app/src/main/java/com/khalti/android/demo/MainActivity.kt b/app/src/main/java/com/khalti/android/demo/MainActivity.kt index cabcf3a1..082a919f 100644 --- a/app/src/main/java/com/khalti/android/demo/MainActivity.kt +++ b/app/src/main/java/com/khalti/android/demo/MainActivity.kt @@ -8,7 +8,7 @@ import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.ui.Modifier -import com.khalti.android.demo.composable.DemoScreenV3 +import com.khalti.android.demo.composable.DemoScreen import com.khalti.android.demo.theme.KhaltiTheme class MainActivity : ComponentActivity() { @@ -20,7 +20,7 @@ class MainActivity : ComponentActivity() { Surface( Modifier.fillMaxSize(), ) { - DemoScreenV3() + DemoScreen() } } } diff --git a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt index fbec6d61..fcd6cb8c 100644 --- a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt +++ b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt @@ -4,7 +4,6 @@ package com.khalti.android.demo.composable import android.net.Uri import android.util.Log -import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -14,144 +13,22 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp -import com.khalti.android.KhaltiPayConfiguration -import com.khalti.android.OpenKhaltiPay -import com.khalti.android.PaymentCancelled -import com.khalti.android.PaymentError -import com.khalti.android.PaymentResult -import com.khalti.android.PaymentSuccess import com.khalti.android.demo.R import com.khalti.android.v3.Environment import com.khalti.android.v3.Khalti import com.khalti.android.v3.KhaltiPayConfig -const val RESULT_TAG = "KHALTI_PAY_RESULT" - @OptIn(ExperimentalMaterial3Api::class) @Composable fun DemoScreen() { - var paymentUrl by remember { mutableStateOf(TextFieldValue("")) } - var returnUrl by remember { mutableStateOf(TextFieldValue("https://redirect.khalti.com")) } - - var urlErrorMessage by remember { mutableStateOf("") } - - val (result, setResult) = remember { mutableStateOf(null) } - - val khaltiPay = rememberLauncherForActivityResult(OpenKhaltiPay()) { - setResult(it) - - when (it) { - is PaymentSuccess -> { - Log.i(RESULT_TAG, "Payment Success") - } - - is PaymentError -> { - Log.i(RESULT_TAG, "Payment Error") - } - - is PaymentCancelled -> { - Log.i(RESULT_TAG, "Payment Cancelled") - } - } - } - - if (result != null) { - ResultDialog(result, setResult) - } - - - Scaffold( - topBar = { - CenterAlignedTopAppBar( - title = { - Text("Khalti Android SDK Demo") - } - - ) - }, - content = { padding -> - Column( - Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Spacer(Modifier.padding(padding)) - Image( - painterResource(R.drawable.khalti_logo_color), - contentDescription = "Khalti Logo", - modifier = Modifier.height(200.dp) - ) - OutlinedTextField( - value = paymentUrl, - label = { Text("Payment URL") }, - placeholder = { Text("Enter payment URL") }, - onValueChange = { - paymentUrl = it - urlErrorMessage = "" - }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), - isError = urlErrorMessage.isNotEmpty(), - supportingText = { Text(urlErrorMessage) } - ) - Spacer(Modifier.height(24.dp)) - OutlinedTextField( - value = returnUrl, - label = { Text("Return URL") }, - onValueChange = { - returnUrl = it - }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp) - ) - Spacer(Modifier.height(40.dp)) - FilledTonalButton( - { - try { - val config = KhaltiPayConfiguration( - paymentUrl.text, - returnUrl.text - ) - khaltiPay.launch(config) - } catch (e: Exception) { - val message = e.message ?: "" - - when (e) { - is IllegalArgumentException, is IllegalStateException -> { - urlErrorMessage = message - } - - else -> Log.e(RESULT_TAG, message) - } - } - } - ) { - Text("Pay with Khalti") - } - } - }, - ) -} - - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun DemoScreenV3() { val khalti = Khalti.init( LocalContext.current, KhaltiPayConfig( diff --git a/khalti-android/src/main/java/com/khalti/android/KhaltiPayConfiguration.kt b/khalti-android/src/main/java/com/khalti/android/KhaltiPayConfiguration.kt deleted file mode 100644 index ccdc2895..00000000 --- a/khalti-android/src/main/java/com/khalti/android/KhaltiPayConfiguration.kt +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2022. The Khalti Authors. All rights reserved. - -package com.khalti.android - -import android.os.Parcel -import android.os.Parcelable - -class KhaltiPayConfiguration(val paymentUrl: String, val returnUrl: String) : Parcelable { - init { - require(paymentUrl.isNotBlank()) { "Payment URL cannot be blank" } - require(returnUrl.isNotBlank()) { "Return URL cannot be blank" } - check(paymentUrl != returnUrl) { "Payment URL and Return URL cannot be same" } - - val validPaymentUrlRegex = Regex("^https://.*pay.khalti.com/?\\?pidx=.+") - require(validPaymentUrlRegex.matches(paymentUrl)) { "Invalid Payment URL" } - } - - constructor(parcel: Parcel) : this( - parcel.readString() ?: "", - parcel.readString() ?: "" - ) - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(paymentUrl) - parcel.writeString(returnUrl) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): KhaltiPayConfiguration { - return KhaltiPayConfiguration(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} diff --git a/khalti-android/src/main/java/com/khalti/android/OpenKhaltiPay.kt b/khalti-android/src/main/java/com/khalti/android/OpenKhaltiPay.kt deleted file mode 100644 index 1da82b81..00000000 --- a/khalti-android/src/main/java/com/khalti/android/OpenKhaltiPay.kt +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) 2022. The Khalti Authors. All rights reserved. - -package com.khalti.android - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.net.Uri -import androidx.activity.result.contract.ActivityResultContract - -class OpenKhaltiPay : ActivityResultContract() { - override fun createIntent(context: Context, input: KhaltiPayConfiguration): Intent { - return Intent(context, PaymentActivity::class.java).apply { - putExtra(CONFIG, input) - } - } - - override fun parseResult(resultCode: Int, intent: Intent?): PaymentResult { - if (resultCode == PAYMENT_URL_LOAD_ERROR) { - return PaymentError( - "Payment URL load error: ${intent?.getStringExtra(PAYMENT_URL_LOAD_ERROR_RESULT)}" - ) - } - - val url = intent?.getStringExtra(RESULT) - ?: return PaymentCancelled() - - with(Uri.parse(url)) { - return when (resultCode) { - Activity.RESULT_OK -> PaymentSuccess( - getQueryParameter("pidx") ?: "", - getQueryParameter("amount")?.toLongOrNull() ?: 0, - getQueryParameter("mobile") ?: "", - getQueryParameter("purchase_order_id") ?: "", - getQueryParameter("purchase_order_name") ?: "", - getQueryParameter("transaction_id") - ?: getQueryParameter("idx") - ?: "", - ) - ERROR -> PaymentError( - getQueryParameter("message") ?: "Payment Failed", - ) - else -> PaymentCancelled() - } - } - } - - companion object { - const val CONFIG = "config" - const val RESULT = "payment-result" - const val ERROR = -2874 - const val PAYMENT_URL_LOAD_ERROR = -2875 - const val PAYMENT_URL_LOAD_ERROR_RESULT = "payment-url-error" - const val DEFAULT_HOME = "kpg://home" - } -} - diff --git a/khalti-android/src/main/java/com/khalti/android/PaymentResult.kt b/khalti-android/src/main/java/com/khalti/android/PaymentResult.kt deleted file mode 100644 index 39be6736..00000000 --- a/khalti-android/src/main/java/com/khalti/android/PaymentResult.kt +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2022. The Khalti Authors. All rights reserved. - -package com.khalti.android - -interface PaymentResult - -data class PaymentSuccess( - val pidx: String, - val amount: Long, - val mobile: String, - val purchaseOrderId: String, - val purchaseOrderName: String, - val transactionId: String -) : PaymentResult - -data class PaymentError( - val message: String -) : PaymentResult - -class PaymentCancelled : PaymentResult From 29338a37684d2ddbe031637cd751e42ffaaa7af8 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 1 Mar 2024 15:16:14 +0545 Subject: [PATCH 083/131] Re-organize files --- .../com/khalti/android/demo/composable/DemoScreen.kt | 6 +++--- .../main/java/com/khalti/android/{v3 => }/Khalti.kt | 9 ++++++--- .../main/java/com/khalti/android/PaymentActivity.kt | 5 ++--- .../src/main/java/com/khalti/android/api/ApiClient.kt | 6 +++--- .../main/java/com/khalti/android/api/ApiService.kt | 3 +-- .../java/com/khalti/android/{v3 => cache}/Store.kt | 2 +- .../com/khalti/android/{v3 => callbacks}/OnMessage.kt | 4 +++- .../android/{v3 => callbacks}/OnPaymentResult.kt | 5 ++++- .../com/khalti/android/{v3 => callbacks}/OnReturn.kt | 4 +++- .../com/khalti/android/{v3 => data}/Environment.kt | 2 +- .../khalti/android/{v3 => data}/KhaltiPayConfig.kt | 2 +- .../com/khalti/android/{v3 => data}/PaymentResult.kt | 2 +- .../khalti/android/service/VerificationRepository.kt | 4 ++-- .../com/khalti/android/service/VerificationService.kt | 2 +- .../khalti/android/{ => view}/EPaymentWebClient.kt | 11 ++++++----- 15 files changed, 38 insertions(+), 29 deletions(-) rename khalti-android/src/main/java/com/khalti/android/{v3 => }/Khalti.kt (90%) rename khalti-android/src/main/java/com/khalti/android/{v3 => cache}/Store.kt (94%) rename khalti-android/src/main/java/com/khalti/android/{v3 => callbacks}/OnMessage.kt (72%) rename khalti-android/src/main/java/com/khalti/android/{v3 => callbacks}/OnPaymentResult.kt (57%) rename khalti-android/src/main/java/com/khalti/android/{v3 => callbacks}/OnReturn.kt (64%) rename khalti-android/src/main/java/com/khalti/android/{v3 => data}/Environment.kt (78%) rename khalti-android/src/main/java/com/khalti/android/{v3 => data}/KhaltiPayConfig.kt (93%) rename khalti-android/src/main/java/com/khalti/android/{v3 => data}/PaymentResult.kt (96%) rename khalti-android/src/main/java/com/khalti/android/{ => view}/EPaymentWebClient.kt (89%) diff --git a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt index fcd6cb8c..a5a51a36 100644 --- a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt +++ b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt @@ -22,9 +22,9 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import com.khalti.android.demo.R -import com.khalti.android.v3.Environment -import com.khalti.android.v3.Khalti -import com.khalti.android.v3.KhaltiPayConfig +import com.khalti.android.data.Environment +import com.khalti.android.Khalti +import com.khalti.android.data.KhaltiPayConfig @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/Khalti.kt similarity index 90% rename from khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt rename to khalti-android/src/main/java/com/khalti/android/Khalti.kt index 7029f426..7d0179a6 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/Khalti.kt @@ -2,14 +2,17 @@ * Copyright (c) 2024. The Khalti Authors. All rights reserved. */ -package com.khalti.android.v3 +package com.khalti.android import android.content.Context import android.content.Intent import android.content.pm.PackageManager.NameNotFoundException -import android.util.Log -import com.khalti.android.PaymentActivity import com.khalti.android.service.VerificationRepository +import com.khalti.android.data.KhaltiPayConfig +import com.khalti.android.callbacks.OnMessage +import com.khalti.android.callbacks.OnPaymentResult +import com.khalti.android.callbacks.OnReturn +import com.khalti.android.cache.Store // Though kotlin provides named and optional parameters // method overloading was required for Java developers diff --git a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt index 777d0e59..23fd62f8 100644 --- a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt +++ b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt @@ -11,7 +11,6 @@ import android.content.IntentFilter import android.net.Uri import android.os.Build import android.os.Bundle -import android.util.Log import android.view.Gravity import android.webkit.* import android.widget.LinearLayout @@ -21,8 +20,8 @@ import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.MaterialToolbar import com.khalti.android.resource.Url import com.khalti.android.service.VerificationRepository -import com.khalti.android.v3.Khalti -import com.khalti.android.v3.Store +import com.khalti.android.cache.Store +import com.khalti.android.view.EPaymentWebClient internal class PaymentActivity : Activity() { private var receiver: BroadcastReceiver? = null diff --git a/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt index 6fe0a2cb..495234ad 100644 --- a/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt @@ -12,9 +12,9 @@ import com.khalti.android.resource.Ok import com.khalti.android.resource.Result import com.khalti.android.resource.Url import com.khalti.android.utils.ErrorUtil -import com.khalti.android.v3.Store -import com.khalti.android.v3.Environment -import com.khalti.android.v3.Khalti +import com.khalti.android.cache.Store +import com.khalti.android.data.Environment +import com.khalti.android.Khalti import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import okhttp3.OkHttpClient diff --git a/khalti-android/src/main/java/com/khalti/android/api/ApiService.kt b/khalti-android/src/main/java/com/khalti/android/api/ApiService.kt index 03b7feb2..e36040b7 100644 --- a/khalti-android/src/main/java/com/khalti/android/api/ApiService.kt +++ b/khalti-android/src/main/java/com/khalti/android/api/ApiService.kt @@ -4,10 +4,9 @@ package com.khalti.android.api -import com.khalti.android.v3.PaymentPayload +import com.khalti.android.data.PaymentPayload import retrofit2.Response import retrofit2.http.Body -import retrofit2.http.Headers import retrofit2.http.POST interface ApiService { diff --git a/khalti-android/src/main/java/com/khalti/android/v3/Store.kt b/khalti-android/src/main/java/com/khalti/android/cache/Store.kt similarity index 94% rename from khalti-android/src/main/java/com/khalti/android/v3/Store.kt rename to khalti-android/src/main/java/com/khalti/android/cache/Store.kt index 1c5f2fd9..ef2f7b39 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/Store.kt +++ b/khalti-android/src/main/java/com/khalti/android/cache/Store.kt @@ -2,7 +2,7 @@ * Copyright (c) 2024. The Khalti Authors. All rights reserved. */ -package com.khalti.android.v3 +package com.khalti.android.cache @Suppress("UNCHECKED_CAST") class Store private constructor() { diff --git a/khalti-android/src/main/java/com/khalti/android/v3/OnMessage.kt b/khalti-android/src/main/java/com/khalti/android/callbacks/OnMessage.kt similarity index 72% rename from khalti-android/src/main/java/com/khalti/android/v3/OnMessage.kt rename to khalti-android/src/main/java/com/khalti/android/callbacks/OnMessage.kt index 566adb46..3ab0bbbf 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/OnMessage.kt +++ b/khalti-android/src/main/java/com/khalti/android/callbacks/OnMessage.kt @@ -2,7 +2,9 @@ * Copyright (c) 2024. The Khalti Authors. All rights reserved. */ -package com.khalti.android.v3 +package com.khalti.android.callbacks + +import com.khalti.android.Khalti fun interface OnMessage { fun invoke(message: String, khalti: Khalti, throwable: Throwable?, code: Number?) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/OnPaymentResult.kt b/khalti-android/src/main/java/com/khalti/android/callbacks/OnPaymentResult.kt similarity index 57% rename from khalti-android/src/main/java/com/khalti/android/v3/OnPaymentResult.kt rename to khalti-android/src/main/java/com/khalti/android/callbacks/OnPaymentResult.kt index ea8efb0a..76362315 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/OnPaymentResult.kt +++ b/khalti-android/src/main/java/com/khalti/android/callbacks/OnPaymentResult.kt @@ -2,7 +2,10 @@ * Copyright (c) 2024. The Khalti Authors. All rights reserved. */ -package com.khalti.android.v3 +package com.khalti.android.callbacks + +import com.khalti.android.Khalti +import com.khalti.android.data.PaymentResult fun interface OnPaymentResult { fun invoke(result: PaymentResult, khalti: Khalti) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/OnReturn.kt b/khalti-android/src/main/java/com/khalti/android/callbacks/OnReturn.kt similarity index 64% rename from khalti-android/src/main/java/com/khalti/android/v3/OnReturn.kt rename to khalti-android/src/main/java/com/khalti/android/callbacks/OnReturn.kt index 8255a46b..6931a2c4 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/OnReturn.kt +++ b/khalti-android/src/main/java/com/khalti/android/callbacks/OnReturn.kt @@ -2,7 +2,9 @@ * Copyright (c) 2024. The Khalti Authors. All rights reserved. */ -package com.khalti.android.v3 +package com.khalti.android.callbacks + +import com.khalti.android.Khalti fun interface OnReturn { fun invoke(khalti: Khalti) diff --git a/khalti-android/src/main/java/com/khalti/android/v3/Environment.kt b/khalti-android/src/main/java/com/khalti/android/data/Environment.kt similarity index 78% rename from khalti-android/src/main/java/com/khalti/android/v3/Environment.kt rename to khalti-android/src/main/java/com/khalti/android/data/Environment.kt index d9f38a4b..4b9fc411 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/Environment.kt +++ b/khalti-android/src/main/java/com/khalti/android/data/Environment.kt @@ -2,7 +2,7 @@ * Copyright (c) 2024. The Khalti Authors. All rights reserved. */ -package com.khalti.android.v3 +package com.khalti.android.data enum class Environment { PROD, TEST diff --git a/khalti-android/src/main/java/com/khalti/android/v3/KhaltiPayConfig.kt b/khalti-android/src/main/java/com/khalti/android/data/KhaltiPayConfig.kt similarity index 93% rename from khalti-android/src/main/java/com/khalti/android/v3/KhaltiPayConfig.kt rename to khalti-android/src/main/java/com/khalti/android/data/KhaltiPayConfig.kt index c3bd4da3..8b42db90 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/KhaltiPayConfig.kt +++ b/khalti-android/src/main/java/com/khalti/android/data/KhaltiPayConfig.kt @@ -2,7 +2,7 @@ * Copyright (c) 2024. The Khalti Authors. All rights reserved. */ -package com.khalti.android.v3 +package com.khalti.android.data import android.net.Uri import android.os.Parcelable diff --git a/khalti-android/src/main/java/com/khalti/android/v3/PaymentResult.kt b/khalti-android/src/main/java/com/khalti/android/data/PaymentResult.kt similarity index 96% rename from khalti-android/src/main/java/com/khalti/android/v3/PaymentResult.kt rename to khalti-android/src/main/java/com/khalti/android/data/PaymentResult.kt index f3833ed8..da004a0e 100644 --- a/khalti-android/src/main/java/com/khalti/android/v3/PaymentResult.kt +++ b/khalti-android/src/main/java/com/khalti/android/data/PaymentResult.kt @@ -2,7 +2,7 @@ * Copyright (c) 2024. The Khalti Authors. All rights reserved. */ -package com.khalti.android.v3 +package com.khalti.android.data import com.google.gson.annotations.SerializedName diff --git a/khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt b/khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt index dfb0d0a7..b5cc0791 100644 --- a/khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt +++ b/khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt @@ -4,8 +4,8 @@ package com.khalti.android.service -import com.khalti.android.v3.Khalti -import com.khalti.android.v3.PaymentResult +import com.khalti.android.Khalti +import com.khalti.android.data.PaymentResult import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch diff --git a/khalti-android/src/main/java/com/khalti/android/service/VerificationService.kt b/khalti-android/src/main/java/com/khalti/android/service/VerificationService.kt index 90c0af6f..6f66941a 100644 --- a/khalti-android/src/main/java/com/khalti/android/service/VerificationService.kt +++ b/khalti-android/src/main/java/com/khalti/android/service/VerificationService.kt @@ -9,7 +9,7 @@ import com.khalti.android.api.ApiService import com.khalti.android.api.safeApiCall import com.khalti.android.resource.KFailure import com.khalti.android.resource.Result -import com.khalti.android.v3.PaymentPayload +import com.khalti.android.data.PaymentPayload import kotlinx.coroutines.Dispatchers class VerificationService { diff --git a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt b/khalti-android/src/main/java/com/khalti/android/view/EPaymentWebClient.kt similarity index 89% rename from khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt rename to khalti-android/src/main/java/com/khalti/android/view/EPaymentWebClient.kt index f56ae4b5..f6e44890 100644 --- a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/view/EPaymentWebClient.kt @@ -1,14 +1,15 @@ -// Copyright (c) 2022. The Khalti Authors. All rights reserved. +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ -package com.khalti.android +package com.khalti.android.view import android.net.Uri import android.os.Build import android.webkit.* import androidx.annotation.RequiresApi -import com.khalti.android.service.VerificationRepository -import com.khalti.android.v3.Store -import com.khalti.android.v3.Khalti +import com.khalti.android.cache.Store +import com.khalti.android.Khalti internal class EPaymentWebClient(val onReturn: () -> Unit) : WebViewClient() { From 164bfafdfced250ae8b494d501af532b63cd0ea0 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Fri, 1 Mar 2024 17:46:30 +0545 Subject: [PATCH 084/131] Remove commented code --- .../main/java/com/khalti/android/demo/composable/DemoScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt index a5a51a36..72fc4ed3 100644 --- a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt +++ b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt @@ -74,7 +74,6 @@ fun DemoScreen() { FilledTonalButton( { khalti.open() -// khalti.verify() } ) { Text("Pay with Khalti") From d507ce911ec0cddcd6aa216c361bddfecf82ea08 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 4 Mar 2024 10:00:57 +0545 Subject: [PATCH 085/131] Trigger onMessage callback when user pressed the back button --- .../com/khalti/android/PaymentActivity.kt | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt index 23fd62f8..ee4c5294 100644 --- a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt +++ b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt @@ -16,6 +16,9 @@ import android.webkit.* import android.widget.LinearLayout import android.widget.LinearLayout.LayoutParams import android.widget.ProgressBar +import android.window.OnBackInvokedDispatcher +import androidx.activity.OnBackPressedDispatcher +import androidx.core.os.BuildCompat import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.MaterialToolbar import com.khalti.android.resource.Url @@ -53,6 +56,8 @@ internal class PaymentActivity : Activity() { webSettings.javaScriptEnabled = true webSettings.domStorageEnabled = true + setupBackPressListener() + val khalti = Store.instance().get("khalti") if (khalti != null) { val config = khalti.config @@ -103,6 +108,20 @@ internal class PaymentActivity : Activity() { super.onDestroy() } + @Deprecated( + "Deprecated in Java", ReplaceWith( + "@Suppress(\"DEPRECATION\") super.onBackPressed()", + "android.app.Activity" + ) + ) + override fun onBackPressed() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + onBackAction() + } + @Suppress("DEPRECATION") + super.onBackPressed() + } + @SuppressLint("UnspecifiedRegisterReceiverFlag") private fun registerBroadcast() { receiver = object : BroadcastReceiver() { @@ -129,5 +148,19 @@ internal class PaymentActivity : Activity() { private fun unregisterBroadcast() { unregisterReceiver(receiver) } + + private fun setupBackPressListener() { + if (Build.VERSION.SDK_INT >= 33) { + val priority = OnBackInvokedDispatcher.PRIORITY_DEFAULT + onBackInvokedDispatcher.registerOnBackInvokedCallback(priority) { + onBackAction() + } + } + } + + private fun onBackAction() { + val khalti = Store.instance().get("khalti") + khalti?.onMessage?.invoke("User Cancelled", khalti, null, null) + } } From a663fd26c17bd5445227b7964f69bed126f03de7 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Tue, 5 Mar 2024 10:11:59 +0545 Subject: [PATCH 086/131] Remove unused file --- .../android/demo/composable/ResultDialog.kt | 50 ------------------- 1 file changed, 50 deletions(-) delete mode 100644 app/src/main/java/com/khalti/android/demo/composable/ResultDialog.kt diff --git a/app/src/main/java/com/khalti/android/demo/composable/ResultDialog.kt b/app/src/main/java/com/khalti/android/demo/composable/ResultDialog.kt deleted file mode 100644 index b3e06e1b..00000000 --- a/app/src/main/java/com/khalti/android/demo/composable/ResultDialog.kt +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2022. The Khalti Authors. All rights reserved. - -package com.khalti.android.demo.composable - -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import com.khalti.android.* - -@Composable -fun ResultDialog(result: PaymentResult, setResult: (PaymentResult?) -> Unit) { - AlertDialog( - title = { - Text( - when (result) { - is PaymentSuccess -> "Payment Success" - is PaymentError -> "Payment Error" - is PaymentCancelled -> "Payment Cancelled" - else -> "Unknown" - } - ) - }, - text = { - when (result) { - is PaymentSuccess -> Column { - Text("Identifier: ${result.pidx}") - Text("Amount: ${result.amount}") - Text("Mobile: ${result.mobile}") - Text("Purchase Order ID: ${result.purchaseOrderId}") - Text("Purchase Order Name: ${result.purchaseOrderName}") - Text("Transaction ID: ${result.transactionId}") - } - is PaymentError -> Text(result.message) - is PaymentCancelled -> Text("The payment was cancelled.") - } - }, - confirmButton = { - FilledTonalButton( - onClick = { - setResult(null) - } - ) { - Text("OK") - } - }, - onDismissRequest = { - setResult(null) - }, - ) -} From 18a874dcc07c10814e59925dbd3c6d1e1e143203 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Tue, 5 Mar 2024 10:13:50 +0545 Subject: [PATCH 087/131] Add experimental version of payment activity for compose ui --- .../com/khalti/android/PaymentV3Activity.kt | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/PaymentV3Activity.kt diff --git a/khalti-android/src/main/java/com/khalti/android/PaymentV3Activity.kt b/khalti-android/src/main/java/com/khalti/android/PaymentV3Activity.kt new file mode 100644 index 00000000..9fa1ab70 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/PaymentV3Activity.kt @@ -0,0 +1,98 @@ +// Copyright (c) 2022. The Khalti Authors. All rights reserved. + +package com.khalti.android + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import android.os.Bundle +import android.webkit.* +import android.window.OnBackInvokedDispatcher +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import com.khalti.android.cache.Store + +internal class PaymentV3Activity : ComponentActivity() { + private var receiver: BroadcastReceiver? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + Text("Test") + } + } + + override fun onDestroy() { + unregisterBroadcast() + super.onDestroy() + } + + @Deprecated( + "Deprecated in Java", ReplaceWith( + "@Suppress(\"DEPRECATION\") super.onBackPressed()", + "android.app.Activity" + ) + ) + override fun onBackPressed() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + onBackAction() + } + @Suppress("DEPRECATION") + super.onBackPressed() + } + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + private fun registerBroadcast() { + receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent != null && intent.action.equals("close_khalti_payment_portal")) { + finish() + } + } + } + if (Build.VERSION.SDK_INT >= 26) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver( + receiver, IntentFilter("close_khalti_payment_portal"), + RECEIVER_NOT_EXPORTED + ) + } else { + registerReceiver( + receiver, IntentFilter("close_khalti_payment_portal"), + ) + } + } + } + + private fun unregisterBroadcast() { + unregisterReceiver(receiver) + } + + private fun setupBackPressListener() { + if (Build.VERSION.SDK_INT >= 33) { + val priority = OnBackInvokedDispatcher.PRIORITY_DEFAULT + onBackInvokedDispatcher.registerOnBackInvokedCallback(priority) { + onBackAction() + } + } + } + + private fun onBackAction() { + val khalti = Store.instance().get("khalti") + khalti?.onMessage?.invoke("User Cancelled", khalti, null, null) + } + + // ---------------------------- UI ---------------------------------- // + + @Composable + fun PaymentView() { + + } +} + From 333794e94b7a6e69b7f5fe61fc2758e1a17a299e Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Tue, 5 Mar 2024 10:14:27 +0545 Subject: [PATCH 088/131] Add experimental version of payment activity for compose ui --- khalti-android/src/main/java/com/khalti/android/Khalti.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/khalti-android/src/main/java/com/khalti/android/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/Khalti.kt index 7d0179a6..49fe78e6 100644 --- a/khalti-android/src/main/java/com/khalti/android/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/Khalti.kt @@ -75,7 +75,7 @@ class Khalti private constructor( //no-op } - val intent = Intent(context, PaymentActivity::class.java) + val intent = Intent(context, PaymentV3Activity::class.java) context.startActivity(intent) } From b53a919a186b30fe56b97462b8ca7c75923433ba Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Tue, 5 Mar 2024 10:23:27 +0545 Subject: [PATCH 089/131] Re-organize ui logic --- .../com/khalti/android/PaymentActivity.kt | 136 ++++++++++-------- 1 file changed, 79 insertions(+), 57 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt index ee4c5294..b2a57ff4 100644 --- a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt +++ b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt @@ -12,18 +12,18 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.view.Gravity +import android.view.View import android.webkit.* import android.widget.LinearLayout import android.widget.LinearLayout.LayoutParams import android.widget.ProgressBar import android.window.OnBackInvokedDispatcher -import androidx.activity.OnBackPressedDispatcher -import androidx.core.os.BuildCompat import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.MaterialToolbar import com.khalti.android.resource.Url import com.khalti.android.service.VerificationRepository import com.khalti.android.cache.Store +import com.khalti.android.data.KhaltiPayConfig import com.khalti.android.view.EPaymentWebClient internal class PaymentActivity : Activity() { @@ -31,74 +31,47 @@ internal class PaymentActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val layout = LinearLayout(this) - layout.orientation = LinearLayout.VERTICAL - layout.gravity = Gravity.CENTER + val rootLayout = LinearLayout(this) + rootLayout.orientation = LinearLayout.VERTICAL + rootLayout.gravity = Gravity.CENTER val params = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) - val appBar = AppBarLayout(this) - val toolbar = MaterialToolbar(this) val progressBar = ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal) progressBar.isIndeterminate = true - toolbar.title = "Pay with Khalti" - toolbar.setNavigationIcon(com.google.android.material.R.drawable.abc_ic_ab_back_material) - toolbar.setNavigationOnClickListener { - finish() - } - - val webView = WebView(this) - val webSettings = webView.settings - - @SuppressLint("SetJavaScriptEnabled") - webSettings.javaScriptEnabled = true - webSettings.domStorageEnabled = true - setupBackPressListener() val khalti = Store.instance().get("khalti") if (khalti != null) { val config = khalti.config - webView.webViewClient = EPaymentWebClient { - val verificationRepo = VerificationRepository() - progressBar.visibility = ProgressBar.VISIBLE - verificationRepo.verify(config.pidx, khalti) { - runOnUiThread { - progressBar.visibility = ProgressBar.GONE - } - } - } - webView.webChromeClient = object : WebChromeClient() { - override fun onProgressChanged(view: WebView?, newProgress: Int) { - progressBar.visibility = - if (newProgress == 100) ProgressBar.GONE else ProgressBar.VISIBLE - } - } - - val baseUrl = if (config.isProd()) { - Url.BASE_PAYMENT_URL_PROD - } else { - Url.BASE_PAYMENT_URL_STAGING - } - - val paymentUri = Uri - .parse(baseUrl.value) - .buildUpon() - .appendQueryParameter("pidx", config.pidx) - - webView.clearCache(true) - webView.loadUrl(paymentUri.toString()) - - appBar.addView(toolbar) - - layout.addView(appBar) - layout.addView(progressBar) - layout.addView(webView, params) - - setContentView(layout, params) + appBar.addView(toolbar()) + + rootLayout.addView(appBar) + rootLayout.addView(progressBar) + rootLayout.addView( + webView( + config, + onLoadComplete = { + progressBar.visibility = + if (it == 100) ProgressBar.GONE else ProgressBar.VISIBLE + }, + onReturn = { + val verificationRepo = VerificationRepository() + progressBar.visibility = ProgressBar.VISIBLE + verificationRepo.verify(config.pidx, khalti) { + runOnUiThread { + progressBar.visibility = ProgressBar.GONE + } + } + + }, + ), params + ) + + setContentView(rootLayout, params) registerBroadcast() } } @@ -162,5 +135,54 @@ internal class PaymentActivity : Activity() { val khalti = Store.instance().get("khalti") khalti?.onMessage?.invoke("User Cancelled", khalti, null, null) } + + // ---------------------------- UI ---------------------------------- // + + private fun toolbar(): View { + val toolbar = MaterialToolbar(this) + toolbar.title = "Pay with Khalti" + toolbar.setNavigationIcon(com.google.android.material.R.drawable.abc_ic_ab_back_material) + toolbar.setNavigationOnClickListener { + finish() + } + + return toolbar + } + + private fun webView( + config: KhaltiPayConfig, + onLoadComplete: (progress: Int) -> Unit, + onReturn: () -> Unit + ): View { + val webView = WebView(this) + val webSettings = webView.settings + + @SuppressLint("SetJavaScriptEnabled") + webSettings.javaScriptEnabled = true + webSettings.domStorageEnabled = true + + webView.webViewClient = EPaymentWebClient(onReturn) + webView.webChromeClient = object : WebChromeClient() { + override fun onProgressChanged(view: WebView?, newProgress: Int) { + onLoadComplete(newProgress) + } + } + + val baseUrl = if (config.isProd()) { + Url.BASE_PAYMENT_URL_PROD + } else { + Url.BASE_PAYMENT_URL_STAGING + } + + val paymentUri = Uri + .parse(baseUrl.value) + .buildUpon() + .appendQueryParameter("pidx", config.pidx) + + webView.clearCache(true) + webView.loadUrl(paymentUri.toString()) + + return webView + } } From 53066d944d543980a3091b2309f5c18b0046ebb2 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Tue, 5 Mar 2024 10:24:08 +0545 Subject: [PATCH 090/131] Add compose dependencies in khalti library --- app/build.gradle | 4 ++-- build.gradle | 2 +- khalti-android/build.gradle | 24 +++++++++++++++++++-- khalti-android/src/main/AndroidManifest.xml | 1 + 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a947aac4..c567c0d6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,7 +39,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion '1.4.1' + kotlinCompilerExtensionVersion '1.5.10' } packagingOptions { resources { @@ -60,7 +60,7 @@ dependencies { implementation 'androidx.activity:activity-compose:1.8.2' implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" - implementation 'androidx.compose.material3:material3:1.1.2' + implementation 'androidx.compose.material3:material3:1.2.0' //implementation "com.khalti:khalti-android:$khaltiVersionName" implementation project(path: ':khalti-android') diff --git a/build.gradle b/build.gradle index a35f2fee..08f497f3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ plugins { id 'com.android.application' version '8.2.0' apply false id 'com.android.library' version '8.2.0' apply false - id 'org.jetbrains.kotlin.android' version '1.8.0' apply false + id 'org.jetbrains.kotlin.android' version '1.9.22' apply false id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' } diff --git a/khalti-android/build.gradle b/khalti-android/build.gradle index ec9c614d..ed146120 100644 --- a/khalti-android/build.gradle +++ b/khalti-android/build.gradle @@ -10,6 +10,11 @@ android { buildFeatures { buildConfig true + compose true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.10" } defaultConfig { @@ -31,26 +36,41 @@ android { buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}\"") } } + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { jvmTarget = '1.8' } } dependencies { - // Core + // ---------- Core ---------- implementation "androidx.core:core-ktx:$core_ktx_version" implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "com.google.android.material:material:$material_version" - // Api + // ---------- Api ---------- implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.squareup.okhttp3:logging-interceptor:4.11.0' + // ---------- Compose ---------- + def composeBom = platform('androidx.compose:compose-bom:2024.02.01') + implementation composeBom + androidTestImplementation composeBom + + implementation 'androidx.compose.material3:material3' + implementation 'androidx.activity:activity-compose:1.8.2' + + // Compose preview support + implementation 'androidx.compose.ui:ui-tooling-preview' + debugImplementation 'androidx.compose.ui:ui-tooling' + + // ---------- Test ---------- testImplementation "junit:junit:$junit_version" androidTestImplementation "androidx.test.ext:junit:$junit_ext_version" diff --git a/khalti-android/src/main/AndroidManifest.xml b/khalti-android/src/main/AndroidManifest.xml index 8c42db92..38ecb67a 100644 --- a/khalti-android/src/main/AndroidManifest.xml +++ b/khalti-android/src/main/AndroidManifest.xml @@ -4,5 +4,6 @@ + \ No newline at end of file From 54900f4888b8ab172f649758168db894d2d85813 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Wed, 6 Mar 2024 17:24:18 +0545 Subject: [PATCH 091/131] Add error type --- .../main/java/com/khalti/android/resource/ErrorType.kt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/resource/ErrorType.kt diff --git a/khalti-android/src/main/java/com/khalti/android/resource/ErrorType.kt b/khalti-android/src/main/java/com/khalti/android/resource/ErrorType.kt new file mode 100644 index 00000000..df8d9da7 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/resource/ErrorType.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.resource + +enum class ErrorType { + generic, network +} \ No newline at end of file From a08a750c3e8c37fec96785a7c64b302a51a34ff8 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Wed, 6 Mar 2024 17:24:35 +0545 Subject: [PATCH 092/131] Add network util --- .../com/khalti/android/utils/NetworkUtil.kt | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/utils/NetworkUtil.kt diff --git a/khalti-android/src/main/java/com/khalti/android/utils/NetworkUtil.kt b/khalti-android/src/main/java/com/khalti/android/utils/NetworkUtil.kt new file mode 100644 index 00000000..9cd195c8 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/utils/NetworkUtil.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.utils + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.os.Build +import androidx.annotation.RequiresApi + + +class NetworkUtil { + + companion object { + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + fun registerListener(context: Context, onNetworkChange: (Boolean) -> Unit) { + val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + onNetworkChange(true) + } + + override fun onLost(network: Network) { + super.onLost(network) + onNetworkChange(false) + } + } + + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + connectivityManager.registerDefaultNetworkCallback(networkCallback) + } else { + val networkRequest = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .build() + + connectivityManager.registerNetworkCallback(networkRequest, networkCallback) + } + } + + @Suppress("DEPRECATION") + fun isNetworkAvailable(context: Context): Boolean { + var result = false + + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val netCapabilities = connectivityManager.activeNetwork ?: return false + val activeNetworkCapability = + connectivityManager.getNetworkCapabilities(netCapabilities) ?: return false + + result = activeNetworkCapability.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || + activeNetworkCapability.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || + activeNetworkCapability.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) + } else { + connectivityManager.run { + connectivityManager.activeNetworkInfo?.run { + result = when (type) { + ConnectivityManager.TYPE_WIFI -> true + ConnectivityManager.TYPE_MOBILE -> true + ConnectivityManager.TYPE_ETHERNET -> true + else -> false + } + } + } + } + + return result + } + } +} \ No newline at end of file From 33484b0ace909e431c97dbd2fd1d4e13b53036f1 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Wed, 6 Mar 2024 17:24:46 +0545 Subject: [PATCH 093/131] Add package util --- .../com/khalti/android/utils/PackageUtil.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/utils/PackageUtil.kt diff --git a/khalti-android/src/main/java/com/khalti/android/utils/PackageUtil.kt b/khalti-android/src/main/java/com/khalti/android/utils/PackageUtil.kt new file mode 100644 index 00000000..e5bd231c --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/utils/PackageUtil.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.utils + +import android.content.Context +import android.content.pm.PackageInfo +import android.content.pm.PackageManager + +class PackageUtil { + companion object { + fun doesPackageExist(context: Context, packageName: String) { + getPackageInfo(context, packageName) != null + } + + fun getPackageInfo(context: Context, packageName: String): PackageInfo? { + return try { + context.packageManager.getPackageInfo(packageName, 0) + } catch (e: PackageManager.NameNotFoundException) { + return null + } + } + } +} \ No newline at end of file From ba9b1515d60a9b0859f8793d6259c2d6f40a34c7 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Wed, 6 Mar 2024 17:25:16 +0545 Subject: [PATCH 094/131] Add composable for error --- .../com/khalti/android/composable/Error.kt | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/composable/Error.kt diff --git a/khalti-android/src/main/java/com/khalti/android/composable/Error.kt b/khalti-android/src/main/java/com/khalti/android/composable/Error.kt new file mode 100644 index 00000000..55566138 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/composable/Error.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.SignalWifiOff +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import com.khalti.android.resource.ErrorType +import com.khalti.android.utils.ErrorUtil + +@Composable +fun KhaltiError( + errorType: ErrorType, + title: String? = null, + message: String? = null, + icon: ImageVector? = null, + action: String = "Try Again", + onAction: (() -> Unit)? = null, +) { + val resolvedIcon = + icon ?: if (errorType == ErrorType.network) Icons.Filled.SignalWifiOff else null + + val resolvedTitle = title + ?: if (errorType == ErrorType.network) "Network unavailable" else null + + val resolvedMessage = message ?: when (errorType) { + ErrorType.generic -> ErrorUtil.GENERIC_ERROR + ErrorType.network -> "Make sure your device is connected to the internet and try again" + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (resolvedIcon != null) { + Icon( + modifier = Modifier + .size(56.dp) + .padding(bottom = 24.dp), + imageVector = Icons.Filled.SignalWifiOff, + contentDescription = "No internet" + ) + } + if (!resolvedTitle.isNullOrEmpty()) { + Text( + modifier = Modifier.padding(bottom = 16.dp), + text = resolvedTitle, + style = MaterialTheme.typography.titleLarge + ) + } + Text( + text = resolvedMessage, style = MaterialTheme.typography.bodyLarge + ) + + if (onAction != null) { + Spacer(modifier = Modifier.height(24.dp)) + OutlinedButton(onClick = onAction) { + Text(text = action.uppercase()) + } + } + + } +} \ No newline at end of file From afbd2416baa26f616ab8930529effa00c6e56938 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Wed, 6 Mar 2024 17:25:37 +0545 Subject: [PATCH 095/131] Add composable for payment page --- .../android/composable/KhaltiPaymentPage.kt | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt diff --git a/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt b/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt new file mode 100644 index 00000000..8318c7f3 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.composable + +import android.annotation.SuppressLint +import android.app.Activity +import android.os.Build +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.khalti.android.Khalti +import com.khalti.android.cache.Store +import com.khalti.android.resource.ErrorType +import com.khalti.android.service.VerificationRepository +import com.khalti.android.utils.NetworkUtil + +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun KhaltiPaymentPage(activity: Activity) { + val showProgress = remember { + mutableStateOf(true) + } + val networkAvailable = remember { + mutableStateOf(NetworkUtil.isNetworkAvailable(activity)) + } + + val pageLoaded = remember { + mutableStateOf(false) + } + val reloadPage = remember { + mutableStateOf(NetworkUtil.isNetworkAvailable(activity)) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + NetworkUtil.registerListener(activity) { + networkAvailable.value = it + } + } + Scaffold( + topBar = { + TopAppBar( + navigationIcon = { + IconButton(onClick = { + onBackAction() + activity.finish() + }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + }, + title = { + Text(text = "Pay With Khalti") + }, + ) + }, + ) { + Surface(modifier = Modifier.padding(top = it.calculateTopPadding())) { + val khalti = Store.instance().get("khalti") + if (khalti != null) { + val config = khalti.config + val webView: @Composable () -> Unit = { + KhaltiWebView( + config = config, + onReturnPageLoaded = { + showProgress.value = true + val verificationRepo = VerificationRepository() + verificationRepo.verify(config.pidx, khalti) { + activity.runOnUiThread { + showProgress.value = false + } + } + + }, + onPageLoaded = { + showProgress.value = false + }, + ) + + } + if (networkAvailable.value) { + if (showProgress.value) { + Box( + Modifier + .fillMaxSize() + .background(Color.LightGray) + ) { + webView() + LinearProgressIndicator( + Modifier + .fillMaxWidth() + .height(68.dp) + .align(Alignment.TopCenter), + color = Color.Gray + ) + } + } else { + webView() + } + } else { + KhaltiError(errorType = ErrorType.network) { + + } + } + } + } + + } +} + +private fun onBackAction() { + val khalti = Store.instance().get("khalti") + khalti?.onMessage?.invoke("User Cancelled", khalti, null, null) +} From 27aa687ac238f6c3ae348bf89707a5d28734f6a4 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Wed, 6 Mar 2024 17:25:55 +0545 Subject: [PATCH 096/131] Add webview composable --- .../android/composable/KhaltiWebview.kt | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/composable/KhaltiWebview.kt diff --git a/khalti-android/src/main/java/com/khalti/android/composable/KhaltiWebview.kt b/khalti-android/src/main/java/com/khalti/android/composable/KhaltiWebview.kt new file mode 100644 index 00000000..7e382708 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/composable/KhaltiWebview.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.composable + +import android.annotation.SuppressLint +import android.net.Uri +import android.webkit.WebChromeClient +import android.webkit.WebView +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import com.khalti.android.data.KhaltiPayConfig +import com.khalti.android.resource.Url +import com.khalti.android.view.EPaymentWebClient + +@SuppressLint("SetJavaScriptEnabled") +@Composable +fun KhaltiWebView( + config: KhaltiPayConfig, + onReturnPageLoaded: () -> Unit, + onPageLoaded: () -> Unit, +) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + WebView(context).apply { + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.setSupportZoom(true) + + this.webViewClient = EPaymentWebClient(onReturnPageLoaded) + this.webChromeClient = object : WebChromeClient() { + override fun onProgressChanged(view: WebView?, newProgress: Int) { + if (newProgress == 100) { + onPageLoaded() + } + } + } + this.clearCache(true) + } + }, + update = { + val baseUrl = if (config.isProd()) { + Url.BASE_PAYMENT_URL_PROD + } else { + Url.BASE_PAYMENT_URL_STAGING + } + + val paymentUri = + Uri.parse(baseUrl.value).buildUpon().appendQueryParameter("pidx", config.pidx) + + it.loadUrl(paymentUri.toString()) + }, + ) +} From 7c70c398b3c383620e888766829db4e4ff651a61 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Wed, 6 Mar 2024 17:26:17 +0545 Subject: [PATCH 097/131] Use package util --- .../src/main/java/com/khalti/android/Khalti.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/Khalti.kt index 49fe78e6..8e0d9020 100644 --- a/khalti-android/src/main/java/com/khalti/android/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/Khalti.kt @@ -13,6 +13,7 @@ import com.khalti.android.callbacks.OnMessage import com.khalti.android.callbacks.OnPaymentResult import com.khalti.android.callbacks.OnReturn import com.khalti.android.cache.Store +import com.khalti.android.utils.PackageUtil // Though kotlin provides named and optional parameters // method overloading was required for Java developers @@ -66,14 +67,10 @@ class Khalti private constructor( fun open() { val packageName = context.packageName val store = Store.instance() - store.put("merchant_package_name", packageName) + val packageInfo = PackageUtil.getPackageInfo(context, packageName) - try { - val packageInfo = context.packageManager.getPackageInfo(packageName, 0) - store.put("merchant_package_version", packageInfo.versionName) - } catch (e: NameNotFoundException) { - //no-op - } + store.put("merchant_package_name", packageName) + store.put("merchant_package_version", packageInfo?.versionName ?: "") val intent = Intent(context, PaymentV3Activity::class.java) context.startActivity(intent) From 7d987bef5c41253c30a228fbd293f3740109ed40 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Wed, 6 Mar 2024 17:26:50 +0545 Subject: [PATCH 098/131] Use segregated composables --- .../com/khalti/android/PaymentV3Activity.kt | 48 +++---------------- 1 file changed, 6 insertions(+), 42 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/PaymentV3Activity.kt b/khalti-android/src/main/java/com/khalti/android/PaymentV3Activity.kt index 9fa1ab70..e534979d 100644 --- a/khalti-android/src/main/java/com/khalti/android/PaymentV3Activity.kt +++ b/khalti-android/src/main/java/com/khalti/android/PaymentV3Activity.kt @@ -10,22 +10,22 @@ import android.content.IntentFilter import android.os.Build import android.os.Bundle import android.webkit.* -import android.window.OnBackInvokedDispatcher import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import com.khalti.android.cache.Store +import androidx.annotation.RequiresApi +import com.khalti.android.composable.KhaltiPaymentPage internal class PaymentV3Activity : ComponentActivity() { private var receiver: BroadcastReceiver? = null + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - Text("Test") + KhaltiPaymentPage(this) } + registerBroadcast() } override fun onDestroy() { @@ -33,20 +33,6 @@ internal class PaymentV3Activity : ComponentActivity() { super.onDestroy() } - @Deprecated( - "Deprecated in Java", ReplaceWith( - "@Suppress(\"DEPRECATION\") super.onBackPressed()", - "android.app.Activity" - ) - ) - override fun onBackPressed() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - onBackAction() - } - @Suppress("DEPRECATION") - super.onBackPressed() - } - @SuppressLint("UnspecifiedRegisterReceiverFlag") private fun registerBroadcast() { receiver = object : BroadcastReceiver() { @@ -59,8 +45,7 @@ internal class PaymentV3Activity : ComponentActivity() { if (Build.VERSION.SDK_INT >= 26) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { registerReceiver( - receiver, IntentFilter("close_khalti_payment_portal"), - RECEIVER_NOT_EXPORTED + receiver, IntentFilter("close_khalti_payment_portal"), RECEIVER_NOT_EXPORTED ) } else { registerReceiver( @@ -73,26 +58,5 @@ internal class PaymentV3Activity : ComponentActivity() { private fun unregisterBroadcast() { unregisterReceiver(receiver) } - - private fun setupBackPressListener() { - if (Build.VERSION.SDK_INT >= 33) { - val priority = OnBackInvokedDispatcher.PRIORITY_DEFAULT - onBackInvokedDispatcher.registerOnBackInvokedCallback(priority) { - onBackAction() - } - } - } - - private fun onBackAction() { - val khalti = Store.instance().get("khalti") - khalti?.onMessage?.invoke("User Cancelled", khalti, null, null) - } - - // ---------------------------- UI ---------------------------------- // - - @Composable - fun PaymentView() { - - } } From 47b82d68e49fb93293f560357b0277114cd3873a Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Wed, 6 Mar 2024 17:27:14 +0545 Subject: [PATCH 099/131] Remove error handling --- .../khalti/android/view/EPaymentWebClient.kt | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/view/EPaymentWebClient.kt b/khalti-android/src/main/java/com/khalti/android/view/EPaymentWebClient.kt index f6e44890..bad7b20b 100644 --- a/khalti-android/src/main/java/com/khalti/android/view/EPaymentWebClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/view/EPaymentWebClient.kt @@ -22,21 +22,6 @@ internal class EPaymentWebClient(val onReturn: () -> Unit) : WebViewClient() { override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean = handleUri(Uri.parse(url)) - @RequiresApi(Build.VERSION_CODES.M) - override fun onReceivedError( - view: WebView?, - request: WebResourceRequest?, - error: WebResourceError? - ) = handleError(error?.description.toString()) - - @SuppressWarnings("deprecation") - @Deprecated("") - override fun onReceivedError( - view: WebView?, - errorCode: Int, - description: String?, - failingUrl: String? - ) = handleError(description) override fun onPageFinished(view: WebView?, url: String?) { super.onPageFinished(view, url) @@ -56,11 +41,4 @@ internal class EPaymentWebClient(val onReturn: () -> Unit) : WebViewClient() { // MPIN url : /account/transaction_pin return false } - - private fun handleError(description: String?) { - val khalti = Store.instance().get("khalti") - if (description != null) { - khalti?.onMessage?.invoke(description, khalti, null, null) - } - } } From 37c72f2f5b0333256d590c19162fdad95e6bd053 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Wed, 6 Mar 2024 17:27:49 +0545 Subject: [PATCH 100/131] Add material icons as dependency --- khalti-android/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/khalti-android/build.gradle b/khalti-android/build.gradle index ed146120..c7cb1365 100644 --- a/khalti-android/build.gradle +++ b/khalti-android/build.gradle @@ -65,6 +65,7 @@ dependencies { implementation 'androidx.compose.material3:material3' implementation 'androidx.activity:activity-compose:1.8.2' + implementation "androidx.compose.material:material-icons-extended:$compose_version" // Compose preview support implementation 'androidx.compose.ui:ui-tooling-preview' From 27b5ed8f4f4101eaf33310e7be9f41de3e1847a1 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Wed, 6 Mar 2024 17:27:56 +0545 Subject: [PATCH 101/131] Add internet permission --- khalti-android/src/main/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/khalti-android/src/main/AndroidManifest.xml b/khalti-android/src/main/AndroidManifest.xml index 38ecb67a..b5382850 100644 --- a/khalti-android/src/main/AndroidManifest.xml +++ b/khalti-android/src/main/AndroidManifest.xml @@ -1,5 +1,7 @@ + + From db0509255e04d6931850a76b70a9d3e20f6523c1 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 7 Mar 2024 10:13:35 +0545 Subject: [PATCH 102/131] Reload page if try again button is pressed --- .../android/composable/KhaltiPaymentPage.kt | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt b/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt index 8318c7f3..efd241d9 100644 --- a/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt +++ b/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt @@ -25,6 +25,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -41,20 +42,12 @@ import com.khalti.android.utils.NetworkUtil @OptIn(ExperimentalMaterial3Api::class) @Composable fun KhaltiPaymentPage(activity: Activity) { - val showProgress = remember { + val isLoading = remember { mutableStateOf(true) } val networkAvailable = remember { mutableStateOf(NetworkUtil.isNetworkAvailable(activity)) } - - val pageLoaded = remember { - mutableStateOf(false) - } - val reloadPage = remember { - mutableStateOf(NetworkUtil.isNetworkAvailable(activity)) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { NetworkUtil.registerListener(activity) { networkAvailable.value = it @@ -88,23 +81,24 @@ fun KhaltiPaymentPage(activity: Activity) { KhaltiWebView( config = config, onReturnPageLoaded = { - showProgress.value = true + isLoading.value = true val verificationRepo = VerificationRepository() verificationRepo.verify(config.pidx, khalti) { activity.runOnUiThread { - showProgress.value = false + isLoading.value = false } } }, onPageLoaded = { - showProgress.value = false + isLoading.value = false }, ) } + if (networkAvailable.value) { - if (showProgress.value) { + if (isLoading.value) { Box( Modifier .fillMaxSize() @@ -114,7 +108,7 @@ fun KhaltiPaymentPage(activity: Activity) { LinearProgressIndicator( Modifier .fillMaxWidth() - .height(68.dp) + .height(4.dp) .align(Alignment.TopCenter), color = Color.Gray ) @@ -124,13 +118,15 @@ fun KhaltiPaymentPage(activity: Activity) { } } else { KhaltiError(errorType = ErrorType.network) { - + networkAvailable.value = NetworkUtil.isNetworkAvailable(activity) } } } } } + + LaunchedEffect(networkAvailable.value) {} } private fun onBackAction() { From 4784e05d70724503d27f76f187d859c50e43d02e Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 7 Mar 2024 10:26:55 +0545 Subject: [PATCH 103/131] Add refresh button --- .../android/composable/KhaltiPaymentPage.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt b/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt index efd241d9..8092c003 100644 --- a/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt +++ b/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -50,9 +51,10 @@ fun KhaltiPaymentPage(activity: Activity) { } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { NetworkUtil.registerListener(activity) { - networkAvailable.value = it +// networkAvailable.value = it } } + val recomposeState = mutableStateOf(false) Scaffold( topBar = { TopAppBar( @@ -70,6 +72,16 @@ fun KhaltiPaymentPage(activity: Activity) { title = { Text(text = "Pay With Khalti") }, + actions = { + IconButton(onClick = { + recomposeState.value = !recomposeState.value + }) { + Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = "Refresh" + ) + } + } ) }, ) { @@ -126,7 +138,7 @@ fun KhaltiPaymentPage(activity: Activity) { } - LaunchedEffect(networkAvailable.value) {} + LaunchedEffect(networkAvailable.value && recomposeState.value) {} } private fun onBackAction() { From ce88d168d1e330a00cb45882545b5c919d405d43 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Thu, 7 Mar 2024 10:51:13 +0545 Subject: [PATCH 104/131] Move network listener inside launchEffect to prevent illegal state exception --- .../android/composable/KhaltiPaymentPage.kt | 69 ++++++++++--------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt b/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt index 8092c003..a8519c69 100644 --- a/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt +++ b/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt @@ -38,6 +38,8 @@ import com.khalti.android.cache.Store import com.khalti.android.resource.ErrorType import com.khalti.android.service.VerificationRepository import com.khalti.android.utils.NetworkUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @OptIn(ExperimentalMaterial3Api::class) @@ -49,40 +51,37 @@ fun KhaltiPaymentPage(activity: Activity) { val networkAvailable = remember { mutableStateOf(NetworkUtil.isNetworkAvailable(activity)) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - NetworkUtil.registerListener(activity) { -// networkAvailable.value = it - } - } val recomposeState = mutableStateOf(false) Scaffold( topBar = { - TopAppBar( - navigationIcon = { - IconButton(onClick = { - onBackAction() - activity.finish() - }) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" - ) - } - }, - title = { - Text(text = "Pay With Khalti") - }, - actions = { - IconButton(onClick = { - recomposeState.value = !recomposeState.value - }) { - Icon( - imageVector = Icons.Filled.Refresh, - contentDescription = "Refresh" - ) - } - } - ) + Surface(shadowElevation = 4.dp) { + TopAppBar( + navigationIcon = { + IconButton(onClick = { + onBackAction() + activity.finish() + }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + }, + title = { + Text(text = "Pay With Khalti") + }, + actions = { + IconButton(onClick = { + recomposeState.value = !recomposeState.value + }) { + Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = "Refresh" + ) + } + }, + ) + } }, ) { Surface(modifier = Modifier.padding(top = it.calculateTopPadding())) { @@ -138,7 +137,13 @@ fun KhaltiPaymentPage(activity: Activity) { } - LaunchedEffect(networkAvailable.value && recomposeState.value) {} + LaunchedEffect(networkAvailable.value && recomposeState.value) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + NetworkUtil.registerListener(activity) { + networkAvailable.value = it + } + } + } } private fun onBackAction() { From eae257aaa3cb8a2cfe22bd6b7a8a75d20cd7d55b Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 11 Mar 2024 14:53:06 +0545 Subject: [PATCH 105/131] Add demo log --- .../com/khalti/android/demo/theme/Color.kt | 7 --- .../com/khalti/android/demo/theme/Theme.kt | 47 ------------------ .../com/khalti/android/demo/theme/Type.kt | 18 ------- .../main/res/drawable/khalti_logo_color.xml | 36 -------------- app/src/main/res/mipmap-xxhdpi/seru.png | Bin 0 -> 63680 bytes 5 files changed, 108 deletions(-) delete mode 100644 app/src/main/java/com/khalti/android/demo/theme/Color.kt delete mode 100644 app/src/main/java/com/khalti/android/demo/theme/Theme.kt delete mode 100644 app/src/main/java/com/khalti/android/demo/theme/Type.kt delete mode 100644 app/src/main/res/drawable/khalti_logo_color.xml create mode 100644 app/src/main/res/mipmap-xxhdpi/seru.png diff --git a/app/src/main/java/com/khalti/android/demo/theme/Color.kt b/app/src/main/java/com/khalti/android/demo/theme/Color.kt deleted file mode 100644 index 985fdc3f..00000000 --- a/app/src/main/java/com/khalti/android/demo/theme/Color.kt +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) 2022. The Khalti Authors. All rights reserved. - -package com.khalti.android.demo.theme - -import androidx.compose.ui.graphics.Color - -val khaltiPurple = Color(0xFF5E338D) \ No newline at end of file diff --git a/app/src/main/java/com/khalti/android/demo/theme/Theme.kt b/app/src/main/java/com/khalti/android/demo/theme/Theme.kt deleted file mode 100644 index 23fee6c5..00000000 --- a/app/src/main/java/com/khalti/android/demo/theme/Theme.kt +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2022. The Khalti Authors. All rights reserved. - -package com.khalti.android.demo.theme - -import android.app.Activity -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.* -import androidx.core.view.ViewCompat - -private val DarkColorScheme = darkColorScheme(khaltiPurple) - -private val LightColorScheme = lightColorScheme(khaltiPurple) - -@Composable -fun KhaltiTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, - content: @Composable () -> Unit -) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - darkTheme -> DarkColorScheme - else -> LightColorScheme - } - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() - ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme - } - } - - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/khalti/android/demo/theme/Type.kt b/app/src/main/java/com/khalti/android/demo/theme/Type.kt deleted file mode 100644 index a00a0326..00000000 --- a/app/src/main/java/com/khalti/android/demo/theme/Type.kt +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2022. The Khalti Authors. All rights reserved. - -package com.khalti.android.demo.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.* -import androidx.compose.ui.unit.sp - -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) -) \ No newline at end of file diff --git a/app/src/main/res/drawable/khalti_logo_color.xml b/app/src/main/res/drawable/khalti_logo_color.xml deleted file mode 100644 index b5132406..00000000 --- a/app/src/main/res/drawable/khalti_logo_color.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/mipmap-xxhdpi/seru.png b/app/src/main/res/mipmap-xxhdpi/seru.png new file mode 100644 index 0000000000000000000000000000000000000000..1c75e51df33101931cd67a096406d20b37a698c2 GIT binary patch literal 63680 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 S8ra1Q^jb+%v0UCF^nU;Z+I)`y literal 0 HcmV?d00001 From 0b8a584a62685ab71489a0e1fb6fd26f6bbf74c9 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 11 Mar 2024 14:55:06 +0545 Subject: [PATCH 106/131] Remove theme --- .../main/java/com/khalti/android/demo/MainActivity.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/khalti/android/demo/MainActivity.kt b/app/src/main/java/com/khalti/android/demo/MainActivity.kt index 082a919f..edd01f94 100644 --- a/app/src/main/java/com/khalti/android/demo/MainActivity.kt +++ b/app/src/main/java/com/khalti/android/demo/MainActivity.kt @@ -9,19 +9,16 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import com.khalti.android.demo.composable.DemoScreen -import com.khalti.android.demo.theme.KhaltiTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - KhaltiTheme { - Surface( - Modifier.fillMaxSize(), - ) { - DemoScreen() - } + Surface( + Modifier.fillMaxSize(), + ) { + DemoScreen() } } } From bd393d44a832261b0842cee80c63a7db82362963 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 11 Mar 2024 14:55:40 +0545 Subject: [PATCH 107/131] Add message event types for onMessage --- .../java/com/khalti/android/resource/OnMessageEvent.kt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/resource/OnMessageEvent.kt diff --git a/khalti-android/src/main/java/com/khalti/android/resource/OnMessageEvent.kt b/khalti-android/src/main/java/com/khalti/android/resource/OnMessageEvent.kt new file mode 100644 index 00000000..32551519 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/resource/OnMessageEvent.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.resource + +enum class OnMessageEvent { + BackPressed, ReturnUrlLoadFailure, NetworkFailure, PaymentLookUpFailure, Unknown +} \ No newline at end of file From 903b54d5b95eb28d1e9d058f2a3c2eb2ea1aae9d Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 11 Mar 2024 14:55:58 +0545 Subject: [PATCH 108/131] Add message payload for onMessage --- .../khalti/android/resource/OnMessagePayload.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/resource/OnMessagePayload.kt diff --git a/khalti-android/src/main/java/com/khalti/android/resource/OnMessagePayload.kt b/khalti-android/src/main/java/com/khalti/android/resource/OnMessagePayload.kt new file mode 100644 index 00000000..265f0af2 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/resource/OnMessagePayload.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.resource + +import com.khalti.android.Khalti + +data class OnMessagePayload( + val event: OnMessageEvent, + val message: String, + val khalti: Khalti, + val throwable: Throwable? = null, + val code: Number? = null, + val needsPaymentConfirmation: Boolean = false, +) From bf43ff5eaaa3fb2749ef155521e22806caa11791 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 11 Mar 2024 14:57:03 +0545 Subject: [PATCH 109/131] Remove old payment activity --- khalti-android/src/main/AndroidManifest.xml | 1 - .../com/khalti/android/PaymentActivity.kt | 188 ------------------ 2 files changed, 189 deletions(-) delete mode 100644 khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt diff --git a/khalti-android/src/main/AndroidManifest.xml b/khalti-android/src/main/AndroidManifest.xml index b5382850..6a6577a2 100644 --- a/khalti-android/src/main/AndroidManifest.xml +++ b/khalti-android/src/main/AndroidManifest.xml @@ -5,7 +5,6 @@ - \ No newline at end of file diff --git a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt deleted file mode 100644 index b2a57ff4..00000000 --- a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright (c) 2022. The Khalti Authors. All rights reserved. - -package com.khalti.android - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.view.Gravity -import android.view.View -import android.webkit.* -import android.widget.LinearLayout -import android.widget.LinearLayout.LayoutParams -import android.widget.ProgressBar -import android.window.OnBackInvokedDispatcher -import com.google.android.material.appbar.AppBarLayout -import com.google.android.material.appbar.MaterialToolbar -import com.khalti.android.resource.Url -import com.khalti.android.service.VerificationRepository -import com.khalti.android.cache.Store -import com.khalti.android.data.KhaltiPayConfig -import com.khalti.android.view.EPaymentWebClient - -internal class PaymentActivity : Activity() { - private var receiver: BroadcastReceiver? = null - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val rootLayout = LinearLayout(this) - rootLayout.orientation = LinearLayout.VERTICAL - rootLayout.gravity = Gravity.CENTER - - val params = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) - val appBar = AppBarLayout(this) - - val progressBar = ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal) - progressBar.isIndeterminate = true - - setupBackPressListener() - - val khalti = Store.instance().get("khalti") - if (khalti != null) { - val config = khalti.config - - appBar.addView(toolbar()) - - rootLayout.addView(appBar) - rootLayout.addView(progressBar) - rootLayout.addView( - webView( - config, - onLoadComplete = { - progressBar.visibility = - if (it == 100) ProgressBar.GONE else ProgressBar.VISIBLE - }, - onReturn = { - val verificationRepo = VerificationRepository() - progressBar.visibility = ProgressBar.VISIBLE - verificationRepo.verify(config.pidx, khalti) { - runOnUiThread { - progressBar.visibility = ProgressBar.GONE - } - } - - }, - ), params - ) - - setContentView(rootLayout, params) - registerBroadcast() - } - } - - override fun onDestroy() { - unregisterBroadcast() - super.onDestroy() - } - - @Deprecated( - "Deprecated in Java", ReplaceWith( - "@Suppress(\"DEPRECATION\") super.onBackPressed()", - "android.app.Activity" - ) - ) - override fun onBackPressed() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - onBackAction() - } - @Suppress("DEPRECATION") - super.onBackPressed() - } - - @SuppressLint("UnspecifiedRegisterReceiverFlag") - private fun registerBroadcast() { - receiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - if (intent != null && intent.action.equals("close_khalti_payment_portal")) { - finish() - } - } - } - if (Build.VERSION.SDK_INT >= 26) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - registerReceiver( - receiver, IntentFilter("close_khalti_payment_portal"), - RECEIVER_NOT_EXPORTED - ) - } else { - registerReceiver( - receiver, IntentFilter("close_khalti_payment_portal"), - ) - } - } - } - - private fun unregisterBroadcast() { - unregisterReceiver(receiver) - } - - private fun setupBackPressListener() { - if (Build.VERSION.SDK_INT >= 33) { - val priority = OnBackInvokedDispatcher.PRIORITY_DEFAULT - onBackInvokedDispatcher.registerOnBackInvokedCallback(priority) { - onBackAction() - } - } - } - - private fun onBackAction() { - val khalti = Store.instance().get("khalti") - khalti?.onMessage?.invoke("User Cancelled", khalti, null, null) - } - - // ---------------------------- UI ---------------------------------- // - - private fun toolbar(): View { - val toolbar = MaterialToolbar(this) - toolbar.title = "Pay with Khalti" - toolbar.setNavigationIcon(com.google.android.material.R.drawable.abc_ic_ab_back_material) - toolbar.setNavigationOnClickListener { - finish() - } - - return toolbar - } - - private fun webView( - config: KhaltiPayConfig, - onLoadComplete: (progress: Int) -> Unit, - onReturn: () -> Unit - ): View { - val webView = WebView(this) - val webSettings = webView.settings - - @SuppressLint("SetJavaScriptEnabled") - webSettings.javaScriptEnabled = true - webSettings.domStorageEnabled = true - - webView.webViewClient = EPaymentWebClient(onReturn) - webView.webChromeClient = object : WebChromeClient() { - override fun onProgressChanged(view: WebView?, newProgress: Int) { - onLoadComplete(newProgress) - } - } - - val baseUrl = if (config.isProd()) { - Url.BASE_PAYMENT_URL_PROD - } else { - Url.BASE_PAYMENT_URL_STAGING - } - - val paymentUri = Uri - .parse(baseUrl.value) - .buildUpon() - .appendQueryParameter("pidx", config.pidx) - - webView.clearCache(true) - webView.loadUrl(paymentUri.toString()) - - return webView - } -} - From d7375957d0f2b145fd067dbdd1f7f661cad7eda3 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 11 Mar 2024 14:57:41 +0545 Subject: [PATCH 110/131] Rename paymentV3activity to paymentActivity --- khalti-android/src/main/AndroidManifest.xml | 2 +- ...aymentV3Activity.kt => PaymentActivity.kt} | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) rename khalti-android/src/main/java/com/khalti/android/{PaymentV3Activity.kt => PaymentActivity.kt} (71%) diff --git a/khalti-android/src/main/AndroidManifest.xml b/khalti-android/src/main/AndroidManifest.xml index 6a6577a2..16665fc0 100644 --- a/khalti-android/src/main/AndroidManifest.xml +++ b/khalti-android/src/main/AndroidManifest.xml @@ -5,6 +5,6 @@ - + \ No newline at end of file diff --git a/khalti-android/src/main/java/com/khalti/android/PaymentV3Activity.kt b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt similarity index 71% rename from khalti-android/src/main/java/com/khalti/android/PaymentV3Activity.kt rename to khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt index e534979d..9654e680 100644 --- a/khalti-android/src/main/java/com/khalti/android/PaymentV3Activity.kt +++ b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt @@ -10,10 +10,12 @@ import android.content.IntentFilter import android.os.Build import android.os.Bundle import android.webkit.* +import android.window.OnBackInvokedDispatcher import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.annotation.RequiresApi import com.khalti.android.composable.KhaltiPaymentPage +import com.khalti.android.composable.onBack internal class PaymentV3Activity : ComponentActivity() { private var receiver: BroadcastReceiver? = null @@ -26,6 +28,7 @@ internal class PaymentV3Activity : ComponentActivity() { KhaltiPaymentPage(this) } registerBroadcast() + setupBackPressListener() } override fun onDestroy() { @@ -33,6 +36,18 @@ internal class PaymentV3Activity : ComponentActivity() { super.onDestroy() } + @Deprecated( + "Deprecated in Java", ReplaceWith( + "@Suppress(\"DEPRECATION\") super.onBackPressed()", "android.app.Activity" + ) + ) + override fun onBackPressed() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + onBack() + } + @Suppress("DEPRECATION") super.onBackPressed() + } + @SuppressLint("UnspecifiedRegisterReceiverFlag") private fun registerBroadcast() { receiver = object : BroadcastReceiver() { @@ -58,5 +73,14 @@ internal class PaymentV3Activity : ComponentActivity() { private fun unregisterBroadcast() { unregisterReceiver(receiver) } + + private fun setupBackPressListener() { + if (Build.VERSION.SDK_INT >= 33) { + val priority = OnBackInvokedDispatcher.PRIORITY_DEFAULT + onBackInvokedDispatcher.registerOnBackInvokedCallback(priority) { + onBack() + } + } + } } From 2fca6eb9b50ea10926bd5d9186cecb734218401c Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 11 Mar 2024 14:58:17 +0545 Subject: [PATCH 111/131] Reflect payment activity rename/removal --- khalti-android/src/main/java/com/khalti/android/Khalti.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/Khalti.kt index 8e0d9020..ec5e9cfa 100644 --- a/khalti-android/src/main/java/com/khalti/android/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/Khalti.kt @@ -6,7 +6,6 @@ package com.khalti.android import android.content.Context import android.content.Intent -import android.content.pm.PackageManager.NameNotFoundException import com.khalti.android.service.VerificationRepository import com.khalti.android.data.KhaltiPayConfig import com.khalti.android.callbacks.OnMessage @@ -72,7 +71,7 @@ class Khalti private constructor( store.put("merchant_package_name", packageName) store.put("merchant_package_version", packageInfo?.versionName ?: "") - val intent = Intent(context, PaymentV3Activity::class.java) + val intent = Intent(context, PaymentActivity::class.java) context.startActivity(intent) } From 146db8789f94eedcf92d66155d5f2f33e9c72091 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 11 Mar 2024 14:58:47 +0545 Subject: [PATCH 112/131] Rename PaymentActivity --- .../src/main/java/com/khalti/android/PaymentActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt index 9654e680..891caa87 100644 --- a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt +++ b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt @@ -17,7 +17,7 @@ import androidx.annotation.RequiresApi import com.khalti.android.composable.KhaltiPaymentPage import com.khalti.android.composable.onBack -internal class PaymentV3Activity : ComponentActivity() { +internal class PaymentActivity : ComponentActivity() { private var receiver: BroadcastReceiver? = null @RequiresApi(Build.VERSION_CODES.LOLLIPOP) From 5618c9353aed41cc1d46959790ef97c9e0c82288 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 11 Mar 2024 14:59:35 +0545 Subject: [PATCH 113/131] Replace crowded parameters with single messagePayload --- .../src/main/java/com/khalti/android/callbacks/OnMessage.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/callbacks/OnMessage.kt b/khalti-android/src/main/java/com/khalti/android/callbacks/OnMessage.kt index 3ab0bbbf..e4557012 100644 --- a/khalti-android/src/main/java/com/khalti/android/callbacks/OnMessage.kt +++ b/khalti-android/src/main/java/com/khalti/android/callbacks/OnMessage.kt @@ -4,8 +4,8 @@ package com.khalti.android.callbacks -import com.khalti.android.Khalti +import com.khalti.android.resource.OnMessagePayload fun interface OnMessage { - fun invoke(message: String, khalti: Khalti, throwable: Throwable?, code: Number?) + fun invoke(payload: OnMessagePayload) } \ No newline at end of file From 1c45a5fd2a6ee772b1c3906ec596bc07b78aac81 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 11 Mar 2024 15:00:36 +0545 Subject: [PATCH 114/131] Pass message eventy --- .../android/composable/KhaltiPaymentPage.kt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt b/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt index a8519c69..b9aff79a 100644 --- a/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt +++ b/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt @@ -7,7 +7,6 @@ package com.khalti.android.composable import android.annotation.SuppressLint import android.app.Activity import android.os.Build -import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -36,10 +35,10 @@ import androidx.compose.ui.unit.dp import com.khalti.android.Khalti import com.khalti.android.cache.Store import com.khalti.android.resource.ErrorType +import com.khalti.android.resource.OnMessageEvent +import com.khalti.android.resource.OnMessagePayload import com.khalti.android.service.VerificationRepository import com.khalti.android.utils.NetworkUtil -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @OptIn(ExperimentalMaterial3Api::class) @@ -58,7 +57,7 @@ fun KhaltiPaymentPage(activity: Activity) { TopAppBar( navigationIcon = { IconButton(onClick = { - onBackAction() + onBack() activity.finish() }) { Icon( @@ -146,7 +145,14 @@ fun KhaltiPaymentPage(activity: Activity) { } } -private fun onBackAction() { +fun onBack() { val khalti = Store.instance().get("khalti") - khalti?.onMessage?.invoke("User Cancelled", khalti, null, null) + khalti?.onMessage?.invoke( + OnMessagePayload( + OnMessageEvent.BackPressed, + "User pressed back", + khalti, + needsPaymentConfirmation = true + ) + ) } From 1c2f37dfaea42825afc115e31c498051a3ca7789 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 11 Mar 2024 15:01:34 +0545 Subject: [PATCH 115/131] Add back error handling --- .../android/service/VerificationRepository.kt | 22 +++++++++++- .../khalti/android/view/EPaymentWebClient.kt | 36 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt b/khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt index b5cc0791..e414a1e5 100644 --- a/khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt +++ b/khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt @@ -6,6 +6,9 @@ package com.khalti.android.service import com.khalti.android.Khalti import com.khalti.android.data.PaymentResult +import com.khalti.android.resource.KFailure +import com.khalti.android.resource.OnMessageEvent +import com.khalti.android.resource.OnMessagePayload import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -29,8 +32,25 @@ class VerificationRepository { ) }, err = { + val messageEvent = when (it) { + is KFailure.NoNetwork, is KFailure.ServerUnreachable -> OnMessageEvent.NetworkFailure + is KFailure.HttpCall, is KFailure.Payment -> OnMessageEvent.PaymentLookUpFailure + else -> OnMessageEvent.Unknown + + } + val needsConfirmations = when (it) { + is KFailure.NoNetwork, is KFailure.ServerUnreachable, is KFailure.Generic -> true + else -> false + } khalti.onMessage.invoke( - it.message ?: "", khalti, it.cause, it.code + OnMessagePayload( + messageEvent, + it.message ?: "", + khalti, + it.cause, + it.code, + needsPaymentConfirmation = needsConfirmations + ) ) }, ) diff --git a/khalti-android/src/main/java/com/khalti/android/view/EPaymentWebClient.kt b/khalti-android/src/main/java/com/khalti/android/view/EPaymentWebClient.kt index bad7b20b..35c31afe 100644 --- a/khalti-android/src/main/java/com/khalti/android/view/EPaymentWebClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/view/EPaymentWebClient.kt @@ -6,10 +6,13 @@ package com.khalti.android.view import android.net.Uri import android.os.Build +import android.util.Log import android.webkit.* import androidx.annotation.RequiresApi import com.khalti.android.cache.Store import com.khalti.android.Khalti +import com.khalti.android.resource.OnMessageEvent +import com.khalti.android.resource.OnMessagePayload internal class EPaymentWebClient(val onReturn: () -> Unit) : WebViewClient() { @@ -22,6 +25,21 @@ internal class EPaymentWebClient(val onReturn: () -> Unit) : WebViewClient() { override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean = handleUri(Uri.parse(url)) + @RequiresApi(Build.VERSION_CODES.M) + override fun onReceivedError( + view: WebView?, + request: WebResourceRequest?, + error: WebResourceError? + ) = handleError(request?.url.toString(), error?.description.toString()) + + @SuppressWarnings("deprecation") + @Deprecated("") + override fun onReceivedError( + view: WebView?, + errorCode: Int, + description: String?, + failingUrl: String? + ) = handleError(failingUrl, description) override fun onPageFinished(view: WebView?, url: String?) { super.onPageFinished(view, url) @@ -41,4 +59,22 @@ internal class EPaymentWebClient(val onReturn: () -> Unit) : WebViewClient() { // MPIN url : /account/transaction_pin return false } + + private fun handleError(failingUrl: String?, description: String?) { + val khalti = Store.instance().get("khalti") + if (khalti != null) { + if (description != null) { + if (failingUrl?.startsWith(khalti.config.returnUrl.toString()) != false) { + khalti.onMessage.invoke( + OnMessagePayload( + OnMessageEvent.ReturnUrlLoadFailure, + description, + khalti, + needsPaymentConfirmation = true + ) + ) + } + } + } + } } From 57cc138a7a86f42a8fc7e0c8bbd4c1c040133b3d Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 11 Mar 2024 15:01:51 +0545 Subject: [PATCH 116/131] Re-invent demo screen page --- .../android/demo/composable/DemoScreen.kt | 72 ++++++++++++------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt index 72fc4ed3..e3b6edfc 100644 --- a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt +++ b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt @@ -2,17 +2,20 @@ package com.khalti.android.demo.composable +import android.annotation.SuppressLint import android.net.Uri import android.util.Log import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -20,14 +23,16 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.khalti.android.demo.R -import com.khalti.android.data.Environment import com.khalti.android.Khalti +import com.khalti.android.data.Environment import com.khalti.android.data.KhaltiPayConfig +import com.khalti.android.demo.R -@OptIn(ExperimentalMaterial3Api::class) +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable +@Preview fun DemoScreen() { val khalti = Khalti.init( LocalContext.current, @@ -41,10 +46,13 @@ fun DemoScreen() { Log.i("Demo | onPaymentResult", paymentResult.toString()) khalti.close() }, - onMessage = { message, khalti, throwable, code -> - Log.i("Demo | onMessage ${if (code != null) "($code)" else ""}", message) - khalti.close() - throwable?.printStackTrace() + onMessage = { payload -> + Log.i( + "Demo | onMessage | ${payload.event} ${if (payload.code != null) "(${payload.code})" else ""}", + payload.message + ) + payload.khalti.close() + payload.throwable?.printStackTrace() }, onReturn = { _ -> Log.i("Demo | onReturn", "OnReturn") @@ -52,32 +60,44 @@ fun DemoScreen() { ) Scaffold( - topBar = { - CenterAlignedTopAppBar( - title = { - Text("Khalti Android SDK Demo V3") - } - - ) - }, - content = { padding -> + content = { Column( - Modifier.fillMaxWidth(), + Modifier.fillMaxWidth().fillMaxHeight(), horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceEvenly ) { - Spacer(Modifier.padding(padding)) + Spacer(Modifier.padding(16.dp)) Image( - painterResource(R.drawable.khalti_logo_color), + painterResource(R.mipmap.seru), contentDescription = "Khalti Logo", modifier = Modifier.height(200.dp) ) - FilledTonalButton( - { - khalti.open() - } + Spacer(Modifier.height(50.dp)) + Column( + horizontalAlignment = Alignment.CenterHorizontally, ) { - Text("Pay with Khalti") + Text(text = "Rs. 22", style = MaterialTheme.typography.titleLarge) + Spacer(Modifier.height(8.dp)) + Text(text = "1 day fee", style = MaterialTheme.typography.bodySmall) + Spacer(Modifier.height(8.dp)) + OutlinedButton( + { + khalti.open() + } + ) { + Text("Pay with Khalti") + } } + Spacer(Modifier.height(50.dp)) + Text( + text = "pidx: ${khalti.config.pidx}", + style = MaterialTheme.typography.bodySmall + ) + Spacer(Modifier.height(50.dp)) + Text( + text = "This is a demo application developed by some merchant.", + style = MaterialTheme.typography.bodySmall + ) } }, ) From 2fb857294d1647b2cf746a477005a7e6f0b163c1 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 11 Mar 2024 17:26:43 +0545 Subject: [PATCH 117/131] Add snackbar for onPaymentResult and onMessage callback --- .../android/demo/composable/DemoScreen.kt | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt index e3b6edfc..b0ae3d26 100644 --- a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt +++ b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt @@ -13,12 +13,15 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -29,30 +32,42 @@ import com.khalti.android.Khalti import com.khalti.android.data.Environment import com.khalti.android.data.KhaltiPayConfig import com.khalti.android.demo.R +import kotlinx.coroutines.launch @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable @Preview fun DemoScreen() { + val scope = rememberCoroutineScope() + val snackBarHostState = remember { + SnackbarHostState() + } + val khalti = Khalti.init( LocalContext.current, KhaltiPayConfig( - "live_secret_key_68791341fdd94846a146f0457ff7b455", - "4MNRZPhuY8ZvvyRyXqG2fF", - Uri.parse("https://webhook.site/ed508278-3ce3-4f6d-98f1-0b6084c5c5cd"), + publicKey = "live_public_key_979320ffda734d8e9f7758ac39ec775f", + pidx = "MoiGc4CvewG9RZmDgUtNTk", + returnUrl = Uri.parse("https://webhook.site/ed508278-3ce3-4f6d-98f1-0b6084c5c5cd"), environment = Environment.TEST ), onPaymentResult = { paymentResult, khalti -> Log.i("Demo | onPaymentResult", paymentResult.toString()) khalti.close() + scope.launch { + snackBarHostState.showSnackbar("Payment successful for pidx: ${khalti.config.pidx}") + } }, onMessage = { payload -> Log.i( - "Demo | onMessage | ${payload.event} ${if (payload.code != null) "(${payload.code})" else ""}", - payload.message + "Demo | onMessage", + "${payload.event} ${if (payload.code != null) "(${payload.code})" else ""} | ${payload.message}" ) payload.khalti.close() payload.throwable?.printStackTrace() + scope.launch { + snackBarHostState.showSnackbar("OnMessage: ${payload.message}") + } }, onReturn = { _ -> Log.i("Demo | onReturn", "OnReturn") @@ -60,9 +75,14 @@ fun DemoScreen() { ) Scaffold( + snackbarHost = { + SnackbarHost(hostState = snackBarHostState) + }, content = { Column( - Modifier.fillMaxWidth().fillMaxHeight(), + Modifier + .fillMaxWidth() + .fillMaxHeight(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceEvenly ) { From 3d1b8c2aff75d6d5f0fd57bd201cea8cda9c1812 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Tue, 12 Mar 2024 18:01:36 +0545 Subject: [PATCH 118/131] Add viewmodel for KhaltiPaymentPage --- .../KhaltiPaymentPage.kt | 65 ++++++++++--------- .../android/payment/KhaltiPaymentViewModel.kt | 48 ++++++++++++++ 2 files changed, 81 insertions(+), 32 deletions(-) rename khalti-android/src/main/java/com/khalti/android/{composable => payment}/KhaltiPaymentPage.kt (77%) create mode 100644 khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentViewModel.kt diff --git a/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt b/khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentPage.kt similarity index 77% rename from khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt rename to khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentPage.kt index b9aff79a..61fdbbb8 100644 --- a/khalti-android/src/main/java/com/khalti/android/composable/KhaltiPaymentPage.kt +++ b/khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentPage.kt @@ -9,10 +9,12 @@ import android.app.Activity import android.os.Build import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Refresh @@ -39,6 +41,7 @@ import com.khalti.android.resource.OnMessageEvent import com.khalti.android.resource.OnMessagePayload import com.khalti.android.service.VerificationRepository import com.khalti.android.utils.NetworkUtil +import kotlinx.coroutines.delay @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @OptIn(ExperimentalMaterial3Api::class) @@ -51,6 +54,9 @@ fun KhaltiPaymentPage(activity: Activity) { mutableStateOf(NetworkUtil.isNetworkAvailable(activity)) } val recomposeState = mutableStateOf(false) + val showProgressDialog = remember { + mutableStateOf(false) + } Scaffold( topBar = { Surface(shadowElevation = 4.dp) { @@ -67,7 +73,7 @@ fun KhaltiPaymentPage(activity: Activity) { } }, title = { - Text(text = "Pay With Khalti") + Text(text = "Payment Gateway") }, actions = { IconButton(onClick = { @@ -84,48 +90,43 @@ fun KhaltiPaymentPage(activity: Activity) { }, ) { Surface(modifier = Modifier.padding(top = it.calculateTopPadding())) { + if (showProgressDialog.value) { + KProgressDialog() + } + val khalti = Store.instance().get("khalti") if (khalti != null) { val config = khalti.config - val webView: @Composable () -> Unit = { - KhaltiWebView( - config = config, - onReturnPageLoaded = { - isLoading.value = true - val verificationRepo = VerificationRepository() - verificationRepo.verify(config.pidx, khalti) { - activity.runOnUiThread { - isLoading.value = false - } - } - - }, - onPageLoaded = { - isLoading.value = false - }, - ) - - } if (networkAvailable.value) { - if (isLoading.value) { - Box( - Modifier - .fillMaxSize() - .background(Color.LightGray) - ) { - webView() + Box( + modifier = Modifier.fillMaxSize() + ) { + KhaltiWebView( + config = config, + onReturnPageLoaded = { + showProgressDialog.value = true + val verificationRepo = VerificationRepository() + verificationRepo.verify(config.pidx, khalti) { + showProgressDialog.value = false + } + + }, + onPageLoaded = { + isLoading.value = false + }, + ) + if (isLoading.value) { LinearProgressIndicator( Modifier - .fillMaxWidth() - .height(4.dp) - .align(Alignment.TopCenter), + .height(6.dp) + .width(200.dp) + .align(Alignment.Center), color = Color.Gray ) } - } else { - webView() } + } else { KhaltiError(errorType = ErrorType.network) { networkAvailable.value = NetworkUtil.isNetworkAvailable(activity) diff --git a/khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentViewModel.kt b/khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentViewModel.kt new file mode 100644 index 00000000..b9749e95 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentViewModel.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.payment + +import android.net.Uri +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.khalti.android.Khalti +import com.khalti.android.resource.Url +import com.khalti.android.service.VerificationRepository +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +data class KhaltiPaymentState( + val isLoading: Boolean = true, + val hasNetwork: Boolean = true, +) + +class KhaltiPaymentViewModel() : ViewModel() { + private val _state = MutableStateFlow((KhaltiPaymentState())) + val state: StateFlow = _state + + fun showLoading() { + _state.update { it.copy(isLoading = true) } + } + + fun hideLoading() { + _state.update { it.copy(isLoading = false) } + } + + fun verifyPaymentStatus(khalti: Khalti) { + // TODO (Ishwor) Show progress + val verificationRepo = VerificationRepository() + verificationRepo.verify(khalti.config.pidx, khalti) { + /*no-op*/ + } + } + + fun toggleNetwork(hasNetwork: Boolean) { + _state.update { it.copy(hasNetwork = hasNetwork) } + } +} \ No newline at end of file From 4a8eb7d77ca857627563884195b31a815543e8a3 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Tue, 12 Mar 2024 18:02:02 +0545 Subject: [PATCH 119/131] Move payment page to payment directory --- .../android/payment/KhaltiPaymentPage.kt | 50 ++++++------------- 1 file changed, 16 insertions(+), 34 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentPage.kt b/khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentPage.kt index 61fdbbb8..47d644f3 100644 --- a/khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentPage.kt +++ b/khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentPage.kt @@ -2,16 +2,13 @@ * Copyright (c) 2024. The Khalti Authors. All rights reserved. */ -package com.khalti.android.composable +package com.khalti.android.payment import android.annotation.SuppressLint import android.app.Activity import android.os.Build -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -28,35 +25,28 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.khalti.android.Khalti import com.khalti.android.cache.Store +import com.khalti.android.composable.KhaltiError +import com.khalti.android.composable.KhaltiWebView import com.khalti.android.resource.ErrorType import com.khalti.android.resource.OnMessageEvent import com.khalti.android.resource.OnMessagePayload -import com.khalti.android.service.VerificationRepository import com.khalti.android.utils.NetworkUtil -import kotlinx.coroutines.delay @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @OptIn(ExperimentalMaterial3Api::class) @Composable -fun KhaltiPaymentPage(activity: Activity) { - val isLoading = remember { - mutableStateOf(true) - } - val networkAvailable = remember { - mutableStateOf(NetworkUtil.isNetworkAvailable(activity)) - } +fun KhaltiPaymentPage(activity: Activity, viewModel: KhaltiPaymentViewModel) { + val state by viewModel.state.collectAsState() val recomposeState = mutableStateOf(false) - val showProgressDialog = remember { - mutableStateOf(false) - } Scaffold( topBar = { Surface(shadowElevation = 4.dp) { @@ -90,33 +80,25 @@ fun KhaltiPaymentPage(activity: Activity) { }, ) { Surface(modifier = Modifier.padding(top = it.calculateTopPadding())) { - if (showProgressDialog.value) { - KProgressDialog() - } - val khalti = Store.instance().get("khalti") if (khalti != null) { val config = khalti.config - - if (networkAvailable.value) { + if (state.hasNetwork) { Box( - modifier = Modifier.fillMaxSize() + Modifier + .fillMaxSize() ) { KhaltiWebView( config = config, onReturnPageLoaded = { - showProgressDialog.value = true - val verificationRepo = VerificationRepository() - verificationRepo.verify(config.pidx, khalti) { - showProgressDialog.value = false - } + viewModel.verifyPaymentStatus(khalti) }, onPageLoaded = { - isLoading.value = false + viewModel.hideLoading() }, ) - if (isLoading.value) { + if (state.isLoading) { LinearProgressIndicator( Modifier .height(6.dp) @@ -129,7 +111,7 @@ fun KhaltiPaymentPage(activity: Activity) { } else { KhaltiError(errorType = ErrorType.network) { - networkAvailable.value = NetworkUtil.isNetworkAvailable(activity) + viewModel.toggleNetwork(NetworkUtil.isNetworkAvailable(activity)) } } } @@ -137,10 +119,10 @@ fun KhaltiPaymentPage(activity: Activity) { } - LaunchedEffect(networkAvailable.value && recomposeState.value) { + LaunchedEffect(state.hasNetwork && recomposeState.value) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { NetworkUtil.registerListener(activity) { - networkAvailable.value = it + viewModel.toggleNetwork(it) } } } From 336491d497fbcdeeda8fc50ad5856b2860af78fc Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Tue, 12 Mar 2024 18:02:38 +0545 Subject: [PATCH 120/131] Reflect addition of view model --- .../java/com/khalti/android/demo/composable/DemoScreen.kt | 8 ++++---- .../src/main/java/com/khalti/android/PaymentActivity.kt | 7 ++++--- .../java/com/khalti/android/view/EPaymentWebClient.kt | 3 +-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt index b0ae3d26..8d7cfa99 100644 --- a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt +++ b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt @@ -47,13 +47,13 @@ fun DemoScreen() { LocalContext.current, KhaltiPayConfig( publicKey = "live_public_key_979320ffda734d8e9f7758ac39ec775f", - pidx = "MoiGc4CvewG9RZmDgUtNTk", + pidx = "tF999DgVNFG26uHHJJCqZb", returnUrl = Uri.parse("https://webhook.site/ed508278-3ce3-4f6d-98f1-0b6084c5c5cd"), environment = Environment.TEST ), onPaymentResult = { paymentResult, khalti -> Log.i("Demo | onPaymentResult", paymentResult.toString()) - khalti.close() +// khalti.close() scope.launch { snackBarHostState.showSnackbar("Payment successful for pidx: ${khalti.config.pidx}") } @@ -61,7 +61,7 @@ fun DemoScreen() { onMessage = { payload -> Log.i( "Demo | onMessage", - "${payload.event} ${if (payload.code != null) "(${payload.code})" else ""} | ${payload.message}" + "${payload.event} ${if (payload.code != null) "(${payload.code})" else ""} | ${payload.message} | ${payload.needsPaymentConfirmation}" ) payload.khalti.close() payload.throwable?.printStackTrace() @@ -105,7 +105,7 @@ fun DemoScreen() { khalti.open() } ) { - Text("Pay with Khalti") + Text("Pay Rs. 22") } } Spacer(Modifier.height(50.dp)) diff --git a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt index 891caa87..fedf134a 100644 --- a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt +++ b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt @@ -14,8 +14,9 @@ import android.window.OnBackInvokedDispatcher import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.annotation.RequiresApi -import com.khalti.android.composable.KhaltiPaymentPage -import com.khalti.android.composable.onBack +import com.khalti.android.payment.KhaltiPaymentPage +import com.khalti.android.payment.KhaltiPaymentViewModel +import com.khalti.android.payment.onBack internal class PaymentActivity : ComponentActivity() { private var receiver: BroadcastReceiver? = null @@ -25,7 +26,7 @@ internal class PaymentActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { - KhaltiPaymentPage(this) + KhaltiPaymentPage(this, KhaltiPaymentViewModel()) } registerBroadcast() setupBackPressListener() diff --git a/khalti-android/src/main/java/com/khalti/android/view/EPaymentWebClient.kt b/khalti-android/src/main/java/com/khalti/android/view/EPaymentWebClient.kt index 35c31afe..5c764ef6 100644 --- a/khalti-android/src/main/java/com/khalti/android/view/EPaymentWebClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/view/EPaymentWebClient.kt @@ -6,11 +6,10 @@ package com.khalti.android.view import android.net.Uri import android.os.Build -import android.util.Log import android.webkit.* import androidx.annotation.RequiresApi -import com.khalti.android.cache.Store import com.khalti.android.Khalti +import com.khalti.android.cache.Store import com.khalti.android.resource.OnMessageEvent import com.khalti.android.resource.OnMessagePayload From 34bc5f92341dbf81626e704dee0c28dcb0b28f61 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Wed, 13 Mar 2024 15:53:50 +0545 Subject: [PATCH 121/131] Add progress dialog --- .../android/composable/KProgressDialog.kt | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 khalti-android/src/main/java/com/khalti/android/composable/KProgressDialog.kt diff --git a/khalti-android/src/main/java/com/khalti/android/composable/KProgressDialog.kt b/khalti-android/src/main/java/com/khalti/android/composable/KProgressDialog.kt new file mode 100644 index 00000000..93ee3849 --- /dev/null +++ b/khalti-android/src/main/java/com/khalti/android/composable/KProgressDialog.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024. The Khalti Authors. All rights reserved. + */ + +package com.khalti.android.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties + +@Composable +fun KProgressDialog() { + Dialog( + onDismissRequest = { + /*no-op*/ + }, + DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(100.dp) + .background(Color.White, shape = RoundedCornerShape(8.dp)) + ) { + CircularProgressIndicator() + } + } +} \ No newline at end of file From e79b72c5c91e322f0a63283da561509d2d11f0e8 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Wed, 13 Mar 2024 15:57:33 +0545 Subject: [PATCH 122/131] Move loadUrl to factory lamba --- .../android/composable/KhaltiWebview.kt | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/khalti-android/src/main/java/com/khalti/android/composable/KhaltiWebview.kt b/khalti-android/src/main/java/com/khalti/android/composable/KhaltiWebview.kt index 7e382708..cd6a65d2 100644 --- a/khalti-android/src/main/java/com/khalti/android/composable/KhaltiWebview.kt +++ b/khalti-android/src/main/java/com/khalti/android/composable/KhaltiWebview.kt @@ -6,6 +6,7 @@ package com.khalti.android.composable import android.annotation.SuppressLint import android.net.Uri +import android.util.Log import android.webkit.WebChromeClient import android.webkit.WebView import androidx.compose.foundation.layout.fillMaxSize @@ -40,19 +41,21 @@ fun KhaltiWebView( } } this.clearCache(true) - } - }, - update = { - val baseUrl = if (config.isProd()) { - Url.BASE_PAYMENT_URL_PROD - } else { - Url.BASE_PAYMENT_URL_STAGING - } - val paymentUri = - Uri.parse(baseUrl.value).buildUpon().appendQueryParameter("pidx", config.pidx) + val baseUrl = if (config.isProd()) { + Url.BASE_PAYMENT_URL_PROD + } else { + Url.BASE_PAYMENT_URL_STAGING + } + + val paymentUri = + Uri.parse(baseUrl.value).buildUpon().appendQueryParameter("pidx", config.pidx) - it.loadUrl(paymentUri.toString()) + this.loadUrl(paymentUri.toString()) + } }, + update = { + it.loadUrl("javascript:window.location.reload(true)") + } ) } From 1c50727973a80c5203e3cc74972cb74e51369352 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Wed, 13 Mar 2024 15:58:40 +0545 Subject: [PATCH 123/131] Show progress dialog on verification --- .../android/demo/composable/DemoScreen.kt | 2 +- .../android/payment/KhaltiPaymentPage.kt | 11 +++++-- .../android/payment/KhaltiPaymentViewModel.kt | 30 ++++++++----------- .../khalti/android/view/EPaymentWebClient.kt | 1 - 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt index 8d7cfa99..f8c4fdbc 100644 --- a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt +++ b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt @@ -47,7 +47,7 @@ fun DemoScreen() { LocalContext.current, KhaltiPayConfig( publicKey = "live_public_key_979320ffda734d8e9f7758ac39ec775f", - pidx = "tF999DgVNFG26uHHJJCqZb", + pidx = "ioeYNt2ReVsUqodQgrZsxi", returnUrl = Uri.parse("https://webhook.site/ed508278-3ce3-4f6d-98f1-0b6084c5c5cd"), environment = Environment.TEST ), diff --git a/khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentPage.kt b/khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentPage.kt index 47d644f3..dc83adf0 100644 --- a/khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentPage.kt +++ b/khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentPage.kt @@ -7,6 +7,7 @@ package com.khalti.android.payment import android.annotation.SuppressLint import android.app.Activity import android.os.Build +import android.webkit.WebView import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height @@ -34,6 +35,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.khalti.android.Khalti import com.khalti.android.cache.Store +import com.khalti.android.composable.KProgressDialog import com.khalti.android.composable.KhaltiError import com.khalti.android.composable.KhaltiWebView import com.khalti.android.resource.ErrorType @@ -47,6 +49,7 @@ import com.khalti.android.utils.NetworkUtil fun KhaltiPaymentPage(activity: Activity, viewModel: KhaltiPaymentViewModel) { val state by viewModel.state.collectAsState() val recomposeState = mutableStateOf(false) + Scaffold( topBar = { Surface(shadowElevation = 4.dp) { @@ -82,6 +85,11 @@ fun KhaltiPaymentPage(activity: Activity, viewModel: KhaltiPaymentViewModel) { Surface(modifier = Modifier.padding(top = it.calculateTopPadding())) { val khalti = Store.instance().get("khalti") if (khalti != null) { + + if (state.progressDialog) { + KProgressDialog() + } + val config = khalti.config if (state.hasNetwork) { Box( @@ -92,10 +100,9 @@ fun KhaltiPaymentPage(activity: Activity, viewModel: KhaltiPaymentViewModel) { config = config, onReturnPageLoaded = { viewModel.verifyPaymentStatus(khalti) - }, onPageLoaded = { - viewModel.hideLoading() + viewModel.toggleLoading(false) }, ) if (state.isLoading) { diff --git a/khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentViewModel.kt b/khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentViewModel.kt index b9749e95..1f2eebe9 100644 --- a/khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentViewModel.kt +++ b/khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentViewModel.kt @@ -4,45 +4,41 @@ package com.khalti.android.payment -import android.net.Uri -import android.util.Log import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.khalti.android.Khalti -import com.khalti.android.resource.Url import com.khalti.android.service.VerificationRepository -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch data class KhaltiPaymentState( val isLoading: Boolean = true, val hasNetwork: Boolean = true, + val progressDialog: Boolean = false, ) -class KhaltiPaymentViewModel() : ViewModel() { +class KhaltiPaymentViewModel : ViewModel() { private val _state = MutableStateFlow((KhaltiPaymentState())) val state: StateFlow = _state - fun showLoading() { - _state.update { it.copy(isLoading = true) } - } - - fun hideLoading() { - _state.update { it.copy(isLoading = false) } - } - fun verifyPaymentStatus(khalti: Khalti) { - // TODO (Ishwor) Show progress + toggleProgressDialog() val verificationRepo = VerificationRepository() verificationRepo.verify(khalti.config.pidx, khalti) { - /*no-op*/ + toggleProgressDialog(false) } } fun toggleNetwork(hasNetwork: Boolean) { _state.update { it.copy(hasNetwork = hasNetwork) } } + + + fun toggleLoading(show: Boolean = true) { + _state.update { it.copy(isLoading = show) } + } + + private fun toggleProgressDialog(show: Boolean = true) { + _state.update { it.copy(progressDialog = show) } + } } \ No newline at end of file diff --git a/khalti-android/src/main/java/com/khalti/android/view/EPaymentWebClient.kt b/khalti-android/src/main/java/com/khalti/android/view/EPaymentWebClient.kt index 5c764ef6..1ac6bff0 100644 --- a/khalti-android/src/main/java/com/khalti/android/view/EPaymentWebClient.kt +++ b/khalti-android/src/main/java/com/khalti/android/view/EPaymentWebClient.kt @@ -14,7 +14,6 @@ import com.khalti.android.resource.OnMessageEvent import com.khalti.android.resource.OnMessagePayload internal class EPaymentWebClient(val onReturn: () -> Unit) : WebViewClient() { - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean = handleUri(request!!.url) From f8e118270d36d1af8f0ba02a5e278abb2af6660e Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Wed, 13 Mar 2024 16:00:25 +0545 Subject: [PATCH 124/131] Close khalti on payment completion --- .../main/java/com/khalti/android/demo/composable/DemoScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt index f8c4fdbc..8ae74016 100644 --- a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt +++ b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt @@ -53,7 +53,7 @@ fun DemoScreen() { ), onPaymentResult = { paymentResult, khalti -> Log.i("Demo | onPaymentResult", paymentResult.toString()) -// khalti.close() + khalti.close() scope.launch { snackBarHostState.showSnackbar("Payment successful for pidx: ${khalti.config.pidx}") } From c1431672b96d587844d39b861e8ef1587cb9369b Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 18 Mar 2024 14:05:08 +0545 Subject: [PATCH 125/131] Upgrade dependencies --- app/build.gradle | 2 +- build.gradle | 4 ++-- dependencies.gradle | 2 +- khalti-android/build.gradle | 7 +++---- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c567c0d6..ee0b1151 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -60,7 +60,7 @@ dependencies { implementation 'androidx.activity:activity-compose:1.8.2' implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" - implementation 'androidx.compose.material3:material3:1.2.0' + implementation 'androidx.compose.material3:material3:1.2.1' //implementation "com.khalti:khalti-android:$khaltiVersionName" implementation project(path: ':khalti-android') diff --git a/build.gradle b/build.gradle index 08f497f3..699cedb5 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { - id 'com.android.application' version '8.2.0' apply false - id 'com.android.library' version '8.2.0' apply false + id 'com.android.application' version '8.3.0' apply false + id 'com.android.library' version '8.3.0' apply false id 'org.jetbrains.kotlin.android' version '1.9.22' apply false id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' } diff --git a/dependencies.gradle b/dependencies.gradle index b3d06e1a..e66759bc 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,6 +1,6 @@ ext { appcompat_version = '1.6.1' - compose_version = '1.5.4' + compose_version = '1.6.3' core_ktx_version = '1.12.0' material_version = '1.11.0' diff --git a/khalti-android/build.gradle b/khalti-android/build.gradle index c7cb1365..4df606c4 100644 --- a/khalti-android/build.gradle +++ b/khalti-android/build.gradle @@ -20,7 +20,6 @@ android { defaultConfig { minSdk libraryMinSdk targetSdk libraryTargetSdk - versionName khaltiVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -56,12 +55,12 @@ dependencies { // ---------- Api ---------- implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' - implementation 'com.squareup.okhttp3:logging-interceptor:4.11.0' + implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0' // ---------- Compose ---------- def composeBom = platform('androidx.compose:compose-bom:2024.02.01') - implementation composeBom - androidTestImplementation composeBom + implementation platform('androidx.compose:compose-bom:2024.02.02') + androidTestImplementation platform('androidx.compose:compose-bom:2024.02.02') implementation 'androidx.compose.material3:material3' implementation 'androidx.activity:activity-compose:1.8.2' From 944c7a4aa9a641845e452f4f6f254e33971d4882 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 18 Mar 2024 14:05:31 +0545 Subject: [PATCH 126/131] Increase minimum sdk to 21 --- metadata.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/metadata.gradle b/metadata.gradle index 0f9c92ae..ffe972b4 100644 --- a/metadata.gradle +++ b/metadata.gradle @@ -1,8 +1,8 @@ ext { - khaltiVersionCode = 3000000 - khaltiVersionName = '3.00.00' + khaltiVersionCode = 2001003 + khaltiVersionName = '2.01.03' - libraryMinSdk = 16 + libraryMinSdk = 21 libraryCompileSdk = 34 libraryTargetSdk = 34 } \ No newline at end of file From 214459d85e2ed08d916a90d13f3be585ca532d76 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Mon, 18 Mar 2024 14:05:40 +0545 Subject: [PATCH 127/131] Upgrade gradle --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ef105d4e..20c89899 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Oct 31 20:08:12 NPT 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From b91f0244bae3a1fc35e186ca91d945d2751900b6 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Tue, 19 Mar 2024 15:19:44 +0545 Subject: [PATCH 128/131] Upgrade dependencies --- khalti-android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/khalti-android/build.gradle b/khalti-android/build.gradle index 4df606c4..fe748747 100644 --- a/khalti-android/build.gradle +++ b/khalti-android/build.gradle @@ -58,7 +58,7 @@ dependencies { implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0' // ---------- Compose ---------- - def composeBom = platform('androidx.compose:compose-bom:2024.02.01') + def composeBom = platform('androidx.compose:compose-bom:2024.02.02') implementation platform('androidx.compose:compose-bom:2024.02.02') androidTestImplementation platform('androidx.compose:compose-bom:2024.02.02') From bd2f67a65bffc0695ad1b77de6d0c71ac1a3cf71 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Tue, 19 Mar 2024 15:20:31 +0545 Subject: [PATCH 129/131] Update scripts for deployment --- scripts/publish-module.gradle | 10 +++------- scripts/publish-root.gradle | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/scripts/publish-module.gradle b/scripts/publish-module.gradle index 18bf705b..b83ea730 100644 --- a/scripts/publish-module.gradle +++ b/scripts/publish-module.gradle @@ -2,6 +2,7 @@ apply plugin: 'maven-publish' apply plugin: 'signing' tasks.register('androidSourcesJar', Jar) { + dependsOn('generateMetadataFileForKhaltiPublication') archiveClassifier.set('sources') if (project.plugins.findPlugin("com.android.library")) { from android.sourceSets.main.java.srcDirs @@ -22,9 +23,7 @@ version = PUBLISH_VERSION afterEvaluate { publishing { publications { - release(MavenPublication) { - // The coordinates of the library, being set from variables that - // we'll set up later + khalti(MavenPublication) { groupId PUBLISH_GROUP_ID artifactId PUBLISH_ARTIFACT_ID version PUBLISH_VERSION @@ -36,9 +35,8 @@ afterEvaluate { from components.java } - artifact androidSourcesJar +// artifact androidSourcesJar - // Mostly self-explanatory metadata pom { name = PUBLISH_ARTIFACT_ID description = PUBLISH_DESCRIPTION @@ -57,8 +55,6 @@ afterEvaluate { } } - // Version control info - if you're using GitHub, follow the - // format as seen here scm { connection = PUBLISH_SCM_CONNECTION developerConnection = PUBLISH_SCM_DEVELOPER_CONNECTION diff --git a/scripts/publish-root.gradle b/scripts/publish-root.gradle index 2754fb6d..7658270a 100644 --- a/scripts/publish-root.gradle +++ b/scripts/publish-root.gradle @@ -1,7 +1,7 @@ // Create variables with empty default values ext["signing.keyId"] = '' ext["signing.password"] = '' -ext["signing.secretKeyRingFile"] = '' +ext["signing.key"] = '' ext["ossrhUsername"] = '' ext["ossrhPassword"] = '' ext["sonatypeStagingProfileId"] = '' From 3b246afeb0ca55d6e3e8034f513a6e2b77d0e134 Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Tue, 19 Mar 2024 16:20:49 +0545 Subject: [PATCH 130/131] Make pidx and environment editable in demo app --- app/build.gradle | 2 +- .../android/demo/composable/DemoScreen.kt | 78 ++++++++++++++++--- .../main/java/com/khalti/android/Khalti.kt | 2 +- 3 files changed, 70 insertions(+), 12 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ee0b1151..88c7abf1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -62,7 +62,7 @@ dependencies { implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" implementation 'androidx.compose.material3:material3:1.2.1' - //implementation "com.khalti:khalti-android:$khaltiVersionName" +// implementation "com.khalti:khalti-android:$khaltiVersionName" implementation project(path: ':khalti-android') testImplementation "junit:junit:$junit_version" diff --git a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt index 8ae74016..84aa60e7 100644 --- a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt +++ b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt @@ -6,24 +6,34 @@ import android.annotation.SuppressLint import android.net.Uri import android.util.Log import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.modifier.modifierLocalConsumer import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview @@ -38,6 +48,7 @@ import kotlinx.coroutines.launch @Composable @Preview fun DemoScreen() { + val scrollState = rememberScrollState() val scope = rememberCoroutineScope() val snackBarHostState = remember { SnackbarHostState() @@ -47,7 +58,7 @@ fun DemoScreen() { LocalContext.current, KhaltiPayConfig( publicKey = "live_public_key_979320ffda734d8e9f7758ac39ec775f", - pidx = "ioeYNt2ReVsUqodQgrZsxi", + pidx = "Prd42EcFeqvVKpHRGN3ZUZ", returnUrl = Uri.parse("https://webhook.site/ed508278-3ce3-4f6d-98f1-0b6084c5c5cd"), environment = Environment.TEST ), @@ -74,6 +85,14 @@ fun DemoScreen() { } ) + val pidx = remember { + mutableStateOf(khalti.config.pidx) + } + val environments = enumValues() + val selectedEnvironment = remember { + mutableStateOf(khalti.config.environment) + } + Scaffold( snackbarHost = { SnackbarHost(hostState = snackBarHostState) @@ -82,17 +101,20 @@ fun DemoScreen() { Column( Modifier .fillMaxWidth() - .fillMaxHeight(), + .fillMaxHeight() + .scrollable( + state = scrollState, + orientation = Orientation.Vertical + ), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceEvenly ) { Spacer(Modifier.padding(16.dp)) Image( painterResource(R.mipmap.seru), contentDescription = "Khalti Logo", - modifier = Modifier.height(200.dp) + modifier = Modifier.height(180.dp) ) - Spacer(Modifier.height(50.dp)) + Spacer(Modifier.height(30.dp)) Column( horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -102,18 +124,54 @@ fun DemoScreen() { Spacer(Modifier.height(8.dp)) OutlinedButton( { + if (pidx.value != khalti.config.pidx) { + khalti.config = khalti.config.copy(pidx = pidx.value) + } + + if (selectedEnvironment.value != khalti.config.environment) { + khalti.config = + khalti.config.copy(environment = selectedEnvironment.value) + } + khalti.open() } ) { Text("Pay Rs. 22") } } - Spacer(Modifier.height(50.dp)) - Text( - text = "pidx: ${khalti.config.pidx}", - style = MaterialTheme.typography.bodySmall + Spacer(Modifier.height(30.dp)) + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + value = pidx.value, + onValueChange = { + pidx.value = it + }, + label = { Text(text = "PIDX") }, ) - Spacer(Modifier.height(50.dp)) + Spacer(Modifier.height(20.dp)) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + Text(text = "Environment") + environments.forEach { + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = (it == selectedEnvironment.value), + onClick = { selectedEnvironment.value = it } + ) + Text( + text = it.name, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + } + Spacer(Modifier.height(30.dp)) Text( text = "This is a demo application developed by some merchant.", style = MaterialTheme.typography.bodySmall diff --git a/khalti-android/src/main/java/com/khalti/android/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/Khalti.kt index ec5e9cfa..7af272f1 100644 --- a/khalti-android/src/main/java/com/khalti/android/Khalti.kt +++ b/khalti-android/src/main/java/com/khalti/android/Khalti.kt @@ -18,7 +18,7 @@ import com.khalti.android.utils.PackageUtil // method overloading was required for Java developers class Khalti private constructor( private val context: Context, - val config: KhaltiPayConfig, + var config: KhaltiPayConfig, val onPaymentResult: OnPaymentResult, val onMessage: OnMessage, val onReturn: OnReturn?, From f9ffac70f496c6d017403b915517e06fb0c8b3cd Mon Sep 17 00:00:00 2001 From: Ishwor-Shrestha Date: Wed, 20 Mar 2024 15:16:08 +0545 Subject: [PATCH 131/131] Add option to add pidx, public key, return url and environment from the UI --- .../android/demo/composable/DemoScreen.kt | 90 ++++++++++++------- 1 file changed, 59 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt index 84aa60e7..9ec7d86e 100644 --- a/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt +++ b/app/src/main/java/com/khalti/android/demo/composable/DemoScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.RadioButton @@ -85,9 +86,15 @@ fun DemoScreen() { } ) + val publicKey = remember { + mutableStateOf(khalti.config.publicKey) + } val pidx = remember { mutableStateOf(khalti.config.pidx) } + val returnUrl = remember { + mutableStateOf(khalti.config.returnUrl) + } val environments = enumValues() val selectedEnvironment = remember { mutableStateOf(khalti.config.environment) @@ -100,12 +107,7 @@ fun DemoScreen() { content = { Column( Modifier - .fillMaxWidth() - .fillMaxHeight() - .scrollable( - state = scrollState, - orientation = Orientation.Vertical - ), + .verticalScroll(state = scrollState), horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(Modifier.padding(16.dp)) @@ -115,31 +117,28 @@ fun DemoScreen() { modifier = Modifier.height(180.dp) ) Spacer(Modifier.height(30.dp)) - Column( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text(text = "Rs. 22", style = MaterialTheme.typography.titleLarge) - Spacer(Modifier.height(8.dp)) - Text(text = "1 day fee", style = MaterialTheme.typography.bodySmall) - Spacer(Modifier.height(8.dp)) - OutlinedButton( - { - if (pidx.value != khalti.config.pidx) { - khalti.config = khalti.config.copy(pidx = pidx.value) - } - - if (selectedEnvironment.value != khalti.config.environment) { - khalti.config = - khalti.config.copy(environment = selectedEnvironment.value) - } - - khalti.open() - } - ) { - Text("Pay Rs. 22") - } - } - Spacer(Modifier.height(30.dp)) + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + value = publicKey.value, + onValueChange = { + publicKey.value = it + }, + label = { Text(text = "Public Key") }, + ) + Spacer(Modifier.height(20.dp)) + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + value = returnUrl.value.toString(), + onValueChange = { + returnUrl.value = Uri.parse(it) + }, + label = { Text(text = "Return Url") }, + ) + Spacer(Modifier.height(20.dp)) TextField( modifier = Modifier .fillMaxWidth() @@ -172,6 +171,35 @@ fun DemoScreen() { } } Spacer(Modifier.height(30.dp)) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text(text = "Rs. 22", style = MaterialTheme.typography.titleLarge) + Spacer(Modifier.height(8.dp)) + Text(text = "1 day fee", style = MaterialTheme.typography.bodySmall) + Spacer(Modifier.height(8.dp)) + OutlinedButton( + { + if (publicKey.value != khalti.config.publicKey + || returnUrl.value != khalti.config.returnUrl + || pidx.value != khalti.config.pidx + || selectedEnvironment.value != khalti.config.environment + ) { + khalti.config = khalti.config.copy( + publicKey = publicKey.value, + returnUrl = returnUrl.value, + pidx = pidx.value, + environment = selectedEnvironment.value, + ) + } + + khalti.open() + } + ) { + Text("Pay Rs. 22") + } + } + Spacer(Modifier.height(30.dp)) Text( text = "This is a demo application developed by some merchant.", style = MaterialTheme.typography.bodySmall