Updated on 2026-08-14

This commit is contained in:
Tangem 2020-06-15 19:29:49 +03:00
parent d8f9a85560
commit 1c82ef2c7b
12 changed files with 332 additions and 57 deletions

View file

@ -85,15 +85,17 @@ dependencies {
implementation 'androidx.navigation:navigation-ui-ktx:2.2.2'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation "androidx.lifecycle:lifecycle-runtime:2.2.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.fragment:fragment:1.2.4'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation "androidx.preference:preference:1.1.1"
implementation 'androidx.biometric:biometric:1.0.1'
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.core:core-ktx:1.3.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'com.google.dagger:dagger:2.24'

View file

@ -14,6 +14,7 @@ import android.os.Build
import android.os.Bundle
import android.text.Html
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
@ -24,7 +25,8 @@ import com.tangem.App
import com.tangem.Constant
import com.tangem.data.Blockchain
import com.tangem.data.dp.PrefsManager
import com.tangem.data.network.ServerApiCommon
import com.tangem.server_android.PayIdResponse
import com.tangem.server_android.Result
import com.tangem.server_android.ServerApiTangem
import com.tangem.server_android.model.CardVerifyAndGetInfo
import com.tangem.tangem_card.data.TangemCard
@ -49,11 +51,13 @@ import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.util.LOG
import com.tangem.util.UtilHelper
import com.tangem.wallet.*
import kotlinx.android.synthetic.main.dialog_pay_id.view.*
import kotlinx.android.synthetic.main.fr_loaded_wallet.*
import kotlinx.android.synthetic.main.layout_btn_details.*
import kotlinx.android.synthetic.main.layout_tangem_card.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import retrofit2.HttpException
import java.io.InputStream
import java.util.*
import kotlin.concurrent.timerTask
@ -62,6 +66,7 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
CardProtocol.Notifications, SharedPreferences.OnSharedPreferenceChangeListener {
companion object {
val TAG: String = LoadedWalletFragment::class.java.simpleName
const val PAY_ID_TANGEM = "\$payid.tangem.com"
}
override val layoutId = R.layout.fr_loaded_wallet
@ -69,7 +74,6 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
private lateinit var viewModel: LoadedWalletViewModel
private lateinit var ctx: TangemContext
private lateinit var mpSecondScanSound: MediaPlayer
private var serverApiCommon: ServerApiCommon = ServerApiCommon()
private var serverApiTangem: ServerApiTangem = ServerApiTangem()
private var lastTag: Tag? = null
private var lastReadSuccess = true
@ -111,6 +115,10 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
srl.isRefreshing = true
}
private var payId: String? = null
private lateinit var btnLoadItems: MutableList<CharSequence>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -127,6 +135,8 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this).get(LoadedWalletViewModel::class.java)
mpSecondScanSound = MediaPlayer.create(activity, R.raw.scan_card_sound)
val engine = CoinEngineFactory.create(ctx)
@ -139,6 +149,37 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
btnExtract.backgroundTintList = inactiveColor
tvWallet.text = ctx.coinData.wallet
btnLoadItems = mutableListOf<CharSequence>(
getString(R.string.loaded_wallet_load_via_app),
getString(R.string.loaded_wallet_load_via_share_address),
getString(R.string.loaded_wallet_load_via_qr)
)
if (ctx.blockchain == Blockchain.Ripple) {
viewModel.getPayId(
Util.byteArrayToHexString(ctx.card?.cid!!),
Util.byteArrayToHexString(ctx.card?.cardPublicKey!!))
.observe(
viewLifecycleOwner, Observer<Result<PayIdResponse>> { result ->
when (result) {
is Result.Success -> {
payId = result.data.payId
btnLoadItems.add(result.data.payId)
}
is Result.Failure -> {
(result.error as? HttpException)?.let {
if (it.code() == 404) {
val createPayId = getString(R.string.loaded_wallet_create_pay_id)
payId = createPayId
btnLoadItems.add(createPayId)
}
}
ctx.error = result.error?.localizedMessage
refresh()
}
}
})
}
// set listeners
srl.setOnRefreshListener { refresh() }
@ -149,16 +190,17 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
btnCopy.setOnClickListener { doShareWallet(false) }
if (Util.bytesToHex(ctx.card?.cid)?.startsWith("10") == true) {
btnLoad?.visibility = View.GONE
}
btnLoad.setOnClickListener {
val items = arrayOf<CharSequence>(getString(R.string.loaded_wallet_load_via_app), getString(R.string.loaded_wallet_load_via_share_address), getString(R.string.loaded_wallet_load_via_qr))//, getString(R.string.via_cryptonit), getString(R.string.via_kraken))
val cw = android.view.ContextThemeWrapper(activity, R.style.AlertDialogTheme)
val dialog = AlertDialog.Builder(cw).setItems(items
val dialog = AlertDialog.Builder(cw).setItems(btnLoadItems.toTypedArray()
) { _, which ->
when (items[which]) {
when (btnLoadItems[which]) {
getString(R.string.loaded_wallet_load_via_app) -> {
try {
val intent = Intent(Intent.ACTION_VIEW, engine.shareWalletUriEx)
@ -176,22 +218,14 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
getString(R.string.loaded_wallet_load_via_qr) -> {
ShowQRCodeDialog.show(activity as AppCompatActivity?, engine.shareWalletUriEx.toString())
}
getString(R.string.loaded_wallet_load_via_cryptonit) -> {
navigateForResult(
Constant.REQUEST_CODE_RECEIVE_TRANSACTION,
R.id.action_loadedWalletFragment_to_prepareCryptonitWithdrawalFragment,
Bundle().apply { ctx.saveToBundle(this) }
)
payId -> {
if (payId == getString(R.string.loaded_wallet_create_pay_id)) {
createPayIdDialog()
} else {
payId?.let { copyText(it) }
}
}
getString(R.string.loaded_wallet_load_via_kraken) -> {
navigateForResult(
Constant.REQUEST_CODE_RECEIVE_TRANSACTION,
R.id.action_loadedWalletFragment_to_prepareKrakenWithdrawalFragment,
Bundle().apply { ctx.saveToBundle(this) }
)
}
else -> {
}
}
@ -297,10 +331,9 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
startVerify(lastTag)
viewModel = ViewModelProviders.of(this).get(LoadedWalletViewModel::class.java)
// set rate info to CoinData
viewModel.getRateInfo().observe(this, Observer<Float> { rate ->
viewModel.getRateInfo().observe(viewLifecycleOwner, Observer<Float> { rate ->
ctx.coinData.rate = rate
ctx.coinData.rateAlter = rate
updateViews()
@ -308,6 +341,54 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
viewModel.requestRateInfo(ctx)
}
private fun createPayIdDialog() {
val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_pay_id, null);
val alertDialogBuilderUserInput = AlertDialog.Builder(context);
alertDialogBuilderUserInput.setView(dialogView);
alertDialogBuilderUserInput
.setCancelable(true)
.setPositiveButton(getString(R.string.create_pay_id_create)) { dialogBox, _ ->
if (dialogView.etPayId.text.isNullOrBlank()) {
Toast.makeText(context, getString(R.string.create_pay_id_empty), Toast.LENGTH_LONG).show()
return@setPositiveButton
}
dialogBox.cancel()
createPayId("${dialogView.etPayId.text}$PAY_ID_TANGEM")
}
.setNegativeButton(getString(R.string.general_cancel)){ dialogBox, _ ->
dialogBox.cancel()
}
alertDialogBuilderUserInput.create().show()
}
private fun createPayId(payId: String) {
viewModel.setPayId(
Util.byteArrayToHexString(ctx.card!!.cid!!),
Util.byteArrayToHexString(ctx.card!!.cardPublicKey!!),
payId,
ctx.coinData.wallet,
ctx.blockchain.id
)
.observe(viewLifecycleOwner, Observer { result ->
when (result) {
is Result.Success -> {
this.payId = payId
if (btnLoadItems.last() == getString(R.string.loaded_wallet_create_pay_id)) {
btnLoadItems.removeAt(btnLoadItems.lastIndex)
}
btnLoadItems.add(payId)
Toast.makeText(context, getString(R.string.create_pay_id_success), Toast.LENGTH_LONG).show()
}
is Result.Failure -> {
Toast.makeText(context, getString(R.string.create_pay_id_error), Toast.LENGTH_LONG).show()
}
}
})
}
override fun onPause() {
super.onPause()
if (timerHideErrorAndMessage != null) {
@ -836,7 +917,7 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
val chooser = Intent.createChooser(intent, getString(R.string.loaded_wallet_chooser_share))
// verify the intent will resolve to at least one activity
if (intent.resolveActivity(activity!!.packageManager) != null) {
if (intent.resolveActivity(requireActivity().packageManager) != null) {
startActivity(chooser)
}
} else {
@ -845,11 +926,14 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
Toast.makeText(activity, R.string.loaded_wallet_toast_copied, Toast.LENGTH_LONG).show()
}
} else {
val txtShare = ctx.coinData.wallet
val clipboard = activity?.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
clipboard.primaryClip = ClipData.newPlainText(txtShare, txtShare)
Toast.makeText(activity, R.string.loaded_wallet_toast_copied, Toast.LENGTH_LONG).show()
copyText(ctx.coinData.wallet)
}
}
private fun copyText(text: String) {
val clipboard = activity?.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
clipboard.primaryClip = ClipData.newPlainText(text, text)
Toast.makeText(activity, R.string.loaded_wallet_toast_copied, Toast.LENGTH_LONG).show()
}
}

View file

@ -1,15 +1,18 @@
package com.tangem.ui.fragment.wallet
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.*
import com.tangem.data.network.ServerApiCommon
import com.tangem.data.network.ServerApiCommon.RateInfoListener
import com.tangem.data.network.model.RateInfoResponse
import com.tangem.server_android.PayIdResponse
import com.tangem.server_android.PayIdService
import com.tangem.server_android.Result
import com.tangem.server_android.SetPayIdResponse
import com.tangem.wallet.TangemContext
class LoadedWalletViewModel : ViewModel() {
private var serverApiCommon: ServerApiCommon = ServerApiCommon()
private val serverApiCommon: ServerApiCommon = ServerApiCommon()
private val payIdService = PayIdService()
private lateinit var ctx: TangemContext
private val rate: MutableLiveData<Float> by lazy {
MutableLiveData<Float>().also {
@ -17,6 +20,8 @@ class LoadedWalletViewModel : ViewModel() {
}
}
private val payId: LiveData<Result<PayIdResponse>> = MutableLiveData()
fun getRateInfo(): LiveData<Float> {
return rate
}
@ -28,7 +33,7 @@ class LoadedWalletViewModel : ViewModel() {
}
private fun loadRateInfo() {
serverApiCommon.setRateInfoListener(object: RateInfoListener{
serverApiCommon.setRateInfoListener(object : RateInfoListener {
override fun onSuccess(rateInfoResponse: RateInfoResponse?) {
rate.value = rateInfoResponse!!.data!!.quote!!.usd!!.price
}
@ -38,4 +43,31 @@ class LoadedWalletViewModel : ViewModel() {
}
})
}
fun getPayId(cardId: String, publicKey: String): LiveData<Result<PayIdResponse>> {
return liveData(viewModelScope.coroutineContext) {
emit(
payIdService.getPayId(
cardId,
publicKey
)
)
}
}
fun setPayId(
cardId: String,
publicKey: String,
payId: String,
address: String,
network: String
): LiveData<Result<SetPayIdResponse>> {
return liveData(viewModelScope.coroutineContext) {
emit(
payIdService.setPayId(
cardId, publicKey, payId, address, network
)
)
}
}
}

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/dialogTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Create Pay ID"
android:layout_gravity="center"
android:textAppearance="?android:attr/textAppearanceLarge" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<EditText
android:id="@+id/etPayId"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:hint="Pay ID"
android:inputType="text" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="$payid.tangem.com"/>
</LinearLayout>
<TextView
android:id="@+id/tvErrorPayId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
</LinearLayout>

View file

@ -116,6 +116,13 @@
<string name="loaded_wallet_load_via_qr">Show QR-code</string>
<string name="loaded_wallet_dialog_show_qr">LOAD</string>
<string name="loaded_wallet_create_pay_id">Create Pay ID</string>
<string name="create_pay_id_create">Create</string>
<string name="create_pay_id_empty">Enter Pay ID first</string>
<string name="create_pay_id_success">New Pay ID was created</string>
<string name="create_pay_id_error">Error creating Pay ID</string>
<!-- LoadedWallet Blockchain Engines -->
<string name="balance_validator_first_line_verified_balance">Verified balance</string>
<string name="balance_validator_first_line_unknown_balance">Unknown balance</string>

View file

@ -33,10 +33,13 @@ dependencies {
// implementation 'com.github.TangemCash.card_android-common:card_android-android:0.1.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.squareup.retrofit2:converter-gson:2.6.2'
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.7"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

View file

@ -0,0 +1,40 @@
package com.tangem.server_android
import kotlinx.coroutines.delay
import java.io.IOException
suspend fun <T> retryIO(
times: Int = 3,
initialDelay: Long = 100,
maxDelay: Long = 1000,
factor: Double = 2.0,
block: suspend () -> T): T
{
var currentDelay = initialDelay
repeat(times - 1) {
try {
return block()
} catch (e: IOException) {
// you can log an error here and/or make a more finer-grained
// analysis of the cause to see if retry is needed
}
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
return block()
}
sealed class Result<out T> {
data class Success<out T>(val data: T) : Result<T>()
data class Failure(val error: Throwable?) : Result<Nothing>()
}
suspend fun <T>performRequest(block: suspend () -> T): Result<T> {
return try {
val result = retryIO { block() }
Result.Success(result)
} catch (exception: Exception) {
Result.Failure(exception)
}
}

View file

@ -0,0 +1,49 @@
package com.tangem.server_android
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class PayIdService {
private val tangemApi: TangemApi by lazy {
provideRetrofit()
.create(TangemApi::class.java)
}
suspend fun getPayId(cardId: String, publicKey: String): Result<PayIdResponse> {
return performRequest { tangemApi.getPayId(cardId, publicKey) }
}
suspend fun setPayId(cardId: String, publicKey: String, payId: String, address: String, network: String): Result<SetPayIdResponse> {
return performRequest { tangemApi.setPayId(cardId, publicKey, payId, address, network) }
}
}
data class PayIdResponse(
val payId: String
)
data class SetPayIdResponse(
val success: Boolean
)
fun provideRetrofit(): Retrofit {
val builder = Retrofit.Builder()
.baseUrl("https://tangem.com/")
.addConverterFactory(GsonConverterFactory.create())
if (BuildConfig.DEBUG)
builder.client(createOkHttpClient())
return builder.build()
}
private fun createOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder().addInterceptor(createHttpLoggingInterceptor()).build()
}
private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor {
val logging = HttpLoggingInterceptor()
logging.level = HttpLoggingInterceptor.Level.BODY
return logging
}

View file

@ -12,6 +12,7 @@ public class Server {
static final String VERIFY = URL_TANGEM + "verify";
static final String VERIFY_AND_GET_INFO = URL_TANGEM + "card/verify-and-get-info";
static final String ARTWORK = URL_TANGEM + "card/artwork";
static final String PAY_ID = ServerURL.API_PAY_ID_TANGEM;
}
}
}

View file

@ -2,4 +2,5 @@ package com.tangem.server_android;
class ServerURL {
static final String API_TANGEM = "https://verify.tangem.com/";
static final String API_PAY_ID_TANGEM = "https://payid.tangem.com/";
}

View file

@ -1,21 +0,0 @@
package com.tangem.server_android;
import com.tangem.server_android.model.CardVerifyAndGetInfo;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.Headers;
import retrofit2.http.POST;
import retrofit2.http.Query;
public interface TangemApi {
@Headers("Content-Type: application/json")
@POST(Server.ApiTangem.Method.VERIFY_AND_GET_INFO)
Call<CardVerifyAndGetInfo.Response> getCardVerifyAndGetInfo(@Body CardVerifyAndGetInfo.Request requestBody);
@GET(Server.ApiTangem.Method.ARTWORK)
Call<ResponseBody> getArtwork(@Query("artworkId") String artworkId, @Query("CID") String CID, @Query("publicKey") String publicKey);
}

View file

@ -0,0 +1,35 @@
package com.tangem.server_android
import com.tangem.server_android.model.CardVerifyAndGetInfo
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.*
interface TangemApi {
@Headers("Content-Type: application/json")
@POST(Server.ApiTangem.Method.VERIFY_AND_GET_INFO)
fun getCardVerifyAndGetInfo(@Body requestBody: CardVerifyAndGetInfo.Request?): Call<CardVerifyAndGetInfo.Response>
@GET(Server.ApiTangem.Method.ARTWORK)
fun getArtwork(
@Query("artworkId") artworkId: String?,
@Query("CID") CID: String?,
@Query("publicKey") publicKey: String?
): Call<ResponseBody>
@GET(Server.ApiTangem.Method.PAY_ID)
suspend fun getPayId(
@Query("cid") cardId: String,
@Query("key") publicKey: String
): PayIdResponse
@POST(Server.ApiTangem.Method.PAY_ID)
suspend fun setPayId(
@Query("cid") cardId: String,
@Query("key") publicKey: String,
@Query("payid") payId: String,
@Query("address") address: String,
@Query("network") network: String
): SetPayIdResponse
}