Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Buy/sell #19

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .idea/.name

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions apps/signer/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ android {
compileSdk = Build.compileSdkVersion

defaultConfig {
applicationId = "com.tonapps.signer"
applicationId = Build.namespacePrefix("signer")
minSdk = Build.minSdkVersion
targetSdk = 34
versionCode = 8
versionName = "0.0.8"
versionCode = 10
versionName = "0.1.0"
}

lint {
Expand Down Expand Up @@ -68,6 +68,7 @@ dependencies {
implementation(Dependence.AndroidX.fragment)
implementation(Dependence.AndroidX.recyclerView)
implementation(Dependence.AndroidX.viewPager2)
implementation(Dependence.AndroidX.splashscreen)

implementation(Dependence.UI.material)
implementation(Dependence.AndroidX.Camera.base)
Expand Down
8 changes: 4 additions & 4 deletions apps/signer/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:supportsRtl="false"
android:theme="@style/Theme.Signer.Starting"
android:hardwareAccelerated="true"
android:largeHeap="true">
android:largeHeap="true"
android:localeConfig="@xml/locales_config">
<activity
android:name="com.tonapps.signer.screen.root.RootActivity"
android:exported="true"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:hardwareAccelerated="true"
android:theme="@style/Theme.Signer.Starting"
android:taskAffinity=""
android:largeHeap="true"
android:screenOrientation="portrait"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode">
Expand All @@ -64,7 +64,7 @@
</intent-filter>
</activity>
<activity
android:theme="@style/Theme.Signer.Starting"
android:theme="@style/Theme.Signer"
android:name="com.tonapps.signer.screen.crash.CrashActivity"
android:process=":crash_activity"
android:exported="false"/>
Expand Down
Binary file modified apps/signer/src/main/ic_launcher-playstore.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.ton.api.pub.PublicKeyEd25519
import org.ton.crypto.hex

class KeyRepository(
private val dataSource: KeyDataSource,
Expand All @@ -27,6 +28,12 @@ class KeyRepository(

init {
Scope.repositories.launch(Dispatchers.IO) {
/*val testKey = KeyEntity(
id = 1,
name = "Test",
publicKey = PublicKeyEd25519(hex("db642e022c80911fe61f19eb4f22d7fb95c1ea0b589c0f74ecf0cbf6db746c13"))
)
_keysEntityFlow.value = listOf(testKey)*/
_keysEntityFlow.value = dataSource.getEntities()
}
}
Expand All @@ -43,10 +50,13 @@ class KeyRepository(
_keysEntityFlow.value?.size ?: 0
}

fun findIdByPublicKey(publicKey: PublicKeyEd25519): Flow<Long> = flow {
val id = dataSource.findIdByPublicKey(publicKey)
emit(id)
}.filterNotNull()
fun findIdByPublicKey(publicKey: PublicKeyEd25519): Long? {
val id = dataSource.findIdByPublicKey(publicKey) ?: return null
if (0 >= id) {
return null
}
return id
}

suspend fun setName(id: Long, name: String) = withContext(Dispatchers.IO) {
dataSource.setName(id, name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,18 @@ object TKDeepLink {
return builder.build()
}

fun buildLinkUri(publicKey: PublicKeyEd25519, name: String): Uri {
fun buildLinkUri(
publicKey: PublicKeyEd25519,
name: String,
local: Boolean
): Uri {
val baseUri = Uri.parse("${APP_SCHEME}://signer/link")
val builder = baseUri.buildUpon()
builder.appendQueryParameter("pk", publicKey.base64())
builder.appendQueryParameter("name", name)
if (local) {
builder.appendQueryParameter("local", "true")
}
return builder.build()
}

Expand All @@ -32,6 +39,7 @@ object TKDeepLink {
val builder = baseUri.buildUpon()
builder.appendQueryParameter("pk", publicKey.base64())
builder.appendQueryParameter("name", name)
builder.appendQueryParameter("local", "true")
return builder.build()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,16 @@ object Password {
val dialog = PasswordDialog(context, ::trySend)
dialog.setOnDismissListener {
if (isActive) {
close(Throwable("User canceled"))
close(UserCancelThrowable )
} else {
close()
}
}
dialog.show()
awaitClose { dialog.destroy() }
}.flowOn(Dispatchers.Main).take(1)

object UserCancelThrowable : Throwable("User canceled") {
private fun readResolve(): Any = UserCancelThrowable
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class CameraFragment: BaseFragment(R.layout.fragment_camera), BaseFragment.Botto
}

private var isFlashAvailable = false
private var isReady = false

private val qrAnalyzer = QRImageAnalyzer()
private val chunks = mutableListOf<String>()
Expand Down Expand Up @@ -95,6 +96,9 @@ class CameraFragment: BaseFragment(R.layout.fragment_camera), BaseFragment.Botto
}

private fun handleBarcode(barcode: Barcode) {
if (isReady) {
return
}
val data = barcode.rawValue ?: return

if (data.startsWith("${Key.SCHEME}://")) {
Expand All @@ -108,12 +112,13 @@ class CameraFragment: BaseFragment(R.layout.fragment_camera), BaseFragment.Botto

val uri = chunks.joinToString("").uriOrNull ?: return
if (rootViewModel.processDeepLink(uri, false)) {
isReady = true
finishDelay()
}
}

private fun finishDelay() {
postDelayed(ModalView.animationDuration) {
postDelayed(300) {
finish()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.tonapps.signer.screen.create

import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.core.view.doOnLayout
import androidx.lifecycle.lifecycleScope
Expand Down Expand Up @@ -53,6 +54,7 @@ class CreateFragment: BaseFragment(R.layout.fragment_create), BaseFragment.Swipe
adapter = PagerAdapter(this, createViewModel.pages)

pagerView = view.findViewById(R.id.pager)
pagerView.offscreenPageLimit = adapter.itemCount
pagerView.isUserInputEnabled = false
pagerView.adapter = adapter

Expand All @@ -65,7 +67,7 @@ class CreateFragment: BaseFragment(R.layout.fragment_create), BaseFragment.Swipe
private fun setPage(index: Int) {
val currentIndex = pagerView.currentItem
if (currentIndex != index) {
pagerView.setCurrentItem(index, true)
post { pagerView.setCurrentItem(index, true) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import kotlinx.coroutines.launch
import org.ton.api.pk.PrivateKeyEd25519
import org.ton.mnemonic.Mnemonic
import com.tonapps.security.tryCallGC
import com.tonapps.signer.extensions.authorizationRequiredError
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.withContext
import javax.crypto.SecretKey

class CreateViewModel(
Expand All @@ -54,15 +58,19 @@ class CreateViewModel(
add(PageType.Name)
}

private val _onReady = Channel<Unit>(Channel.BUFFERED)
val onReady: Flow<Unit> = _onReady.receiveAsFlow()
private val _onReady = MutableSharedFlow<Unit>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val onReady: Flow<Unit> = _onReady.asSharedFlow()

private val _currentPage = MutableStateFlow(pages.first())
private val _currentPage = MutableSharedFlow<PageType>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val currentPage = _currentPage.asSharedFlow()

private val _uiTopOffset = MutableStateFlow(0)
val uiTopOffset = _uiTopOffset.asStateFlow()

init {
_currentPage.tryEmit(pages.first())
}

fun pageIndex() = currentPage.map { pageIndex(it) }

fun page(pageType: PageType) = currentPage.filter { it == pageType }
Expand Down Expand Up @@ -98,22 +106,27 @@ class CreateViewModel(
return true
}

fun addKey(context: Context) {
viewModelScope.launch(Dispatchers.IO) {
try {
val name = args.name ?: throw IllegalStateException("Name is null")
val secret = masterSecret(context).single()
Password.setUnlock()

if (import) {
val mnemonic = args.mnemonic ?: throw IllegalStateException("Mnemonic is null")
addNewKey(secret, name, mnemonic)
} else {
createNewKey(secret, name)
}

_onReady.trySend(Unit)
} catch (e: Throwable) {
fun addKey(context: Context) = flow {
try {
val name = args.name ?: throw IllegalStateException("Name is null")
val secret = masterSecret(context).single()
Password.setUnlock()

if (import) {
val mnemonic = args.mnemonic ?: throw IllegalStateException("Mnemonic is null")
addNewKey(secret, name, mnemonic)
} else {
createNewKey(secret, name)
}

emit(true)
_onReady.tryEmit(Unit)

} catch (e: Throwable) {
emit(false)
if (e is Password.UserCancelThrowable) {
context.authorizationRequiredError()
} else {
CrashActivity.open(e, context)
_currentPage.tryEmit(pages.first())
}
Expand All @@ -131,14 +144,21 @@ class CreateViewModel(

private fun createMasterSecret(password: CharArray) = flow {
emit(vault.createMasterSecret(password))
}.take(1)
}.take(1).flowOn(Dispatchers.IO)

private suspend fun createNewKey(secret: SecretKey, name: String) {
private suspend fun createNewKey(
secret: SecretKey,
name: String
) = withContext(Dispatchers.IO) {
val mnemonic = Mnemonic.generate()
addNewKey(secret, name, mnemonic)
}

private suspend fun addNewKey(secret: SecretKey, name: String, mnemonic: List<String>) {
private suspend fun addNewKey(
secret: SecretKey,
name: String,
mnemonic: List<String>
) = withContext(Dispatchers.IO) {
val seed = Mnemonic.toSeed(mnemonic)
val publicKey = PrivateKeyEd25519(seed).publicKey()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.tonapps.signer.screen.create.child

import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import androidx.lifecycle.lifecycleScope
import com.tonapps.signer.R
import com.tonapps.signer.extensions.authorizationRequiredError
import com.tonapps.signer.screen.create.CreateViewModel
import com.tonapps.signer.screen.create.pager.PageType
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.koin.androidx.viewmodel.ext.android.getViewModel
Expand Down Expand Up @@ -56,6 +59,10 @@ class CreateNameFragment: BaseFragment(R.layout.fragment_create_name) {
nameInput.focus()
}

collectFlow(createViewModel.page(PageType.RepeatPassword)) {
nameInput.clear()
}

collectFlow(createViewModel.uiTopOffset) {
contentView.setPaddingTop(it)
}
Expand All @@ -66,12 +73,23 @@ class CreateNameFragment: BaseFragment(R.layout.fragment_create_name) {
if (name.isBlank()) {
return
}

createViewModel.setName(name)
createViewModel.addKey(requireContext())
collectFlow(createViewModel.addKey(requireContext())) { done ->
if (!done) {
applyDefaultState()
}
}
applyLoadingState()
}

private fun applyLoadingState() {
loaderView.visibility = View.VISIBLE
doneButton.visibility = View.GONE
nameInput.hideKeyboard()
}

private fun applyDefaultState() {
loaderView.visibility = View.GONE
doneButton.visibility = View.VISIBLE
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ class CreatePasswordFragment : BaseFragment(R.layout.fragment_create_password) {
passwordInput.focusWithKeyboard()
}

if (isRepeat) {
collectFlow(createViewModel.page(PageType.Password)) {
passwordInput.clear()
}
}

collectFlow(createViewModel.uiTopOffset) {
contentView.setPaddingTop(it)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,10 @@ class KeyFragment: BaseFragment(R.layout.fragment_key), BaseFragment.SwipeBack {
}

private fun setExportByUri(publicKey: PublicKeyEd25519, name: String) {
val uri = TKDeepLink.buildLinkUri(publicKey, name)
qrView.setContent(uri.toString())
qrView.setContent(TKDeepLink.buildLinkUri(publicKey, name, false).toString())

exportTonkeeperView.setOnClickListener {
TKDeepLink.openOrInstall(requireContext(), uri)
TKDeepLink.openOrInstall(requireContext(), TKDeepLink.buildLinkUri(publicKey, name, true))
}

exportTonkeeperWebView.setOnClickListener {
Expand Down
Loading