mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 09:49:25 +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
|
* 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
|
* 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 reportDeviceRemoved(deviceAuthToken: String)
|
||||||
suspend fun removeDevice(deviceAuthToken: String, parentUserId: String, parentPasswordSecondHash: String, deviceId: String)
|
suspend fun removeDevice(deviceAuthToken: String, parentUserId: String, parentPasswordSecondHash: String, deviceId: String)
|
||||||
suspend fun isDeviceRemoved(deviceAuthToken: String): Boolean
|
suspend fun isDeviceRemoved(deviceAuthToken: String): Boolean
|
||||||
|
suspend fun createIdentityToken(deviceAuthToken: String, parentUserId: String, parentPasswordSecondHash: String): String
|
||||||
}
|
}
|
||||||
|
|
||||||
class MailServerBlacklistedException: RuntimeException()
|
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
|
* 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
|
* 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 {
|
override suspend fun isDeviceRemoved(deviceAuthToken: String): Boolean {
|
||||||
throw IOException()
|
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
|
* 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
|
* 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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,16 +16,13 @@
|
||||||
package io.timelimit.android.ui.diagnose
|
package io.timelimit.android.ui.diagnose
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.extensions.showSafe
|
import io.timelimit.android.extensions.showSafe
|
||||||
|
import io.timelimit.android.util.Clipboard
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
|
|
||||||
|
@ -51,16 +48,11 @@ class DiagnoseExceptionDialogFragment: DialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val message = getStackTraceString(arguments!!.getSerializable(EXCEPTION) as Exception)
|
val message = getStackTraceString(requireArguments().getSerializable(EXCEPTION) as Exception)
|
||||||
val clipboard = context!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
||||||
|
|
||||||
return AlertDialog.Builder(context!!, theme)
|
return AlertDialog.Builder(requireContext(), theme)
|
||||||
.setMessage(message)
|
.setMessage(message)
|
||||||
.setNeutralButton(R.string.diagnose_sync_copy_to_clipboard) { _, _ ->
|
.setNeutralButton(R.string.diagnose_sync_copy_to_clipboard) { _, _ -> Clipboard.setAndToast(requireContext(), message) }
|
||||||
clipboard.setPrimaryClip(ClipData.newPlainText("TimeLimit", message))
|
|
||||||
|
|
||||||
Toast.makeText(context, R.string.diagnose_sync_copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
.setPositiveButton(R.string.generic_ok, null)
|
.setPositiveButton(R.string.generic_ok, null)
|
||||||
.create()
|
.create()
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,10 @@ import io.timelimit.android.livedata.liveDataFromNullableValue
|
||||||
import io.timelimit.android.livedata.mergeLiveData
|
import io.timelimit.android.livedata.mergeLiveData
|
||||||
import io.timelimit.android.ui.MainActivity
|
import io.timelimit.android.ui.MainActivity
|
||||||
import io.timelimit.android.ui.diagnose.DiagnoseExceptionDialogFragment
|
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.FragmentWithCustomTitle
|
||||||
|
import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
|
import io.timelimit.android.util.Clipboard
|
||||||
import java.lang.RuntimeException
|
import java.lang.RuntimeException
|
||||||
|
|
||||||
class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
|
class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
|
@ -40,6 +43,8 @@ class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
private const val PAGE_ERROR = 1
|
private const val PAGE_ERROR = 1
|
||||||
private const val PAGE_WAIT = 2
|
private const val PAGE_WAIT = 2
|
||||||
private const val PAGE_DONE = 3
|
private const val PAGE_DONE = 3
|
||||||
|
private const val PAGE_AUTH = 4
|
||||||
|
private const val PAGE_TOKEN = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -49,22 +54,18 @@ class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
activityModel.resetProcessPurchaseSuccess()
|
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)
|
val binding = FragmentPurchaseBinding.inflate(inflater, container, false)
|
||||||
var processingPurchaseError = false
|
var processingPurchaseError = false
|
||||||
|
|
||||||
mergeLiveData(activityModel.status, model.status).observe(viewLifecycleOwner, {
|
mergeLiveData(activityModel.status, model.status).observe(viewLifecycleOwner) { (activityStatus, fragmentStatus) ->
|
||||||
status ->
|
|
||||||
|
|
||||||
val (activityStatus, fragmentStatus) = status!!
|
|
||||||
|
|
||||||
if (fragmentStatus != null) {
|
if (fragmentStatus != null) {
|
||||||
when (fragmentStatus) {
|
when (fragmentStatus) {
|
||||||
PurchaseFragmentPreparing -> binding.flipper.displayedChild = PAGE_WAIT
|
PurchaseModel.Status.Preparing -> binding.flipper.displayedChild = PAGE_WAIT
|
||||||
is PurchaseFragmentReady -> {
|
is PurchaseModel.Status.ReadyRegular -> {
|
||||||
when (activityStatus) {
|
when (activityStatus) {
|
||||||
null -> binding.flipper.displayedChild = PAGE_WAIT
|
null -> binding.flipper.displayedChild = PAGE_WAIT
|
||||||
ActivityPurchaseModelStatus.Working -> 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
|
ActivityPurchaseModelStatus.Done -> binding.flipper.displayedChild = PAGE_DONE
|
||||||
}.let { }
|
}.let { }
|
||||||
}
|
}
|
||||||
is PurchaseFragmentError -> {
|
is PurchaseModel.Status.Error -> {
|
||||||
binding.flipper.displayedChild = PAGE_ERROR
|
binding.flipper.displayedChild = PAGE_ERROR
|
||||||
|
|
||||||
binding.errorReason = when (fragmentStatus) {
|
binding.errorReason = when (fragmentStatus) {
|
||||||
PurchaseFragmentErrorBillingNotSupportedByDevice -> getString(R.string.purchase_error_not_supported_by_device)
|
PurchaseModel.Status.Error.Unrecoverable.BillingNotSupportedByDevice -> getString(R.string.purchase_error_not_supported_by_device)
|
||||||
PurchaseFragmentErrorBillingNotSupportedByAppVariant -> getString(R.string.purchase_error_not_supported_by_app_variant)
|
is PurchaseModel.Status.Error.Recoverable.NetworkError -> getString(R.string.error_network)
|
||||||
is PurchaseFragmentNetworkError -> getString(R.string.error_network)
|
PurchaseModel.Status.Error.Unrecoverable.ExistingPaymentError -> getString(R.string.purchase_error_existing_payment)
|
||||||
PurchaseFragmentExistingPaymentError -> getString(R.string.purchase_error_existing_payment)
|
PurchaseModel.Status.Error.Unrecoverable.ServerRejectedError -> getString(R.string.purchase_error_server_rejected)
|
||||||
PurchaseFragmentServerRejectedError -> getString(R.string.purchase_error_server_rejected)
|
PurchaseModel.Status.Error.Unrecoverable.ServerClientCombinationUnsupported -> getString(R.string.purchase_error_server_different_key)
|
||||||
PurchaseFragmentServerHasDifferentPublicKey -> getString(R.string.purchase_error_server_different_key)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.showRetryButton = when (fragmentStatus) {
|
binding.showRetryButton = when (fragmentStatus) {
|
||||||
is PurchaseFragmentRecoverableError -> true
|
is PurchaseModel.Status.Error.Recoverable -> true
|
||||||
is PurchaseFragmentUnrecoverableError -> false
|
is PurchaseModel.Status.Error.Unrecoverable -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.showErrorDetailsButton = when (fragmentStatus) {
|
binding.showErrorDetailsButton = when (fragmentStatus) {
|
||||||
is PurchaseFragmentNetworkError -> true
|
is PurchaseModel.Status.Error.Recoverable.NetworkError -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
processingPurchaseError = 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 { }
|
}.let { }
|
||||||
} else {
|
} else {
|
||||||
binding.flipper.displayedChild = PAGE_WAIT
|
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 {
|
binding.handlers = object: PurchaseFragmentHandlers {
|
||||||
override fun retryAtErrorScreenClicked() {
|
override fun retryAtErrorScreenClicked() {
|
||||||
if (processingPurchaseError) {
|
if (processingPurchaseError) {
|
||||||
activityModel.queryAndProcessPurchasesAsync()
|
activityModel.queryAndProcessPurchasesAsync()
|
||||||
} else {
|
} else {
|
||||||
model.retry(activityModel)
|
model.retry()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,7 +139,7 @@ class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
val status = model.status.value
|
val status = model.status.value
|
||||||
|
|
||||||
val exception = when (status) {
|
val exception = when (status) {
|
||||||
is PurchaseFragmentNetworkError -> status.exception
|
is PurchaseModel.Status.Error.Recoverable.NetworkError -> status.exception
|
||||||
else -> RuntimeException("other error")
|
else -> RuntimeException("other error")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,6 +153,18 @@ class PurchaseFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
override fun buyForOneYear() {
|
override fun buyForOneYear() {
|
||||||
activityModel.startPurchase(PurchaseIds.SKU_YEAR, checkAtBackend = true, activity = requireActivity())
|
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
|
return binding.root
|
||||||
|
@ -152,4 +178,6 @@ interface PurchaseFragmentHandlers {
|
||||||
fun showErrorDetails()
|
fun showErrorDetails()
|
||||||
fun buyForOneMonth()
|
fun buyForOneMonth()
|
||||||
fun buyForOneYear()
|
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.livedata.castDown
|
||||||
import io.timelimit.android.logic.AppLogic
|
import io.timelimit.android.logic.AppLogic
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
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.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.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
class PurchaseModel(application: Application): AndroidViewModel(application) {
|
class PurchaseModel(application: Application): AndroidViewModel(application) {
|
||||||
private val logic: AppLogic by lazy { DefaultAppLogic.with(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 val lock = Mutex()
|
||||||
|
private var activityPurchaseModel: ActivityPurchaseModel? = null
|
||||||
|
private var auth: ActivityViewModel? = null
|
||||||
|
|
||||||
val status = statusInternal.castDown()
|
val status = statusInternal.castDown()
|
||||||
|
|
||||||
fun retry(activityPurchaseModel: ActivityPurchaseModel) {
|
fun init(activityPurchaseModel: ActivityPurchaseModel, auth: ActivityViewModel) {
|
||||||
if (this.status.value is PurchaseFragmentRecoverableError || this.status.value == null) {
|
this.activityPurchaseModel = activityPurchaseModel
|
||||||
prepare(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 {
|
runAsync {
|
||||||
lock.withLock {
|
lock.withLock {
|
||||||
try {
|
try {
|
||||||
statusInternal.value = PurchaseFragmentPreparing
|
statusInternal.value = Status.Preparing
|
||||||
|
|
||||||
if (!BuildConfig.storeCompilant) {
|
|
||||||
statusInternal.value = PurchaseFragmentErrorBillingNotSupportedByAppVariant
|
|
||||||
} else {
|
|
||||||
val server = logic.serverLogic.getServerConfigCoroutine()
|
val server = logic.serverLogic.getServerConfigCoroutine()
|
||||||
|
|
||||||
val canDoPurchase = if (server.hasAuthToken)
|
suspend fun canDoPurchase() = if (server.hasAuthToken) server.api.canDoPurchase(server.deviceAuthToken)
|
||||||
server.api.canDoPurchase(server.deviceAuthToken)
|
else CanDoPurchaseStatus.NoForUnknownReason
|
||||||
else
|
|
||||||
CanDoPurchaseStatus.NoForUnknownReason
|
if (!BuildConfig.storeCompilant) {
|
||||||
|
if (auth.isParentAuthenticated()) {
|
||||||
|
val authData = auth.authenticatedUser.value?.first
|
||||||
|
|
||||||
|
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 is CanDoPurchaseStatus.Yes) {
|
||||||
if (canDoPurchase.publicKey?.contentEquals(Base64.decode(BuildConfig.googlePlayKey, 0)) == false) {
|
if (canDoPurchase.publicKey?.contentEquals(Base64.decode(BuildConfig.googlePlayKey, 0)) == false) {
|
||||||
statusInternal.value = PurchaseFragmentServerHasDifferentPublicKey
|
statusInternal.value = Status.Error.Unrecoverable.ServerClientCombinationUnsupported
|
||||||
} else {
|
} else {
|
||||||
val skus = activityPurchaseModel.queryProducts(PurchaseIds.BUY_SKUS)
|
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(),
|
monthPrice = skus.find { it.productId == PurchaseIds.SKU_MONTH }?.oneTimePurchaseOfferDetails?.formattedPrice.toString(),
|
||||||
yearPrice = skus.find { it.productId == PurchaseIds.SKU_YEAR }?.oneTimePurchaseOfferDetails?.formattedPrice.toString()
|
yearPrice = skus.find { it.productId == PurchaseIds.SKU_YEAR }?.oneTimePurchaseOfferDetails?.formattedPrice.toString()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (canDoPurchase == CanDoPurchaseStatus.NotDueToOldPurchase) {
|
} else if (canDoPurchase == CanDoPurchaseStatus.NotDueToOldPurchase) {
|
||||||
statusInternal.value = PurchaseFragmentExistingPaymentError
|
statusInternal.value = Status.Error.Unrecoverable.ExistingPaymentError
|
||||||
} else {
|
} else {
|
||||||
statusInternal.value = PurchaseFragmentServerRejectedError
|
statusInternal.value = Status.Error.Unrecoverable.ServerRejectedError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ex: BillingNotSupportedException) {
|
} catch (ex: BillingNotSupportedException) {
|
||||||
statusInternal.value = PurchaseFragmentErrorBillingNotSupportedByDevice
|
statusInternal.value = Status.Error.Unrecoverable.BillingNotSupportedByDevice
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
statusInternal.value = PurchaseFragmentNetworkError(ex)
|
statusInternal.value = Status.Error.Recoverable.NetworkError(ex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
|
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
|
<variable
|
||||||
name="priceData"
|
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" />
|
<import type="android.view.View" />
|
||||||
</data>
|
</data>
|
||||||
|
@ -196,5 +200,75 @@
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</RelativeLayout>
|
</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>
|
</io.timelimit.android.ui.view.SafeViewFlipper>
|
||||||
</layout>
|
</layout>
|
||||||
|
|
|
@ -1293,11 +1293,17 @@
|
||||||
Sie haben bereits bezahlt. Sie können die Vollversion erst kurz vor dem Ablaufen (oder danach) verlängern
|
Sie haben bereits bezahlt. Sie können die Vollversion erst kurz vor dem Ablaufen (oder danach) verlängern
|
||||||
</string>
|
</string>
|
||||||
<string name="purchase_error_server_rejected">Der Server akzeptiert momentan keine Bezahlungen</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_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_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_title">Vollversion erforderlich</string>
|
||||||
<string name="purchase_required_dialog_text">
|
<string name="purchase_required_dialog_text">
|
||||||
Diese Funktion benötigt die Vollversion.
|
Diese Funktion benötigt die Vollversion.
|
||||||
|
|
|
@ -1335,11 +1335,16 @@
|
||||||
You can only extend the premium version if it expired or shortly before that.
|
You can only extend the premium version if it expired or shortly before that.
|
||||||
</string>
|
</string>
|
||||||
<string name="purchase_error_server_rejected">The server does not accept payments currently</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_title">Purchase succeeded</string>
|
||||||
<string name="purchase_done_text">Press back to go back to the App</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_title">Premium version required</string>
|
||||||
<string name="purchase_required_dialog_text">
|
<string name="purchase_required_dialog_text">
|
||||||
The feature requires the premium version.
|
The feature requires the premium version.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue