mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 01:39:22 +02:00
Show identity token at purchase screen when not using play store builds
This commit is contained in:
parent
8dbc800b6c
commit
61253a422c
10 changed files with 312 additions and 75 deletions
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -55,6 +55,7 @@ interface ServerApi {
|
|||
suspend fun reportDeviceRemoved(deviceAuthToken: String)
|
||||
suspend fun removeDevice(deviceAuthToken: String, parentUserId: String, parentPasswordSecondHash: String, deviceId: String)
|
||||
suspend fun isDeviceRemoved(deviceAuthToken: String): Boolean
|
||||
suspend fun createIdentityToken(deviceAuthToken: String, parentUserId: String, parentPasswordSecondHash: String): String
|
||||
}
|
||||
|
||||
class MailServerBlacklistedException: RuntimeException()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -98,4 +98,12 @@ class DummyServerApi: ServerApi {
|
|||
override suspend fun isDeviceRemoved(deviceAuthToken: String): Boolean {
|
||||
throw IOException()
|
||||
}
|
||||
|
||||
override suspend fun createIdentityToken(
|
||||
deviceAuthToken: String,
|
||||
parentUserId: String,
|
||||
parentPasswordSecondHash: String
|
||||
): String {
|
||||
throw IOException()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -626,4 +626,43 @@ class HttpServerApi(private val endpointWithoutSlashAtEnd: String): ServerApi {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createIdentityToken(
|
||||
deviceAuthToken: String,
|
||||
parentUserId: String,
|
||||
parentPasswordSecondHash: String
|
||||
): String {
|
||||
httpClient.newCall(
|
||||
Request.Builder()
|
||||
.url("$endpointWithoutSlashAtEnd/parent/create-identity-token")
|
||||
.post(createJsonRequestBody { writer ->
|
||||
writer.beginObject()
|
||||
writer.name(DEVICE_AUTH_TOKEN).value(deviceAuthToken)
|
||||
writer.name(PARENT_USER_ID).value(parentUserId)
|
||||
writer.name(PARENT_PASSWORD_SECOND_HASH).value(parentPasswordSecondHash)
|
||||
writer.name("purpose").value("purchase")
|
||||
writer.endObject()
|
||||
})
|
||||
.header("Content-Encoding", "gzip")
|
||||
.build()
|
||||
).waitForResponse().use { response ->
|
||||
response.assertSuccess()
|
||||
|
||||
return Threads.network.executeAndWait {
|
||||
val reader = JsonReader(response.body!!.charStream())
|
||||
var token: String? = null
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
"token" -> token = reader.nextString()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
token!!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -16,16 +16,13 @@
|
|||
package io.timelimit.android.ui.diagnose
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.extensions.showSafe
|
||||
import io.timelimit.android.util.Clipboard
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
|
@ -51,16 +48,11 @@ class DiagnoseExceptionDialogFragment: DialogFragment() {
|
|||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val message = getStackTraceString(arguments!!.getSerializable(EXCEPTION) as Exception)
|
||||
val clipboard = context!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val message = getStackTraceString(requireArguments().getSerializable(EXCEPTION) as Exception)
|
||||
|
||||
return AlertDialog.Builder(context!!, theme)
|
||||
return AlertDialog.Builder(requireContext(), theme)
|
||||
.setMessage(message)
|
||||
.setNeutralButton(R.string.diagnose_sync_copy_to_clipboard) { _, _ ->
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("TimeLimit", message))
|
||||
|
||||
Toast.makeText(context, R.string.diagnose_sync_copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
.setNeutralButton(R.string.diagnose_sync_copy_to_clipboard) { _, _ -> Clipboard.setAndToast(requireContext(), message) }
|
||||
.setPositiveButton(R.string.generic_ok, null)
|
||||
.create()
|
||||
}
|
||||
|
|
|
@ -28,7 +28,10 @@ import io.timelimit.android.livedata.liveDataFromNullableValue
|
|||
import io.timelimit.android.livedata.mergeLiveData
|
||||
import io.timelimit.android.ui.MainActivity
|
||||
import io.timelimit.android.ui.diagnose.DiagnoseExceptionDialogFragment
|
||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||
import io.timelimit.android.ui.main.getActivityViewModel
|
||||
import io.timelimit.android.util.Clipboard
|
||||
import java.lang.RuntimeException
|
||||
|
||||
class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
|
||||
|
@ -40,6 +43,8 @@ class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
|
|||
private const val PAGE_ERROR = 1
|
||||
private const val PAGE_WAIT = 2
|
||||
private const val PAGE_DONE = 3
|
||||
private const val PAGE_AUTH = 4
|
||||
private const val PAGE_TOKEN = 5
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -49,22 +54,18 @@ class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
|
|||
activityModel.resetProcessPurchaseSuccess()
|
||||
}
|
||||
|
||||
model.retry(activityModel)
|
||||
model.init(activityModel, getActivityViewModel(requireActivity()))
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val binding = FragmentPurchaseBinding.inflate(inflater, container, false)
|
||||
var processingPurchaseError = false
|
||||
|
||||
mergeLiveData(activityModel.status, model.status).observe(viewLifecycleOwner, {
|
||||
status ->
|
||||
|
||||
val (activityStatus, fragmentStatus) = status!!
|
||||
|
||||
mergeLiveData(activityModel.status, model.status).observe(viewLifecycleOwner) { (activityStatus, fragmentStatus) ->
|
||||
if (fragmentStatus != null) {
|
||||
when (fragmentStatus) {
|
||||
PurchaseFragmentPreparing -> binding.flipper.displayedChild = PAGE_WAIT
|
||||
is PurchaseFragmentReady -> {
|
||||
PurchaseModel.Status.Preparing -> binding.flipper.displayedChild = PAGE_WAIT
|
||||
is PurchaseModel.Status.ReadyRegular -> {
|
||||
when (activityStatus) {
|
||||
null -> binding.flipper.displayedChild = PAGE_WAIT
|
||||
ActivityPurchaseModelStatus.Working -> binding.flipper.displayedChild = PAGE_WAIT
|
||||
|
@ -82,42 +83,55 @@ class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
|
|||
ActivityPurchaseModelStatus.Done -> binding.flipper.displayedChild = PAGE_DONE
|
||||
}.let { }
|
||||
}
|
||||
is PurchaseFragmentError -> {
|
||||
is PurchaseModel.Status.Error -> {
|
||||
binding.flipper.displayedChild = PAGE_ERROR
|
||||
|
||||
binding.errorReason = when (fragmentStatus) {
|
||||
PurchaseFragmentErrorBillingNotSupportedByDevice -> getString(R.string.purchase_error_not_supported_by_device)
|
||||
PurchaseFragmentErrorBillingNotSupportedByAppVariant -> getString(R.string.purchase_error_not_supported_by_app_variant)
|
||||
is PurchaseFragmentNetworkError -> getString(R.string.error_network)
|
||||
PurchaseFragmentExistingPaymentError -> getString(R.string.purchase_error_existing_payment)
|
||||
PurchaseFragmentServerRejectedError -> getString(R.string.purchase_error_server_rejected)
|
||||
PurchaseFragmentServerHasDifferentPublicKey -> getString(R.string.purchase_error_server_different_key)
|
||||
PurchaseModel.Status.Error.Unrecoverable.BillingNotSupportedByDevice -> getString(R.string.purchase_error_not_supported_by_device)
|
||||
is PurchaseModel.Status.Error.Recoverable.NetworkError -> getString(R.string.error_network)
|
||||
PurchaseModel.Status.Error.Unrecoverable.ExistingPaymentError -> getString(R.string.purchase_error_existing_payment)
|
||||
PurchaseModel.Status.Error.Unrecoverable.ServerRejectedError -> getString(R.string.purchase_error_server_rejected)
|
||||
PurchaseModel.Status.Error.Unrecoverable.ServerClientCombinationUnsupported -> getString(R.string.purchase_error_server_different_key)
|
||||
}
|
||||
|
||||
binding.showRetryButton = when (fragmentStatus) {
|
||||
is PurchaseFragmentRecoverableError -> true
|
||||
is PurchaseFragmentUnrecoverableError -> false
|
||||
is PurchaseModel.Status.Error.Recoverable -> true
|
||||
is PurchaseModel.Status.Error.Unrecoverable -> false
|
||||
}
|
||||
|
||||
binding.showErrorDetailsButton = when (fragmentStatus) {
|
||||
is PurchaseFragmentNetworkError -> true
|
||||
is PurchaseModel.Status.Error.Recoverable.NetworkError -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
processingPurchaseError = false
|
||||
}
|
||||
PurchaseModel.Status.WaitingForAuth -> {
|
||||
binding.flipper.displayedChild = PAGE_AUTH
|
||||
}
|
||||
is PurchaseModel.Status.ReadyToken -> {
|
||||
binding.flipper.displayedChild = PAGE_TOKEN
|
||||
|
||||
binding.purchaseToken = fragmentStatus.token
|
||||
}
|
||||
}.let { }
|
||||
} else {
|
||||
binding.flipper.displayedChild = PAGE_WAIT
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getActivityViewModel(requireActivity()).authenticatedUser.observe(viewLifecycleOwner) { user ->
|
||||
if (user != null && model.status.value is PurchaseModel.Status.WaitingForAuth) {
|
||||
model.retry()
|
||||
}
|
||||
}
|
||||
|
||||
binding.handlers = object: PurchaseFragmentHandlers {
|
||||
override fun retryAtErrorScreenClicked() {
|
||||
if (processingPurchaseError) {
|
||||
activityModel.queryAndProcessPurchasesAsync()
|
||||
} else {
|
||||
model.retry(activityModel)
|
||||
model.retry()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,7 +139,7 @@ class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
|
|||
val status = model.status.value
|
||||
|
||||
val exception = when (status) {
|
||||
is PurchaseFragmentNetworkError -> status.exception
|
||||
is PurchaseModel.Status.Error.Recoverable.NetworkError -> status.exception
|
||||
else -> RuntimeException("other error")
|
||||
}
|
||||
|
||||
|
@ -139,6 +153,18 @@ class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
|
|||
override fun buyForOneYear() {
|
||||
activityModel.startPurchase(PurchaseIds.SKU_YEAR, checkAtBackend = true, activity = requireActivity())
|
||||
}
|
||||
|
||||
override fun showAuthDialog() {
|
||||
(requireActivity() as ActivityViewModelHolder).showAuthenticationScreen()
|
||||
}
|
||||
|
||||
override fun copyPurchaseTokenToClipboard() {
|
||||
model.status.value?.also {
|
||||
if (it is PurchaseModel.Status.ReadyToken) {
|
||||
Clipboard.setAndToast(requireContext(), it.token)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return binding.root
|
||||
|
@ -152,4 +178,6 @@ interface PurchaseFragmentHandlers {
|
|||
fun showErrorDetails()
|
||||
fun buyForOneMonth()
|
||||
fun buyForOneYear()
|
||||
fun showAuthDialog()
|
||||
fun copyPurchaseTokenToClipboard()
|
||||
}
|
||||
|
|
|
@ -25,77 +25,128 @@ import io.timelimit.android.extensions.BillingNotSupportedException
|
|||
import io.timelimit.android.livedata.castDown
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionChildAddLimitAuthentication
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionParentDeviceAuthentication
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionParentPasswordAuthentication
|
||||
import io.timelimit.android.sync.network.CanDoPurchaseStatus
|
||||
import io.timelimit.android.sync.network.api.NotFoundHttpError
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class PurchaseModel(application: Application): AndroidViewModel(application) {
|
||||
private val logic: AppLogic by lazy { DefaultAppLogic.with(application) }
|
||||
private val statusInternal = MutableLiveData<PurchaseFragmentStatus>()
|
||||
private val statusInternal = MutableLiveData<Status>()
|
||||
private val lock = Mutex()
|
||||
private var activityPurchaseModel: ActivityPurchaseModel? = null
|
||||
private var auth: ActivityViewModel? = null
|
||||
|
||||
val status = statusInternal.castDown()
|
||||
|
||||
fun retry(activityPurchaseModel: ActivityPurchaseModel) {
|
||||
if (this.status.value is PurchaseFragmentRecoverableError || this.status.value == null) {
|
||||
prepare(activityPurchaseModel)
|
||||
fun init(activityPurchaseModel: ActivityPurchaseModel, auth: ActivityViewModel) {
|
||||
this.activityPurchaseModel = activityPurchaseModel
|
||||
this.auth = auth
|
||||
|
||||
retry()
|
||||
}
|
||||
|
||||
fun retry() {
|
||||
if (
|
||||
this.status.value is Status.Error.Recoverable ||
|
||||
this.status.value is Status.WaitingForAuth ||
|
||||
this.status.value == null
|
||||
) {
|
||||
val activityPurchaseModel = activityPurchaseModel
|
||||
val auth = auth
|
||||
|
||||
if (activityPurchaseModel != null && auth != null) {
|
||||
prepare(activityPurchaseModel, auth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepare(activityPurchaseModel: ActivityPurchaseModel) {
|
||||
private fun prepare(activityPurchaseModel: ActivityPurchaseModel, auth: ActivityViewModel) {
|
||||
runAsync {
|
||||
lock.withLock {
|
||||
try {
|
||||
statusInternal.value = PurchaseFragmentPreparing
|
||||
statusInternal.value = Status.Preparing
|
||||
|
||||
val server = logic.serverLogic.getServerConfigCoroutine()
|
||||
|
||||
suspend fun canDoPurchase() = if (server.hasAuthToken) server.api.canDoPurchase(server.deviceAuthToken)
|
||||
else CanDoPurchaseStatus.NoForUnknownReason
|
||||
|
||||
if (!BuildConfig.storeCompilant) {
|
||||
statusInternal.value = PurchaseFragmentErrorBillingNotSupportedByAppVariant
|
||||
} else {
|
||||
val server = logic.serverLogic.getServerConfigCoroutine()
|
||||
if (auth.isParentAuthenticated()) {
|
||||
val authData = auth.authenticatedUser.value?.first
|
||||
|
||||
val canDoPurchase = if (server.hasAuthToken)
|
||||
server.api.canDoPurchase(server.deviceAuthToken)
|
||||
else
|
||||
CanDoPurchaseStatus.NoForUnknownReason
|
||||
try {
|
||||
val token = when (authData) {
|
||||
ApplyActionParentDeviceAuthentication -> server.api.createIdentityToken(
|
||||
deviceAuthToken = server.deviceAuthToken,
|
||||
parentUserId = "",
|
||||
parentPasswordSecondHash = "device"
|
||||
)
|
||||
is ApplyActionParentPasswordAuthentication -> server.api.createIdentityToken(
|
||||
deviceAuthToken = server.deviceAuthToken,
|
||||
parentUserId = authData.parentUserId,
|
||||
parentPasswordSecondHash = authData.secondPasswordHash
|
||||
)
|
||||
is ApplyActionChildAddLimitAuthentication -> throw RuntimeException(
|
||||
"child can not do that"
|
||||
)
|
||||
null -> throw RuntimeException("missing user")
|
||||
}
|
||||
|
||||
statusInternal.value = Status.ReadyToken(token)
|
||||
} catch (ex: NotFoundHttpError) {
|
||||
statusInternal.value = Status.Error.Unrecoverable.ServerClientCombinationUnsupported
|
||||
}
|
||||
} else statusInternal.value = Status.WaitingForAuth
|
||||
} else {
|
||||
val canDoPurchase = canDoPurchase()
|
||||
|
||||
if (canDoPurchase is CanDoPurchaseStatus.Yes) {
|
||||
if (canDoPurchase.publicKey?.contentEquals(Base64.decode(BuildConfig.googlePlayKey, 0)) == false) {
|
||||
statusInternal.value = PurchaseFragmentServerHasDifferentPublicKey
|
||||
statusInternal.value = Status.Error.Unrecoverable.ServerClientCombinationUnsupported
|
||||
} else {
|
||||
val skus = activityPurchaseModel.queryProducts(PurchaseIds.BUY_SKUS)
|
||||
|
||||
statusInternal.value = PurchaseFragmentReady(
|
||||
statusInternal.value = Status.ReadyRegular(
|
||||
monthPrice = skus.find { it.productId == PurchaseIds.SKU_MONTH }?.oneTimePurchaseOfferDetails?.formattedPrice.toString(),
|
||||
yearPrice = skus.find { it.productId == PurchaseIds.SKU_YEAR }?.oneTimePurchaseOfferDetails?.formattedPrice.toString()
|
||||
)
|
||||
}
|
||||
} else if (canDoPurchase == CanDoPurchaseStatus.NotDueToOldPurchase) {
|
||||
statusInternal.value = PurchaseFragmentExistingPaymentError
|
||||
statusInternal.value = Status.Error.Unrecoverable.ExistingPaymentError
|
||||
} else {
|
||||
statusInternal.value = PurchaseFragmentServerRejectedError
|
||||
statusInternal.value = Status.Error.Unrecoverable.ServerRejectedError
|
||||
}
|
||||
}
|
||||
} catch (ex: BillingNotSupportedException) {
|
||||
statusInternal.value = PurchaseFragmentErrorBillingNotSupportedByDevice
|
||||
statusInternal.value = Status.Error.Unrecoverable.BillingNotSupportedByDevice
|
||||
} catch (ex: Exception) {
|
||||
statusInternal.value = PurchaseFragmentNetworkError(ex)
|
||||
statusInternal.value = Status.Error.Recoverable.NetworkError(ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class PurchaseFragmentStatus
|
||||
sealed class PurchaseFragmentError: PurchaseFragmentStatus()
|
||||
class PurchaseFragmentReady(val monthPrice: String, val yearPrice: String): PurchaseFragmentStatus()
|
||||
object PurchaseFragmentPreparing: PurchaseFragmentStatus()
|
||||
|
||||
sealed class PurchaseFragmentUnrecoverableError: PurchaseFragmentError()
|
||||
sealed class PurchaseFragmentRecoverableError: PurchaseFragmentError()
|
||||
|
||||
object PurchaseFragmentErrorBillingNotSupportedByDevice: PurchaseFragmentUnrecoverableError()
|
||||
object PurchaseFragmentErrorBillingNotSupportedByAppVariant: PurchaseFragmentUnrecoverableError()
|
||||
data class PurchaseFragmentNetworkError(val exception: Exception): PurchaseFragmentRecoverableError()
|
||||
object PurchaseFragmentExistingPaymentError: PurchaseFragmentUnrecoverableError()
|
||||
object PurchaseFragmentServerRejectedError: PurchaseFragmentUnrecoverableError()
|
||||
object PurchaseFragmentServerHasDifferentPublicKey: PurchaseFragmentUnrecoverableError()
|
||||
sealed class Status {
|
||||
sealed class Error: Status() {
|
||||
sealed class Recoverable: Error() {
|
||||
data class NetworkError(val exception: Exception): Recoverable()
|
||||
}
|
||||
sealed class Unrecoverable: Error() {
|
||||
object BillingNotSupportedByDevice: Unrecoverable()
|
||||
object ExistingPaymentError: Unrecoverable()
|
||||
object ServerRejectedError: Unrecoverable()
|
||||
object ServerClientCombinationUnsupported: Unrecoverable()
|
||||
}
|
||||
}
|
||||
class ReadyRegular(val monthPrice: String, val yearPrice: String): Status()
|
||||
class ReadyToken(val token: String): Status()
|
||||
object Preparing: Status()
|
||||
object WaitingForAuth: Status()
|
||||
}
|
||||
}
|
33
app/src/main/java/io/timelimit/android/util/Clipboard.kt
Normal file
33
app/src/main/java/io/timelimit/android/util/Clipboard.kt
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program 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 version 3 of the License.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.util
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.getSystemService
|
||||
import io.timelimit.android.R
|
||||
|
||||
object Clipboard {
|
||||
fun setAndToast(context: Context, content: String) {
|
||||
context.getSystemService<ClipboardManager>()?.also { clipboard ->
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("TimeLimit", content))
|
||||
|
||||
Toast.makeText(context, R.string.diagnose_sync_copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -36,7 +36,11 @@
|
|||
|
||||
<variable
|
||||
name="priceData"
|
||||
type="io.timelimit.android.ui.payment.PurchaseFragmentReady" />
|
||||
type="io.timelimit.android.ui.payment.PurchaseModel.Status.ReadyRegular" />
|
||||
|
||||
<variable
|
||||
name="purchaseToken"
|
||||
type="String" />
|
||||
|
||||
<import type="android.view.View" />
|
||||
</data>
|
||||
|
@ -196,5 +200,75 @@
|
|||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:padding="8dp"
|
||||
android:orientation="vertical"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/purchase_requires_auth_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:layout_marginTop="4dp"
|
||||
android:onClick="@{() -> handlers.showAuthDialog()}"
|
||||
android:text="@string/add_user_authentication_required_btn"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:padding="8dp"
|
||||
android:orientation="vertical"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/purchase_token_explain"
|
||||
android:autoLink="web"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
android:layout_gravity="center_horizontal"
|
||||
tools:text="12345678"
|
||||
android:text="@{purchaseToken}"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:layout_marginTop="4dp"
|
||||
android:onClick="@{() -> handlers.copyPurchaseTokenToClipboard()}"
|
||||
android:text="@string/diagnose_sync_copy_to_clipboard"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
</io.timelimit.android.ui.view.SafeViewFlipper>
|
||||
</layout>
|
||||
|
|
|
@ -1293,11 +1293,17 @@
|
|||
Sie haben bereits bezahlt. Sie können die Vollversion erst kurz vor dem Ablaufen (oder danach) verlängern
|
||||
</string>
|
||||
<string name="purchase_error_server_rejected">Der Server akzeptiert momentan keine Bezahlungen</string>
|
||||
<string name="purchase_error_server_different_key">Der Server unterstützt Bezahlungen, aber nicht über diesen App-Build</string>
|
||||
<string name="purchase_error_server_different_key">Der Server unterstützt möglicherweise Bezahlungen, aber nicht über diesen App-Build</string>
|
||||
|
||||
<string name="purchase_done_title">Bezahlung abgeschlossen</string>
|
||||
<string name="purchase_done_text">Betätigen Sie die Zurück-Taste, um zur App zurückzukehren</string>
|
||||
|
||||
<string name="purchase_requires_auth_text">Sie müssen sich für den Kauf anmelden</string>
|
||||
<string name="purchase_token_explain">Unter https://timelimit.io/de/#how-to-purchase erfahren Sie,
|
||||
wie Sie den Kauf durchführen können.
|
||||
Ihr Kauftoken lautet:
|
||||
</string>
|
||||
|
||||
<string name="purchase_required_dialog_title">Vollversion erforderlich</string>
|
||||
<string name="purchase_required_dialog_text">
|
||||
Diese Funktion benötigt die Vollversion.
|
||||
|
|
|
@ -1335,11 +1335,16 @@
|
|||
You can only extend the premium version if it expired or shortly before that.
|
||||
</string>
|
||||
<string name="purchase_error_server_rejected">The server does not accept payments currently</string>
|
||||
<string name="purchase_error_server_different_key">The server does support payments, but not using this app build</string>
|
||||
<string name="purchase_error_server_different_key">The server maybe does support payments, but not using this app build</string>
|
||||
|
||||
<string name="purchase_done_title">Purchase succeeded</string>
|
||||
<string name="purchase_done_text">Press back to go back to the App</string>
|
||||
|
||||
<string name="purchase_requires_auth_text">You have to authenticate for the purchase</string>
|
||||
<string name="purchase_token_explain">See https://timelimit.io/en/#how-to-purchase for details on the purchase.
|
||||
Your purchase token is:
|
||||
</string>
|
||||
|
||||
<string name="purchase_required_dialog_title">Premium version required</string>
|
||||
<string name="purchase_required_dialog_text">
|
||||
The feature requires the premium version.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue