diff --git a/README.md b/README.md index e077bce..6f5534f 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ The `:shared` module is Kotlin Multiplatform and currently includes: - Payment checkout state and PhonePe checkout URL preparation Android still contains platform UI and integrations such as Compose screens, -Google Sign-In UI, PhonePe SDK/WebView handling, Firebase messaging, Android +Google Sign-In UI, PhonePe WebView handling, Firebase messaging, Android navigation, and Android-specific sharing/image picking. ## Toolchain diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1f3646f..0731314 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -66,22 +66,14 @@ dependencies { //google sign in dependencies implementation("com.google.android.gms:play-services-auth:20.7.0") // for Google Sign-In - implementation("com.squareup.retrofit2:retrofit:2.9.0") - implementation("com.squareup.retrofit2:converter-gson:2.9.0") implementation("androidx.datastore:datastore-preferences:1.0.0") //coil dependency implementation("io.coil-kt:coil-compose:2.5.0") - //logging dependency - implementation ("com.squareup.okhttp3:logging-interceptor:4.12.0") - //icon dependencies implementation("androidx.compose.material:material-icons-extended:1.6.1") - //phonepe payment integration - Latest stable version - implementation("phonepe.intentsdk.android.release:IntentSDK:5.2.0") - implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) @@ -98,4 +90,4 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/talkeys_new/MainActivity.kt b/app/src/main/java/com/example/talkeys_new/MainActivity.kt index 90c318b..00fb95d 100644 --- a/app/src/main/java/com/example/talkeys_new/MainActivity.kt +++ b/app/src/main/java/com/example/talkeys_new/MainActivity.kt @@ -2,14 +2,12 @@ package com.example.talkeys_new import android.Manifest import android.content.Context -import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -19,15 +17,12 @@ import androidx.compose.ui.Modifier import androidx.core.content.ContextCompat import androidx.core.view.WindowCompat import com.example.talkeys_new.navigation.AppNavigation -import com.example.talkeys_new.screens.authentication.TokenManager import com.example.talkeys_new.ui.theme.Talkeys_NewTheme import com.example.talkeys_new.utils.ConsentDialogHelper import com.example.talkeys_new.utils.FCMInitializationManager import com.example.talkeys_new.utils.FCMTokenManager -import com.example.talkeys_new.utils.PhonePePaymentManager import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability -import com.google.firebase.messaging.FirebaseMessaging import kotlinx.coroutines.flow.first // Koin is initialized in TalkeysApplication.onCreate() @@ -46,25 +41,6 @@ class MainActivity : ComponentActivity() { } } - // PhonePe payment result launcher with enhanced handling - private val phonePePaymentLauncher: ActivityResultLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - Log.d(TAG, "๐Ÿ” PhonePe payment result received") - Log.d(TAG, "๐Ÿ“‹ Result code: ${result.resultCode}") - Log.d(TAG, "๐Ÿ“‹ Result includes data=${result.data != null}") - - // โœ… CRITICAL: Always verify payment status regardless of result code - // WebView JavaScript errors can cause wrong result codes even when payment succeeds - PhonePePaymentManager.handlePaymentResult( - resultCode = result.resultCode, - data = result.data - ) { paymentResult -> - Log.d(TAG, "๐Ÿ’ณ Payment result processed: $paymentResult") - handlePhonePePaymentResult(paymentResult) - } - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -351,259 +327,6 @@ class MainActivity : ComponentActivity() { } } - /** - * Handle PhonePe payment result - * This method is called after the payment flow completes - */ - private fun handlePhonePePaymentResult(paymentResult: PhonePePaymentManager.PaymentResult) { - when (paymentResult) { - is PhonePePaymentManager.PaymentResult.Success -> { - Log.d(TAG, "Payment successful") - navigateToRegistrationSuccess() - } - is PhonePePaymentManager.PaymentResult.Failed -> { - Log.d(TAG, "Payment failed: ${paymentResult.message}") - showPaymentFailedMessage(paymentResult.message) - } - is PhonePePaymentManager.PaymentResult.Pending -> { - Log.d(TAG, "Payment pending: ${paymentResult.message}") - showPaymentPendingMessage(paymentResult.message) - } - is PhonePePaymentManager.PaymentResult.Completed -> { - Log.d(TAG, "Payment completed: ${paymentResult.message}") - // Legacy support - this should now be handled by automatic verification - navigateToRegistrationSuccess() - } - is PhonePePaymentManager.PaymentResult.Cancelled -> { - Log.d(TAG, "Payment cancelled: ${paymentResult.message}") - showPaymentCancelledMessage() - } - is PhonePePaymentManager.PaymentResult.Error -> { - Log.e(TAG, "Payment error: ${paymentResult.message}") - showPaymentErrorMessage(paymentResult.message) - } - } - } - - /** - * Navigate to registration success screen - */ - private fun navigateToRegistrationSuccess() { - // Since we can't directly navigate from MainActivity, we'll use a different approach - // You might want to use a shared ViewModel or event bus for this - Log.d(TAG, "Payment successful - user should be navigated to success screen") - - // For now, just log the success - // The actual navigation should be handled in the payment screen or through a callback - } - - /** - * Initiate integrated payment flow (book ticket + PhonePe payment) - * This is the recommended method that handles the complete flow - */ - fun initiateIntegratedPayment( - eventId: String, - passType: String, - friends: List, - authToken: String? = null, - onResult: (PhonePePaymentManager.PaymentResult) -> Unit - ) { - Log.d(TAG, "Initiating integrated payment for event: $eventId") - - PhonePePaymentManager.bookTicketAndPay( - activity = this, - eventId = eventId, - passType = passType, - friends = friends, - activityResultLauncher = phonePePaymentLauncher, - authToken = authToken, - onResult = onResult - ) - } - - /** - * Legacy method: Initiate PhonePe payment with pre-obtained token - * Use initiateIntegratedPayment() for the complete flow - * - * @param token Payment token from your backend (Create Order API response) - * @param orderId Order ID from your backend (Create Order API response) - */ - fun initiatePhonePePayment(token: String, orderId: String) { - Log.d(TAG, "Initiating PhonePe payment") - - PhonePePaymentManager.startCheckout( - activity = this, - token = token, - orderId = orderId, - activityResultLauncher = phonePePaymentLauncher - ) - } - - /** - * Check order status after payment completion - * This should call your backend's Order Status API - */ - private fun checkOrderStatus() { - Log.d(TAG, "Checking order status via backend API") - - // Get the current merchant order ID (you'll need to store this during payment) - val merchantOrderId = getCurrentMerchantOrderId() - - if (merchantOrderId != null) { - // Use PhonePePaymentManager to verify payment status - PhonePePaymentManager.verifyPaymentStatusOnServer( - merchantOrderId = merchantOrderId, - authToken = getCurrentAuthToken() - ) { result -> - when (result) { - is PhonePePaymentManager.PaymentResult.Success -> { - Log.d(TAG, "โœ… Payment verified as successful: ${result.message}") - handlePaymentSuccess(result.passId, result.passUUID) - } - is PhonePePaymentManager.PaymentResult.Failed -> { - Log.d(TAG, "โŒ Payment verified as failed: ${result.message}") - handlePaymentFailure(result.message) - } - is PhonePePaymentManager.PaymentResult.Pending -> { - Log.d(TAG, "โณ Payment still pending: ${result.message}") - handlePaymentPending(result.message) - } - is PhonePePaymentManager.PaymentResult.Error -> { - Log.e(TAG, "๐Ÿšจ Error checking payment status: ${result.message}") - handlePaymentError(result.message) - } - else -> { - Log.w(TAG, "โš ๏ธ Unknown payment result: $result") - handlePaymentError("Unknown payment status") - } - } - } - } else { - Log.e(TAG, "No merchant order ID found for status check") - handlePaymentError("No order ID available for verification") - } - } - - /** - * Get current merchant order ID (implement based on how you store it) - */ - private fun getCurrentMerchantOrderId(): String? { - // TODO: Implement based on how you store the merchant order ID - // This could be from SharedPreferences, ViewModel, or passed as parameter - - // Example implementation: - // return getSharedPreferences("payment", Context.MODE_PRIVATE) - // .getString("current_merchant_order_id", null) - - // For testing, you can use your recent order ID: - return "TKT_1760870435366_hz7wihgz7" // Replace with actual implementation - } - - /** - * Get current auth token (implement based on your auth system) - */ - private fun getCurrentAuthToken(): String? { - // TODO: Implement based on your authentication system - // This could be from TokenManager, SharedPreferences, etc. - - // Example implementation: - // return TokenManager.getCurrentToken() - - return null // Replace with actual token retrieval - } - - /** - * Handle successful payment verification - */ - private fun handlePaymentSuccess(passId: String, passUUID: String?) { - Log.d(TAG, "Payment success confirmed") - - // Navigate to success screen or show success message - // Example: navigateToRegistrationSuccess(passId, passUUID) - - clearCurrentMerchantOrderId() - } - - /** - * Handle failed payment verification - */ - private fun handlePaymentFailure(message: String) { - Log.d(TAG, "Payment failure confirmed: $message") - - // Show failure message to user - // Example: showPaymentFailedDialog(message) - - // Clear stored order ID - clearCurrentMerchantOrderId() - } - - /** - * Handle pending payment verification - */ - private fun handlePaymentPending(message: String) { - Log.d(TAG, "Payment still pending: $message") - - // Show pending message and maybe retry after delay - // Example: showPaymentPendingDialog(message) - } - - /** - * Handle payment verification error - */ - private fun handlePaymentError(message: String) { - Log.e(TAG, "Payment verification error: $message") - - // Show error message to user - // Example: showPaymentErrorDialog(message) - - // Clear stored order ID - clearCurrentMerchantOrderId() - } - - /** - * Clear stored merchant order ID - */ - private fun clearCurrentMerchantOrderId() { - // TODO: Implement based on how you store the merchant order ID - // Example: - // getSharedPreferences("payment", Context.MODE_PRIVATE) - // .edit() - // .remove("current_merchant_order_id") - // .apply() - } - - /** - * Show payment cancelled message to user - */ - private fun showPaymentCancelledMessage() { - Log.d(TAG, "TODO: Show payment cancelled UI to user") - // TODO: Implement UI to show payment was cancelled - } - - /** - * Show payment failed message to user - */ - private fun showPaymentFailedMessage(errorMessage: String) { - Log.e(TAG, "TODO: Show payment failed UI to user: $errorMessage") - // TODO: Implement UI to show payment failed - } - - /** - * Show payment pending message to user - */ - private fun showPaymentPendingMessage(message: String) { - Log.d(TAG, "TODO: Show payment pending UI to user: $message") - // TODO: Implement UI to show payment is pending - } - - /** - * Show payment error message to user - */ - private fun showPaymentErrorMessage(errorMessage: String) { - Log.e(TAG, "TODO: Show payment error UI to user: $errorMessage") - // TODO: Implement UI to show payment error - } - companion object { private const val TAG = "MainActivity" } diff --git a/app/src/main/java/com/example/talkeys_new/MyFirebaseMessagingService.kt b/app/src/main/java/com/example/talkeys_new/MyFirebaseMessagingService.kt index a2e0667..e0488ef 100644 --- a/app/src/main/java/com/example/talkeys_new/MyFirebaseMessagingService.kt +++ b/app/src/main/java/com/example/talkeys_new/MyFirebaseMessagingService.kt @@ -67,28 +67,8 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { // for the current user Log.d(TAG, "TODO: send FCM token to backend (not yet implemented)") - // Example of what this would look like with a real API: - /* - val apiService = RetrofitClient.getApiService() - apiService.updateFCMToken( - userId = getCurrentUserId(), - token = token, - deviceInfo = getDeviceInfo() - ).enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - Log.d(TAG, "โœ… Token successfully sent to server") - } else { - Log.e(TAG, "โŒ Failed to send token to server: ${response.code()}") - } - } - - override fun onFailure(call: Call, t: Throwable) { - Log.e(TAG, "๐ŸŒ Network error sending token", t) - } - }) - */ - + // Add this through shared Ktor once the backend exposes an FCM-token + // registration endpoint. } private fun storeTokenMetadata(token: String, timestamp: String) { diff --git a/app/src/main/java/com/example/talkeys_new/TalkeysApplication.kt b/app/src/main/java/com/example/talkeys_new/TalkeysApplication.kt index 9e6a0c0..89e5676 100644 --- a/app/src/main/java/com/example/talkeys_new/TalkeysApplication.kt +++ b/app/src/main/java/com/example/talkeys_new/TalkeysApplication.kt @@ -1,9 +1,6 @@ package com.example.talkeys_new import android.app.Application -import com.phonepe.intent.sdk.api.PhonePeKt -import com.phonepe.intent.sdk.api.models.PhonePeEnvironment -import com.example.talkeys_new.utils.PhonePeConfig import com.talkeys.shared.initKoin import org.koin.android.ext.koin.androidContext @@ -17,64 +14,5 @@ class TalkeysApplication : Application() { initKoin { androidContext(this@TalkeysApplication) } - - // Initialize PhonePe SDK - initializePhonePeSDK() - } - - private fun initializePhonePeSDK() { - try { - val environment = if (PhonePeConfig.IS_PRODUCTION) { - PhonePeEnvironment.RELEASE - } else { - PhonePeEnvironment.SANDBOX - } - - android.util.Log.d("PhonePe", "Initializing PhonePe SDK (${PhonePeConfig.getEnvironmentName()})") - - val result = PhonePeKt.init( - context = this, - merchantId = PhonePeConfig.MERCHANT_ID, - flowId = PhonePeConfig.generateFlowId(), - phonePeEnvironment = environment, - enableLogging = !PhonePeConfig.IS_PRODUCTION, // Enable logging in debug - appId = null // Optional - can be your app's package name - ) - - if (result) { - android.util.Log.d("PhonePe", "PhonePe SDK initialized successfully") - - // Verify SDK state - verifySDKState() - } else { - android.util.Log.e("PhonePe", "PhonePe SDK initialization failed") - } - } catch (e: Exception) { - android.util.Log.e("PhonePe", "Exception during SDK initialization: ${e.message}", e) - } - } - - /** - * Verify SDK state after initialization - */ - private fun verifySDKState() { - try { - android.util.Log.d("PhonePe", "๐Ÿ” Verifying SDK state...") - - // Check if PhonePe app is installed - val packageManager = packageManager - try { - packageManager.getPackageInfo("com.phonepe.app", 0) - android.util.Log.d("PhonePe", "๐Ÿ“ฑ PhonePe app is installed - will use native app") - } catch (e: Exception) { - android.util.Log.d("PhonePe", "๐ŸŒ PhonePe app not installed - will use WebView fallback") - } - - android.util.Log.d("PhonePe", "โœ… SDK verification complete") - - } catch (e: Exception) { - android.util.Log.w("PhonePe", "โš ๏ธ SDK verification failed: ${e.message}") - } } - } diff --git a/app/src/main/java/com/example/talkeys_new/dataModels/DataClasses.kt b/app/src/main/java/com/example/talkeys_new/dataModels/DataClasses.kt index 6a76e2f..cedda7f 100644 --- a/app/src/main/java/com/example/talkeys_new/dataModels/DataClasses.kt +++ b/app/src/main/java/com/example/talkeys_new/dataModels/DataClasses.kt @@ -1,19 +1,5 @@ package com.example.talkeys_new.dataModels -data class UserResponse( - val accessToken: String, - val name: String -) - -data class VerifyRequest( - val token: String -) - -data class VerifyResponse( - val accessToken: String, - val name: String -) - data class EventResponse( val _id: String, val name: String, diff --git a/app/src/main/java/com/example/talkeys_new/network/NetworkConfig.kt b/app/src/main/java/com/example/talkeys_new/network/NetworkConfig.kt deleted file mode 100644 index 331d2ec..0000000 --- a/app/src/main/java/com/example/talkeys_new/network/NetworkConfig.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.example.talkeys_new.network - -import com.example.talkeys_new.BuildConfig -import okhttp3.CertificatePinner -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import java.util.concurrent.TimeUnit - -/** - * NetworkConfig provides centralized network security configuration. - * - * Features: - * - Certificate pinning for API endpoints - * - Configurable network timeouts - * - Debug-only HTTP logging - * - Reusable OkHttpClient factory - */ -object NetworkConfig { - - private const val API_HOST = "api.talkeys.xyz" - - // Network timeout configuration (in seconds) - private const val CONNECT_TIMEOUT = 30L - private const val READ_TIMEOUT = 30L - private const val WRITE_TIMEOUT = 30L - - /** - * Certificate pinning configuration for API endpoints. - * - * IMPORTANT: These are placeholder SHA-256 pins. - * Replace with actual certificate pins before production deployment. - * - * To get your certificate pins, run: - * openssl s_client -connect api.talkeys.xyz:443 | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 - */ - private val certificatePinner = CertificatePinner.Builder() - .add( - API_HOST, - // Placeholder pins - MUST be replaced with actual pins - "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", - "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=" - ) - .build() - - /** - * Creates a configured OkHttpClient with security settings. - * - * @param enableCertificatePinning Whether to enable certificate pinning (default: false for development) - * @return Configured OkHttpClient instance - */ - fun createOkHttpClient(enableCertificatePinning: Boolean = false): OkHttpClient { - val builder = OkHttpClient.Builder() - .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS) - .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS) - .writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS) - - // Enable certificate pinning only when explicitly requested - // Typically enabled in production builds - if (enableCertificatePinning) { - builder.certificatePinner(certificatePinner) - } - - // Add logging interceptor only in DEBUG builds - // This prevents sensitive data from being logged in production - if (BuildConfig.DEBUG) { - val loggingInterceptor = HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY - } - builder.addInterceptor(loggingInterceptor) - } - - return builder.build() - } - - /** - * Creates an OkHttpClient with custom interceptors. - * Useful when you need to add authentication or other custom interceptors. - * - * @param interceptors List of custom interceptors to add - * @param enableCertificatePinning Whether to enable certificate pinning - * @return Configured OkHttpClient instance - */ - fun createOkHttpClientWithInterceptors( - interceptors: List, - enableCertificatePinning: Boolean = false - ): OkHttpClient { - val builder = OkHttpClient.Builder() - .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS) - .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS) - .writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS) - - // Add custom interceptors first (e.g., auth interceptor) - interceptors.forEach { builder.addInterceptor(it) } - - // Enable certificate pinning if requested - if (enableCertificatePinning) { - builder.certificatePinner(certificatePinner) - } - - // Add logging interceptor last (only in DEBUG) - if (BuildConfig.DEBUG) { - val loggingInterceptor = HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY - } - builder.addInterceptor(loggingInterceptor) - } - - return builder.build() - } -} diff --git a/app/src/main/java/com/example/talkeys_new/screens/authentication/AuthService.kt b/app/src/main/java/com/example/talkeys_new/screens/authentication/AuthService.kt deleted file mode 100644 index 140e14d..0000000 --- a/app/src/main/java/com/example/talkeys_new/screens/authentication/AuthService.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.talkeys_new.screens.authentication - - -import com.example.talkeys_new.dataModels.UserResponse -import retrofit2.http.POST -import retrofit2.Response -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory -import retrofit2.http.Header - -// This interface defines the structure for our API calls related to authentication -interface AuthService { - // This function makes a POST request to the "verify" endpoint - // It takes the JWT token in the Authorization header (prefixed with "Bearer ") - // The function is marked with `suspend` because it's a coroutine (runs asynchronously) - // It returns a Response object wrapping UserResponse, which contains data from the server - @POST("verify") - suspend fun verifyToken( - @Header("Authorization") bearerToken: String - ): Response - - // This object helps us create an instance of the AuthService using Retrofit - object RetrofitClient { - // The base URL of our backend server (all API calls will be made relative to this URL) - private const val BASE_URL = "https://api.talkeys.xyz/" - - // 'instance' is the singleton object that we use to access AuthService functions - val instance: AuthService by lazy { - // We build the Retrofit instance only once (lazy initialization) - Retrofit.Builder() - .baseUrl(BASE_URL) // Set the base URL for all network calls - .addConverterFactory(GsonConverterFactory.create()) // Convert JSON responses to Kotlin objects - .build() // Build the Retrofit object - .create(AuthService::class.java) // Create an implementation of AuthService interface - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/talkeys_new/screens/events/AuthInterceptor.kt b/app/src/main/java/com/example/talkeys_new/screens/events/AuthInterceptor.kt deleted file mode 100644 index 0babb19..0000000 --- a/app/src/main/java/com/example/talkeys_new/screens/events/AuthInterceptor.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.example.talkeys_new.screens.events - -import com.example.talkeys_new.screens.authentication.TokenManager -import com.example.talkeys_new.utils.Result -import kotlinx.coroutines.runBlocking -import okhttp3.Interceptor -import okhttp3.Request -import okhttp3.Response -import android.util.Log - -/** - * Interceptor that adds authentication headers to outgoing requests - * Handles token retrieval errors gracefully - */ -class AuthInterceptor(private val tokenManager: TokenManager) : Interceptor { - - private val TAG = "AuthInterceptor" - - override fun intercept(chain: Interceptor.Chain): Response { - val originalRequest = chain.request() - - // Skip authentication for certain endpoints if needed - if (shouldSkipAuth(originalRequest)) { - return chain.proceed(originalRequest) - } - - // Get token and add to request - val authenticatedRequest = runBlocking { - val tokenResult = tokenManager.getToken() - - when (tokenResult) { - is Result.Success -> { - val token = tokenResult.data - if (!token.isNullOrEmpty()) { - Log.d(TAG, "Adding auth header with token") - originalRequest.newBuilder() - .addHeader("Authorization", "Bearer $token") - .build() - } else { - Log.w(TAG, "No token available, proceeding without authentication") - originalRequest - } - } - is Result.Error -> { - Log.e(TAG, "Error retrieving token: ${tokenResult.message}") - // Proceed with the original request without the auth header - // The server will reject it if authentication is required - originalRequest - } - else -> { - Log.w(TAG, "Unexpected token result state, proceeding without authentication") - originalRequest - } - } - } - - return try { - chain.proceed(authenticatedRequest) - } catch (e: Exception) { - Log.e(TAG, "Error during request execution", e) - throw e - } - } - - /** - * Determines if authentication should be skipped for certain requests - * @param request The original request - * @return true if auth should be skipped, false otherwise - */ - private fun shouldSkipAuth(request: Request): Boolean { - val path = request.url.encodedPath - - // Skip authentication for login, signup, and public endpoints - return path.contains("/login") || - path.contains("/signup") || - path.contains("/verify") || - path.contains("/public/") - } -} diff --git a/app/src/main/java/com/example/talkeys_new/screens/events/EventApiService.kt b/app/src/main/java/com/example/talkeys_new/screens/events/EventApiService.kt deleted file mode 100644 index b9c3405..0000000 --- a/app/src/main/java/com/example/talkeys_new/screens/events/EventApiService.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.talkeys_new.screens.events - -import com.example.talkeys_new.dataModels.EventListResponse -import com.example.talkeys_new.dataModels.EventResponse -import com.example.talkeys_new.dataModels.EventDetailResponse -import retrofit2.Response -import retrofit2.http.GET -import retrofit2.http.Path - -interface EventApiService { - - @GET("getEvents") - suspend fun getAllEvents(): Response - - @GET("getEventById/{id}") - suspend fun getEventById( - @Path("id") eventId: String - ): Response -} diff --git a/app/src/main/java/com/example/talkeys_new/screens/events/EventsRepository.kt b/app/src/main/java/com/example/talkeys_new/screens/events/EventsRepository.kt deleted file mode 100644 index 83c43fe..0000000 --- a/app/src/main/java/com/example/talkeys_new/screens/events/EventsRepository.kt +++ /dev/null @@ -1,164 +0,0 @@ -package com.example.talkeys_new.screens.events - -import android.util.Log -import com.example.talkeys_new.dataModels.EventResponse -import com.example.talkeys_new.utils.NetworkUtils -import com.example.talkeys_new.utils.Result -import com.example.talkeys_new.utils.cache.CacheManager -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import retrofit2.Response -import java.io.IOException - -/** - * Repository class that handles all events-related API calls - * Implements proper error handling using Result wrapper - */ -class EventsRepository(private val api: EventApiService) { - private val TAG = "EventsRepository" - - /** - * Get all events from the API with caching support - * @param forceRefresh If true, bypasses cache and fetches fresh data - * @return Result containing List or error - */ - suspend fun getAllEvents(forceRefresh: Boolean = false): Result> = withContext(Dispatchers.IO) { - return@withContext try { - // Check cache first unless force refresh is requested - if (!forceRefresh) { - val cachedEvents = CacheManager.eventsCache.get(CacheManager.Keys.ALL_EVENTS) - if (cachedEvents != null) { - Log.d(TAG, "Returning cached events: ${cachedEvents.size}") - return@withContext Result.Success(cachedEvents) - } - } - - Log.d(TAG, "Making API call to getAllEvents...") - - val result = NetworkUtils.safeApiCall { - api.getAllEvents() - } - - when (result) { - is Result.Success -> { - val events = result.data.data.events - Log.d(TAG, "Events extracted: ${events.size}") - - // Cache the events - CacheManager.eventsCache.put(CacheManager.Keys.ALL_EVENTS, events) - Log.d(TAG, "Events cached successfully") - - Result.Success(events) - } - is Result.Error -> { - Log.e(TAG, "API call failed: ${result.message}") - result - } - else -> Result.Error( - exception = IllegalStateException("Unexpected result state"), - message = "Unexpected result state" - ) - } - } catch (e: Exception) { - Log.e(TAG, "Exception in getAllEvents: ${e.message}", e) - Result.Error( - exception = e, - message = "Failed to load events: ${e.message}" - ) - } - } - - /** - * Get event by ID from the API with caching support - * @param eventId The ID of the event to retrieve - * @param forceRefresh If true, bypasses cache and fetches fresh data - * @return Result containing EventResponse or error - */ - suspend fun getEventById(eventId: String, forceRefresh: Boolean = false): Result = withContext(Dispatchers.IO) { - return@withContext try { - if (eventId.isBlank()) { - return@withContext Result.Error( - exception = IllegalArgumentException("Event ID cannot be empty"), - message = "Event ID cannot be empty" - ) - } - - // Check cache first unless force refresh is requested - if (!forceRefresh) { - val cacheKey = CacheManager.Keys.eventDetail(eventId) - val cachedEvent = CacheManager.eventDetailsCache.get(cacheKey) - if (cachedEvent != null) { - Log.d(TAG, "Returning cached event: ${cachedEvent.name}") - return@withContext Result.Success(cachedEvent) - } - } - - Log.d(TAG, "Making API call to getEventById with ID: $eventId") - - val result = NetworkUtils.safeApiCall { - api.getEventById(eventId) - } - - when (result) { - is Result.Success -> { - val eventDetail = result.data.data - if (eventDetail != null) { - // Convert EventDetail to EventResponse - val eventResponse = EventResponse( - _id = eventDetail._id, - name = eventDetail.name, - category = eventDetail.category, - ticketPrice = eventDetail.ticketPrice, - mode = eventDetail.mode, - location = eventDetail.location, - duration = eventDetail.duration, - slots = eventDetail.slots, - visibility = eventDetail.visibility, - startDate = eventDetail.startDate, - startTime = eventDetail.startTime, - endRegistrationDate = eventDetail.endRegistrationDate, - totalSeats = eventDetail.totalSeats, - eventDescription = eventDetail.eventDescription, - photographs = eventDetail.photographs, - prizes = eventDetail.prizes, - isTeamEvent = eventDetail.isTeamEvent, - isPaid = eventDetail.isPaid, - isLive = eventDetail.isLive, - organizerName = eventDetail.organizerName, - organizerEmail = eventDetail.organizerEmail, - organizerContact = eventDetail.organizerContact - ) - - // Cache the event details - val cacheKey = CacheManager.Keys.eventDetail(eventId) - CacheManager.eventDetailsCache.put(cacheKey, eventResponse) - Log.d(TAG, "Event details cached for ID: $eventId") - - Log.d(TAG, "Converted event: ${eventResponse.name}") - Result.Success(eventResponse) - } else { - Log.e(TAG, "Event data is null") - Result.Error( - exception = IOException("Event not found"), - message = "Event not found" - ) - } - } - is Result.Error -> { - Log.e(TAG, "API call failed: ${result.message}") - result - } - else -> Result.Error( - exception = IllegalStateException("Unexpected result state"), - message = "Unexpected result state" - ) - } - } catch (e: Exception) { - Log.e(TAG, "Exception in getEventById: ${e.message}", e) - Result.Error( - exception = e, - message = "Failed to load event details: ${e.message}" - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/talkeys_new/screens/events/EventsViewModel.kt b/app/src/main/java/com/example/talkeys_new/screens/events/EventsViewModel.kt deleted file mode 100644 index f46076b..0000000 --- a/app/src/main/java/com/example/talkeys_new/screens/events/EventsViewModel.kt +++ /dev/null @@ -1,323 +0,0 @@ -package com.example.talkeys_new.screens.events - -import android.content.Context -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.example.talkeys_new.dataModels.EventResponse -import com.example.talkeys_new.utils.LikedEventsManager -import com.example.talkeys_new.utils.Result -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import java.util.concurrent.CancellationException - -/** - * ViewModel for managing event-related state and business logic - */ -class EventViewModel( - private val repository: EventsRepository, - private val context: Context -) : ViewModel() { - - companion object { - private const val TAG = "EventViewModel" - } - - // Liked events manager - private val likedEventsManager = LikedEventsManager.getInstance(context) - - // Expose liked event IDs as StateFlow - val likedEventIds: StateFlow> = likedEventsManager.likedEventIds - - // Selected event state - private val _selectedEvent = MutableStateFlow(null) - val selectedEvent: StateFlow = _selectedEvent.asStateFlow() - - // Single event loading state - private val _eventLoading = MutableStateFlow(false) - val eventLoading: StateFlow = _eventLoading.asStateFlow() - - // Single event error state - private val _eventError = MutableStateFlow(null) - val eventError: StateFlow = _eventError.asStateFlow() - - // All events storage (exposed for screens that need unfiltered events like LikedEventsScreen) - private val _allEvents = MutableStateFlow>(emptyList()) - val allEvents: StateFlow> = _allEvents.asStateFlow() - - // Filtered events list (for ExploreEvents screen with live/past filter) - private val _eventList = MutableStateFlow>(emptyList()) - val eventList: StateFlow> = _eventList.asStateFlow() - - // Loading state for all events - private val _isLoading = MutableStateFlow(false) - val isLoading: StateFlow = _isLoading.asStateFlow() - - // Error message for all events - private val _errorMessage = MutableStateFlow(null) - val errorMessage: StateFlow = _errorMessage.asStateFlow() - - // Filter state for live/past events - private val _showLiveEvents = MutableStateFlow(true) - val showLiveEvents: StateFlow = _showLiveEvents.asStateFlow() - - // Loading state to prevent multiple simultaneous requests - private var isCurrentlyFetching = false - - /** - * Fetches all events from the repository with caching support - * @param forceRefresh If true, bypasses cache and fetches fresh data - * Handles loading states, errors, and prevents duplicate requests - */ - fun fetchAllEvents(forceRefresh: Boolean = false) { - // Prevent multiple simultaneous requests - if (isCurrentlyFetching) { - Log.d(TAG, "Already fetching events, skipping duplicate request") - return - } - - viewModelScope.launch { - try { - isCurrentlyFetching = true - Log.d(TAG, "Starting to fetch events... (forceRefresh: $forceRefresh)") - Log.d(TAG, "Setting isLoading to true") - - _isLoading.value = true - _errorMessage.value = null - - Log.d(TAG, "Calling repository.getAllEvents with forceRefresh: $forceRefresh") - when (val result = repository.getAllEvents(forceRefresh)) { - is Result.Success -> { - val events = result.data - Log.d(TAG, "Events received: ${events.size}") - _allEvents.value = events - filterEvents() // Apply current filter - _errorMessage.value = null - } - is Result.Error -> { - Log.e(TAG, "Error fetching events: ${result.message}") - _errorMessage.value = result.message - _eventList.value = emptyList() - } - is Result.Loading -> { - // Already handled by setting _isLoading to true above - } - } - } catch (e: CancellationException) { - // Don't log cancellation exceptions as errors - Log.d(TAG, "Request cancelled") - throw e - } catch (e: Exception) { - Log.e(TAG, "Unexpected error occurred: ${e.message}", e) - _errorMessage.value = "An unexpected error occurred. Please try again." - } finally { - Log.d(TAG, "Setting isLoading to false") - _isLoading.value = false - isCurrentlyFetching = false - Log.d(TAG, "Finished fetching events - isLoading: ${_isLoading.value}, isCurrentlyFetching: $isCurrentlyFetching") - } - } - } - - /** - * Toggles between live and past events filter - */ - fun toggleEventFilter() { - _showLiveEvents.value = !_showLiveEvents.value - filterEvents() - } - - /** - * Filters events based on the current filter state - * Uses multiple criteria to determine if an event is live or past: - * 1. Backend isLive flag (primary) - * 2. Event dates (fallback for better accuracy) - */ - private fun filterEvents() { - val allEvents = _allEvents.value - - if (allEvents.isEmpty()) { - _eventList.value = emptyList() - return - } - - val currentTime = System.currentTimeMillis() - - val filtered = if (_showLiveEvents.value) { - allEvents.filter { event -> - // Primary check: Use backend isLive flag - val isLiveFromBackend = event.isLive == true - - // Secondary check: Date-based fallback - val isLiveByDate = try { - // If endRegistrationDate is available and in the future, consider it live - event.endRegistrationDate?.let { endRegDate -> - // This is a simplified check. In production, you'd parse the actual date - // For now, we'll rely mainly on the backend isLive flag - true - } ?: true - } catch (e: Exception) { - Log.w(TAG, "Error parsing event dates for ${event.name}: ${e.message}") - true // Default to live if date parsing fails - } - - // Use backend flag as primary source of truth - isLiveFromBackend - } - } else { - allEvents.filter { event -> - event.isLive == false - } - } - - Log.d(TAG, "Filtering events - Show live: ${_showLiveEvents.value}, " + - "Total events: ${allEvents.size}, Filtered count: ${filtered.size}") - - // Additional logging for debugging - val liveCount = allEvents.count { it.isLive == true } - val pastCount = allEvents.count { it.isLive == false } - Log.d(TAG, "Event distribution - Live: $liveCount, Past: $pastCount") - - _eventList.value = filtered - } - - /** - * Fetches a specific event by ID with caching support - * @param eventId The ID of the event to fetch - * @param forceRefresh If true, bypasses cache and fetches fresh data - */ - fun fetchEventById(eventId: String, forceRefresh: Boolean = false) { - if (eventId.isBlank()) { - Log.e(TAG, "Event ID is blank or empty") - _eventError.value = "Invalid event ID" - return - } - - viewModelScope.launch { - _eventLoading.value = true - _eventError.value = null - - try { - Log.d(TAG, "Fetching event with ID: $eventId (forceRefresh: $forceRefresh)") - - when (val result = repository.getEventById(eventId, forceRefresh)) { - is Result.Success -> { - val event = result.data - _selectedEvent.value = event - Log.d(TAG, "Successfully fetched event: ${event.name}") - } - is Result.Error -> { - _eventError.value = result.message - Log.e(TAG, "Error fetching event: ${result.message}") - } - is Result.Loading -> { - // Already handled by setting _eventLoading to true above - } - } - } catch (e: CancellationException) { - Log.d(TAG, "Event fetch cancelled for ID: $eventId") - throw e - } catch (e: Exception) { - Log.e(TAG, "Exception fetching event: ${e.message}", e) - _eventError.value = "An unexpected error occurred. Please try again." - } finally { - _eventLoading.value = false - } - } - } - - /** - * Clears the selected event - */ - fun clearSelectedEvent() { - _selectedEvent.value = null - _eventError.value = null - } - - /** - * Clears all error messages - */ - fun clearErrors() { - _errorMessage.value = null - _eventError.value = null - } - - /** - * Refreshes the events list by forcing a cache refresh - */ - fun refreshEvents() { - fetchAllEvents(forceRefresh = true) - } - - /** - * Returns the current filter state as a readable string - */ - fun getCurrentFilterDescription(): String { - return if (_showLiveEvents.value) "Live Events" else "Past Events" - } - - /** - * Returns the count of events in the current filter - */ - fun getCurrentEventCount(): Int { - return _eventList.value.size - } - - /** - * Checks if there are any events available - */ - fun hasEvents(): Boolean { - return _allEvents.value.isNotEmpty() - } - - // ========== Liked Events Methods ========== - - /** - * Like an event - * @param eventId The ID of the event to like - */ - fun likeEvent(eventId: String) { - likedEventsManager.likeEvent(eventId) - Log.d(TAG, "Event liked: $eventId") - } - - /** - * Unlike an event - * @param eventId The ID of the event to unlike - */ - fun unlikeEvent(eventId: String) { - likedEventsManager.unlikeEvent(eventId) - Log.d(TAG, "Event unliked: $eventId") - } - - /** - * Toggle like state for an event - * @param eventId The ID of the event to toggle - * @return true if event is now liked, false if unliked - */ - fun toggleLike(eventId: String): Boolean { - val isNowLiked = likedEventsManager.toggleLike(eventId) - Log.d(TAG, "Event $eventId like toggled: $isNowLiked") - return isNowLiked - } - - /** - * Check if an event is liked - * @param eventId The ID of the event to check - * @return true if the event is liked, false otherwise - */ - fun isEventLiked(eventId: String): Boolean { - return likedEventsManager.isEventLiked(eventId) - } - - /** - * Get all liked events from the current events list - * @return List of liked EventResponse objects - */ - fun getLikedEvents(): List { - val likedIds = likedEventsManager.getLikedEventIds() - return _allEvents.value.filter { event -> likedIds.contains(event._id) } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/talkeys_new/screens/events/RetrofitClient.kt b/app/src/main/java/com/example/talkeys_new/screens/events/RetrofitClient.kt deleted file mode 100644 index 4dd1c11..0000000 --- a/app/src/main/java/com/example/talkeys_new/screens/events/RetrofitClient.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.example.talkeys_new.screens.events - -import android.content.Context -import com.example.talkeys_new.network.NetworkConfig -import com.example.talkeys_new.screens.authentication.TokenManager -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory - -fun provideEventApiService(context: Context): EventApiService { - val tokenManager = TokenManager(context) - - // Use centralized network configuration with AuthInterceptor - val client = NetworkConfig.createOkHttpClientWithInterceptors( - interceptors = listOf(AuthInterceptor(tokenManager)), - enableCertificatePinning = false // Enable in production - ) - - val retrofit = Retrofit.Builder() - .baseUrl("https://api.talkeys.xyz/") - .addConverterFactory(GsonConverterFactory.create()) - .client(client) - .build() - - return retrofit.create(EventApiService::class.java) -} \ No newline at end of file diff --git a/app/src/main/java/com/example/talkeys_new/screens/events/SharedEventsViewModelProvider.kt b/app/src/main/java/com/example/talkeys_new/screens/events/SharedEventsViewModelProvider.kt index 74c7d79..d4086bc 100644 --- a/app/src/main/java/com/example/talkeys_new/screens/events/SharedEventsViewModelProvider.kt +++ b/app/src/main/java/com/example/talkeys_new/screens/events/SharedEventsViewModelProvider.kt @@ -24,8 +24,9 @@ import org.koin.compose.koinInject * [com.talkeys.shared.di.sharedModule]) and create the ViewModel via * the shared KMP factory, so Android and iOS get identical behaviour. * - * Existing Retrofit-backed [EventViewModel] is NOT removed or altered. - * Screens can migrate incrementally by switching to these helpers. + * The current event read pipeline is shared KMP. Android keeps platform UI and + * action handling, while events list/detail data comes from shared repositories + * and shared ViewModels. */ @Composable diff --git a/app/src/main/java/com/example/talkeys_new/screens/payment/PhonePePaymentScreen.kt b/app/src/main/java/com/example/talkeys_new/screens/payment/PhonePePaymentScreen.kt deleted file mode 100644 index 2d78600..0000000 --- a/app/src/main/java/com/example/talkeys_new/screens/payment/PhonePePaymentScreen.kt +++ /dev/null @@ -1,335 +0,0 @@ -package com.example.talkeys_new.screens.payment - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.activity.result.contract.ActivityResultContracts -import androidx.activity.compose.rememberLauncherForActivityResult -import android.app.Activity -import com.example.talkeys_new.MainActivity -import com.example.talkeys_new.utils.PhonePePaymentManager -import com.example.talkeys_new.utils.PhonePeIntegrationHelper - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun PhonePePaymentScreen( - modifier: Modifier = Modifier -) { - val context = LocalContext.current - val activity = context as? Activity - var amount by remember { mutableStateOf("") } - var orderId by remember { mutableStateOf("") } - var token by remember { mutableStateOf("") } - var isLoading by remember { mutableStateOf(false) } - var paymentStatus by remember { mutableStateOf("Ready to pay") } - var showDebugInfo by remember { mutableStateOf(false) } - - // Payment result launcher - val paymentLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { result -> - PhonePePaymentManager.handlePaymentResult( - resultCode = result.resultCode, - data = result.data - ) { paymentResult -> - isLoading = false - when (paymentResult) { - is PhonePePaymentManager.PaymentResult.Success -> { - paymentStatus = "โœ… Payment Successful! Pass ID: ${paymentResult.passId}" - } - is PhonePePaymentManager.PaymentResult.Failed -> { - paymentStatus = "โŒ Payment Failed: ${paymentResult.message}" - } - is PhonePePaymentManager.PaymentResult.Pending -> { - paymentStatus = "โณ Payment Pending: ${paymentResult.message}" - } - is PhonePePaymentManager.PaymentResult.Error -> { - paymentStatus = "๐Ÿšจ Error: ${paymentResult.message}" - } - else -> { - paymentStatus = "Unknown result: $paymentResult" - } - } - } - } - - Column( - modifier = modifier - .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Text( - text = "PhonePe Payment", - fontSize = 24.sp, - fontWeight = FontWeight.Bold - ) - - Card( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) - ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Text( - text = "Payment Details", - fontSize = 18.sp, - fontWeight = FontWeight.Medium - ) - - OutlinedTextField( - value = amount, - onValueChange = { amount = it }, - label = { Text("Amount") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = orderId, - onValueChange = { orderId = it }, - label = { Text("Order ID") }, - placeholder = { Text("Enter order ID from backend") }, - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = token, - onValueChange = { token = it }, - label = { Text("Payment Token") }, - placeholder = { Text("Enter token from backend") }, - modifier = Modifier.fillMaxWidth() - ) - } - } - - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer) - ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = "Instructions:", - fontWeight = FontWeight.Medium - ) - Text( - text = "1. Get Order ID and Token from your backend's Create Order API", - fontSize = 14.sp - ) - Text( - text = "2. Enter the values above and click Pay Now", - fontSize = 14.sp - ) - Text( - text = "3. After payment, check Order Status API for final result", - fontSize = 14.sp - ) - } - } - - // Payment Status Display - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = when { - paymentStatus.contains("โœ…") -> MaterialTheme.colorScheme.primaryContainer - paymentStatus.contains("โŒ") -> MaterialTheme.colorScheme.errorContainer - paymentStatus.contains("โณ") -> MaterialTheme.colorScheme.secondaryContainer - else -> MaterialTheme.colorScheme.surfaceVariant - } - ) - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - Text( - text = "Payment Status:", - fontWeight = FontWeight.Medium - ) - Text( - text = paymentStatus, - fontSize = 14.sp - ) - } - } - - // Payment Buttons - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - // Complete Payment Flow Button (Recommended) - Button( - onClick = { - activity?.let { act -> - isLoading = true - paymentStatus = "Starting complete payment flow..." - - PhonePeIntegrationHelper.initiateCompletePaymentFlow( - activity = act, - eventId = "EVENT_123", // Replace with actual event ID - passType = "standard", - friends = emptyList(), - launcher = paymentLauncher, - authToken = null, // Add your auth token here - onResult = { result -> - // Result will be handled by paymentLauncher - } - ) - } ?: run { - paymentStatus = "Error: Activity not available" - } - }, - enabled = !isLoading, - modifier = Modifier - .weight(1f) - .height(50.dp) - ) { - if (isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - color = MaterialTheme.colorScheme.onPrimary - ) - } else { - Text("Complete Flow") - } - } - - // Legacy Token Payment Button - Button( - onClick = { - if (orderId.isNotBlank() && token.isNotBlank()) { - activity?.let { act -> - isLoading = true - paymentStatus = "Starting token-based payment..." - - PhonePePaymentManager.startCheckout( - activity = act, - token = token, - orderId = orderId, - activityResultLauncher = paymentLauncher - ) - } ?: run { - paymentStatus = "Error: Activity not available" - } - } - }, - enabled = orderId.isNotBlank() && token.isNotBlank() && !isLoading, - modifier = Modifier - .weight(1f) - .height(50.dp) - ) { - Text("Token Payment") - } - } - - // Debug and Testing Section - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiaryContainer) - ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "Debug & Testing:", - fontWeight = FontWeight.Medium - ) - Button( - onClick = { showDebugInfo = !showDebugInfo } - ) { - Text(if (showDebugInfo) "Hide" else "Show") - } - } - - if (showDebugInfo) { - Text( - text = PhonePeIntegrationHelper.getSDKStatus(), - fontSize = 12.sp, - fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace - ) - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Button( - onClick = { - orderId = "TEST_ORDER_${System.currentTimeMillis()}" - token = "DOC" // Replace with actual token from backend - amount = "100.00" - }, - modifier = Modifier.weight(1f) - ) { - Text("Fill Sample") - } - - Button( - onClick = { - orderId = "" - token = "" - amount = "" - paymentStatus = "Ready to pay" - }, - modifier = Modifier.weight(1f) - ) { - Text("Clear") - } - } - - // WebView Error Handler - Button( - onClick = { - paymentStatus = "Checking WebView errors..." - PhonePeIntegrationHelper.handleWebViewErrors { result -> - paymentStatus = "WebView check result: $result" - } - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Handle WebView Errors") - } - - // Manual Verification - Button( - onClick = { - if (orderId.isNotBlank()) { - paymentStatus = "Manual verification..." - PhonePeIntegrationHelper.verifyPaymentManually( - merchantOrderId = orderId, - authToken = null - ) { result -> - paymentStatus = "Manual verification: $result" - } - } - }, - enabled = orderId.isNotBlank(), - modifier = Modifier.fillMaxWidth() - ) { - Text("Manual Verify Payment") - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/talkeys_new/utils/MockPaymentHandler.kt b/app/src/main/java/com/example/talkeys_new/utils/MockPaymentHandler.kt deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/main/java/com/example/talkeys_new/utils/NetworkUtils.kt b/app/src/main/java/com/example/talkeys_new/utils/NetworkUtils.kt deleted file mode 100644 index b8d71cb..0000000 --- a/app/src/main/java/com/example/talkeys_new/utils/NetworkUtils.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.example.talkeys_new.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 kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import retrofit2.HttpException -import retrofit2.Response -import java.io.IOException - -/** - * Utility class for network operations and connectivity monitoring - */ -class NetworkUtils(private val context: Context) { - - private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - - /** - * Checks if the device is currently connected to the internet - * @return true if connected, false otherwise - */ - fun isNetworkAvailable(): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val network = connectivityManager.activeNetwork ?: return false - val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false - - capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && - capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - } else { - @Suppress("DEPRECATION") - connectivityManager.activeNetworkInfo?.isConnected == true - } - } - - /** - * Provides a Flow that emits network connectivity status changes - * @return Flow that emits true when connected, false when disconnected - */ - fun observeNetworkConnectivity(): Flow = callbackFlow { - val callback = object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - trySend(true) - } - - override fun onLost(network: Network) { - trySend(false) - } - } - - val request = NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .build() - - connectivityManager.registerNetworkCallback(request, callback) - - // Initial value - trySend(isNetworkAvailable()) - - awaitClose { - connectivityManager.unregisterNetworkCallback(callback) - } - }.distinctUntilChanged() - - companion object { - /** - * Safely executes a network call and wraps the response in a Result - * @param apiCall The suspend function that makes the API call - * @return Result representing success, error, or loading state - */ - suspend fun safeApiCall(apiCall: suspend () -> Response): Result { - return try { - val response = apiCall() - if (response.isSuccessful) { - val body = response.body() - if (body != null) { - Result.Success(body) - } else { - Result.Error( - exception = IOException("Empty response body"), - message = "Server returned empty response", - code = response.code() - ) - } - } else { - val errorMsg = response.errorBody()?.string() ?: "Unknown error" - Result.Error( - exception = HttpException(response), - message = getErrorMessage(response.code(), errorMsg), - code = response.code() - ) - } - } catch (e: IOException) { - Result.Error( - exception = e, - message = "Network error: Please check your internet connection" - ) - } catch (e: HttpException) { - Result.Error( - exception = e, - message = getErrorMessage(e.code(), e.message()), - code = e.code() - ) - } catch (e: Exception) { - Result.Error( - exception = e, - message = "An unexpected error occurred: ${e.localizedMessage ?: "Unknown error"}" - ) - } - } - - /** - * Maps HTTP error codes to user-friendly error messages - * @param code HTTP status code - * @param fallbackMsg Fallback message if no specific message is defined for the code - * @return User-friendly error message - */ - private fun getErrorMessage(code: Int, fallbackMsg: String): String { - return when (code) { - 400 -> "Bad request: The server couldn't understand the request" - 401 -> "Unauthorized: Please log in again" - 403 -> "Forbidden: You don't have permission to access this resource" - 404 -> "Not found: The requested resource doesn't exist" - 408 -> "Request timeout: Please try again" - 429 -> "Too many requests: Please wait before trying again" - 500 -> "Server error: Something went wrong on our end" - 502 -> "Bad gateway: The server is temporarily unavailable" - 503 -> "Service unavailable: The server is currently unavailable" - 504 -> "Gateway timeout: The server took too long to respond" - else -> fallbackMsg - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/talkeys_new/utils/PhonePeConfig.kt b/app/src/main/java/com/example/talkeys_new/utils/PhonePeConfig.kt deleted file mode 100644 index 0cc03ad..0000000 --- a/app/src/main/java/com/example/talkeys_new/utils/PhonePeConfig.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.talkeys_new.utils - -import com.talkeys.shared.config.ProductionConfig - -/** - * PhonePe mobile configuration. - * - * Only public SDK identifiers belong here. Order creation, signing, and - * verification must stay on the backend. - */ -object PhonePeConfig { - - // Public identifiers used by the mobile SDK. - const val MERCHANT_ID = "M22ZDT307F584" - const val CLIENT_ID = ProductionConfig.PHONEPE_CLIENT_ID - - // Environment Configuration - const val IS_PRODUCTION = ProductionConfig.IS_PHONEPE_PRODUCTION - - // API Endpoints (you'll need these for backend integration) - const val SANDBOX_BASE_URL = "https://api-preprod.phonepe.com/apis/pg-sandbox" - const val PRODUCTION_BASE_URL = "https://api.phonepe.com/apis/hermes" - - fun getBaseUrl(): String { - return if (IS_PRODUCTION) PRODUCTION_BASE_URL else SANDBOX_BASE_URL - } - - fun getEnvironmentName(): String { - return if (IS_PRODUCTION) "PRODUCTION" else "SANDBOX" - } - - /** - * Generate unique flow ID for each payment session - */ - fun generateFlowId(): String { - return "TALKEYS_${System.currentTimeMillis()}" - } - - /** - * CLIENT_ID is used for SDK initialization and is safe to embed in the app. - */ -} diff --git a/app/src/main/java/com/example/talkeys_new/utils/PhonePeIntegrationHelper.kt b/app/src/main/java/com/example/talkeys_new/utils/PhonePeIntegrationHelper.kt deleted file mode 100644 index 8056b4b..0000000 --- a/app/src/main/java/com/example/talkeys_new/utils/PhonePeIntegrationHelper.kt +++ /dev/null @@ -1,203 +0,0 @@ -package com.example.talkeys_new.utils - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.util.Log -import androidx.activity.result.ActivityResultLauncher -import com.phonepe.intent.sdk.api.PhonePeKt -import com.phonepe.intent.sdk.api.models.PhonePeEnvironment -import com.talkeys.shared.data.payment.Friend - -/** - * Production-ready PhonePe Integration Helper - * - * This class provides utility methods for PhonePe integration with proper error handling, - * WebView fallback, and environment switching. - */ -object PhonePeIntegrationHelper { - - private const val TAG = "PhonePeHelper" - - /** - * Complete payment flow with automatic backend integration - * - * @param activity Current activity - * @param eventId Event ID for ticket booking - * @param passType Pass type (e.g., "standard", "premium") - * @param friends List of friends for group booking - * @param launcher Activity result launcher - * @param authToken User authentication token - * @param onResult Callback for payment result - */ - fun initiateCompletePaymentFlow( - activity: Activity, - eventId: String, - passType: String, - friends: List = emptyList(), - launcher: ActivityResultLauncher, - authToken: String? = null, - onResult: (PhonePePaymentManager.PaymentResult) -> Unit - ) { - Log.d(TAG, "๐Ÿš€ Starting complete payment flow for event: $eventId") - - PhonePePaymentManager.bookTicketAndPay( - activity = activity, - eventId = eventId, - passType = passType, - friends = friends, - activityResultLauncher = launcher, - authToken = authToken, - onResult = onResult - ) - } - - /** - * Manual payment verification for WebView issues - * Call this when WebView shows errors but you want to check actual payment status - */ - fun verifyPaymentManually( - merchantOrderId: String, - authToken: String? = null, - onResult: (PhonePePaymentManager.PaymentResult) -> Unit - ) { - Log.d(TAG, "๐Ÿ” Manual payment verification requested") - - PhonePePaymentManager.manualPaymentVerification( - merchantOrderId = merchantOrderId, - authToken = authToken, - onResult = onResult - ) - } - - /** - * Handle WebView JavaScript errors - * Call this when you see JavaScript console errors in WebView - */ - fun handleWebViewErrors(onResult: (PhonePePaymentManager.PaymentResult) -> Unit) { - Log.d(TAG, "๐Ÿšจ Handling WebView JavaScript errors") - - PhonePePaymentManager.handleWebViewError(onResult) - } - - /** - * Check PhonePe app installation status - */ - fun isPhonePeAppInstalled(context: Context): Boolean { - return try { - context.packageManager.getPackageInfo("com.phonepe.app", 0) - true - } catch (e: Exception) { - false - } - } - - /** - * Get current SDK status for debugging - */ - fun getSDKStatus(): String { - return """ - PhonePe SDK Status: - - Environment: ${PhonePeConfig.getEnvironmentName()} - - Production Mode: ${PhonePeConfig.IS_PRODUCTION} - - ${PhonePePaymentManager.getCurrentPaymentInfo()} - """.trimIndent() - } - - /** - * Environment validation helper - */ - fun validateEnvironmentSetup(): List { - val issues = mutableListOf() - - // Check merchant ID format - if (PhonePeConfig.MERCHANT_ID.isBlank()) { - issues.add("Merchant ID is empty") - } - - // Check client ID format - if (PhonePeConfig.CLIENT_ID.isBlank()) { - issues.add("Client ID is empty") - } - - // Check environment consistency - if (PhonePeConfig.IS_PRODUCTION) { - if (PhonePeConfig.MERCHANT_ID.startsWith("PGTESTPAYUAT")) { - issues.add("Production mode but using test merchant ID") - } - } else { - if (!PhonePeConfig.MERCHANT_ID.startsWith("PGTESTPAYUAT") && - !PhonePeConfig.MERCHANT_ID.startsWith("M")) { - issues.add("Sandbox mode but merchant ID format unclear") - } - } - - return issues - } - - /** - * Force payment verification - use when WebView shows errors - */ - fun forceVerifyCurrentPayment(): String { - return PhonePePaymentManager.forcePaymentVerification() - } - - /** - * Open PhonePe app directly (if installed) - */ - fun openPhonePeApp(context: Context): Boolean { - return try { - val intent = context.packageManager.getLaunchIntentForPackage("com.phonepe.app") - if (intent != null) { - context.startActivity(intent) - true - } else { - false - } - } catch (e: Exception) { - Log.e(TAG, "Failed to open PhonePe app: ${e.message}") - false - } - } - - /** - * Open PhonePe in Play Store for installation - */ - fun openPhonePeInPlayStore(context: Context) { - try { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.phonepe.app")) - context.startActivity(intent) - } catch (e: Exception) { - // Fallback to web browser - val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=com.phonepe.app")) - context.startActivity(intent) - } - } - - /** - * Reinitialize PhonePe SDK (for troubleshooting) - */ - fun reinitializeSDK(context: Context): Boolean { - return try { - val environment = if (PhonePeConfig.IS_PRODUCTION) { - PhonePeEnvironment.RELEASE - } else { - PhonePeEnvironment.SANDBOX - } - - PhonePeKt.init( - context = context, - merchantId = PhonePeConfig.MERCHANT_ID, - flowId = PhonePeConfig.generateFlowId(), - phonePeEnvironment = environment, - enableLogging = !PhonePeConfig.IS_PRODUCTION, - appId = null - ) - } catch (e: Exception) { - Log.e(TAG, "Failed to reinitialize SDK: ${e.message}") - false - } - } -} diff --git a/app/src/main/java/com/example/talkeys_new/utils/PhonePePaymentManager.kt b/app/src/main/java/com/example/talkeys_new/utils/PhonePePaymentManager.kt deleted file mode 100644 index 7a3d38b..0000000 --- a/app/src/main/java/com/example/talkeys_new/utils/PhonePePaymentManager.kt +++ /dev/null @@ -1,722 +0,0 @@ -package com.example.talkeys_new.utils - -import android.app.Activity -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri -import android.util.Log -import androidx.activity.result.ActivityResultLauncher -import com.phonepe.intent.sdk.api.PhonePeKt -import com.phonepe.intent.sdk.api.PhonePeInitException -import com.phonepe.intent.sdk.api.models.PhonePeEnvironment -import com.talkeys.shared.data.payment.PaymentRepository -import com.talkeys.shared.data.payment.Friend -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject - -/** - * PhonePe Payment Manager - * Handles PhonePe payment integration and checkout flow with backend integration - */ -object PhonePePaymentManager : KoinComponent { - - private const val TAG = "PhonePePayment" - private const val B2B_PG_REQUEST_CODE = 777 - - // PRODUCTION CONFIGURATION - Should match PhonePeConfig - private const val PRODUCTION_MODE = PhonePeConfig.IS_PRODUCTION - - private val paymentRepository: PaymentRepository by inject() - - // Store current payment details for verification - private var currentMerchantOrderId: String? = null - private var currentPaymentCallback: ((PaymentResult) -> Unit)? = null - private var currentAuthToken: String? = null - - /** - * Book ticket and initiate PhonePe payment - * This method handles the complete flow: book ticket -> get payment token -> start PhonePe - */ - fun bookTicketAndPay( - activity: Activity, - eventId: String, - passType: String, - friends: List, - activityResultLauncher: ActivityResultLauncher, - authToken: String? = null, - onResult: (PaymentResult) -> Unit - ) { - currentPaymentCallback = onResult - currentAuthToken = authToken - - // โœ… Add timeout mechanism for unresponsive payment screens - CoroutineScope(Dispatchers.IO).launch { - Log.d(TAG, "๐Ÿ• Starting 60-second payment timeout timer...") - delay(60000) // Wait 60 seconds - - currentMerchantOrderId?.let { orderId -> - Log.d(TAG, "โฐ Payment timeout reached, checking status") - Log.d(TAG, "๐Ÿ’ก This handles cases where PhonePe WebView becomes unresponsive") - Log.d(TAG, "๐Ÿ” WebView might show errors but payment could still succeed") - - verifyPaymentStatusOnServer(orderId, authToken) { result -> - Log.d(TAG, "๐Ÿ“‹ Timeout verification completed") - when (result) { - is PaymentResult.Success -> { - Log.d(TAG, "โœ… Payment succeeded despite WebView timeout/errors!") - Log.d(TAG, "๐ŸŽ‰ This proves WebView errors are cosmetic") - onResult(result) - clearCurrentPayment() - } - is PaymentResult.Pending -> { - Log.d(TAG, "โณ Payment still pending after timeout - will retry") - // Don't clear - let user try again - } - is PaymentResult.Failed -> { - Log.d(TAG, "โŒ Payment confirmed failed after timeout") - onResult(result) - clearCurrentPayment() - } - else -> { - Log.d(TAG, "โ“ Unknown status after timeout: $result") - // Don't override if user is still trying - } - } - } - } ?: run { - Log.w(TAG, "โš ๏ธ No merchant order ID found for timeout verification") - } - } - - CoroutineScope(Dispatchers.IO).launch { - try { - if (PRODUCTION_MODE) { - Log.i(TAG, "Production: Booking ticket for event: $eventId") - } else { - Log.d(TAG, "Booking ticket for event: $eventId") - } - - val result = paymentRepository.bookTicket(eventId, passType, friends, authToken) - - result.fold( - onSuccess = { paymentOrderData -> - Log.d(TAG, "Ticket booked successfully. Starting PhonePe payment...") - - // Store merchant order ID for verification - currentMerchantOrderId = paymentOrderData.merchantOrderId - - withContext(Dispatchers.Main) { - // โœ… Log what backend is sending - val paymentToken = paymentOrderData.token - val orderId = paymentOrderData.orderId - - Log.d(TAG, "Received payment data from backend (env=${PhonePeConfig.getEnvironmentName()})") - - // โœ… Debug token format - debugTokenFormat(paymentToken) - - if (paymentToken.startsWith("http") && paymentToken.contains("mercury")) { - val extractedToken = extractTokenFromUrl(paymentToken) - startPhonePeCheckout( - activity = activity, - token = extractedToken, - orderId = orderId, - activityResultLauncher = activityResultLauncher - ) - } else { - startPhonePeCheckout( - activity = activity, - token = paymentToken, - orderId = orderId, - activityResultLauncher = activityResultLauncher - ) - } - } - }, - onFailure = { exception -> - Log.e(TAG, "Failed to book ticket: ${exception.message}", exception) - withContext(Dispatchers.Main) { - onResult(PaymentResult.Error("Failed to create payment order: ${exception.message}")) - } - } - ) - } catch (e: Exception) { - Log.e(TAG, "Unexpected error during ticket booking: ${e.message}", e) - withContext(Dispatchers.Main) { - onResult(PaymentResult.Error("Unexpected error: ${e.message}")) - } - } - } - } - - /** - * Initiate PhonePe standard checkout with enhanced app detection and WebView fallback - * - * @param activity Current activity context - * @param token Payment token received from backend (Create Order API response) - * @param orderId Order ID received from backend (Create Order API response) - * @param activityResultLauncher Activity result launcher to handle payment result - */ - private fun startPhonePeCheckout( - activity: Activity, - token: String, - orderId: String, - activityResultLauncher: ActivityResultLauncher - ) { - try { - Log.d(TAG, "Starting PhonePe checkout") - - // โœ… Enhanced token validation - if (token.isBlank()) { - throw IllegalArgumentException("Payment token is empty") - } - if (orderId.isBlank()) { - throw IllegalArgumentException("Order ID is empty") - } - - // โœ… Validate token format - PhonePe tokens should be longer than JWT tokens - if (token.startsWith("eyJ")) { - Log.e(TAG, "โŒ JWT token detected instead of PhonePe token!") - Log.e(TAG, "โŒ Backend is sending JWT token, but PhonePe expects their own token format") - Log.e(TAG, "โŒ Backend must return the token from PhonePe Create Order API response") - throw IllegalArgumentException("Invalid token format: JWT token received instead of PhonePe token") - } - - if (token.length < 200) { - Log.w(TAG, "โš ๏ธ Token seems shorter than expected for PhonePe format") - Log.w(TAG, "โš ๏ธ Expected PhonePe tokens are typically 400+ characters") - } - - val isPhonePeInstalled = checkPhonePeAppInstallation(activity) - Log.d(TAG, "PhonePe app installed=$isPhonePeInstalled, sdkEnv=${PhonePeConfig.getEnvironmentName()}") - - // โœ… Try PhonePe SDK first (will use app if available, WebView if not) - try { - PhonePeKt.startCheckoutPage( - context = activity, - token = token, - orderId = orderId, - activityResultLauncher = activityResultLauncher - ) - - Log.d(TAG, "โœ… PhonePe checkout initiated successfully") - - if (!isPhonePeInstalled) { - Log.d(TAG, "๐ŸŒ Will use WebView fallback since PhonePe app not installed") - Log.d(TAG, "๐Ÿ’ก WebView may show JavaScript errors - these are cosmetic and don't affect payment") - } - - } catch (ex: Exception) { - Log.e(TAG, "โŒ PhonePe SDK failed, trying manual WebView fallback: ${ex.message}") - - // โœ… Manual WebView fallback for edge cases - startWebViewFallback(activity, token, orderId) - } - - } catch (ex: PhonePeInitException) { - Log.e(TAG, "PhonePe initialization error: ${ex.message}", ex) - Log.e(TAG, "SDK env=${PhonePeConfig.getEnvironmentName()}") - - // Try to reinitialize SDK once - if (reinitializeSDK(activity)) { - Log.d(TAG, "๐Ÿ”„ SDK reinitialized, retrying payment...") - startPhonePeCheckout(activity, token, orderId, activityResultLauncher) - } else { - handlePaymentError(ex, "PhonePe SDK not initialized properly. Please restart the app.") - } - } catch (ex: IllegalArgumentException) { - Log.e(TAG, "โŒ Invalid payment parameters: ${ex.message}", ex) - Log.e(TAG, "๐Ÿ’ก Solution: Check backend token format - should be PhonePe token, not JWT") - handlePaymentError(ex, "Invalid payment data: ${ex.message}") - } catch (ex: Exception) { - Log.e(TAG, "โŒ Error starting PhonePe checkout: ${ex.message}", ex) - Log.e(TAG, "๐Ÿ’ก Check token format and environment configuration") - handlePaymentError(ex, "Transaction could not be started: ${ex.message}") - } - } - - /** - * Enhanced PhonePe app installation check with multiple methods - */ - private fun checkPhonePeAppInstallation(activity: Activity): Boolean { - val packageManager = activity.packageManager - - return try { - // Method 1: Check package info - packageManager.getPackageInfo("com.phonepe.app", 0) - Log.d(TAG, "โœ… PhonePe app detected via package info") - true - } catch (e: PackageManager.NameNotFoundException) { - try { - // Method 2: Check if PhonePe intent can be resolved - val intent = Intent(Intent.ACTION_VIEW, Uri.parse("phonepe://")) - val resolveInfo = packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) - if (resolveInfo != null) { - Log.d(TAG, "โœ… PhonePe app detected via intent resolution") - true - } else { - Log.d(TAG, "โŒ PhonePe app not found - will use WebView") - false - } - } catch (e2: Exception) { - Log.d(TAG, "โŒ PhonePe app not installed: ${e2.message}") - false - } - } catch (e: Exception) { - Log.w(TAG, "โš ๏ธ Error checking PhonePe installation: ${e.message}") - false - } - } - - /** - * Manual WebView fallback when SDK fails - */ - private fun startWebViewFallback(activity: Activity, token: String, orderId: String) { - try { - Log.d(TAG, "๐ŸŒ Starting manual WebView fallback") - - // Create PhonePe payment URL - val paymentUrl = if (token.startsWith("http")) { - token - } else { - // Construct URL from token - val baseUrl = if (PhonePeConfig.IS_PRODUCTION) { - "https://mercury.phonepe.com/transact/pg" - } else { - "https://mercury-t2.phonepe.com/transact/pg" - } - "$baseUrl?token=$token" - } - - Log.d(TAG, "๐ŸŒ Opening PhonePe WebView fallback") - - // Open in WebView or browser - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(paymentUrl)) - activity.startActivity(intent) - - Log.d(TAG, "โœ… WebView fallback initiated") - Log.d(TAG, "๐Ÿ’ก User will complete payment in browser/WebView") - Log.d(TAG, "๐Ÿ’ก Use manual verification after payment completion") - - } catch (e: Exception) { - Log.e(TAG, "โŒ WebView fallback failed: ${e.message}") - handlePaymentError(e, "Payment could not be started: ${e.message}") - } - } - - /** - * Attempt to reinitialize PhonePe SDK - */ - private fun reinitializeSDK(activity: Activity): Boolean { - return try { - Log.d(TAG, "๐Ÿ”„ Attempting to reinitialize PhonePe SDK...") - - val environment = if (PhonePeConfig.IS_PRODUCTION) { - PhonePeEnvironment.RELEASE - } else { - PhonePeEnvironment.SANDBOX - } - - val result = PhonePeKt.init( - context = activity.applicationContext, - merchantId = PhonePeConfig.MERCHANT_ID, - flowId = PhonePeConfig.generateFlowId(), - phonePeEnvironment = environment, - enableLogging = !PhonePeConfig.IS_PRODUCTION, - appId = null - ) - - if (result) { - Log.d(TAG, "โœ… PhonePe SDK reinitialized successfully") - true - } else { - Log.e(TAG, "โŒ PhonePe SDK reinitialization failed") - false - } - } catch (e: Exception) { - Log.e(TAG, "โŒ Error reinitializing SDK: ${e.message}") - false - } - } - - /** - * Extract token from PhonePe payment URL - * URL format: https://mercury-t2.phonepe.com/transact/pg?token=ACTUAL_TOKEN - */ - private fun extractTokenFromUrl(paymentUrl: String): String { - return try { - val uri = android.net.Uri.parse(paymentUrl) - val token = uri.getQueryParameter("token") - - if (token != null) { - Log.d(TAG, "Extracted checkout token from payment URL") - token - } else { - Log.w(TAG, "No token parameter found in URL, using full URL") - paymentUrl - } - } catch (e: Exception) { - Log.e(TAG, "Failed to extract token from URL: ${e.message}") - paymentUrl - } - } - - /** - * Legacy method for direct PhonePe checkout (for backward compatibility) - */ - fun startCheckout( - activity: Activity, - token: String, - orderId: String, - activityResultLauncher: ActivityResultLauncher - ) { - startPhonePeCheckout(activity, token, orderId, activityResultLauncher) - } - - /** - * Handle payment result from ActivityResultLauncher - * This method automatically verifies payment status with backend - * - * @param resultCode Result code from the payment activity - * @param data Intent data from the payment activity - * @param onPaymentResult Callback to handle the payment result (optional, uses stored callback if not provided) - */ - fun handlePaymentResult( - resultCode: Int, - data: Intent?, - onPaymentResult: ((PaymentResult) -> Unit)? = null - ) { - Log.d(TAG, "๐Ÿ” Handling payment result with code: $resultCode") - - Log.d(TAG, "Payment result includes data=${data != null}") - - val callback = onPaymentResult ?: currentPaymentCallback - - // โœ… CRITICAL FIX: Always verify payment status regardless of result code - // WebView JavaScript errors can cause wrong result codes even when payment succeeds - Log.d(TAG, "๐Ÿšจ WEBVIEW BYPASS: Always verifying payment status due to known WebView issues") - Log.d(TAG, "๐Ÿ’ก JavaScript errors in PhonePe WebView don't indicate payment failure") - - currentMerchantOrderId?.let { merchantOrderId -> - Log.d(TAG, "๐Ÿ” Starting payment verification") - Log.d(TAG, "๐Ÿ“ฑ Result code was: $resultCode (ignoring due to WebView issues)") - - CoroutineScope(Dispatchers.IO).launch { - // โœ… Immediate verification + retry mechanism - Log.d(TAG, "โšก Immediate verification attempt...") - verifyPaymentStatusWithRetry(merchantOrderId, currentAuthToken, callback, maxRetries = 3) - } - } ?: run { - Log.e(TAG, "โŒ No merchant order ID found for verification") - CoroutineScope(Dispatchers.Main).launch { - callback?.invoke(PaymentResult.Error("Payment verification failed: No order ID")) - } - } - } - - /** - * Verify payment status with retry mechanism for WebView issues - */ - private suspend fun verifyPaymentStatusWithRetry( - merchantOrderId: String, - authToken: String?, - callback: ((PaymentResult) -> Unit)?, - maxRetries: Int = 3, - currentAttempt: Int = 1 - ) { - Log.d(TAG, "๐Ÿ”„ Payment verification attempt $currentAttempt/$maxRetries") - - try { - val result = paymentRepository.verifyPaymentStatus(merchantOrderId, authToken) - - result.fold( - onSuccess = { paymentStatusData -> - Log.d(TAG, "โœ… Payment verification successful (attempt $currentAttempt)") - Log.d(TAG, "๐Ÿ“Š Raw Status from backend: '${paymentStatusData.paymentStatus}'") - Log.d(TAG, "๐Ÿ“Š Status uppercase: '${paymentStatusData.paymentStatus.uppercase()}'") - - withContext(Dispatchers.Main) { - when (paymentStatusData.paymentStatus.uppercase()) { - "COMPLETED" -> { - Log.d(TAG, "๐ŸŽ‰ Payment COMPLETED - WebView issues bypassed!") - callback?.invoke( - PaymentResult.Success( - message = "Payment completed successfully", - passId = paymentStatusData.passId, - passUUID = paymentStatusData.passUUID - ) - ) - clearCurrentPayment() - return@withContext - } - "FAILED" -> { - Log.d(TAG, "โŒ Payment FAILED") - callback?.invoke(PaymentResult.Failed("Payment failed")) - clearCurrentPayment() - return@withContext - } - "PENDING" -> { - Log.d(TAG, "โณ Payment PENDING - will retry...") - if (currentAttempt < maxRetries) { - // Retry after delay for pending payments - delay(5000) // Wait 5 seconds - verifyPaymentStatusWithRetry(merchantOrderId, authToken, callback, maxRetries, currentAttempt + 1) - } else { - callback?.invoke(PaymentResult.Pending("Payment is still pending after $maxRetries attempts")) - clearCurrentPayment() - } - return@withContext - } - else -> { - Log.w(TAG, "๐Ÿค” Unknown status: ${paymentStatusData.paymentStatus}") - if (currentAttempt < maxRetries) { - delay(3000) - verifyPaymentStatusWithRetry(merchantOrderId, authToken, callback, maxRetries, currentAttempt + 1) - } else { - callback?.invoke(PaymentResult.Error("Unknown payment status: ${paymentStatusData.paymentStatus}")) - clearCurrentPayment() - } - } - } - } - }, - onFailure = { exception -> - Log.e(TAG, "โŒ Payment verification failed (attempt $currentAttempt): ${exception.message}") - - if (currentAttempt < maxRetries) { - Log.d(TAG, "๐Ÿ”„ Retrying verification in 3 seconds...") - delay(3000) - verifyPaymentStatusWithRetry(merchantOrderId, authToken, callback, maxRetries, currentAttempt + 1) - } else { - Log.e(TAG, "๐Ÿ’ฅ All verification attempts failed") - withContext(Dispatchers.Main) { - callback?.invoke(PaymentResult.Error("Payment verification failed after $maxRetries attempts: ${exception.message}")) - } - clearCurrentPayment() - } - } - ) - } catch (e: Exception) { - Log.e(TAG, "๐Ÿ’ฅ Unexpected error during verification (attempt $currentAttempt): ${e.message}") - - if (currentAttempt < maxRetries) { - delay(3000) - verifyPaymentStatusWithRetry(merchantOrderId, authToken, callback, maxRetries, currentAttempt + 1) - } else { - withContext(Dispatchers.Main) { - callback?.invoke(PaymentResult.Error("Verification error after $maxRetries attempts: ${e.message}")) - } - clearCurrentPayment() - } - } - } - - /** - * Verify payment status on server (as per backend developer's specification) - * GET /api/payment/app-status-check/:merchantOrderId - */ - fun verifyPaymentStatusOnServer( - merchantOrderId: String, - authToken: String? = null, - onResult: ((PaymentResult) -> Unit)? - ) { - Log.d(TAG, "Verifying payment status") - - CoroutineScope(Dispatchers.IO).launch { - try { - val result = paymentRepository.verifyPaymentStatus(merchantOrderId, authToken) - - result.fold( - onSuccess = { paymentStatusData -> - Log.d(TAG, "Payment verification successful. Status: ${paymentStatusData.paymentStatus}") - - withContext(Dispatchers.Main) { - when (paymentStatusData.paymentStatus.uppercase()) { - "COMPLETED" -> { - onResult?.invoke( - PaymentResult.Success( - message = "Payment completed successfully", - passId = paymentStatusData.passId, - passUUID = paymentStatusData.passUUID - ) - ) - } - "FAILED" -> { - onResult?.invoke(PaymentResult.Failed("Payment failed")) - } - "PENDING" -> { - onResult?.invoke(PaymentResult.Pending("Payment is still pending")) - } - else -> { - onResult?.invoke(PaymentResult.Error("Unknown payment status: ${paymentStatusData.paymentStatus}")) - } - } - } - clearCurrentPayment() - }, - onFailure = { exception -> - Log.e(TAG, "Payment verification failed: ${exception.message}", exception) - withContext(Dispatchers.Main) { - onResult?.invoke(PaymentResult.Error("Payment verification failed: ${exception.message}")) - } - clearCurrentPayment() - } - ) - } catch (e: Exception) { - Log.e(TAG, "Unexpected error during payment verification: ${e.message}", e) - withContext(Dispatchers.Main) { - onResult?.invoke(PaymentResult.Error("Verification error: ${e.message}")) - } - clearCurrentPayment() - } - } - } - - /** - * Debug token format and provide recommendations - */ - private fun debugTokenFormat(token: String) { - // Non-sensitive format diagnostics only โ€” token contents never logged. - when { - token.startsWith("eyJ") -> Log.e(TAG, "Unexpected JWT-format token from backend") - token.startsWith("http") -> Log.w(TAG, "Token in URL form; will extract param") - token.length < 200 -> Log.w(TAG, "Token shorter than expected for PhonePe format") - } - if (token.contains(" ") || token.contains("\n") || token.contains("\r")) { - Log.w(TAG, "Token contains whitespace; may be malformed") - } - } - - /** - * Clear current payment data - */ - private fun clearCurrentPayment() { - currentMerchantOrderId = null - currentPaymentCallback = null - currentAuthToken = null - } - - /** - * Handle payment errors - */ - private fun handlePaymentError(exception: Exception, message: String) { - Log.e(TAG, "Payment Error: $message", exception) - - currentPaymentCallback?.invoke(PaymentResult.Error(message)) - clearCurrentPayment() - - when (exception) { - is PhonePeInitException -> { - Log.e(TAG, "SDK needs to be re-initialized") - // Either re-init the SDK and call startCheckoutPage again - // or use other ways to complete your transaction - } - else -> { - Log.e(TAG, "General payment error occurred") - } - } - } - - /** - * Sealed class to represent different payment result states - */ - sealed class PaymentResult { - data class Success( - val message: String, - val passId: String, - val passUUID: String? = null // Made optional to match backend response - ) : PaymentResult() - - data class Failed(val message: String) : PaymentResult() - data class Pending(val message: String) : PaymentResult() - data class Cancelled(val message: String) : PaymentResult() - data class Error(val message: String) : PaymentResult() - - // Legacy support - data class Completed(val message: String) : PaymentResult() - } - - /** - * Manual payment verification for debugging WebView issues - * Call this method when WebView shows "Something went wrong" - */ - fun manualPaymentVerification( - merchantOrderId: String, - authToken: String? = null, - onResult: (PaymentResult) -> Unit - ) { - Log.d(TAG, "๐Ÿ” Manual payment verification requested") - Log.d(TAG, "๐Ÿ’ก This bypasses WebView issues and checks actual payment status") - - verifyPaymentStatusOnServer(merchantOrderId, authToken) { result -> - Log.d(TAG, "๐Ÿ“‹ Manual verification completed") - onResult(result) - } - } - - /** - * Get current payment details for debugging - */ - fun getCurrentPaymentInfo(): String { - return """ - Current Payment Info: - - Has Merchant Order ID: ${currentMerchantOrderId != null} - - Has Callback: ${currentPaymentCallback != null} - - Has Auth Token: ${currentAuthToken != null} - - SDK Environment: ${PhonePeConfig.getEnvironmentName()} - """.trimIndent() - } - - /** - * Handle WebView JavaScript errors - bypass and verify payment - * Call this when you see JavaScript errors in logs but want to check payment status - */ - fun handleWebViewError(onResult: (PaymentResult) -> Unit) { - Log.d(TAG, "๐Ÿšจ WebView error handler called") - Log.d(TAG, "๐Ÿ’ก This bypasses WebView JavaScript issues and checks actual payment status") - - currentMerchantOrderId?.let { merchantOrderId -> - Log.d(TAG, "๐Ÿ” Checking payment status") - - CoroutineScope(Dispatchers.IO).launch { - verifyPaymentStatusWithRetry(merchantOrderId, currentAuthToken, { result -> - Log.d(TAG, "๐Ÿ“‹ WebView error status check completed") - onResult(result) - }, maxRetries = 2) - } - } ?: run { - Log.e(TAG, "โŒ No current payment to verify") - onResult(PaymentResult.Error("No active payment to verify")) - } - } - - /** - * Force payment verification - use when WebView shows errors - */ - fun forcePaymentVerification(): String { - return currentMerchantOrderId?.let { orderId -> - Log.d(TAG, "๐Ÿ” Force verification requested") - - CoroutineScope(Dispatchers.IO).launch { - verifyPaymentStatusWithRetry(orderId, currentAuthToken, currentPaymentCallback, maxRetries = 1) - } - - "Verification started" - } ?: "No active payment to verify" - } - - /** - * Data class for payment request - */ - data class PaymentRequest( - val token: String, - val orderId: String, - val amount: Double, - val description: String? = null - ) -} diff --git a/app/src/main/java/com/example/talkeys_new/utils/WebViewErrorHandler.kt b/app/src/main/java/com/example/talkeys_new/utils/WebViewErrorHandler.kt deleted file mode 100644 index 44990bd..0000000 --- a/app/src/main/java/com/example/talkeys_new/utils/WebViewErrorHandler.kt +++ /dev/null @@ -1,157 +0,0 @@ -package com.example.talkeys_new.utils - -import android.util.Log -import android.webkit.ConsoleMessage -import android.webkit.WebChromeClient -import android.webkit.WebView - -/** - * WebView Error Handler for PhonePe Integration - * - * This class helps capture and handle WebView JavaScript errors that occur - * during PhonePe payment processing. These errors are often cosmetic and - * don't indicate actual payment failure. - */ -class WebViewErrorHandler { - - companion object { - private const val TAG = "WebViewError" - - /** - * Create a WebChromeClient that captures JavaScript console messages - */ - fun createErrorCapturingWebChromeClient(): WebChromeClient { - return object : WebChromeClient() { - override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { - val message = consoleMessage.message() - val level = consoleMessage.messageLevel() - when (level) { - ConsoleMessage.MessageLevel.ERROR -> { - Log.e(TAG, "WebView JavaScript error received") - - // Check for known PhonePe WebView errors - analyzePhonePeError(message) - } - ConsoleMessage.MessageLevel.WARNING -> { - Log.w(TAG, "WebView warning received") - } - ConsoleMessage.MessageLevel.LOG -> { - Log.d(TAG, "WebView console log received") - } - else -> { - Log.i(TAG, "WebView console info received") - } - } - - return true // Return true to prevent default handling - } - } - } - - /** - * Analyze PhonePe-specific WebView errors - */ - private fun analyzePhonePeError(message: String) { - when { - message.contains("Content Security Policy") -> { - Log.d(TAG, "๐Ÿ’ก CSP Error Analysis:") - Log.d(TAG, " - This is a Content Security Policy violation") - Log.d(TAG, " - Common in PhonePe WebView, usually cosmetic") - Log.d(TAG, " - Payment can still succeed despite this error") - Log.d(TAG, " - Always verify payment status via backend API") - } - - message.contains("worker") && message.contains("blob:") -> { - Log.d(TAG, "๐Ÿ’ก Web Worker Error Analysis:") - Log.d(TAG, " - PhonePe trying to create web worker") - Log.d(TAG, " - Blocked by CSP policy") - Log.d(TAG, " - This doesn't affect payment processing") - Log.d(TAG, " - Payment verification should still work") - } - - message.contains("Error during payment processing") -> { - Log.d(TAG, "๐Ÿ’ก Payment Processing Error Analysis:") - Log.d(TAG, " - Generic payment processing error") - Log.d(TAG, " - Could be cosmetic WebView error") - Log.d(TAG, " - CRITICAL: Verify payment status on backend") - Log.d(TAG, " - Use PhonePeIntegrationHelper.handleWebViewErrors()") - } - - message.contains("phonepe.com") -> { - Log.d(TAG, "๐Ÿ’ก PhonePe Domain Error Analysis:") - Log.d(TAG, " - Error from PhonePe domain") - Log.d(TAG, " - Likely WebView compatibility issue") - Log.d(TAG, " - Payment might have succeeded on server") - Log.d(TAG, " - Check payment status via API") - } - - else -> { - Log.d(TAG, "๐Ÿ’ก Unknown WebView Error:") - Log.d(TAG, " - Unrecognized error pattern") - Log.d(TAG, " - Monitor for payment impact") - Log.d(TAG, " - Verify payment status if needed") - } - } - } - - /** - * Handle WebView errors and suggest next steps - */ - fun handleWebViewError( - errorMessage: String, - onSuggestVerification: () -> Unit - ) { - Log.d(TAG, "WebView error handler invoked") - - val shouldVerify = when { - errorMessage.contains("Error during payment processing") -> { - Log.d(TAG, " Recommendation: VERIFY PAYMENT STATUS") - true - } - errorMessage.contains("Content Security Policy") -> { - Log.d(TAG, " Recommendation: Monitor, likely cosmetic") - false - } - errorMessage.contains("worker") -> { - Log.d(TAG, " Recommendation: Ignore, cosmetic error") - false - } - else -> { - Log.d(TAG, " Recommendation: Monitor and verify if payment expected") - true - } - } - - if (shouldVerify) { - Log.d(TAG, " ๐Ÿšจ Triggering payment verification...") - onSuggestVerification() - } - } - - /** - * Log WebView error statistics - */ - fun logErrorStatistics() { - Log.d(TAG, "๐Ÿ“Š WebView Error Statistics:") - Log.d(TAG, " - CSP Errors: Common, usually harmless") - Log.d(TAG, " - Worker Errors: Common, cosmetic only") - Log.d(TAG, " - Payment Errors: Require verification") - Log.d(TAG, " - Unknown Errors: Monitor and investigate") - } - - /** - * Get recommendations for WebView errors - */ - fun getErrorRecommendations(): List { - return listOf( - "โœ… Always verify payment status via backend API", - "โš ๏ธ Don't rely on WebView result codes alone", - "๐Ÿ”„ Use automatic payment verification with retry", - "๐Ÿ“ฑ Consider encouraging PhonePe app installation", - "๐ŸŒ WebView is fallback when app not installed", - "๐Ÿšจ JavaScript errors are often cosmetic", - "๐Ÿ’ก Payment can succeed despite WebView errors" - ) - } - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 5343a75..22e8137 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,9 +17,6 @@ dependencyResolutionManagement { repositories { google() mavenCentral() - maven { - url = uri("https://phonepe.mycloudrepo.io/public/repositories/phonepe-intentsdk-android") - } } } diff --git a/shared/src/commonMain/kotlin/com/talkeys/shared/auth/AuthModels.kt b/shared/src/commonMain/kotlin/com/talkeys/shared/auth/AuthModels.kt index d1232c7..b8044fb 100644 --- a/shared/src/commonMain/kotlin/com/talkeys/shared/auth/AuthModels.kt +++ b/shared/src/commonMain/kotlin/com/talkeys/shared/auth/AuthModels.kt @@ -5,9 +5,9 @@ import kotlinx.serialization.Serializable /** * Backend response for `POST /verify`. * - * Shape matches the live Android Retrofit contract: - * `UserResponse(accessToken: String, name: String)` - * Citation: app/src/main/java/com/example/talkeys_new/dataModels/DataClasses.kt:3-4 + * Shape matches the documented live auth contract: + * `{ accessToken: String, name: String }` + * Citation: CURRENT_CLIENT_API_AUDIT.md ยง1 * * No other fields are present in the observed backend response. */ diff --git a/shared/src/commonMain/kotlin/com/talkeys/shared/auth/AuthRepository.kt b/shared/src/commonMain/kotlin/com/talkeys/shared/auth/AuthRepository.kt index 2f071ae..3cab68e 100644 --- a/shared/src/commonMain/kotlin/com/talkeys/shared/auth/AuthRepository.kt +++ b/shared/src/commonMain/kotlin/com/talkeys/shared/auth/AuthRepository.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.asStateFlow * * Responsible for: * 1. Verifying a Google ID token with the backend via `POST /verify` - * (Citation: app/.../AuthService.kt:17-20, CURRENT_CLIENT_API_AUDIT.md ยง1) + * (Citation: CURRENT_CLIENT_API_AUDIT.md ยง1) * 2. Saving the returned JWT access token into [TokenStorage] * 3. Clearing tokens on sign-out (local only โ€” no backend logout endpoint exists) * @@ -41,10 +41,10 @@ class AuthRepository( * Send the Google ID token to `POST /verify` and, on success, * persist the returned JWT. * - * Request shape โ€” exactly matches the live Android Retrofit call: + * Request shape โ€” exactly matches the documented live Android auth call: * Header `Authorization: Bearer ` * No body. - * Citation: app/.../AuthService.kt:17-20 + * Citation: CURRENT_CLIENT_API_AUDIT.md ยง1 * * Response shape โ€” `{ accessToken, name }`: * Citation: app/.../DataClasses.kt:3-4 @@ -58,7 +58,7 @@ class AuthRepository( headers { append(HttpHeaders.Authorization, "Bearer $googleIdToken") } - // No body โ€” matches Retrofit `POST("verify")` with no @Body + // No body โ€” the backend reads the Google ID token from Authorization. } if (response.status.isSuccess()) { diff --git a/shared/src/commonMain/kotlin/com/talkeys/shared/config/ProductionConfig.kt b/shared/src/commonMain/kotlin/com/talkeys/shared/config/ProductionConfig.kt index 5f7b1e1..5b23dd4 100644 --- a/shared/src/commonMain/kotlin/com/talkeys/shared/config/ProductionConfig.kt +++ b/shared/src/commonMain/kotlin/com/talkeys/shared/config/ProductionConfig.kt @@ -18,10 +18,6 @@ object ProductionConfig { const val API_TIMEOUT_SECONDS = 30L const val API_RETRY_COUNT = 3 - // PhonePe Configuration - const val PHONEPE_ENVIRONMENT = "PRODUCTION" - const val PHONEPE_CLIENT_ID = "SU2504181253408025787154" - // Payment Configuration const val DEFAULT_CURRENCY = "INR" const val PAYMENT_TIMEOUT_MINUTES = 15 @@ -48,13 +44,6 @@ object ProductionConfig { return API_BASE_URL } - /** - * Get PhonePe environment string - */ - fun getPhonePeEnvironment(): String { - return if (IS_PHONEPE_PRODUCTION) "PRODUCTION" else "SANDBOX" - } - /** * Check if feature is enabled */