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
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
deleted file mode 100644
index fb7f4a8a..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/gradle.xml b/.idea/gradle.xml
deleted file mode 100644
index 30b621c0..00000000
--- a/.idea/gradle.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index cb1a6338..00000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7f..00000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 2bda88c7..88c7abf1 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.5.10'
}
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,13 +56,13 @@ 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.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/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()
}
}
}
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..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
@@ -2,121 +2,209 @@
package com.khalti.android.demo.composable
+import android.annotation.SuppressLint
+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.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.foundation.verticalScroll
+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.text.input.TextFieldValue
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import com.khalti.android.*
+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
-const val RESULT_TAG = "KHALTI_PAY_RESULT"
-
-@OptIn(ExperimentalMaterial3Api::class)
+@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
+@Preview
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)
+ val scrollState = rememberScrollState()
+ val scope = rememberCoroutineScope()
+ val snackBarHostState = remember {
+ SnackbarHostState()
+ }
- when (it) {
- is PaymentSuccess -> {
- Log.i(RESULT_TAG, "Payment Success")
+ val khalti = Khalti.init(
+ LocalContext.current,
+ KhaltiPayConfig(
+ publicKey = "live_public_key_979320ffda734d8e9f7758ac39ec775f",
+ pidx = "Prd42EcFeqvVKpHRGN3ZUZ",
+ 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}")
}
- is PaymentError -> {
- Log.i(RESULT_TAG, "Payment Error")
- }
- is PaymentCancelled -> {
- Log.i(RESULT_TAG, "Payment Cancelled")
+ },
+ onMessage = { payload ->
+ Log.i(
+ "Demo | onMessage",
+ "${payload.event} ${if (payload.code != null) "(${payload.code})" else ""} | ${payload.message} | ${payload.needsPaymentConfirmation}"
+ )
+ payload.khalti.close()
+ payload.throwable?.printStackTrace()
+ scope.launch {
+ snackBarHostState.showSnackbar("OnMessage: ${payload.message}")
}
+ },
+ onReturn = { _ ->
+ Log.i("Demo | onReturn", "OnReturn")
}
- }
+ )
- if (result != null) {
- ResultDialog(result, setResult)
+ 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)
}
-
Scaffold(
- topBar = {
- CenterAlignedTopAppBar(
- title = {
- Text("Khalti Android SDK Demo")
- }
-
- )
+ snackbarHost = {
+ SnackbarHost(hostState = snackBarHostState)
},
- content = { padding ->
+ content = {
Column(
- Modifier.fillMaxWidth(),
+ Modifier
+ .verticalScroll(state = scrollState),
horizontalAlignment = Alignment.CenterHorizontally,
) {
- 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)
+ modifier = Modifier.height(180.dp)
)
- OutlinedTextField(
- value = paymentUrl,
- label = { Text("Payment URL") },
- placeholder = { Text("Enter payment URL") },
+ Spacer(Modifier.height(30.dp))
+ TextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ value = publicKey.value,
onValueChange = {
- paymentUrl = it
- urlErrorMessage = ""
+ publicKey.value = it
},
+ label = { Text(text = "Public Key") },
+ )
+ Spacer(Modifier.height(20.dp))
+ TextField(
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") },
+ .padding(horizontal = 16.dp),
+ value = returnUrl.value.toString(),
onValueChange = {
- returnUrl = it
+ returnUrl.value = Uri.parse(it)
},
+ label = { Text(text = "Return Url") },
+ )
+ Spacer(Modifier.height(20.dp))
+ TextField(
modifier = Modifier
.fillMaxWidth()
- .padding(horizontal = 24.dp)
+ .padding(horizontal = 16.dp),
+ value = pidx.value,
+ onValueChange = {
+ pidx.value = it
+ },
+ label = { Text(text = "PIDX") },
)
- Spacer(Modifier.height(40.dp))
- FilledTonalButton(
- {
- try {
- val config = KhaltiPayConfiguration(
- paymentUrl.text,
- returnUrl.text
+ 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)
)
- khaltiPay.launch(config)
- } catch (e: Exception) {
- val message = e.message ?: ""
-
- when (e) {
- is IllegalArgumentException, is IllegalStateException -> {
- urlErrorMessage = message
- }
- else -> Log.e(RESULT_TAG, message)
- }
}
}
+ }
+ Spacer(Modifier.height(30.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(
+ {
+ 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
+ )
}
},
)
-}
\ No newline at end of file
+}
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)
- },
- )
-}
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 00000000..1c75e51d
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/seru.png differ
diff --git a/build.gradle b/build.gradle
index bd9dade1..699cedb5 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.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 5ca6a668..e66759bc 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -1,10 +1,10 @@
ext {
- appcompat_version = '1.5.1'
- compose_version = '1.3.0'
- core_ktx_version = '1.9.0'
- material_version = '1.7.0'
+ appcompat_version = '1.6.1'
+ compose_version = '1.6.3'
+ 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
diff --git a/gradle.properties b/gradle.properties
index 3c5031eb..f19c7b9b 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -20,4 +20,5 @@ 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.nonFinalResIds=false
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index aaf5e0bc..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-7.4-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
diff --git a/khalti-android/build.gradle b/khalti-android/build.gradle
index d81c9301..fe748747 100644
--- a/khalti-android/build.gradle
+++ b/khalti-android/build.gradle
@@ -1,12 +1,22 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
+ id 'kotlin-parcelize'
}
android {
namespace 'com.khalti.android'
compileSdk libraryCompileSdk
+ buildFeatures {
+ buildConfig true
+ compose true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.10"
+ }
+
defaultConfig {
minSdk libraryMinSdk
targetSdk libraryTargetSdk
@@ -16,25 +26,53 @@ android {
}
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 {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
+
kotlinOptions {
jvmTarget = '1.8'
}
}
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.12.0'
+
+ // ---------- Compose ----------
+ 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')
+
+ 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'
+ debugImplementation 'androidx.compose.ui:ui-tooling'
+
+ // ---------- Test ----------
testImplementation "junit:junit:$junit_version"
+
androidTestImplementation "androidx.test.ext:junit:$junit_ext_version"
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
}
diff --git a/khalti-android/src/main/AndroidManifest.xml b/khalti-android/src/main/AndroidManifest.xml
index 8c42db92..16665fc0 100644
--- a/khalti-android/src/main/AndroidManifest.xml
+++ b/khalti-android/src/main/AndroidManifest.xml
@@ -1,5 +1,7 @@
+
+
diff --git a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt b/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt
deleted file mode 100644
index 65a69fc2..00000000
--- a/khalti-android/src/main/java/com/khalti/android/EPaymentWebClient.kt
+++ /dev/null
@@ -1,84 +0,0 @@
-// Copyright (c) 2022. The Khalti Authors. All rights reserved.
-
-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(
- private val activity: Activity,
- private val returnUrl: String
-) : WebViewClient() {
-
- @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
- override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?):
- Boolean = handleUri(view, request!!.url)
-
- @SuppressWarnings("deprecation")
- @Deprecated("")
- override fun shouldOverrideUrlLoading(view: WebView?, url: String?):
- Boolean = handleUri(view, 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)
-
- 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
- }
-
- 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/Khalti.kt b/khalti-android/src/main/java/com/khalti/android/Khalti.kt
new file mode 100644
index 00000000..7af272f1
--- /dev/null
+++ b/khalti-android/src/main/java/com/khalti/android/Khalti.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2024. The Khalti Authors. All rights reserved.
+ */
+
+package com.khalti.android
+
+import android.content.Context
+import android.content.Intent
+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
+import com.khalti.android.utils.PackageUtil
+
+// Though kotlin provides named and optional parameters
+// method overloading was required for Java developers
+class Khalti private constructor(
+ private val context: Context,
+ var config: KhaltiPayConfig,
+ val onPaymentResult: OnPaymentResult,
+ val onMessage: OnMessage,
+ val onReturn: OnReturn?,
+) {
+ companion object {
+ fun init(
+ context: Context,
+ config: KhaltiPayConfig,
+ onPaymentResult: OnPaymentResult,
+ onMessage: OnMessage,
+ onReturn: OnReturn,
+ ): Khalti {
+ val khalti = Khalti(
+ context,
+ config,
+ onPaymentResult,
+ onMessage,
+ onReturn,
+ )
+
+ Store.instance().put("khalti", khalti)
+ return khalti
+ }
+
+ fun init(
+ context: Context,
+ config: KhaltiPayConfig,
+ onPaymentResult: OnPaymentResult,
+ onMessage: OnMessage,
+ ): Khalti {
+ val khalti = Khalti(
+ context,
+ config,
+ onPaymentResult,
+ onMessage,
+ null,
+ )
+
+ Store.instance().put("khalti", khalti)
+
+ return khalti
+ }
+ }
+
+ fun open() {
+ val packageName = context.packageName
+ val store = Store.instance()
+ val packageInfo = PackageUtil.getPackageInfo(context, packageName)
+
+ store.put("merchant_package_name", packageName)
+ store.put("merchant_package_version", packageInfo?.versionName ?: "")
+
+ val intent = Intent(context, PaymentActivity::class.java)
+ context.startActivity(intent)
+ }
+
+ fun verify() {
+ val verificationRepo = VerificationRepository()
+ verificationRepo.verify(config.pidx, this)
+ }
+
+ fun close() {
+ val intent = Intent("close_khalti_payment_portal")
+ context.sendBroadcast(intent)
+ }
+}
\ No newline at end of file
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/PaymentActivity.kt b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt
index b82e67fa..fedf134a 100644
--- a/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt
+++ b/khalti-android/src/main/java/com/khalti/android/PaymentActivity.kt
@@ -3,72 +3,85 @@
package com.khalti.android
import android.annotation.SuppressLint
-import android.app.Activity
-import android.net.Uri
+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.view.Gravity
import android.webkit.*
-import android.widget.LinearLayout
-import android.widget.LinearLayout.LayoutParams
-import android.widget.ProgressBar
-import com.google.android.material.appbar.AppBarLayout
-import com.google.android.material.appbar.MaterialToolbar
+import android.window.OnBackInvokedDispatcher
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.annotation.RequiresApi
+import com.khalti.android.payment.KhaltiPaymentPage
+import com.khalti.android.payment.KhaltiPaymentViewModel
+import com.khalti.android.payment.onBack
-internal class PaymentActivity : Activity() {
+internal class PaymentActivity : ComponentActivity() {
+ private var receiver: BroadcastReceiver? = null
+
+ @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- val layout = LinearLayout(this)
- layout.orientation = LinearLayout.VERTICAL
- layout.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()
+ setContent {
+ KhaltiPaymentPage(this, KhaltiPaymentViewModel())
}
+ registerBroadcast()
+ setupBackPressListener()
+ }
- val webView = WebView(this)
- val webSettings = webView.settings
+ override fun onDestroy() {
+ unregisterBroadcast()
+ super.onDestroy()
+ }
- @SuppressLint("SetJavaScriptEnabled")
- webSettings.javaScriptEnabled = true
- webSettings.domStorageEnabled = true
+ @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()
+ }
- 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
+ @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()
}
}
-
- val paymentUri = Uri.parse(it.paymentUrl).buildUpon()
- .appendQueryParameter("home", OpenKhaltiPay.DEFAULT_HOME)
- .build()
- webView.loadUrl(paymentUri.toString())
}
+ 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"),
+ )
+ }
+ }
+ }
- appBar.addView(toolbar)
-
- layout.addView(appBar)
- layout.addView(progressBar)
- layout.addView(webView, params)
+ private fun unregisterBroadcast() {
+ unregisterReceiver(receiver)
+ }
- setContentView(layout, params)
+ private fun setupBackPressListener() {
+ if (Build.VERSION.SDK_INT >= 33) {
+ val priority = OnBackInvokedDispatcher.PRIORITY_DEFAULT
+ onBackInvokedDispatcher.registerOnBackInvokedCallback(priority) {
+ onBack()
+ }
+ }
}
}
+
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
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..495234ad
--- /dev/null
+++ b/khalti-android/src/main/java/com/khalti/android/api/ApiClient.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2024. The Khalti Authors. All rights reserved.
+ */
+
+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
+import com.khalti.android.resource.Result
+import com.khalti.android.resource.Url
+import com.khalti.android.utils.ErrorUtil
+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
+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
+import java.util.concurrent.TimeUnit
+
+class ApiClient {
+ companion object {
+ private const val TIME_OUT = 30L
+
+ fun build(): ApiService {
+ 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"
+ }
+ val url = if (khalti!!.config.environment == Environment.PROD) {
+ Url.BASE_KHALTI_URL_PROD
+ } else {
+ Url.BASE_KHALTI_URL_STAGING
+ }.value
+
+ val loggingInterceptor = HttpLoggingInterceptor()
+ loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
+
+ val okHttpClient = OkHttpClient.Builder().readTimeout(TIME_OUT, TimeUnit.SECONDS)
+ .connectTimeout(TIME_OUT, TimeUnit.SECONDS).addInterceptor {
+ 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", BuildConfig.VERSION_NAME)
+ .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()
+
+ it.proceed(request)
+ }.addInterceptor(loggingInterceptor).build()
+
+ return Retrofit.Builder().baseUrl(url)
+ .addConverterFactory(GsonConverterFactory.create()).client(okHttpClient).build()
+ .create(ApiService::class.java)
+ }
+ }
+}
+
+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(
+ message = "Error", cause = Throwable(
+ ErrorUtil.parseError(
+ if (response.errorBody() != null) String(
+ response.errorBody()!!.bytes()
+ ) else "", response.code().toString()
+ )
+ ), code = response.code()
+ )
+ )
+ } 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
new file mode 100644
index 00000000..e36040b7
--- /dev/null
+++ b/khalti-android/src/main/java/com/khalti/android/api/ApiService.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) 2024. The Khalti Authors. All rights reserved.
+ */
+
+package com.khalti.android.api
+
+import com.khalti.android.data.PaymentPayload
+import retrofit2.Response
+import retrofit2.http.Body
+import retrofit2.http.POST
+
+interface ApiService {
+ @POST("epayment/lookup/")
+ suspend fun verify(@Body body: Map): Response
+}
\ No newline at end of file
diff --git a/khalti-android/src/main/java/com/khalti/android/cache/Store.kt b/khalti-android/src/main/java/com/khalti/android/cache/Store.kt
new file mode 100644
index 00000000..ef2f7b39
--- /dev/null
+++ b/khalti-android/src/main/java/com/khalti/android/cache/Store.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2024. The Khalti Authors. All rights reserved.
+ */
+
+package com.khalti.android.cache
+
+@Suppress("UNCHECKED_CAST")
+class Store private constructor() {
+
+ companion object {
+ @Volatile
+ private var instance: Store? = null
+
+ fun instance(): Store {
+ return instance ?: synchronized(this) {
+ instance ?: Store().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
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
new file mode 100644
index 00000000..e4557012
--- /dev/null
+++ b/khalti-android/src/main/java/com/khalti/android/callbacks/OnMessage.kt
@@ -0,0 +1,11 @@
+/*
+ * Copyright (c) 2024. The Khalti Authors. All rights reserved.
+ */
+
+package com.khalti.android.callbacks
+
+import com.khalti.android.resource.OnMessagePayload
+
+fun interface OnMessage {
+ fun invoke(payload: OnMessagePayload)
+}
\ No newline at end of file
diff --git a/khalti-android/src/main/java/com/khalti/android/callbacks/OnPaymentResult.kt b/khalti-android/src/main/java/com/khalti/android/callbacks/OnPaymentResult.kt
new file mode 100644
index 00000000..76362315
--- /dev/null
+++ b/khalti-android/src/main/java/com/khalti/android/callbacks/OnPaymentResult.kt
@@ -0,0 +1,12 @@
+/*
+ * Copyright (c) 2024. The Khalti Authors. All rights reserved.
+ */
+
+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)
+}
\ No newline at end of file
diff --git a/khalti-android/src/main/java/com/khalti/android/callbacks/OnReturn.kt b/khalti-android/src/main/java/com/khalti/android/callbacks/OnReturn.kt
new file mode 100644
index 00000000..6931a2c4
--- /dev/null
+++ b/khalti-android/src/main/java/com/khalti/android/callbacks/OnReturn.kt
@@ -0,0 +1,11 @@
+/*
+ * Copyright (c) 2024. The Khalti Authors. All rights reserved.
+ */
+
+package com.khalti.android.callbacks
+
+import com.khalti.android.Khalti
+
+fun interface OnReturn {
+ fun invoke(khalti: Khalti)
+}
\ No newline at end of file
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
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
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..cd6a65d2
--- /dev/null
+++ b/khalti-android/src/main/java/com/khalti/android/composable/KhaltiWebview.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2024. The Khalti Authors. All rights reserved.
+ */
+
+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
+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)
+
+ 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)
+
+ this.loadUrl(paymentUri.toString())
+ }
+ },
+ update = {
+ it.loadUrl("javascript:window.location.reload(true)")
+ }
+ )
+}
diff --git a/khalti-android/src/main/java/com/khalti/android/data/Environment.kt b/khalti-android/src/main/java/com/khalti/android/data/Environment.kt
new file mode 100644
index 00000000..4b9fc411
--- /dev/null
+++ b/khalti-android/src/main/java/com/khalti/android/data/Environment.kt
@@ -0,0 +1,9 @@
+/*
+ * Copyright (c) 2024. The Khalti Authors. All rights reserved.
+ */
+
+package com.khalti.android.data
+
+enum class Environment {
+ PROD, TEST
+}
\ No newline at end of file
diff --git a/khalti-android/src/main/java/com/khalti/android/data/KhaltiPayConfig.kt b/khalti-android/src/main/java/com/khalti/android/data/KhaltiPayConfig.kt
new file mode 100644
index 00000000..8b42db90
--- /dev/null
+++ b/khalti-android/src/main/java/com/khalti/android/data/KhaltiPayConfig.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2024. The Khalti Authors. All rights reserved.
+ */
+
+package com.khalti.android.data
+
+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 {
+
+ fun isProd(): Boolean = environment == Environment.PROD
+}
diff --git a/khalti-android/src/main/java/com/khalti/android/data/PaymentResult.kt b/khalti-android/src/main/java/com/khalti/android/data/PaymentResult.kt
new file mode 100644
index 00000000..da004a0e
--- /dev/null
+++ b/khalti-android/src/main/java/com/khalti/android/data/PaymentResult.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2024. The Khalti Authors. All rights reserved.
+ */
+
+package com.khalti.android.data
+
+import com.google.gson.annotations.SerializedName
+
+data class PaymentResult(
+ val status: String,
+ val payload: PaymentPayload? = null,
+ val message: String? = null
+)
+
+data class PaymentPayload(
+ @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
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
new file mode 100644
index 00000000..dc83adf0
--- /dev/null
+++ b/khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentPage.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2024. The Khalti Authors. All rights reserved.
+ */
+
+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
+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
+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.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+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.KProgressDialog
+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.utils.NetworkUtil
+
+@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun KhaltiPaymentPage(activity: Activity, viewModel: KhaltiPaymentViewModel) {
+ val state by viewModel.state.collectAsState()
+ val recomposeState = mutableStateOf(false)
+
+ Scaffold(
+ topBar = {
+ Surface(shadowElevation = 4.dp) {
+ TopAppBar(
+ navigationIcon = {
+ IconButton(onClick = {
+ onBack()
+ activity.finish()
+ }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = "Back"
+ )
+ }
+ },
+ title = {
+ Text(text = "Payment Gateway")
+ },
+ actions = {
+ IconButton(onClick = {
+ recomposeState.value = !recomposeState.value
+ }) {
+ Icon(
+ imageVector = Icons.Filled.Refresh,
+ contentDescription = "Refresh"
+ )
+ }
+ },
+ )
+ }
+ },
+ ) {
+ 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(
+ Modifier
+ .fillMaxSize()
+ ) {
+ KhaltiWebView(
+ config = config,
+ onReturnPageLoaded = {
+ viewModel.verifyPaymentStatus(khalti)
+ },
+ onPageLoaded = {
+ viewModel.toggleLoading(false)
+ },
+ )
+ if (state.isLoading) {
+ LinearProgressIndicator(
+ Modifier
+ .height(6.dp)
+ .width(200.dp)
+ .align(Alignment.Center),
+ color = Color.Gray
+ )
+ }
+ }
+
+ } else {
+ KhaltiError(errorType = ErrorType.network) {
+ viewModel.toggleNetwork(NetworkUtil.isNetworkAvailable(activity))
+ }
+ }
+ }
+ }
+
+ }
+
+ LaunchedEffect(state.hasNetwork && recomposeState.value) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ NetworkUtil.registerListener(activity) {
+ viewModel.toggleNetwork(it)
+ }
+ }
+ }
+}
+
+fun onBack() {
+ val khalti = Store.instance().get("khalti")
+ khalti?.onMessage?.invoke(
+ OnMessagePayload(
+ OnMessageEvent.BackPressed,
+ "User pressed back",
+ khalti,
+ needsPaymentConfirmation = true
+ )
+ )
+}
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..1f2eebe9
--- /dev/null
+++ b/khalti-android/src/main/java/com/khalti/android/payment/KhaltiPaymentViewModel.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2024. The Khalti Authors. All rights reserved.
+ */
+
+package com.khalti.android.payment
+
+import androidx.lifecycle.ViewModel
+import com.khalti.android.Khalti
+import com.khalti.android.service.VerificationRepository
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
+
+data class KhaltiPaymentState(
+ val isLoading: Boolean = true,
+ val hasNetwork: Boolean = true,
+ val progressDialog: Boolean = false,
+)
+
+class KhaltiPaymentViewModel : ViewModel() {
+ private val _state = MutableStateFlow((KhaltiPaymentState()))
+ val state: StateFlow = _state
+
+ fun verifyPaymentStatus(khalti: Khalti) {
+ toggleProgressDialog()
+ val verificationRepo = VerificationRepository()
+ verificationRepo.verify(khalti.config.pidx, khalti) {
+ 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/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
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..ad29df5a
--- /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(failureMessage: String, throwable: Throwable? = null, val code: Number? = null) :
+ Exception(failureMessage, throwable) {
+
+ class NoNetwork(message: String, cause: Throwable? = null) : KFailure(message, cause)
+
+ class ServerUnreachable(message: String, cause: Throwable? = null) :
+ KFailure(message, cause)
+
+ class HttpCall(message: String, cause: Throwable? = null, code: Number?) :
+ KFailure(message, cause, code)
+
+ 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
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
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,
+)
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)
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
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..e414a1e5
--- /dev/null
+++ b/khalti-android/src/main/java/com/khalti/android/service/VerificationRepository.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2024. The Khalti Authors. All rights reserved.
+ */
+
+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
+
+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
+ ), khalti
+ )
+ },
+ 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(
+ OnMessagePayload(
+ messageEvent,
+ it.message ?: "",
+ khalti,
+ it.cause,
+ it.code,
+ needsPaymentConfirmation = needsConfirmations
+ )
+ )
+ },
+ )
+ }
+ }
+}
\ 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
new file mode 100644
index 00000000..6f66941a
--- /dev/null
+++ b/khalti-android/src/main/java/com/khalti/android/service/VerificationService.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2024. The Khalti Authors. All rights reserved.
+ */
+
+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.data.PaymentPayload
+import kotlinx.coroutines.Dispatchers
+
+class VerificationService {
+ private val apiService: ApiService by lazy {
+ ApiClient.build()
+ }
+
+ suspend fun verify(pidx: String): Result {
+ return safeApiCall(Dispatchers.IO) {
+ apiService.verify(mapOf("pidx" to pidx))
+ }
+ }
+}
\ No newline at end of file
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
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..ba9f4f28
--- /dev/null
+++ b/khalti-android/src/main/java/com/khalti/android/utils/JsonUtil.kt
@@ -0,0 +1,58 @@
+/*
+ * 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/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
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
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
new file mode 100644
index 00000000..1ac6bff0
--- /dev/null
+++ b/khalti-android/src/main/java/com/khalti/android/view/EPaymentWebClient.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2024. The Khalti Authors. All rights reserved.
+ */
+
+package com.khalti.android.view
+
+import android.net.Uri
+import android.os.Build
+import android.webkit.*
+import androidx.annotation.RequiresApi
+import com.khalti.android.Khalti
+import com.khalti.android.cache.Store
+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)
+
+ @SuppressWarnings("deprecation")
+ @Deprecated("")
+ 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)
+
+ val khalti = Store.instance().get("khalti")
+ val returnUrl = khalti?.config?.returnUrl?.toString() ?: ""
+
+ if (url?.startsWith(returnUrl) != false) {
+ khalti?.onReturn?.invoke(khalti)
+ onReturn()
+ }
+ }
+
+ private fun handleUri(uri: Uri): Boolean {
+ // TODO (Ishwor) Handle redirection to Khalti app for setting MPIN
+ val url = uri.toString()
+ // 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
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/metadata.gradle b/metadata.gradle
index 83f17d31..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
- libraryCompileSdk = 33
- libraryTargetSdk = 33
+ libraryMinSdk = 21
+ libraryCompileSdk = 34
+ libraryTargetSdk = 34
}
\ No newline at end of file
diff --git a/scripts/publish-module.gradle b/scripts/publish-module.gradle
index ce576f81..b83ea730 100644
--- a/scripts/publish-module.gradle
+++ b/scripts/publish-module.gradle
@@ -1,7 +1,8 @@
apply plugin: 'maven-publish'
apply plugin: 'signing'
-task androidSourcesJar(type: Jar) {
+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"] = ''