diff --git a/app/build.gradle b/app/build.gradle index b612618a1f..e80024ee8a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletFragment.kt b/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletFragment.kt index 2eca438aa8..d9204523e3 100644 --- a/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletFragment.kt @@ -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 + + 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( + 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 -> + 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(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 { rate -> + viewModel.getRateInfo().observe(viewLifecycleOwner, Observer { 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() + } + } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletViewModel.kt b/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletViewModel.kt index fe020d4831..e3e3fda021 100644 --- a/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletViewModel.kt +++ b/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletViewModel.kt @@ -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 by lazy { MutableLiveData().also { @@ -17,6 +20,8 @@ class LoadedWalletViewModel : ViewModel() { } } + private val payId: LiveData> = MutableLiveData() + fun getRateInfo(): LiveData { 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> { + return liveData(viewModelScope.coroutineContext) { + emit( + payIdService.getPayId( + cardId, + publicKey + ) + ) + } + } + + fun setPayId( + cardId: String, + publicKey: String, + payId: String, + address: String, + network: String + ): LiveData> { + return liveData(viewModelScope.coroutineContext) { + emit( + payIdService.setPayId( + cardId, publicKey, payId, address, network + ) + ) + } + } } \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_pay_id.xml b/app/src/main/res/layout/dialog_pay_id.xml new file mode 100644 index 0000000000..3543687c36 --- /dev/null +++ b/app/src/main/res/layout/dialog_pay_id.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4077dc857d..78b38d6bb1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -116,6 +116,13 @@ Show QR-code LOAD + Create Pay ID + Create + Enter Pay ID first + New Pay ID was created + Error creating Pay ID + + Verified balance Unknown balance diff --git a/server-android/build.gradle b/server-android/build.gradle index a853878415..7c6568c89a 100644 --- a/server-android/build.gradle +++ b/server-android/build.gradle @@ -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' diff --git a/server-android/src/main/java/com/tangem/server_android/NetworkUtils.kt b/server-android/src/main/java/com/tangem/server_android/NetworkUtils.kt new file mode 100644 index 0000000000..b631e7faa9 --- /dev/null +++ b/server-android/src/main/java/com/tangem/server_android/NetworkUtils.kt @@ -0,0 +1,40 @@ +package com.tangem.server_android + +import kotlinx.coroutines.delay +import java.io.IOException + + +suspend fun 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 { + data class Success(val data: T) : Result() + data class Failure(val error: Throwable?) : Result() +} + +suspend fun performRequest(block: suspend () -> T): Result { + return try { + val result = retryIO { block() } + Result.Success(result) + } catch (exception: Exception) { + Result.Failure(exception) + } +} \ No newline at end of file diff --git a/server-android/src/main/java/com/tangem/server_android/PayIdService.kt b/server-android/src/main/java/com/tangem/server_android/PayIdService.kt new file mode 100644 index 0000000000..804eb5fddb --- /dev/null +++ b/server-android/src/main/java/com/tangem/server_android/PayIdService.kt @@ -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 { + return performRequest { tangemApi.getPayId(cardId, publicKey) } + } + + suspend fun setPayId(cardId: String, publicKey: String, payId: String, address: String, network: String): Result { + 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 +} \ No newline at end of file diff --git a/server-android/src/main/java/com/tangem/server_android/Server.java b/server-android/src/main/java/com/tangem/server_android/Server.java index ba1f2f2ed9..92e024cd93 100644 --- a/server-android/src/main/java/com/tangem/server_android/Server.java +++ b/server-android/src/main/java/com/tangem/server_android/Server.java @@ -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; } } } \ No newline at end of file diff --git a/server-android/src/main/java/com/tangem/server_android/ServerURL.java b/server-android/src/main/java/com/tangem/server_android/ServerURL.java index 64fec024e6..ba1ec7cb3d 100644 --- a/server-android/src/main/java/com/tangem/server_android/ServerURL.java +++ b/server-android/src/main/java/com/tangem/server_android/ServerURL.java @@ -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/"; } \ No newline at end of file diff --git a/server-android/src/main/java/com/tangem/server_android/TangemApi.java b/server-android/src/main/java/com/tangem/server_android/TangemApi.java deleted file mode 100644 index 655975d1b2..0000000000 --- a/server-android/src/main/java/com/tangem/server_android/TangemApi.java +++ /dev/null @@ -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 getCardVerifyAndGetInfo(@Body CardVerifyAndGetInfo.Request requestBody); - - @GET(Server.ApiTangem.Method.ARTWORK) - Call getArtwork(@Query("artworkId") String artworkId, @Query("CID") String CID, @Query("publicKey") String publicKey); - -} \ No newline at end of file diff --git a/server-android/src/main/java/com/tangem/server_android/TangemApi.kt b/server-android/src/main/java/com/tangem/server_android/TangemApi.kt new file mode 100644 index 0000000000..e120ef75c1 --- /dev/null +++ b/server-android/src/main/java/com/tangem/server_android/TangemApi.kt @@ -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 + + @GET(Server.ApiTangem.Method.ARTWORK) + fun getArtwork( + @Query("artworkId") artworkId: String?, + @Query("CID") CID: String?, + @Query("publicKey") publicKey: String? + ): Call + + @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 + +} \ No newline at end of file