PaymentOption.kt

/*
 * This file is part of GNU Taler
 * (C) 2026 Taler Systems S.A.
 *
 * GNU Taler is free software; you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 3, or (at your option) any later version.
 *
 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

package net.taler.merchantpos.paymentoptions

import android.net.Uri

import androidx.activity.ComponentActivity
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector

import net.taler.common.Amount
import net.taler.merchantpos.R

enum class PaymentOptionStatus
{
    Disabled,
    Unavailable,
    NotConfigured,
    SignInRequired,
    PermissionRequired,
    NotPaired,
    Disconnected,

    /** Payments are available without an external device connection. */
    Ready,

    Connected,
}

data class PaymentDevice(
    /** Stable across device status updates. */
    val id: String,
    val name: String? = null,
    /** Fallback when [name] is absent. */
    @StringRes val nameRes: Int? = null,
    val serialNumber: String? = null,
    val batteryPercent: Int? = null,
    val connected: Boolean = false,
    /** Device error; does not change payment availability. */
    @StringRes val statusRes: Int? = null,
)

data class PaymentOptionState(
    val id: String,
    val enabled: Boolean = false,
    val status: PaymentOptionStatus = PaymentOptionStatus.Disabled,
    val account: String? = null,
    /** Includes disconnected devices. */
    val devices: List<PaymentDevice> = emptyList(),
)

class PaymentOptionActions(
    val setEnabled: (Boolean) -> Unit,
    val refresh: () -> Unit,
    val reportFailure: () -> Unit,
)

/** Payment method implementation and settings UI. */
interface PaymentOption
{
    /** Stable identifier used in preferences and navigation. */
    val id: String

    @get:StringRes
    val nameRes: Int

    @get:StringRes
    val descriptionRes: Int

    val icon: ImageVector
        @Composable get

    val enabledByDefault: Boolean get() = false

    val alwaysEnabled: Boolean get() = false

    /**
     * Tracks the option's own connection.
     * Optional displays are reported through [PaymentOptionState.devices].
     */
    val tracksConnection: Boolean get() = false

    /** Full logo for checkout tiles; [icon] is used in lists. */
    val logo: ImageVector?
        @Composable get() = null

    val hasSettings: Boolean get() = false

    @get:StringRes
    val settingsTitleRes: Int get() = R.string.payment_option_settings_title

    val chargesExternally: Boolean get() = false

    /** Must be acknowledged before enabling the option. */
    @get:StringRes
    val activationNoticeRes: Int? get() = null

    val canRefund: Boolean get() = false

    /**
     * Returns false if charging cannot start; otherwise calls [onResult] on the main thread.
     * Persist the pending charge first: process death can prevent callback delivery.
     */
    fun startCharge(
        charge: ExternalCharge,
        onResult: (ChargeResult) -> Unit,
    ): Boolean = false

    /** Whether the required application credentials are configured. */
    fun isConfigured(): Boolean = true

    /** Called only while enabled. Exceptions become [PaymentOptionStatus.Unavailable]. */
    fun probe(): PaymentOptionState = PaymentOptionState(id, true, PaymentOptionStatus.Ready)

    /** Called for every option in [net.taler.merchantpos.MainActivity.onCreate], before STARTED. */
    fun registerLaunchers(
        activity: ComponentActivity,
        manager: PaymentOptionsManager,
    ) = Unit

    /** Called at startup and periodically while the app is in the foreground. */
    suspend fun maintainSession() = Unit

    /** Returns true if this option consumed the redirect. */
    suspend fun handleRedirect(uri: Uri): Boolean = false

    fun canCharge(state: PaymentOptionState): Boolean = state.enabled && state.status == PaymentOptionStatus.Connected

    /** [transactionCode] is the reference returned by the original charge. */
    suspend fun refund(
        transactionCode: String?,
        amount: Amount,
        reason: String,
    ): RefundOutcome = RefundOutcome.NotSupported

    /** Clear the session on merchant logout. */
    fun onLogout() = Unit

    @Composable
    fun DetailContent(
        state: PaymentOptionState,
        actions: PaymentOptionActions,
    ) = Unit
}