Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.example.talkeys_new.R
import kotlinx.coroutines.launch

@OptIn(ExperimentalMaterial3Api::class)
@Composable
Expand All @@ -32,7 +30,6 @@ fun EventPaymentScreen(
eventPrice: String,
navController: NavController
) {
val context = LocalContext.current
val scrollState = rememberScrollState()

// Convert price to double for calculations
Expand Down Expand Up @@ -234,9 +231,7 @@ private fun PhonePePaymentSection(
navController: NavController,
onPaymentInitiated: () -> Unit
) {
val context = LocalContext.current
var isLoading by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val paymentViewModel = sharedPaymentCheckoutViewModel()
val checkoutState by paymentViewModel.checkoutState.collectAsState()
val errorMessage = checkoutState.errorMessage
Expand Down Expand Up @@ -355,21 +350,13 @@ private fun PhonePePaymentSection(
val passType = determinePassType(amount)
val friends = getUserSelectedFriends()

scope.launch {
val tokenManager = com.example.talkeys_new.screens.authentication.TokenManager(context)
val tokenResult = tokenManager.getToken()
val authToken = when (tokenResult) {
is com.example.talkeys_new.utils.Result.Success -> tokenResult.data?.takeIf { it.isNotBlank() }
else -> null
}

paymentViewModel.startCheckout(
eventId = eventId,
passType = passType,
friends = friends,
authToken = authToken
)
}
paymentViewModel.startCheckout(
eventId = eventId,
passType = passType,
friends = friends,
teamCode = null,
authToken = null
)
},
modifier = Modifier
.fillMaxWidth()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.example.talkeys_new.screens.payment

import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.content.Intent
import android.net.http.SslError
import android.webkit.*
import androidx.compose.foundation.layout.*
Expand Down Expand Up @@ -94,7 +95,12 @@ fun WebViewPaymentScreen(
// WebView
AndroidView(
factory = { context ->
WebView(context).apply {
WebView(context).apply webView@{
CookieManager.getInstance().apply {
setAcceptCookie(true)
setAcceptThirdPartyCookies(this@webView, true)
}

settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
Expand All @@ -104,7 +110,15 @@ fun WebViewPaymentScreen(
loadWithOverviewMode = true
useWideViewPort = true
javaScriptCanOpenWindowsAutomatically = true
setSupportMultipleWindows(true)
mediaPlaybackRequiresUserGesture = false
loadsImagesAutomatically = true
cacheMode = WebSettings.LOAD_DEFAULT
allowContentAccess = true
userAgentString = userAgentString
?.replace("; wv", "")
?.replace("Version/4.0 ", "")
?: userAgentString

// Allow mixed content for payment gateways
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
Expand All @@ -122,7 +136,7 @@ fun WebViewPaymentScreen(
super.onPageFinished(view, url)
isLoading = false
currentUrl = url ?: ""
Log.d("WebViewPayment", "Payment page finished loading")
Log.d("WebViewPayment", "Payment page finished loading for host=${runCatching { android.net.Uri.parse(url).host }.getOrNull() ?: "unknown"}")

// Check for session expired error
view?.evaluateJavascript(
Expand Down Expand Up @@ -218,6 +232,24 @@ fun WebViewPaymentScreen(
isLoading = false
}
}

override fun onCreateWindow(
view: WebView?,
isDialog: Boolean,
isUserGesture: Boolean,
resultMsg: android.os.Message?
): Boolean {
val popupUrl = view?.hitTestResult?.extra
if (!popupUrl.isNullOrBlank()) {
runCatching {
context.startActivity(Intent(Intent.ACTION_VIEW, android.net.Uri.parse(popupUrl)))
}.onFailure { error ->
Log.e("WebViewPayment", "Failed to open popup URL: ${error.message}")
}
return false
}
return super.onCreateWindow(view, isDialog, isUserGesture, resultMsg)
}

override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean = true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,20 @@ fun ProfileScreen(navController: NavController) {
val userProfile = remember(sharedProfileState, localGoogleProfile) {
val content = sharedProfileState as? ProfileUiState.Content
content?.profile?.let { profile ->
val resolvedDisplayName = firstNonBlank(
profile.displayName,
profile.name,
localGoogleProfile.givenName,
localGoogleProfile.name
)
val resolvedEmail = firstNonBlank(profile.email, localGoogleProfile.email)
UserProfile(
id = profile.id,
name = profile.displayName ?: profile.name,
email = profile.email,
profileImageUrl = profile.avatarUrl,
givenName = profile.displayName ?: profile.name,
familyName = ""
id = firstNonBlank(profile.id, localGoogleProfile.id),
name = resolvedDisplayName,
email = resolvedEmail,
profileImageUrl = profile.avatarUrl ?: localGoogleProfile.profileImageUrl,
givenName = resolvedDisplayName,
familyName = localGoogleProfile.familyName
)
} ?: localGoogleProfile
}
Expand Down Expand Up @@ -951,6 +958,10 @@ private fun getUserDisplayName(userProfile: UserProfile): String {
}
}

private fun firstNonBlank(vararg values: String?): String {
return values.firstOrNull { !it.isNullOrBlank() }?.trim().orEmpty()
}

private fun getUserEmail(userProfile: UserProfile): String {
return if (userProfile.email.isNotEmpty()) {
"@${userProfile.email.substringBefore("@")}"
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<resources>
<string name="app_name">TalkeysApk</string>
<string name="default_web_client_id">563385258779-75kq583ov98fk7h3dqp5em0639769a61.apps.googleusercontent.com</string>
<string name="default_notification_channel_id">fcm_default_channel</string>
<string name="msg_token_fmt">FCM Registration Token: %1$s</string>
</resources>
</resources>
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package com.talkeys.shared.data.payment

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.JsonNames

@Serializable
data class BookTicketRequest(
val eventId: String,
val passType: String,
val friends: List<Friend>
val friends: List<Friend>,
val teamCode: String? = null
)

@Serializable
Expand All @@ -23,18 +27,27 @@ data class BookTicketResponse(
)

@Serializable
@OptIn(ExperimentalSerializationApi::class)
data class PaymentOrderData(
val passId: String,
val merchantOrderId: String,
val orderId: String, // Backend sends "orderId" not "phonePeOrderId"
val amount: Int,
val amountInPaisa: Int,
val totalTickets: Int,
val token: String, // Backend sends "token" not "paymentUrl"
val event: EventInfo,
val qrStrings: List<QrString>,
val friends: List<Friend>
)
@JsonNames("orderId", "phonePeOrderId")
val orderId: String? = null,
val amount: Int? = null,
val amountInPaisa: Int? = null,
val totalTickets: Int? = null,
@JsonNames("token", "paymentToken", "payment_token")
val token: String? = null,
@SerialName("paymentUrl")
@JsonNames("paymentUrl", "checkoutUrl", "redirectUrl", "url", "redirectURL")
val paymentUrl: String? = null,
val event: EventInfo? = null,
val qrStrings: List<QrString> = emptyList(),
val friends: List<Friend> = emptyList()
) {
fun checkoutTokenOrUrl(): String? = paymentUrl?.takeIf { it.isNotBlank() }
?: token?.takeIf { it.isNotBlank() }
}

@Serializable
data class EventInfo(
Expand All @@ -58,4 +71,4 @@ data class PaymentStatusData(
val passId: String,
val passUUID: String? = null, // Make optional since backend may not always include it
val paymentStatus: String
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ package com.talkeys.shared.data.payment

import com.talkeys.shared.network.PaymentApiService
import com.talkeys.shared.config.ProductionConfig
import com.talkeys.shared.auth.TokenStorage
import co.touchlab.kermit.Logger

open class PaymentRepository(private val paymentApiService: PaymentApiService) {
open class PaymentRepository(
private val paymentApiService: PaymentApiService,
private val tokenStorage: TokenStorage? = null
) {

private val logger = Logger.withTag("PaymentRepository")

Expand All @@ -22,23 +26,28 @@ open class PaymentRepository(private val paymentApiService: PaymentApiService) {
eventId: String,
passType: String,
friends: List<Friend>,
teamCode: String? = null,
authToken: String? = null
): Result<PaymentOrderData> {
// Production validation
if (friends.size > ProductionConfig.MAX_FRIENDS_PER_BOOKING) {
return Result.failure(Exception("Maximum ${ProductionConfig.MAX_FRIENDS_PER_BOOKING} friends allowed per booking"))
}

val resolvedAuthToken = resolveAuthToken(authToken)
?: return Result.failure(Exception("Please login to continue"))

logger.i { "Production: Booking ticket for event: $eventId, passType: $passType, friends: ${friends.size}" }

val request = BookTicketRequest(
eventId = eventId,
passType = passType,
friends = friends
friends = friends,
teamCode = teamCode?.trim()?.takeIf { it.isNotEmpty() }
)

return try {
val result = paymentApiService.bookTicketApp(request, authToken)
val result = paymentApiService.bookTicketApp(request, resolvedAuthToken)

result.fold(
onSuccess = { response ->
Expand Down Expand Up @@ -66,10 +75,13 @@ open class PaymentRepository(private val paymentApiService: PaymentApiService) {
* Verify payment status after PhonePe payment completion
*/
open suspend fun verifyPaymentStatus(merchantOrderId: String, authToken: String? = null): Result<PaymentStatusData> {
val resolvedAuthToken = resolveAuthToken(authToken)
?: return Result.failure(Exception("Please login to verify payment"))

logger.d { "Verifying payment status" }

return try {
val result = paymentApiService.checkPaymentStatus(merchantOrderId, authToken)
val result = paymentApiService.checkPaymentStatus(merchantOrderId, resolvedAuthToken)

result.fold(
onSuccess = { response ->
Expand Down Expand Up @@ -120,4 +132,9 @@ open class PaymentRepository(private val paymentApiService: PaymentApiService) {
Result.failure(e)
}
}

private suspend fun resolveAuthToken(authToken: String?): String? {
return authToken?.trim()?.takeIf { it.isNotBlank() }
?: tokenStorage?.getToken()?.trim()?.takeIf { it.isNotBlank() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import org.koin.dsl.module
val sharedModule = module {
single { ApiClient() }
single { PaymentApiService(get()) }
single { PaymentRepository(get()) }
single { PaymentRepository(get(), get()) }

// Phase 4 — Events vertical slice (read-only)
single { EventsApi(get<ApiClient>()) }
Expand Down
Loading