diff --git a/.github/labeler.yml b/.github/labeler.yml index 327c66ca3..12b39db78 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -20,8 +20,6 @@ "area: intents": - changed-files: - any-glob-to-any-file: - - "services/code/**/intents/**" - - "services/flipchat/**/intents/**" - "services/opencode/**/intents/**" "area: auth": @@ -52,7 +50,6 @@ - changed-files: - any-glob-to-any-file: - "libs/network/**" - - "services/code/**" - "services/flipcash/**" - "services/flipcash-compose/**" - "services/opencode/**" diff --git a/apps/codeApp/.gitignore b/apps/codeApp/.gitignore deleted file mode 100644 index a2def884f..000000000 --- a/apps/codeApp/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/build -google-services.json diff --git a/apps/codeApp/build.gradle.kts b/apps/codeApp/build.gradle.kts deleted file mode 100644 index 9eeafb2ad..000000000 --- a/apps/codeApp/build.gradle.kts +++ /dev/null @@ -1,218 +0,0 @@ -import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - id(Plugins.android_application) - id(Plugins.kotlin_android) - id(Plugins.kotlin_parcelize) - id(Plugins.kotlin_kapt) - id(Plugins.kotlin_serialization) - id(Plugins.androidx_navigation_safeargs) - id(Plugins.hilt) - id(Plugins.google_services) - id(Plugins.firebase_crashlytics) - id(Plugins.firebase_perf) - id(Plugins.bugsnag_android_gradle) - id(Plugins.secrets_gradle_plugin) - id(Plugins.versioning_gradle_plugin) - id(Plugins.jetbrains_compose_compiler) -} - -val contributorsSigningConfig = ContributorsSignatory(rootProject) - -android { - // static namespace - namespace = Gradle.codeNamespace - compileSdk = Android.compileSdkVersion - - defaultConfig { - versionCode = versioning.getVersionCode() - versionName = Packaging.Code.versionName - applicationId = Gradle.codeNamespace - minSdk = Android.minSdkVersion - targetSdk = Android.targetSdkVersion - testInstrumentationRunner = Android.testInstrumentationRunner - - buildConfigField("String", "MIXPANEL_API_KEY", "\"${tryReadProperty(rootProject.rootDir, "MIXPANEL_API_KEY")}\"") - buildConfigField("String", "KADO_API_KEY", "\"${tryReadProperty(rootProject.rootDir, "KADO_API_KEY")}\"") - buildConfigField("Boolean", "NOTIFY_ERRORS", "false") - } - - signingConfigs { - create("contributors") { - storeFile = contributorsSigningConfig.keystore - storePassword = contributorsSigningConfig.keystorePassword - keyAlias = contributorsSigningConfig.keyAlias - keyPassword = contributorsSigningConfig.keyPassword - } - } - - buildFeatures { - buildConfig = true - compose = true - } - - buildTypes { - getByName("release") { - resValue("string", "applicationId", Gradle.codeNamespace) - isMinifyEnabled = true - isShrinkResources = true - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") - } - getByName("debug") { - applicationIdSuffix = ".dev" - resValue("string", "applicationId", "${Gradle.codeNamespace}.dev") - signingConfig = signingConfigs.getByName("contributors") - - val debugMinifyEnabled = tryReadProperty(rootProject.rootDir, "DEBUG_MINIFY", "false").toBooleanStrictOrNull() ?: false - isMinifyEnabled = debugMinifyEnabled - isShrinkResources = debugMinifyEnabled - - if (debugMinifyEnabled) { - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - - configure { - mappingFileUploadEnabled = tryReadProperty(rootProject.rootDir, "DEBUG_CRASHLYTICS_UPLOAD", "false").toBooleanStrictOrNull() ?: false - } - } - } - - compileOptions { - sourceCompatibility(Versions.java) - targetCompatibility(Versions.java) - isCoreLibraryDesugaringEnabled = true - } - - packaging { - resources.excludes.add("**/*.proto") - resources.excludes.add("META-INF/LICENSE.md") - resources.excludes.add("META-INF/LICENSE-notice.md") - } -} - -dependencies { - // libs (not included with services) - implementation(project(":libs:locale:public")) - implementation(project(":libs:vibrator:public")) - implementation(project(":libs:messaging")) - implementation(project(":libs:permissions:public")) - implementation(project(":libs:quickresponse")) - implementation(project(":libs:requests")) - - // code services - implementation(project(":services:code")) - - // ui components - implementation(project(":ui:analytics")) - implementation(project(":ui:biometrics")) - implementation(project(":ui:components")) - implementation(project(":ui:navigation")) - implementation(project(":ui:resources")) - implementation(project(":ui:scanner")) - implementation(project(":ui:theme")) - - // kik code scanner - implementation(project(":vendor:kik:scanner")) - // tipkit - implementation(project(":vendor:tipkit:tipkit-m2")) - - coreLibraryDesugaring(Libs.android_desugaring) - - //standard libraries - implementation(Libs.kotlinx_collections_immutable) - implementation(Libs.kotlinx_serialization_json) - implementation(Libs.androidx_core) - implementation(Libs.androidx_constraint_layout) - implementation(Libs.androidx_lifecycle_runtime) - implementation(Libs.androidx_lifecycle_viewmodel) - implementation(Libs.androidx_navigation_fragment) - implementation(Libs.androidx_navigation_ui) - - //hilt dependency injection - implementation(Libs.hilt) - implementation("androidx.webkit:webkit:1.13.0") - kapt(Libs.hilt_android_compiler) - kapt(Libs.hilt_compiler) - androidTestImplementation(Libs.hilt) - androidTestImplementation(Libs.hilt_android_test) - kaptAndroidTest(Libs.hilt_android_compiler) - testImplementation(Libs.hilt_android_test) - kaptTest(Libs.hilt_android_compiler) - - androidTestImplementation("io.mockk:mockk:1.13.16") - - //Jetpack compose - implementation(platform(Libs.compose_bom)) - implementation(Libs.compose_ui) - implementation(Libs.compose_accompanist) - implementation(Libs.compose_foundation) - implementation(Libs.compose_material) - implementation(Libs.compose_materialIconsExtended) - implementation(Libs.compose_activities) - implementation(Libs.compose_view_models) - implementation(Libs.compose_livedata) - implementation(Libs.compose_navigation) - implementation(Libs.compose_paging) - implementation(Libs.compose_webview) - - implementation(Libs.androidx_activity) - - // cameraX - implementation(Libs.androidx_camerax_core) - implementation(Libs.androidx_camerax_camera2) - implementation(Libs.androidx_camerax_lifecycle) - implementation(Libs.androidx_camerax_view) - - implementation(Libs.coil3) - implementation(Libs.coil3_network) - - implementation(Libs.androidx_browser) - implementation(Libs.androidx_constraint_layout_compose) - - implementation(Libs.slf4j) - implementation(Libs.grpc_android) - - implementation(platform(Libs.firebase_bom)) - implementation(Libs.firebase_analytics) - implementation(Libs.firebase_crashlytics) - implementation(Libs.firebase_messaging) - - implementation(Libs.hilt_nav_compose) - implementation(Libs.lib_phone_number_port) - implementation(Libs.mp_android_chart) - implementation(Libs.mixpanel) - - implementation(Libs.retrofit) - implementation(Libs.retrofit_converter_gson) - implementation(Libs.okhttp_logging_interceptor) - - androidTestImplementation(Libs.androidx_test_runner) - androidTestImplementation(Libs.androidx_junit) - androidTestImplementation(Libs.junit) - androidTestImplementation(Libs.espresso_core) - androidTestImplementation(Libs.espresso_contrib) { - exclude(module = "protobuf-lite") - } - androidTestImplementation(Libs.espresso_intents) - implementation(Libs.androidx_room_runtime) - implementation(Libs.androidx_room_ktx) - implementation(Libs.androidx_room_rxjava3) - implementation(Libs.androidx_room_paging) - kapt(Libs.androidx_room_compiler) - - implementation(Libs.markwon_core) - implementation(Libs.markwon_linkify) - implementation(Libs.markwon_ext_strikethrough) - - implementation(Libs.play_service_auth) - implementation(Libs.play_service_auth_phone) - - implementation(Libs.timber) - implementation(Libs.bugsnag) - - implementation(Libs.haze) -} diff --git a/apps/codeApp/proguard-rules.pro b/apps/codeApp/proguard-rules.pro deleted file mode 100644 index c355f26c9..000000000 --- a/apps/codeApp/proguard-rules.pro +++ /dev/null @@ -1,69 +0,0 @@ --optimizationpasses 5 --dontusemixedcaseclassnames --verbose -#-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*,!code/allocation/variable --obfuscationdictionary shuffled-dictionary.txt --classobfuscationdictionary shuffled-dictionary.txt - --keepclasseswithmembernames class * { - native ; -} - --keepclassmembers class **.R$* { - public static ; -} - -# Room --keep class * extends androidx.room.RoomDatabase --keep @androidx.room.Entity class * --keep class net.sqlcipher.** { *; } - -## Code API --keep class com.codeinc.gen.** {*;} --keep class com.google.protobuf.** { *; } - -# Keep our scan classes that interact with native --keep class com.kik.scan.** { *; } - -# BouncyCastle --keep public class org.bouncycastle.** # Refine this further! --keepclassmembers class org.bouncycastle.crypto.** { - ; -} - --assumenosideeffects class android.util.Log { - public static int v(...); - public static int i(...); - public static int w(...); - public static int d(...); - public static int e(...); -} - --keepattributes SourceFile,LineNumberTable # Keep file names and line numbers. --keep public class * extends java.lang.Exception --keep public class * extends com.getcode.network.repository.ErrorSubmitIntent --keep public class * extends com.getcode.network.repository.ErrorSubmitIntentException --keep public class * extends com.getcode.network.repository.WithdrawException --keep public class * extends com.getcode.network.repository.FetchUpgradeableIntentsException --keep public class * extends com.getcode.network.repository.AirdropException - -# https://github.com/firebase/firebase-android-sdk/issues/3688 --keep class org.json.** { *; } --keepclassmembers class org.json.** { *; } - -# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items). - -keep,allowobfuscation,allowshrinking interface retrofit2.Call - -keep,allowobfuscation,allowshrinking class retrofit2.Response - - # With R8 full mode generic signatures are stripped for classes that are not - # kept. Suspend functions are wrapped in continuations where the type argument - # is used. - -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation - -# libsodium --keep class com.ionspin.kotlin.crypto.** { *; } --keep class com.sun.jna.** { *; } --dontwarn java.awt.Component --dontwarn java.awt.GraphicsEnvironment --dontwarn java.awt.HeadlessException --dontwarn java.awt.Window \ No newline at end of file diff --git a/apps/codeApp/shuffled-dictionary.txt b/apps/codeApp/shuffled-dictionary.txt deleted file mode 100644 index 1c04f878b..000000000 --- a/apps/codeApp/shuffled-dictionary.txt +++ /dev/null @@ -1,811 +0,0 @@ -Isht -BQUQ -lYNE -YWWv -zhIz -dRko -ihUe -duhS -oRft -jWAX -KLsZ -xbYj -NxZs -ercX -Zikf -jygi -xRWw -BBdO -hOvC -HEqd -QlDS -zchV -mrtd -tAMc -beae -NzVI -WGdk -mlen -mTqY -zOWI -yDek -WkdC -EvHb -fjdG -tXQX -lysK -tJrI -nTyR -QoId -teja -tBns -WdjJ -rzBH -CsrS -rGDS -mgig -lKwS -dsew -PEIO -Iagi -FzdP -jyBf -dkuR -gwNv -GoKt -bHKA -YPEH -sRnT -JfEF -ifmJ -zyoT -dgLf -XeSR -MbvH -EviK -etia -MrSO -sXtv -Bgfm -lRiu -RwJl -tUXp -ZwyR -daJE -Duhs -qXtR -QXRy -NKZp -EVoJ -APSm -YgnC -Xsxv -VlNx -fYSo -NWos -JpCK -bOOW -PpQM -xMex -kZmG -fgZr -Jxmu -BVUp -LBel -WrBi -JFtE -jMcJ -IiRY -rmmT -LyLw -GxwL -xwEO -MDtQ -tXXA -yJqI -XWEk -IchN -snux -ycvY -vrwB -juTi -wISO -OIcx -enkB -ELNl -ymTW -ZAhU -NZjR -bYGt -EHts -lzjc -FUZJ -oGFN -GRql -kKUH -iRfG -kKBN -gavV -CtCD -Jlcd -rkLy -RGmg -sacF -UsUf -rJfG -mLPF -IsTH -gYXw -XNZg -QeWt -zONE -osyG -cLMf -RhpR -oaoi -vaPm -peYL -DGbn -IWRQ -FOMD -zTcd -Hvbw -vqPu -lWCZ -ZwrR -IPnJ -BDfW -ZLPb -mdFn -ZKGS -dOQK -vOTL -nGQY -aoOn -sJwn -DrgD -bIwW -zJIB -Fdra -COnA -EULK -WlWi -bAif -LgwE -yMjR -HdjF -vCmv -LyGj -lGkZ -FIHF -MTiY -YHSv -bsEG -qluJ -PZRG -fXfC -nscO -UIxm -rIyJ -YUDD -ffjW -GXpA -bQji -IAyQ -FZXu -kclk -ryYv -DZOO -rNcr -xLYb -BZJL -RfqD -DOnu -Skiq -VFQs -fuzj -cYxv -FGTC -VWfA -GcgM -EffG -YsaB -Kvpz -uMqE -iBWV -RNlt -NSDn -VpKD -IOhC -KGja -fznE -LldB -KTWJ -SGiI -tGCd -mKFS -kpjE -inNw -YFhv -IoDg -xPxa -ZSGd -VQAo -WaFi -FJOL -bEPM -OraL -CosS -Qpuw -ugST -yNMr -Esbm -FYIB -nXLw -IAJC -wvIu -rEdl -rAIt -RbZu -BGAB -JvuF -ChCH -ywvf -GwoF -lmGe -oGcJ -tGtZ -wkSy -uXTa -OmAD -FPNM -yOYL -eiJD -GGzr -rFrR -Wtyr -smhO -Qusu -MHBp -gYfm -AQMx -JVnn -dcdI -cVrJ -tTwl -QRJf -oyRp -ZaOh -Dmjk -wDaF -vVWP -twIb -FajC -orbD -UTlY -yJOD -FTaL -tiNW -xSNV -lOiN -KrqV -oHaN -JCWM -EKhf -rbdj -dipY -mkjj -WELS -FwtI -zMLd -PyQf -yEtV -xClf -hrPJ -Yftz -HlbL -cyha -oynP -ncsf -atCi -TZCr -iwXC -WuLG -GdVK -OLYf -FgLC -AKUb -nKYb -BWnv -nkhv -Omrp -gsSt -wJnx -HZHh -dLna -PmVN -juAs -MsNP -kjBm -WoRy -ujpz -WskB -ZxUN -DjCe -fyOr -JQIG -utyB -ifvv -uDEA -usVw -OCSt -RCtd -LYDI -HejM -SwhM -gKcH -Bahm -hUTr -SzWQ -fxar -PAdX -icPy -eCYN -SucB -Yjnf -sdxY -sogd -wQCZ -KbLO -wSbt -kLsz -OkpF -MPmo -YcUL -TPJF -SgMm -ekQU -FKYn -qZQR -Fdtt -XVNw -GwFc -XauS -vQrD -jmoJ -UzaQ -zsKx -ndeh -FeuU -teGY -Trkd -pRLL -zeHL -SGPZ -nUti -qQws -BLbX -yqHy -lmFr -TMYU -NlbJ -bRAt -bCFJ -pAuF -fcrw -SVWh -sSde -DAGR -AduC -hVYM -fUSZ -BbRe -RrFF -ZKFL -UtSD -BVqP -UBfb -EgjY -hpxM -cyQy -MLSV -fpdI -eBuR -Kyua -tYDV -CNdO -ZQUw -yDMT -RHAZ -KSms -lgaB -bxzl -YlIs -MVGF -ENts -dgRD -SybV -Mpne -DLah -anUP -nbFc -KZDx -VhDU -UnCj -mFPU -QpML -VKRz -sMQB -JzdA -ZtHp -AkzT -LdLP -CBrv -CtHu -vTfo -uwce -ihuY -xqIU -rvyG -YIeY -nhXV -PvbA -gahw -QTPg -Tcxn -WDuH -HbXF -AJpF -kvzs -VhWQ -dvhm -CGnG -ppzZ -Azky -sWsN -UrPx -Znvg -ngaV -Frhs -lpAR -RpIm -eHlK -UvCX -jHQK -bRCz -Ntid -vVcM -wdim -mttW -qtHO -hwTH -DxKD -shKW -vgBc -oWSA -LeTC -aweS -tsjN -BXOK -vgrh -CSvq -rwUV -cnhU -VFuP -qzBz -Nbay -mKib -TQOI -IxTd -ufEE -uHls -pSTp -JDNU -qHMb -wyBD -Uxof -hPWy -wyZc -arfA -AoZs -qpkG -duyB -teoC -BHLw -rEUj -MMmn -tbUY -jcMO -RbjW -ofCJ -CKNe -YxsR -ozgG -UgOi -JBJO -PZkK -kCIL -JtNn -TVAI -zQsm -VZgL -rukZ -UIEr -Gaoa -tFRW -YPAz -FoKp -hxOK -Zrbo -PbMj -mkaj -Jfvu -RVRh -keIJ -lXOG -HkcV -sbFt -wFcX -iaHo -ncGe -vLSU -PvJz -XlId -Iqbc -FNHp -OauE -Cnsp -cYdn -dTjy -ARVt -GjAP -NeOb -QBRs -teHM -NNFq -pQtt -Ydde -nRZl -jVjd -LGOI -NYIF -PwqS -Twdw -zLme -xomi -PEYK -OjFT -JnFx -vMSR -MtCN -Vmbv -CHXq -hVyA -gzBT -PmnH -cAqk -Fcie -llOf -uVTH -goDD -IEJJ -kGDH -xJPI -LIce -ZtnA -NatG -fXys -hikO -jeXD -nHEB -uVNP -OAJq -gdhL -yPaj -MyJn -wvXg -WuvS -FMXw -ZtSa -cTtl -ioBO -fvdC -ZTbi -ZSon -CYoZ -pODY -HhcT -ABlS -sWns -iDwj -cPNL -DMIa -Hexg -nYen -lPYb -nSND -QDYO -gGIC -Sgvg -yIpe -SiFd -WXRO -IxHN -bkSq -dlVA -LKYw -muTP -HRnO -KiMy -uAzD -alTU -oEID -EmRn -BAzw -LFgd -qaUi -crwT -OIbV -nnET -pZpN -GXiC -aPKz -pwdU -ijjR -Hohs -DbdX -FyRB -fmkK -GFkw -EUuW -LAFO -TbwB -VEmz -Uvdu -ypFl -IBzE -bzWY -KUOp -JFIw -VOTC -nIJJ -thMd -zCuy -ZkaW -vNvX -mUfe -iCoZ -ZpvJ -fbsS -Cfzj -orEO -KpTn -lzzD -JBcp -ISWa -DAfh -HlJM -BdsO -aytI -LSvL -BarO -Wnth -cQyc -qwUp -Fwgx -QXaW -ZiVt -epbZ -VVVC -xtVZ -lPKP -YpCp -wSqt -ssNm -wFLC -NulV -rsqe -Rgde -XKRR -Ludw -mYXR -STdD -vQPY -OXnG -uvkJ -GoHg -nROB -duCc -Qjrw -WZUp -vvVZ -Rvtc -VyFa -LLdY -qlPD -ueda -ClMc -bMVB -zmXD -asSs -wyaY -bJRq -fIsz -yhWi -fZXO -qfLK -HBIx -ReOh -RbVD -Zfeo -LNOU -oHYO -xOjl -XnVx -SwJK -foCJ -PxwF -JmoH -rqKo -rBjl -sNJV -GcFn -Moky -WzQl -WTzB -AIjG -SDfe -dZXz -VIDB -Zlww -UzuA -nXUD -bUrp -QNdb -FkSO -imLC -WqLj -qSbN -mfvq -bPog -uYVI -CKbj -BNcy -RLng -GHjM -FFwD -qSfw -ZvlI -JITU -rHGC -Wigr -zdHB -Orfj -QKgP -oVhJ -SOml -kEuj -GrKj -lVMG -xTxC -HCUN -ZWTO -vclb -wWcc -dckr -vevq -pHjd -zBFk -oneP -ZhKt -ABmP -oJxu -QPfa -yjEq -oPvN -ZsAI -waDy -VKNw -QzHV -KPNY -rhwm diff --git a/apps/codeApp/src/debug/AndroidManifest.xml b/apps/codeApp/src/debug/AndroidManifest.xml deleted file mode 100644 index 16c292d86..000000000 --- a/apps/codeApp/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/apps/codeApp/src/debug/res/values/ic_launcher_background.xml b/apps/codeApp/src/debug/res/values/ic_launcher_background.xml deleted file mode 100644 index 01e1320a6..000000000 --- a/apps/codeApp/src/debug/res/values/ic_launcher_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - #9C0222 - - diff --git a/apps/codeApp/src/debug/res/values/strings.xml b/apps/codeApp/src/debug/res/values/strings.xml deleted file mode 100644 index 1fa80e5c0..000000000 --- a/apps/codeApp/src/debug/res/values/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - Code Dev - com.getcode.dev.accountprovider - \ No newline at end of file diff --git a/apps/codeApp/src/debug/res/xml/authenticator.xml b/apps/codeApp/src/debug/res/xml/authenticator.xml deleted file mode 100644 index 97a429c67..000000000 --- a/apps/codeApp/src/debug/res/xml/authenticator.xml +++ /dev/null @@ -1,7 +0,0 @@ - - \ No newline at end of file diff --git a/apps/codeApp/src/main/AndroidManifest.xml b/apps/codeApp/src/main/AndroidManifest.xml deleted file mode 100644 index 6d0ddd390..000000000 --- a/apps/codeApp/src/main/AndroidManifest.xml +++ /dev/null @@ -1,232 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/codeApp/src/main/ic_launcher-playstore.png b/apps/codeApp/src/main/ic_launcher-playstore.png deleted file mode 100644 index 3f8aae461..000000000 Binary files a/apps/codeApp/src/main/ic_launcher-playstore.png and /dev/null differ diff --git a/apps/codeApp/src/main/java/com/getcode/AccountProvider.kt b/apps/codeApp/src/main/java/com/getcode/AccountProvider.kt deleted file mode 100644 index 580aca624..000000000 --- a/apps/codeApp/src/main/java/com/getcode/AccountProvider.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.getcode - -import android.accounts.AccountManager -import android.content.ContentProvider -import android.content.ContentValues -import android.database.Cursor -import android.database.MatrixCursor -import android.net.Uri -import com.getcode.util.AccountUtils -import kotlinx.coroutines.runBlocking - -class AccountProvider : ContentProvider() { - - override fun onCreate(): Boolean { - - return true - } - - override fun query( - uri: Uri, - projection: Array?, - selection: String?, - selectionArgs: Array?, - sortOrder: String? - ): Cursor? { - val context = context ?: return null - val token = runBlocking { AccountUtils.getToken(context) } - val cursor = MatrixCursor(arrayOf(AccountManager.KEY_AUTHTOKEN)) - cursor.addRow(arrayOf(token)) - return cursor - } - - override fun insert(uri: Uri, values: ContentValues?): Uri? { - // Handle insertion of new data if needed - return null - } - - override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { - // Handle deletion of data if needed - return 0 - } - - override fun update( - uri: Uri, - values: ContentValues?, - selection: String?, - selectionArgs: Array? - ): Int { - // Handle updating of data if needed - return 0 - } - - override fun getType(uri: Uri): String { - // Return the MIME type of data based on the URI pattern - return "vnd.android.cursor.dir/vnd.getcode.account" - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/App.kt b/apps/codeApp/src/main/java/com/getcode/App.kt deleted file mode 100644 index 12e2fd88c..000000000 --- a/apps/codeApp/src/main/java/com/getcode/App.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.getcode - -import android.app.Application -import androidx.appcompat.app.AppCompatDelegate -import com.bugsnag.android.Bugsnag -import com.getcode.crypt.MnemonicCache -import com.getcode.manager.AuthManager -import com.getcode.network.integrity.DeviceCheck -import com.getcode.utils.ErrorUtils -import com.getcode.utils.trace -import com.google.firebase.Firebase -import com.google.firebase.initialize -import dagger.hilt.android.HiltAndroidApp -import io.reactivex.rxjava3.plugins.RxJavaPlugins -import timber.log.Timber -import javax.inject.Inject - -@HiltAndroidApp -class App : Application() { - - @Inject - lateinit var authManager: AuthManager - - override fun onCreate() { - super.onCreate() - - Firebase.initialize(this) - DeviceCheck.register(this) - MnemonicCache.init(this) - authManager.init() - - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) - - RxJavaPlugins.setErrorHandler { - ErrorUtils.handleError(it) - } - - if (BuildConfig.DEBUG) { - Timber.plant(object : Timber.DebugTree() { - override fun createStackElementTag(element: StackTraceElement): String { - val elementTag = super.createStackElementTag(element) - .orEmpty() - .split("$") - .filter { it.isNotEmpty() } - .take(2) - .joinToString(" ") - .replace("_", " ") - - val methodName = element.methodName - .split("$") - .firstOrNull() - .orEmpty() - - return String.format( - "%s | %s ", - elementTag, - methodName - ) - } - }) - } else { - Bugsnag.start(this) - } - trace("app onCreate end") - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/CodeApp.kt b/apps/codeApp/src/main/java/com/getcode/CodeApp.kt deleted file mode 100644 index 9d91cbcbd..000000000 --- a/apps/codeApp/src/main/java/com/getcode/CodeApp.kt +++ /dev/null @@ -1,209 +0,0 @@ -package com.getcode - -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import androidx.lifecycle.Lifecycle -import cafe.adriel.voyager.core.stack.StackEvent -import cafe.adriel.voyager.navigator.CurrentScreen -import cafe.adriel.voyager.navigator.Navigator -import cafe.adriel.voyager.transitions.ScreenTransition -import cafe.adriel.voyager.transitions.ScreenTransitionContent -import cafe.adriel.voyager.transitions.SlideTransition -import com.getcode.libs.biometrics.BiometricsError -import com.getcode.navigation.core.BottomSheetNavigator -import com.getcode.navigation.core.CombinedNavigator -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.LoginScreen -import com.getcode.navigation.transitions.SheetSlideTransition -import com.getcode.theme.LocalCodeColors -import com.getcode.ui.components.AuthCheck -import com.getcode.ui.components.bars.BottomBarContainer -import com.getcode.ui.theme.CodeScaffold -import com.getcode.ui.components.ModalContainer -import com.getcode.ui.components.OnLifecycleEvent -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.components.bars.TopBarContainer -import com.getcode.ui.modals.ConfirmationModals -import com.getcode.ui.utils.getActivity -import com.getcode.navigation.extensions.getActivityScopedViewModel -import com.getcode.ui.LocalTopBarPadding -import com.getcode.ui.biometrics.LocalBiometricsState -import com.getcode.ui.biometrics.rememberBiometricsState -import com.getcode.ui.theme.CodeTheme -import com.getcode.ui.core.measured -import com.getcode.ui.biometrics.views.BiometricsBlockingView -import dev.bmcreations.tipkit.TipScaffold -import dev.bmcreations.tipkit.engines.TipsEngine - -@Composable -fun CodeApp(tipsEngine: TipsEngine) { - val tlvm = getActivityScopedViewModel() - val state by tlvm.state.collectAsState() - val activity = LocalContext.current.getActivity() - val biometricsState = rememberBiometricsState( - requireBiometrics = state.requireBiometrics, - onError = { error -> - if (error == BiometricsError.NoBiometrics) { - tlvm.onMissingBiometrics() - } - } - ) - - OnLifecycleEvent { _, event -> - if (event == Lifecycle.Event.ON_RESUME) { - tlvm.onResume() - } - } - - CodeTheme { - val appState = rememberCodeAppState() - CompositionLocalProvider(LocalBiometricsState provides biometricsState) { - AppNavHost { - val codeNavigator = LocalCodeNavigator.current - TipScaffold(tipsEngine = tipsEngine) { - CodeScaffold( - scaffoldState = appState.scaffoldState - ) { innerPaddingModifier -> - Navigator( - screen = MainRoot, - ) { navigator -> - appState.navigator = codeNavigator - - LaunchedEffect(navigator.lastItem) { - // update global navigator for platform access to support push/pop from a single - // navigator current - codeNavigator.screensNavigator = navigator - } - - var topBarHeight by remember { - mutableStateOf(0.dp) - } - - val (isVisibleTopBar, isVisibleBackButton) = appState.isVisibleTopBar - if (isVisibleTopBar && appState.currentTitle.isNotBlank()) { - AppBarWithTitle( - modifier = Modifier.measured { topBarHeight = it.height }, - title = appState.currentTitle, - titleAlignment = Alignment.CenterHorizontally, - backButton = isVisibleBackButton, - onBackIconClicked = appState::upPress - ) - } else { - topBarHeight = 0.dp - } - - CompositionLocalProvider( - LocalTopBarPadding provides PaddingValues(top = topBarHeight), - ) { - Box( - modifier = Modifier - .padding(innerPaddingModifier) - ) { - when (navigator.lastEvent) { - StackEvent.Push, - StackEvent.Pop -> { - when (navigator.lastItem) { - is LoginScreen, is MainRoot -> CrossfadeTransition( - navigator = navigator - ) - - else -> SlideTransition(navigator = navigator) - } - } - - StackEvent.Idle, - StackEvent.Replace -> CurrentScreen() - } - } - } - - //Listen for authentication changes here - AuthCheck( - navigator = codeNavigator, - onNavigate = { screens -> - codeNavigator.replaceAll(screens) - }, - onSwitchAccounts = { seed -> - activity?.let { - tlvm.logout(it) { - appState.navigator.replaceAll(LoginScreen(seed)) - } - } - } - ) - } - } - ModalContainer(codeNavigator, appState) - } - } - } - BiometricsBlockingView(modifier = Modifier.fillMaxSize(), biometricsState) - TopBarContainer(appState.barMessages) - BottomBarContainer(appState.barMessages) - ConfirmationModals(Modifier.fillMaxSize()) - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun AppNavHost(content: @Composable () -> Unit) { - var combinedNavigator by remember { - mutableStateOf(null) - } - BottomSheetNavigator( - modifier = Modifier.fillMaxSize(), - sheetBackgroundColor = LocalCodeColors.current.background, - sheetContentColor = LocalCodeColors.current.onBackground, - sheetContent = { sheetNav -> - combinedNavigator = combinedNavigator?.apply { sheetNavigator = sheetNav } - ?: CombinedNavigator(sheetNav) - combinedNavigator?.let { - CompositionLocalProvider(LocalCodeNavigator provides it) { - SheetSlideTransition(navigator = it) - } - } - - }, - onHide = com.getcode.services.manager.ModalManager::clear - ) { sheetNav -> - combinedNavigator = - combinedNavigator?.apply { sheetNavigator = sheetNav } ?: CombinedNavigator(sheetNav) - combinedNavigator?.let { - CompositionLocalProvider(LocalCodeNavigator provides it) { - content() - } - } - } -} - -@Composable -private fun CrossfadeTransition( - navigator: Navigator, - modifier: Modifier = Modifier, - content: ScreenTransitionContent = { it.Content() } -) { - ScreenTransition( - navigator = navigator, - modifier = modifier, - content = content, - transition = { fadeIn() togetherWith fadeOut() } - ) -} diff --git a/apps/codeApp/src/main/java/com/getcode/CodeAppState.kt b/apps/codeApp/src/main/java/com/getcode/CodeAppState.kt deleted file mode 100644 index 0218d83e3..000000000 --- a/apps/codeApp/src/main/java/com/getcode/CodeAppState.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.getcode - -import androidx.compose.material.ScaffoldState -import androidx.compose.material.rememberScaffoldState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import com.getcode.services.manager.ModalManager -import com.getcode.navigation.core.CodeNavigator -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.AccessKeyLoginScreen -import com.getcode.navigation.screens.LoginPhoneVerificationScreen -import com.getcode.navigation.screens.LoginScreen -import com.getcode.navigation.screens.NamedScreen -import com.getcode.ui.components.bars.BarManager -import com.getcode.ui.components.bars.BarMessages -import com.getcode.ui.components.bars.rememberBarManager -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch - -/** - * Remembers and creates an instance of [CodeAppState] - */ -@Composable -fun rememberCodeAppState( - scaffoldState: ScaffoldState = rememberScaffoldState(), - navigator: CodeNavigator = LocalCodeNavigator.current, - barManager: BarManager = rememberBarManager(), - coroutineScope: CoroutineScope = rememberCoroutineScope() -) = - remember(scaffoldState, navigator , barManager, coroutineScope) { - CodeAppState(scaffoldState, navigator, barManager, coroutineScope) - } - -/** - * Responsible for holding state related to [CodeApp] and containing UI-related logic. - */ -@Stable -class CodeAppState( - val scaffoldState: ScaffoldState, - var navigator: CodeNavigator, - private val barManager: BarManager, - coroutineScope: CoroutineScope -) { - init { - coroutineScope.launch { - com.getcode.services.manager.ModalManager.messages.collect { currentMessages -> - modalMessage.value = currentMessages.firstOrNull() - } - } - } - // ---------------------------------------------------------- - // Navigation state source of truth - // ---------------------------------------------------------- - - val currentTitle: String - @Composable get() { - val lastItem = navigator.lastItem - return (lastItem as? NamedScreen)?.name.orEmpty() - } - - @Composable - fun getScreen() = navigator.lastItem - - val isVisibleTopBar: Pair - @Composable get() { - val screen = getScreen() - val isModalVisible = navigator.isVisible - val loginScreen = screen as? LoginScreen - val isLoginScreen = loginScreen != null - val isSeedInput = screen is AccessKeyLoginScreen - val isPhoneEntry = screen is LoginPhoneVerificationScreen - if (isModalVisible) { - return false to false - } - return Pair( - !isLoginScreen, - isSeedInput || isPhoneEntry - ) - } - - val barMessages: BarMessages - get() = barManager.barMessages - - val modalMessage = MutableStateFlow(null) - - fun upPress() { - if (navigator.pop().not()) { - navigator.hide() - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/Locals.kt b/apps/codeApp/src/main/java/com/getcode/Locals.kt deleted file mode 100644 index cd17a7088..000000000 --- a/apps/codeApp/src/main/java/com/getcode/Locals.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.getcode - -import androidx.compose.runtime.ProvidableCompositionLocal -import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.graphics.painter.BitmapPainter -import com.getcode.network.repository.BetaOptions -import com.getcode.ui.biometrics.BiometricsState -import com.getcode.util.DeeplinkHandler -import com.getcode.util.PhoneUtils - -val LocalSession: ProvidableCompositionLocal = staticCompositionLocalOf { null } -val LocalPhoneFormatter: ProvidableCompositionLocal = staticCompositionLocalOf { null } -val LocalDeeplinks: ProvidableCompositionLocal = staticCompositionLocalOf { null } -val LocalBetaFlags: ProvidableCompositionLocal = staticCompositionLocalOf { BetaOptions.Defaults } -val LocalDownloadQrCode: ProvidableCompositionLocal = staticCompositionLocalOf { null } diff --git a/apps/codeApp/src/main/java/com/getcode/MainRoot.kt b/apps/codeApp/src/main/java/com/getcode/MainRoot.kt deleted file mode 100644 index 553d8b5e3..000000000 --- a/apps/codeApp/src/main/java/com/getcode/MainRoot.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.getcode - -import android.os.Parcelable -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import com.getcode.navigation.screens.ScanScreen -import com.getcode.theme.CodeTheme -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -internal data object MainRoot : Screen, Parcelable { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - private fun readResolve(): Any = this - - @Composable - override fun Content() { - Box( - modifier = Modifier - .fillMaxSize() - .background(CodeTheme.colors.background) - ) - } -} - -typealias AppHomeScreen = ScanScreen \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/SessionController.kt b/apps/codeApp/src/main/java/com/getcode/SessionController.kt deleted file mode 100644 index 4338e2552..000000000 --- a/apps/codeApp/src/main/java/com/getcode/SessionController.kt +++ /dev/null @@ -1,1901 +0,0 @@ -package com.getcode - -import android.Manifest -import android.annotation.SuppressLint -import android.app.Activity -import android.app.NotificationManager -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.view.WindowManager -import androidx.core.app.NotificationManagerCompat -import androidx.core.net.toUri -import com.getcode.analytics.CodeAnalyticsManager -import com.getcode.analytics.CodeAnalyticsService -import com.getcode.manager.CashLinkManager -import com.getcode.manager.AuthManager -import com.getcode.manager.GiftCardManager -import com.getcode.manager.SessionManager -import com.getcode.model.BuyModuleFeature -import com.getcode.model.CameraGesturesFeature -import com.getcode.model.Currency -import com.getcode.model.Feature -import com.getcode.model.FlippableTipCardFeature -import com.getcode.model.GalleryFeature -import com.getcode.model.IntentMetadata -import com.getcode.model.InvertedDragZoomFeature -import com.getcode.model.Kin -import com.getcode.model.KinAmount -import com.getcode.services.model.PrefsBool -import com.getcode.model.RequestKinFeature -import com.getcode.model.SocialUser -import com.getcode.model.TwitterUser -import com.getcode.model.notifications.NotificationType -import com.getcode.models.Bill -import com.getcode.models.BillState -import com.getcode.models.BillToast -import com.getcode.models.ConfirmationState -import com.getcode.models.DeepLinkRequest -import com.getcode.models.LoginConfirmation -import com.getcode.models.PrivatePaymentConfirmation -import com.getcode.models.PaymentValuation -import com.getcode.models.SocialUserPaymentConfirmation -import com.getcode.models.amountFloored -import com.getcode.network.BalanceController -import com.getcode.network.NotificationCollectionHistoryController -import com.getcode.network.TipController -import com.getcode.network.client.Client -import com.getcode.network.client.RemoteSendException -import com.getcode.network.client.awaitEstablishRelationship -import com.getcode.network.client.cancelRemoteSend -import com.getcode.network.client.fetchLimits -import com.getcode.network.client.loginToThirdParty -import com.getcode.network.client.receiveFromPrimaryIfWithinLimits -import com.getcode.network.client.receiveIfNeeded -import com.getcode.network.client.receiveRemoteSuspend -import com.getcode.network.client.rejectLogin -import com.getcode.network.client.requestFirstKinAirdrop -import com.getcode.network.client.sendRemotely -import com.getcode.network.client.sendRequestToReceiveBill -import com.getcode.network.exchange.Exchange -import com.getcode.network.repository.AppSettingsRepository -import com.getcode.network.repository.BetaFlagsRepository -import com.getcode.network.repository.BetaOptions -import com.getcode.network.repository.FeatureRepository -import com.getcode.network.repository.PaymentRepository -import com.getcode.network.repository.PrefRepository -import com.getcode.network.repository.ReceiveTransactionRepository -import com.getcode.network.repository.StatusRepository -import com.getcode.util.IntentUtils -import com.getcode.utils.Kin -import com.getcode.extensions.formatted -import com.getcode.manager.BottomBarManager -import com.getcode.manager.TopBarManager -import com.getcode.services.model.CodePayload -import com.getcode.model.Domain -import com.getcode.services.model.Kind -import com.getcode.services.model.payload.Username -import com.getcode.model.toPublicKey -import com.getcode.services.utils.catchSafely -import com.getcode.services.utils.nonce -import com.getcode.solana.organizer.GiftCardAccount -import com.getcode.solana.organizer.Organizer -import com.getcode.ui.core.RestrictionType -import com.getcode.util.permissions.PermissionChecker -import com.getcode.util.permissions.PermissionResult -import com.getcode.util.resources.ResourceHelper -import com.getcode.util.showNetworkError -import com.getcode.util.vibration.Vibrator -import com.getcode.utils.ErrorUtils -import com.getcode.utils.TraceType -import com.getcode.utils.hexEncodedString -import com.getcode.utils.trace -import com.getcode.view.main.scanner.UiElement -import com.kik.kikx.kikcodes.implementation.KikCodeAnalyzer -import com.kik.kikx.models.ScannableKikCode -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.distinctUntilChangedBy -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeout -import org.kin.sdk.base.tools.Base58 -import timber.log.Timber -import java.util.Timer -import java.util.TimerTask -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.concurrent.schedule -import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds - -sealed interface PresentationStyle { - data object Hidden : PresentationStyle - sealed interface Visible - - data object Pop : PresentationStyle, Visible - data object Slide : PresentationStyle, Visible -} - -data class SessionState( - val isCameraPermissionGranted: Boolean? = null, - val vibrateOnScan: Boolean = false, - val balance: KinAmount? = null, - val logScanTimes: Boolean = false, - val showNetworkOffline: Boolean = false, - val autoStartCamera: Boolean? = null, - val isCameraScanEnabled: Boolean = true, - val presentationStyle: PresentationStyle = PresentationStyle.Hidden, - val billState: BillState = BillState.Default, - val restrictionType: RestrictionType? = null, - val isRemoteSendLoading: Boolean = false, - val splatTipCard: Boolean = false, - val notificationUnreadCount: Int = 0, - val chatUnreadCount: Int = 0, - val buyModule: Feature = BuyModuleFeature(), - val requestKin: Feature = RequestKinFeature(), - val cameraGestures: Feature = CameraGesturesFeature(), - val invertedDragZoom: Feature = InvertedDragZoomFeature(), - val gallery: Feature = GalleryFeature(), - val flippableTipCard: Feature = FlippableTipCardFeature(), - val scannerElements: List = listOf( - UiElement.GIVE_KIN, - UiElement.TIP_CARD, - UiElement.BALANCE - ), - val tipCardConnected: Boolean = false, -) - -sealed interface SessionEvent { - data object PresentTipEntry : SessionEvent - data object RequestNotificationPermissions : SessionEvent - data class SendIntent(val intent: Intent) : SessionEvent - data class OnChatPaidForSuccessfully(val intentId: com.getcode.model.ID, val user: SocialUser): SessionEvent -} - -@SuppressLint("CheckResult") -@Singleton -class SessionController @Inject constructor( - private val client: Client, - private val receiveTransactionRepository: ReceiveTransactionRepository, - private val paymentRepository: PaymentRepository, - private val balanceController: BalanceController, - private val historyController: NotificationCollectionHistoryController, - private val tipController: TipController, - private val prefRepository: PrefRepository, - private val analytics: CodeAnalyticsService, - private val authManager: AuthManager, - private val networkObserver: com.getcode.utils.network.NetworkConnectivityListener, - private val resources: ResourceHelper, - private val vibrator: Vibrator, - private val currencyUtils: com.getcode.utils.CurrencyUtils, - private val exchange: Exchange, - private val giftCardManager: GiftCardManager, - private val mnemonicManager: com.getcode.services.manager.MnemonicManager, - private val cashLinkManager: CashLinkManager, - private val permissionChecker: PermissionChecker, - private val notificationManager: NotificationManagerCompat, - private val codeAnalyzer: KikCodeAnalyzer, - private val sessionManager: SessionManager, - appSettings: AppSettingsRepository, - betaFlagsRepository: BetaFlagsRepository, - features: FeatureRepository, -) { - val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - val state = MutableStateFlow(SessionState()) - - private val _eventFlow: MutableSharedFlow = MutableSharedFlow() - val eventFlow: SharedFlow = _eventFlow.asSharedFlow() - - private var sheetDismissTimer: TimerTask? = null - - init { - onDrawn() - - appSettings.observe() - .map { it.cameraStartByDefault } - .distinctUntilChanged() - .onEach { cameraAutoStart -> - state.update { - it.copy(autoStartCamera = cameraAutoStart) - } - }.launchIn(scope) - - features.buyModule - .distinctUntilChanged() - .onEach { module -> - state.update { - it.copy(buyModule = module) - } - }.launchIn(scope) - - features.requestKin - .distinctUntilChanged() - .onEach { module -> - state.update { - it.copy(requestKin = module) - } - }.launchIn(scope) - - features.cameraGestures - .distinctUntilChanged() - .onEach { module -> - state.update { - it.copy(cameraGestures = module) - } - }.launchIn(scope) - - features.invertedDragZoom - .distinctUntilChanged() - .onEach { module -> - state.update { - it.copy(invertedDragZoom = module) - } - }.launchIn(scope) - - features.galleryEnabled - .distinctUntilChanged() - .onEach { module -> - state.update { - it.copy(gallery = module) - } - }.launchIn(scope) - - features.tipCardFlippable - .distinctUntilChanged() - .onEach { module -> - state.update { - it.copy(flippableTipCard = module) - } - }.launchIn(scope) - - betaFlagsRepository.observe() - .distinctUntilChanged() - .onEach { betaFlags -> - state.update { it.copy(scannerElements = buildScannerElements(betaFlags)) } - }.launchIn(scope) - - tipController.showTwitterSplat - .onEach { splat -> - scope.launch { - if (splat) { - delay(300) - } else { - delay(500) - } - state.update { - it.copy(splatTipCard = splat) - } - } - } - .filter { it } - .onEach { delay(500) } - .flatMapLatest { tipController.connectedAccount } - .filter { tipController.verificationInProgress.value } - .filterNotNull() - .distinctUntilChanged() - .filter { state.value.isCameraScanEnabled } - .onEach { - when (it) { - is TwitterUser -> { - analytics.tipCardLinked() - TopBarManager.showMessage( - topBarMessage = TopBarManager.TopBarMessage( - type = TopBarManager.TopBarMessageType.SUCCESS, - title = resources.getString(R.string.success_title_xConnected), - message = resources.getString(R.string.success_description_xConnected), - primaryText = resources.getString(R.string.action_showMyTipCard), - primaryAction = ::presentShareableTipCard, - secondaryText = resources.getString(R.string.action_later), - secondaryAction = { - tipController.onSeenTipCardBanner() - } - ) - ) - } - } - }.launchIn(scope) - - tipController.connectedAccount - .onEach { account -> - state.update { - it.copy( - tipCardConnected = account != null - ) - } - }.launchIn(scope) - - StatusRepository().getIsUpgradeRequired(BuildConfig.VERSION_CODE) - .subscribeOn(Schedulers.computation()) - .timeout(15_000L, TimeUnit.MILLISECONDS) - .onErrorComplete { false } - .subscribe { isUpgradeRequired -> - state.update { m -> m.copy(restrictionType = if (isUpgradeRequired) RestrictionType.FORCE_UPGRADE else null) } - } - - SessionManager.authState - .filter { it.userPrefsUpdated } - .flatMapLatest { - prefRepository.observeOrDefault( - PrefsBool.IS_ELIGIBLE_GET_FIRST_KIN_AIRDROP, - false - ) - } - .map { it } - .distinctUntilChanged() - .filter { it } - .mapNotNull { SessionManager.getKeyPair() } - .catchSafely( - action = { owner -> - val amount = client.requestFirstKinAirdrop(owner).getOrThrow() - prefRepository.set(PrefsBool.IS_ELIGIBLE_GET_FIRST_KIN_AIRDROP, false) - balanceController.fetchBalanceSuspend() - - val organizer = SessionManager.getOrganizer() - val receiveWithinLimits = organizer?.let { - client.receiveFromPrimaryIfWithinLimits(it) - } ?: Completable.complete() - receiveWithinLimits.subscribe({}, {}) - - showToast(amount = amount, isDeposit = true, initialDelay = 1.seconds) - - historyController.fetch() - }, - onFailure = { - ErrorUtils.handleError(it) - prefRepository.set(PrefsBool.IS_ELIGIBLE_GET_FIRST_KIN_AIRDROP, false) - } - ) - .launchIn(scope) - - combine( - exchange.observeLocalRate(), - balanceController.observeRawBalance(), - ) { rate, balance -> - if (balance == -1.0) { - null - } else { - KinAmount.newInstance(Kin.fromKin(balance), rate) - } - }.filterNotNull().onEach { balanceInKin -> - state.update { - it.copy(balance = balanceInKin) - } - }.launchIn(scope) - - historyController.unreadCount - .distinctUntilChanged() - .map { it } - .onEach { count -> - state.update { it.copy(notificationUnreadCount = count) } - }.launchIn(scope) - - prefRepository.observeOrDefault(PrefsBool.LOG_SCAN_TIMES, false) - .flowOn(Dispatchers.IO) - .onEach { log -> - withContext(Dispatchers.Main) { - state.update { - it.copy(logScanTimes = log) - } - } - }.launchIn(scope) - - prefRepository.observeOrDefault(PrefsBool.VIBRATE_ON_SCAN, false) - .flowOn(Dispatchers.IO) - .onEach { enabled -> - withContext(Dispatchers.Main) { - state.update { - it.copy(vibrateOnScan = enabled) - } - } - }.launchIn(scope) - - prefRepository.observeOrDefault(PrefsBool.SHOW_CONNECTIVITY_STATUS, false) - .flowOn(Dispatchers.IO) - .onEach { enabled -> - withContext(Dispatchers.Main) { - state.update { - it.copy(showNetworkOffline = enabled) - } - } - }.launchIn(scope) - - SessionManager.authState - .distinctUntilChangedBy { it.isTimelockUnlocked } - .onEach { - it.let { state -> - if (state.isTimelockUnlocked) { - this@SessionController.state.update { m -> m.copy(restrictionType = RestrictionType.TIMELOCK_UNLOCKED) } - } - } - }.launchIn(scope) - } - - private fun buildScannerElements( - betaOptions: BetaOptions, - ): List { - val actions = mutableListOf(UiElement.GIVE_KIN) - actions += if (betaOptions.tipCardOnHomeScreen) { - UiElement.TIP_CARD - } else { - UiElement.GET_KIN - } - - actions += UiElement.BALANCE - - return actions - } - - fun onCameraScanning(scanning: Boolean) { - state.update { it.copy(isCameraScanEnabled = scanning) } - } - - fun onCameraPermissionResult(result: PermissionResult) { - state.update { it.copy(isCameraPermissionGranted = result == PermissionResult.Granted) } - } - - fun showBill( - bill: Bill, - vibrate: Boolean = false - ) { - val amountFloor = bill.amountFloored - if (amountFloor.fiat == 0.0 || bill.amount.kin.toKinTruncatingLong() == 0L) return - val owner = SessionManager.getKeyPair() ?: return - - if (!networkObserver.isConnected) { - return ErrorUtils.showNetworkError(resources) - } - - val organizer = SessionManager.getOrganizer() ?: return - - // Don't show the remote send and cancel buttons for first kin - when (bill) { - is Bill.Cash -> { - if (bill.kind == Bill.Kind.firstKin) { - state.update { - it.copy( - billState = it.billState.copy( - primaryAction = null, - secondaryAction = null, - ) - ) - } - } else { - state.update { - it.copy( - billState = it.billState.copy( - primaryAction = BillState.Action.Send { onRemoteSend() }, - secondaryAction = BillState.Action.Cancel(::cancelSend) - ) - ) - } - } - } - - else -> Unit - } - - cashLinkManager.awaitBillGrab( - amount = amountFloor, - organizer = organizer, - owner = owner, - onGrabbed = { - cancelSend(PresentationStyle.Pop) - vibrator.vibrate() - - scope.launch { - client.fetchLimits(true).subscribe({}, ErrorUtils::handleError) - } - }, - onTimeout = { - cancelSend(style = PresentationStyle.Slide) - analytics.billTimeoutReached( - bill.amount.kin, - bill.amount.rate.currency, - CodeAnalyticsManager.BillPresentationStyle.Slide - ) - }, - onError = { cancelSend(style = PresentationStyle.Slide) }, - present = { data -> - if (!bill.didReceive) { - trace( - tag = "Bill", - message = "Pull out cash", - metadata = { - "amount" to bill.amount - }, - type = TraceType.User, - ) - } - presentSend(data, bill, vibrate) - } - ) - } - - private fun presentSend(data: List, bill: Bill, isVibrate: Boolean = false) { - if (bill.didReceive) { - state.update { - val billState = it.billState - it.copy( - billState = billState.copy( - valuation = PaymentValuation( - bill.amount - ), - ) - ) - } - } - - val style: PresentationStyle = - if (bill.didReceive) PresentationStyle.Pop else PresentationStyle.Slide - - state.update { - val billState = it.billState - it.copy( - presentationStyle = style, - billState = billState.copy( - bill = Bill.Cash( - data = data, - amount = bill.amount, - didReceive = bill.didReceive - ), - valuation = PaymentValuation(bill.amount), - showToast = bill.didReceive - ) - ) - } - - if (style is PresentationStyle.Visible) { - analytics.billShown( - bill.amountFloored.kin, - bill.amountFloored.rate.currency, - when (style) { - PresentationStyle.Pop -> CodeAnalyticsManager.BillPresentationStyle.Pop - PresentationStyle.Slide -> CodeAnalyticsManager.BillPresentationStyle.Slide - } - ) - } - - if (isVibrate) { - vibrator.vibrate() - } - } - - fun cancelSend(style: PresentationStyle = PresentationStyle.Slide) { - cashLinkManager.cancelSend() - BottomBarManager.clearByType(BottomBarManager.BottomBarMessageType.INFO) - - scope.launch { - val shown = showToastIfNeeded(style) - withContext(Dispatchers.Main) { - state.update { - it.copy( - presentationStyle = style, - ) - } - - state.update { - it.copy( - billState = it.billState.copy( - bill = null, - valuation = null, - primaryAction = null, - secondaryAction = null, - ) - ) - } - } - - historyController.fetch() - balanceController.fetchBalanceSuspend() - - if (shown) { - delay(5.seconds.inWholeMilliseconds) - } - withContext(Dispatchers.Main) { - state.update { - it.copy( - billState = it.billState.copy(showToast = false) - ) - } - } - } - } - - private fun showToastIfNeeded( - style: PresentationStyle, - ): Boolean { - val billState = state.value.billState - val bill = billState.bill ?: return false - - if (style is PresentationStyle.Pop || billState.showToast) { - showToast( - amount = bill.metadata.kinAmount, - isDeposit = when (style) { - PresentationStyle.Slide -> true - PresentationStyle.Pop -> false - else -> false - }, - ) - - return true - } - - return false - } - - private fun showToast( - amount: KinAmount, - isDeposit: Boolean = false, - initialDelay: Duration = 500.milliseconds - ) { - scope.launch { - delay(initialDelay) - if (amount.kin.toKinTruncatingLong() == 0L) { - state.update { uiModel -> - val billState = uiModel.billState - uiModel.copy( - billState = billState.copy( - toast = null - ) - ) - } - return@launch - } - - state.update { - it.copy( - billState = it.billState.copy( - showToast = true, - toast = BillToast(amount = amount, isDeposit = isDeposit) - ) - ) - } - - delay(5.seconds) - - state.update { uiModel -> - val billState = uiModel.billState - uiModel.copy( - billState = billState.copy( - showToast = false - ) - ) - } - - // wait for animation to run - delay(500.milliseconds) - state.update { uiModel -> - val billState = uiModel.billState - uiModel.copy( - billState = billState.copy( - toast = null - ) - ) - } - } - } - - private fun onCodeScan(payload: ByteArray) { - if (DEBUG_SCAN_TIMES) { - Timber.tag("codescan").d("start") - scanProcessingTime = System.currentTimeMillis() - } - - if (state.value.vibrateOnScan) { - vibrator.tick() - } - - val organizer = SessionManager.getOrganizer() ?: return - - val codePayload = CodePayload.fromList(payload.toList()) - - if (scannedRendezvous.contains(codePayload.rendezvous.publicKey)) { - Timber.d("Nonce previously received: ${codePayload.nonce.hexEncodedString()}") - return - } - - scannedRendezvous.add(codePayload.rendezvous.publicKey) - - if (!networkObserver.isConnected) { - scannedRendezvous.remove(codePayload.rendezvous.publicKey) - return ErrorUtils.showNetworkError(resources) - } - - when (codePayload.kind) { - Kind.Cash, - Kind.GiftCard -> { - trace( - tag = "Bill", - message = "Scanned cash", - type = TraceType.User, - ) - attemptReceive(organizer, codePayload) - } - - Kind.RequestPayment, - Kind.RequestPaymentV2 -> { - trace( - tag = "Bill", - message = "Scanned request card", - type = TraceType.User, - ) - attemptPayment(codePayload) - } - - Kind.Login -> { - trace( - tag = "Bill", - message = "Scanned login card", - type = TraceType.User, - ) - attemptLogin(codePayload) - } - - Kind.Tip -> { - trace( - tag = "Bill", - message = "Scanned tip card", - type = TraceType.User, - ) - attemptTip(codePayload) - } - } - } - - private fun attemptPayment(payload: CodePayload, request: DeepLinkRequest? = null) = - scope.launch { - val (amount, p) = paymentRepository.attemptRequest(payload) ?: return@launch - BottomBarManager.clear() - - presentRequest(amount = amount, payload = p, request = request) - - // Ensure that we preemptively pull funds into the - // correct account before we attempt to pay a request - client.receiveIfNeeded().subscribe({}, ErrorUtils::handleError) - } - - private fun attemptTip(codePayload: CodePayload, request: DeepLinkRequest? = null) = - scope.launch { - BottomBarManager.clear() - val username = codePayload.username ?: request?.tipRequest?.username ?: return@launch - presentTipCard(payload = codePayload, username = username) - - // Ensure that we preemptively pull funds into the - // correct account before we attempt to tip - client.receiveIfNeeded().subscribe({}, ErrorUtils::handleError) - } - - fun presentShareableTipCard() = scope.launch { - val username = tipController.connectedAccount.value?.username ?: return@launch - val code = CodePayload( - kind = Kind.Tip, - value = Username(username) - ) - - val hasSeenTipCard = tipController.hasSeenTipCard() - tipController.clearTwitterSplat() - - trace( - tag = "Bill", - message = "Show my tip card", - type = TraceType.User, - ) - - withContext(Dispatchers.Main) { - state.update { - val billState = it.billState.copy( - bill = Bill.Tip(code, canFlip = state.value.flippableTipCard.enabled), - primaryAction = BillState.Action.Share { onRemoteSend() }, - secondaryAction = BillState.Action.Cancel(::cancelSend) - ) - - it.copy( - presentationStyle = PresentationStyle.Slide, - billState = billState, - ) - } - } - - if (!hasSeenTipCard) { - showNotificationPermissionHintIfNeeded() - } - } - - private suspend fun showNotificationPermissionHintIfNeeded() { - val isDenied = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - permissionChecker.isDenied(Manifest.permission.POST_NOTIFICATIONS) - } else { - false - } - - val channel = notificationManager.getNotificationChannel(NotificationType.ChatMessage.name) - val isChannelOff = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - channel?.importance == NotificationManager.IMPORTANCE_NONE - } else { - false - } - - val show = isDenied || isChannelOff - - if (show) { - delay(400) - com.getcode.services.manager.ModalManager.showMessage( - com.getcode.services.manager.ModalManager.Message( - icon = R.drawable.ic_bell, - title = resources.getString(R.string.title_turnOnNotifications), - subtitle = resources.getString(R.string.subtitle_turnOnNotifications), - onPositive = { - when { - isDenied -> { - scope.launch { - _eventFlow.emit(SessionEvent.RequestNotificationPermissions) - } - } - - else -> { - @SuppressLint("NewApi") - channel?.importance = NotificationManager.IMPORTANCE_DEFAULT - } - } - - }, - positiveText = resources.getString(R.string.action_allowPushNotifications) - ) - ) - } - } - - private suspend fun presentTipCard( - payload: CodePayload, - username: String, - ) { - vibrator.vibrate() - - withContext(Dispatchers.Main) { - state.update { - val billState = it.billState.copy( - bill = Bill.Tip(payload), - primaryAction = null, - secondaryAction = null, - ) - - it.copy( - presentationStyle = PresentationStyle.Pop, - billState = billState, - ) - } - } - - // Tip codes are always the same, we need to - // ensure that we can scan the same code again - scannedRendezvous.remove(payload.rendezvous.publicKey) - - runCatching { tipController.fetch(username, payload) } - .onFailure { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = resources.getString(R.string.error_title_tipCardNotActivated), - message = resources.getString(R.string.error_description_tipCardNotActivated), - primaryText = resources.getString(R.string.action_tweetThem), - primaryAction = { - val intent = IntentUtils.tweet( - resources.getString( - R.string.subtitle_linkingTwitterPrompt, username - ) - ) - scope.launch { - _eventFlow.emit(SessionEvent.SendIntent(intent)) - } - cancelTip() - }, - secondaryText = resources.getString(R.string.action_notNow), - secondaryAction = ::cancelTip - ) - ) - }.onSuccess { - delay(300.milliseconds) - _eventFlow.emit(SessionEvent.PresentTipEntry) - analytics.tipCardShown(username) - } - } - - fun presentTipConfirmation(amount: KinAmount) { - val scannedUserData = tipController.scannedUserData?.second - val payload = scannedUserData ?: return - val metadata = tipController.userMetadata ?: return - - state.update { - val billState = it.billState.copy( - socialUserPaymentConfirmation = SocialUserPaymentConfirmation( - state = ConfirmationState.AwaitingConfirmation, - payload = payload, - amount = amount, - metadata = metadata, - ) - ) - - it.copy(billState = billState) - } - } - - fun presentPrivatePaymentConfirmation(socialUser: SocialUser, amount: KinAmount) { - val payload = CodePayload( - kind = Kind.Tip, - value = Username(socialUser.username), - ) - - - state.update { - val billState = it.billState.copy( - socialUserPaymentConfirmation = SocialUserPaymentConfirmation( - state = ConfirmationState.AwaitingConfirmation, - payload = payload, - amount = amount, - metadata = socialUser, - isPrivate = true, - showScrim = true - ) - ) - - it.copy(billState = billState) - } - } - - fun completeTipPayment() = scope.launch { - val tipConfirmation = state.value.billState.socialUserPaymentConfirmation ?: return@launch - val metadata = tipController.userMetadata ?: return@launch - - val amount = tipConfirmation.amount - - fun showError() { - TopBarManager.showMessage( - resources.getString(R.string.error_title_payment_failed), - resources.getString(R.string.error_description_payment_failed), - ) - - state.update { uiModel -> - uiModel.copy( - presentationStyle = PresentationStyle.Hidden, - billState = uiModel.billState.copy( - bill = null, - showToast = false, - socialUserPaymentConfirmation = null, - toast = null, - valuation = null, - primaryAction = null, - secondaryAction = null, - ) - ) - } - } - - if (state.value.billState.socialUserPaymentConfirmation == null) { - showError() - return@launch - } - - state.update { - val billState = it.billState - it.copy( - billState = billState.copy( - socialUserPaymentConfirmation = tipConfirmation.copy(state = ConfirmationState.Sending) - ), - ) - } - - runCatching { - paymentRepository.completeTipPayment(tipConfirmation.metadata, tipConfirmation.amount) - }.onSuccess { - historyController.fetch() - state.update { - val billState = it.billState - val confirmation = it.billState.socialUserPaymentConfirmation ?: return@update it - - it.copy( - billState = billState.copy( - socialUserPaymentConfirmation = confirmation.copy(state = ConfirmationState.Sent), - ), - ) - } - delay(400.milliseconds) - cancelTip() - showToast(tipConfirmation.amount, isDeposit = false) - }.onFailure { - showError() - } - } - - fun completePrivatePayment() = scope.launch { - val confirmation = state.value.billState.socialUserPaymentConfirmation ?: return@launch - val user = confirmation.metadata - val amount = confirmation.amount - - state.update { - val billState = it.billState - it.copy( - billState = billState.copy( - socialUserPaymentConfirmation = billState.socialUserPaymentConfirmation?.copy(state = ConfirmationState.Sending) - ), - ) - } - - runCatching { - paymentRepository.payForFriendship(user, amount) - }.onSuccess { - historyController.fetch() - - state.update { s -> - val billState = s.billState - val socialUserPaymentConfirmation = s.billState.socialUserPaymentConfirmation ?: return@update s - - s.copy( - billState = billState.copy( - socialUserPaymentConfirmation = socialUserPaymentConfirmation.copy(state = ConfirmationState.Sent), - ), - ) - } - delay(1.seconds) - cancelTip() - delay(400.milliseconds) - showToast(amount, isDeposit = false) - _eventFlow.emit(SessionEvent.OnChatPaidForSuccessfully(it, user)) - }.onFailure { - TopBarManager.showMessage( - resources.getString(R.string.error_title_payment_failed), - resources.getString(R.string.error_description_payment_failed), - ) - - state.update { uiModel -> - uiModel.copy( - presentationStyle = PresentationStyle.Hidden, - billState = uiModel.billState.copy( - bill = null, - showToast = false, - socialUserPaymentConfirmation = null, - toast = null, - valuation = null, - primaryAction = null, - secondaryAction = null, - ) - ) - } - } - } - - fun cancelTipEntry() { - // Cancelling from amount entry is triggered by a UI event. - // To distinguish between a valid "Next" action that will - // also dismiss the entry screen, we need to check explicitly - if (state.value.billState.socialUserPaymentConfirmation == null) { - cancelTip() - } - } - - fun cancelTip() { - tipController.reset() - state.update { - val billState = it.billState.copy( - bill = null, - socialUserPaymentConfirmation = null, - valuation = null, - primaryAction = null, - secondaryAction = null, - ) - - it.copy( - presentationStyle = PresentationStyle.Slide, - billState = billState - ) - } - } - - fun presentRequest( - amount: KinAmount, - payload: CodePayload?, - request: DeepLinkRequest? = null - ) = scope.launch { - val code: CodePayload - if (payload != null) { - code = payload - } else { - val fiat = com.getcode.model.Fiat(currency = amount.rate.currency, amount = amount.fiat) - - code = CodePayload( - kind = Kind.RequestPayment, - value = fiat, - nonce = nonce - ) - - val organizer = SessionManager.getOrganizer() ?: return@launch - client.sendRequestToReceiveBill( - destination = organizer.primaryVault, - fiat = fiat, - rendezvous = code.rendezvous - ) - } - - val isReceived = payload != null - val presentationStyle = if (isReceived) PresentationStyle.Pop else PresentationStyle.Slide - state.update { - var billState = it.billState.copy( - bill = Bill.Payment(amount, code, request), - valuation = PaymentValuation(amount), - primaryAction = null, - ) - - if (isReceived) { - billState = billState.copy( - privatePaymentConfirmation = PrivatePaymentConfirmation( - state = ConfirmationState.AwaitingConfirmation, - payload = code, - requestedAmount = amount, - localAmount = amount.replacing(exchange.localRate) - ), - ) - } - - it.copy( - presentationStyle = presentationStyle, - billState = billState, - ) - } - - analytics.requestShown(amount = amount) - - if (DEBUG_SCAN_TIMES) { - if (scanProcessingTime > 0) { - Timber.tag("codescan") - .d("scan processing took ${System.currentTimeMillis() - scanProcessingTime}") - scanProcessingTime = 0 - } - } - - - // vibrate with every payment request presentation (regardless of debug setting) - vibrator.vibrate() - } - - fun completePayment() = scope.launch { - // keep bill active while sending - cashLinkManager.cancelBillTimeout() - - val paymentConfirmation = state.value.billState.privatePaymentConfirmation ?: return@launch - state.update { - val billState = it.billState - it.copy( - billState = billState.copy( - privatePaymentConfirmation = paymentConfirmation.copy(state = ConfirmationState.Sending) - ), - ) - } - runCatching { - paymentRepository.completePayment( - paymentConfirmation.requestedAmount, - paymentConfirmation.payload.rendezvous - ) - }.onSuccess { - historyController.fetch() - - state.update { - val billState = it.billState - val confirmation = it.billState.privatePaymentConfirmation ?: return@update it - - it.copy( - billState = billState.copy( - privatePaymentConfirmation = confirmation.copy(state = ConfirmationState.Sent), - ), - ) - } - - delay(400.milliseconds) - cancelPayment(false) - }.onFailure { error -> - TopBarManager.showMessage( - resources.getString(R.string.error_title_payment_failed), - resources.getString(R.string.error_description_payment_failed), - ) - - // Allow the payment request to be scanned again upon a failure state hit - scannedRendezvous.remove(paymentConfirmation.payload.rendezvous.publicKey) - - ErrorUtils.handleError(error) - state.update { uiModel -> - uiModel.copy( - presentationStyle = PresentationStyle.Hidden, - billState = uiModel.billState.copy( - bill = null, - showToast = false, - privatePaymentConfirmation = null, - toast = null, - valuation = null, - primaryAction = null, - secondaryAction = null, - ) - ) - } - } - } - - private fun cancelPayment(rejected: Boolean, ignoreRedirect: Boolean = false) { - val paymentRendezous = state.value.billState.privatePaymentConfirmation - val bill = state.value.billState.bill ?: return - val amount = bill.amount - val request = bill.metadata.request - - // only remove the scanned nonce if rejected; completed payments are one time events - if (rejected) { - paymentRendezous?.let { - scannedRendezvous.remove(it.payload.rendezvous.publicKey) - } - } - - analytics.requestHidden(amount = amount) - - state.update { - it.copy( - presentationStyle = PresentationStyle.Slide, - billState = it.billState.copy( - bill = null, - privatePaymentConfirmation = null, - valuation = null, - primaryAction = null, - secondaryAction = null, - ) - ) - } - - scope.launch { - delay(300) - if (rejected) { - if (!ignoreRedirect) { - request?.cancelUrl?.let { url -> - val intent = Intent( - Intent.ACTION_VIEW, - url.toUri() - ).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - _eventFlow.emit(SessionEvent.SendIntent(intent)) - } - } - } else { - showToast(amount, isDeposit = false) - - if (!ignoreRedirect) { - request?.successUrl?.let { url -> - val intent = Intent( - Intent.ACTION_VIEW, - url.toUri() - ).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - _eventFlow.emit(SessionEvent.SendIntent(intent)) - } - } - } - } - } - - fun rejectPayment(ignoreRedirect: Boolean = false) { - val payload = state.value.billState.privatePaymentConfirmation?.payload - cancelPayment(true, ignoreRedirect) - payload ?: return - - scope.launch { - paymentRepository.rejectPayment(payload) - } - } - - private fun attemptLogin(codePayload: CodePayload, request: DeepLinkRequest? = null) { - val (payload, loginAttempt) = paymentRepository.attemptLogin(codePayload) ?: return - BottomBarManager.clear() - - presentLoginCard( - payload = payload, - domain = loginAttempt.domain, - request = request, - ) - } - - private fun presentLoginCard( - payload: CodePayload, - domain: Domain, - request: DeepLinkRequest? = null - ) { - vibrator.vibrate() - - state.update { - it.copy( - presentationStyle = PresentationStyle.Pop, - billState = it.billState.copy( - bill = Bill.Login( - amount = KinAmount.Zero, - payload = payload, - request = request, - ), - loginConfirmation = LoginConfirmation( - state = ConfirmationState.AwaitingConfirmation, - payload = payload, - domain = domain - ), - primaryAction = null, - secondaryAction = null, - ) - ) - } - } - - fun completeLogin() = scope.launch { - val organizer = SessionManager.getOrganizer() ?: return@launch - val loginConfirmation = state.value.billState.loginConfirmation ?: return@launch - val domain = loginConfirmation.domain - - state.update { - val billState = it.billState - it.copy( - billState = billState.copy( - loginConfirmation = loginConfirmation.copy(state = ConfirmationState.Sending) - ), - ) - } - - runCatching { - val relationship = if (organizer.relationshipFor(domain) == null) { - client.awaitEstablishRelationship(organizer, domain).getOrNull() - } else { - Timber.d("Skipping, relationship already exists.") - organizer.relationshipFor(domain) - } - - if (relationship == null) { - throw IllegalStateException("Relationship not found") - } - - client.loginToThirdParty( - rendezvous = loginConfirmation.payload.rendezvous.publicKeyBytes.toPublicKey(), - relationship = relationship.getCluster().authority.keyPair - ).getOrThrow() - }.onFailure { - TopBarManager.showMessage( - resources.getString(R.string.error_title_login_failed), - resources.getString(R.string.error_description_login_failed), - ) - ErrorUtils.handleError(it) - - state.update { uiModel -> - uiModel.copy( - presentationStyle = PresentationStyle.Hidden, - billState = uiModel.billState.copy( - bill = null, - showToast = false, - loginConfirmation = null, - toast = null, - valuation = null, - primaryAction = null, - secondaryAction = null, - ) - ) - } - }.onSuccess { - state.update { - val billState = it.billState - val confirmation = it.billState.loginConfirmation ?: return@update it - - it.copy( - billState = billState.copy( - loginConfirmation = confirmation.copy(state = ConfirmationState.Sent), - ), - ) - } - - delay(400.milliseconds) - cancelLogin(rejected = false) - } - } - - private fun cancelLogin(rejected: Boolean) { - val bill = state.value.billState.bill ?: return - val request = bill.metadata.request - state.update { - it.copy( - presentationStyle = PresentationStyle.Slide, - billState = it.billState.copy( - bill = null, - showToast = false, - privatePaymentConfirmation = null, - loginConfirmation = null, - toast = null, - valuation = null, - primaryAction = null, - secondaryAction = null, - ) - ) - } - - scope.launch { - delay(300) - if (rejected) { - request?.cancelUrl?.let { url -> - val intent = Intent( - Intent.ACTION_VIEW, - url.toUri() - ).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - _eventFlow.emit(SessionEvent.SendIntent(intent)) - } - } else { - request?.successUrl?.let { url -> - val intent = Intent( - Intent.ACTION_VIEW, - url.toUri() - ).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - _eventFlow.emit(SessionEvent.SendIntent(intent)) - } - } - } - } - - fun rejectLogin() { - val rendezvous = state.value.billState.loginConfirmation?.payload?.rendezvous - if (rendezvous == null) { - Timber.e("Failed to reject login, no rendezous found in login confirmation.") - return - } - - cancelLogin(rejected = true) - - scope.launch { - client.rejectLogin(rendezvous) - } - } - - @SuppressLint("CheckResult") - private fun attemptReceive(organizer: Organizer, payload: CodePayload) { - analytics.grabStart() - receiveTransactionRepository.start(organizer, payload.rendezvous) - .doOnNext { metadata -> - Timber.d("metadata=$metadata") - val kinAmount = when (metadata) { - is IntentMetadata.SendPrivatePayment -> metadata.metadata.amount - is IntentMetadata.ReceivePaymentsPublicly -> metadata.metadata.amount - else -> return@doOnNext - } - - analytics.grab(kin = kinAmount.kin, currencyCode = kinAmount.rate.currency) - - val exchangeCurrency = kinAmount.rate.currency.name - val exchangeRate = kinAmount.rate.fx - val amountNative = kinAmount.kin.toKinValueDouble() - - Timber.i( - "StartTransaction $exchangeCurrency $exchangeRate x $amountNative = (${exchangeRate * amountNative})" - ) - - BottomBarManager.clear() - - showBill( - Bill.Cash(kinAmount, didReceive = true), - vibrate = true - ) - if (DEBUG_SCAN_TIMES) { - Timber.tag("codescan") - .d("scan processing took ${System.currentTimeMillis() - scanProcessingTime}") - scanProcessingTime = 0 - } - } - .flatMapCompletable { - Completable.concatArray( - balanceController.fetchBalance(), - client.fetchLimits(isForce = true), - ) - } - .subscribe({ - scope.launch { historyController.fetch() } - }, { - scannedRendezvous.remove(payload.rendezvous.publicKey) - ErrorUtils.handleError(it) - }) - } - - fun startSheetDismissTimer(function: () -> Unit) { - sheetDismissTimer?.cancel() - sheetDismissTimer = Timer().schedule((1000 * 60).toLong()) { - function() - } - } - - fun stopSheetDismissTimer() { - sheetDismissTimer?.cancel() - } - - fun resetScreenTimeout(activity: Activity) { - activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - Timer().schedule(10000) { - activity.runOnUiThread { - activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } - } - } - - fun onImageSelected( - uri: Uri - ) { - fun onScanningStop() { - codeAnalyzer.onCodeScanned = {} - codeAnalyzer.onNoCodeFound = {} - } - - codeAnalyzer.onCodeScanned = { - onScanningStop() - onCodeScan(it) - } - - codeAnalyzer.onNoCodeFound = { - onScanningStop() - - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = resources.getString(R.string.error_title_noCodeFound), - message = resources.getString(R.string.error_description_noCodeFound) - ) - ) - } - - codeAnalyzer.analyze(uri) - } - - fun onCodeScan( - code: ScannableKikCode, - ) { - if (state.value.billState.bill == null) { - if (code is ScannableKikCode.RemoteKikCode) { - onCodeScan(code.payloadId) - } - } - } - - fun logout(activity: Activity) { - authManager.logout(activity, onComplete = {}) - } - - - @SuppressLint("CheckResult") - fun onRemoteSend() { - val bill = state.value.billState.bill - when (bill) { - is Bill.Cash -> { - shareGiftCard() - } - - is Bill.Tip -> { - shareTipCard() - } - - else -> Unit - } - } - - private fun shareGiftCard() { - val giftCard = giftCardManager.createGiftCard() - val amount = cashLinkManager.amount - var loadingIndicatorTimer: TimerTask? = null - - if (!networkObserver.isConnected) { - ErrorUtils.showNetworkError(resources) - return - } - - client.sendRemotely( - amount = amount, - rendezvousKey = cashLinkManager.rendezvous.publicKeyBytes.toPublicKey(), - giftCard = giftCard - ) - .doOnSubscribe { - state.update { it.copy(isRemoteSendLoading = true) } - } - .doOnComplete { - loadingIndicatorTimer?.cancel() - loadingIndicatorTimer = Timer().schedule(1000) { - state.update { it.copy(isRemoteSendLoading = false) } - } - - analytics.remoteSendOutgoing( - kin = amount.kin, - currencyCode = amount.rate.currency - ) - } - .doOnError { - loadingIndicatorTimer?.cancel() - state.update { it.copy(isRemoteSendLoading = false) } - } - .timeout(15, TimeUnit.SECONDS) - .subscribe( - { showRemoteSendDialog(giftCard, amount) }, - ErrorUtils::handleError - ) - } - - private fun shareTipCard() = scope.launch { - val connectedAccount = tipController.connectedAccount.value ?: return@launch - withContext(Dispatchers.Main) { - val shareIntent = - IntentUtils.tipCard(connectedAccount.username, connectedAccount.platform) - - _eventFlow.emit(SessionEvent.SendIntent(shareIntent)) - } - } - - private fun cancelRemoteSend(giftCard: GiftCardAccount, amount: KinAmount) = - scope.launch { - val organizer = SessionManager.getOrganizer() ?: return@launch - client.cancelRemoteSend(giftCard, amount.kin, organizer) - .onSuccess { - analytics.remoteSendIncoming( - kin = amount.kin, - currencyCode = amount.rate.currency, - isVoiding = true - ) - } - } - - private fun showRemoteSendDialog( - giftCard: GiftCardAccount, - amount: KinAmount - ) { - val shareIntent = IntentUtils.cashLink( - entropy = giftCardManager.getEntropy(giftCard), - formattedAmount = amount.formatted( - currency = currencyUtils.getCurrency(amount.rate.currency.name) ?: Currency.Kin, - resources = resources - ) - ) - - scope.launch(Dispatchers.IO) { - withContext(Dispatchers.Main) { - _eventFlow.emit(SessionEvent.SendIntent(shareIntent)) - } - delay(2500) - - cashLinkManager.cancelBillTimeout() - - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = resources.getString(R.string.prompt_title_didYouSendLink), - subtitle = resources.getString(R.string.prompt_description_didYouSendLink), - positiveText = resources.getString(R.string.action_yes), - negativeText = resources.getString(R.string.action_noTryAgain), - tertiaryText = resources.getString(R.string.action_cancelSend), - onPositive = { - cancelSend(style = PresentationStyle.Pop) - vibrator.vibrate() - }, - onNegative = { showRemoteSendDialog(giftCard, amount) }, - onTertiary = { - cancelRemoteSend(giftCard, amount) - cancelSend(style = PresentationStyle.Slide) - }, - onClose = { fromAction -> - if (!fromAction) { - cancelSend(style = PresentationStyle.Pop) - vibrator.vibrate() - } - }, - type = BottomBarManager.BottomBarMessageType.INFO, - isDismissible = false, - timeoutSeconds = 60 - ) - ) - } - } - - fun handleRequest(request: DeepLinkRequest?) { - if (request != null) { - when { - request.paymentRequest != null -> { - val payment = request.paymentRequest!! - scope.launch { - if (state.value.balance == null) { - balanceController.fetchBalanceSuspend() - state.update { - val amount = KinAmount.newInstance( - Kin.fromKin(balanceController.rawBalance), exchange.localRate - ) - it.copy(balance = amount) - } - } - val fiat = payment.fiat - val kind = - if (payment.fees.isEmpty()) Kind.RequestPayment else Kind.RequestPaymentV2 - val payload = CodePayload( - kind = kind, - value = fiat, - nonce = request.clientSecret - ) - - if (scannedRendezvous.contains(payload.rendezvous.publicKey)) { - Timber.d("Nonce previously received: ${payload.nonce.hexEncodedString()}") - return@launch - } - - scannedRendezvous.add(payload.rendezvous.publicKey) - attemptPayment(payload, request) - } - } - - request.loginRequest != null -> { - val payload = CodePayload( - kind = Kind.Login, - value = Kin.fromKin(0), - nonce = request.clientSecret, - ) - - if (scannedRendezvous.contains(payload.rendezvous.publicKey)) { - Timber.d("Nonce previously received: ${payload.nonce.hexEncodedString()}") - return - } - - scannedRendezvous.add(payload.rendezvous.publicKey) - attemptLogin(payload, request) - } - - request.tipRequest != null -> { - val tip = request.tipRequest!! - val payload = CodePayload( - kind = Kind.Tip, - value = Username(tip.username) - ) - - if (scannedRendezvous.contains(payload.rendezvous.publicKey)) { - Timber.d("Nonce previously received: ${payload.nonce.hexEncodedString()}") - return - } - - scannedRendezvous.add(payload.rendezvous.publicKey) - - attemptTip(payload, request) - } - - request.imageRequest != null -> { - val image = request.imageRequest!! - onImageSelected(image.uri) - } - } - } - } - - fun openCashLink(cashLink: String?) { - Timber.d("openCashLink:$cashLink") - val base58Entropy = cashLink?.trim()?.replace("\n", "") ?: return - if (base58Entropy.isEmpty()) { - Timber.d("cash link empty") - return - } - if (openedLinks.contains(base58Entropy)) { - Timber.d("cash link already opened in session") - return - } - - analytics.cashLinkGrabStart() - - openedLinks.add(base58Entropy) - - try { - val mnemonic = mnemonicManager.fromEntropyBase58(base58Entropy) - val giftCardAccount = giftCardManager.createGiftCard(mnemonic) - - scope.launch { - withContext(Dispatchers.IO) { - withTimeout(15000) { - balanceController.fetchBalanceSuspend() - try { - //Get the amount on the card - val amount = client.receiveRemoteSuspend(giftCardAccount) - analytics.remoteSendIncoming( - kin = amount.kin, - currencyCode = amount.rate.currency, - isVoiding = false - ) - analytics.cashLinkGrab(amount.kin, amount.rate.currency) - analytics.onBillReceived() - - historyController.fetch() - - scope.launch(Dispatchers.Main) { - BottomBarManager.clear() - showBill( - Bill.Cash( - amount = amount, - didReceive = true, - kind = Bill.Kind.remote - ), - vibrate = true - ) - removeLinkWithDelay(base58Entropy) - } - } catch (ex: Exception) { - onRemoteSendError(ex) - } - } - } - } - } catch (e: Base58.AddressFormatException.InvalidCharacter) { - onRemoteSendError(e) - } catch (e: Exception) { - onRemoteSendError(e) - } - } - - - private fun onRemoteSendError(throwable: Throwable) { - when (throwable) { - is RemoteSendException.GiftCardClaimedException -> - TopBarManager.showMessage( - resources.getString(R.string.error_title_alreadyCollected), - resources.getString(R.string.error_description_alreadyCollected) - ) - - is RemoteSendException.GiftCardExpiredException -> - TopBarManager.showMessage( - resources.getString(R.string.error_title_linkExpired), - resources.getString(R.string.error_description_linkExpired) - ) - - else -> { - throwable.printStackTrace() - TopBarManager.showMessage( - resources.getString(R.string.error_title_failedToCollect), - resources.getString(R.string.error_description_failedToCollect) - ) - val traceableError = Throwable( - message = "Failed to receive remote send", - cause = throwable - ) - ErrorUtils.handleError(traceableError) - } - } - } - - private fun onDrawn() { - analytics.onAppStarted() - } - - companion object { - private val openedLinks = mutableListOf() - private val scannedRendezvous = mutableListOf() - - private const val DEBUG_SCAN_TIMES = true - private var scanProcessingTime = 0L - - fun removeLinkWithDelay(link: String) { - CoroutineScope(Dispatchers.IO).launch { - delay(15000) - openedLinks.remove(link) - } - } - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/TopLevelViewModel.kt b/apps/codeApp/src/main/java/com/getcode/TopLevelViewModel.kt deleted file mode 100644 index c6816d19d..000000000 --- a/apps/codeApp/src/main/java/com/getcode/TopLevelViewModel.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.getcode - -import android.app.Activity -import androidx.lifecycle.viewModelScope -import com.getcode.manager.AuthManager -import com.getcode.manager.TopBarManager -import com.getcode.services.model.PrefsBool -import com.getcode.network.repository.AppSettingsRepository -import com.getcode.network.repository.BetaFlagsRepository -import com.getcode.network.repository.BetaOptions -import com.getcode.network.repository.FeatureRepository -import com.getcode.util.resources.ResourceHelper -import com.getcode.view.BaseViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class TopLevelViewModel @Inject constructor( - private val authManager: AuthManager, - private val appSettings: AppSettingsRepository, - betaFlagsRepository: BetaFlagsRepository, - features: FeatureRepository, - resources: ResourceHelper, -) : BaseViewModel(resources) { - - private val _eventFlow: MutableSharedFlow = MutableSharedFlow() - val eventFlow: SharedFlow = _eventFlow.asSharedFlow() - - private val betaFlags = betaFlagsRepository.observe() - - private val requireBiometrics = MutableStateFlow(null) - - init { - viewModelScope.launch { - requireBiometrics.value = appSettings.get(PrefsBool.REQUIRE_BIOMETRICS) - } - } - - val state = combine( - betaFlags, - features.buyModule.map { it.available }, - requireBiometrics, - ) { beta, buykinAvailable, requireBiometrics -> - State(beta, buykinAvailable, requireBiometrics) - } .stateIn(viewModelScope, started = SharingStarted.Eagerly, State.Empty) - - data class State( - val betaFlags: BetaOptions, - val buyModuleAvailable: Boolean, - val requireBiometrics: Boolean?, - ) { - companion object { - val Empty = State( - betaFlags = BetaOptions.Defaults, - buyModuleAvailable = false, - requireBiometrics = null - ) - } - } - - sealed interface Event { - data object LogoutRequested: Event - data object LogoutCompleted: Event - } - - fun onResume() { - viewModelScope.launch { - requireBiometrics.value = appSettings.get(PrefsBool.REQUIRE_BIOMETRICS) - } - } - - fun onMissingBiometrics() { - // biometrics required by user, but now not enrolled - // show a top bar error and let them in - TopBarManager.showMessage( - getString(R.string.error_title_missingBiometrics), - getString(R.string.error_description_missingBiometrics) - ) - appSettings.update(setting = PrefsBool.REQUIRE_BIOMETRICS, value = false, fromUser = false) - requireBiometrics.value = false - } - - fun logout(activity: Activity, onComplete: () -> Unit = {}) { - _eventFlow.tryEmit(Event.LogoutRequested) - authManager.logout(activity) { - _eventFlow.tryEmit(Event.LogoutCompleted) - onComplete() - } - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/api/KadoApi.kt b/apps/codeApp/src/main/java/com/getcode/api/KadoApi.kt deleted file mode 100644 index c88e365f4..000000000 --- a/apps/codeApp/src/main/java/com/getcode/api/KadoApi.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.getcode.api - -import com.google.gson.JsonObject -import okhttp3.ResponseBody -import retrofit2.Response -import retrofit2.http.GET -import retrofit2.http.Path - -interface KadoApi { - @GET("v2/public/orders/{orderId}") - suspend fun getOrderStatus(@Path("orderId") orderId: String): Response -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/inject/ApiModule.kt b/apps/codeApp/src/main/java/com/getcode/inject/ApiModule.kt deleted file mode 100644 index 8792d9803..000000000 --- a/apps/codeApp/src/main/java/com/getcode/inject/ApiModule.kt +++ /dev/null @@ -1,117 +0,0 @@ -package com.getcode.inject - -import android.content.Context -import com.getcode.BuildConfig -import com.getcode.api.KadoApi -import com.getcode.model.Currency -import com.getcode.model.CurrencyCode -import com.getcode.network.repository.PrefRepository -import com.getcode.services.R -import com.getcode.services.db.CurrencyProvider -import com.getcode.services.model.PrefsString -import com.getcode.util.AccountAuthenticator -import com.getcode.util.locale.LocaleHelper -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.CurrencyUtils -import com.mixpanel.android.mpmetrics.MixpanelAPI -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import io.reactivex.rxjava3.core.Scheduler -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.schedulers.Schedulers -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory -import javax.inject.Named -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object ApiModule { - - @Singleton - @Provides - fun provideCompositeDisposable(): CompositeDisposable { - return CompositeDisposable() - } - - @Provides - fun provideScheduler(): Scheduler = Schedulers.io() - - @Singleton - @Provides - fun provideAccountAuthenticator( - @ApplicationContext context: Context, - ): AccountAuthenticator { - return AccountAuthenticator(context) - } - - @Singleton - @Provides - fun provideMixpanelApi(@ApplicationContext context: Context): MixpanelAPI { - return MixpanelAPI.getInstance(context, BuildConfig.MIXPANEL_API_KEY) - } - - @Singleton - @Provides - fun providesHttpLoggingInterceptor() = HttpLoggingInterceptor() - .apply { - level = HttpLoggingInterceptor.Level.BODY - } - - @Singleton - @Provides - fun providesOkHttpClient(httpLoggingInterceptor: HttpLoggingInterceptor): OkHttpClient = - OkHttpClient - .Builder() - .addInterceptor(httpLoggingInterceptor) - .build() - - @Singleton - @Provides - @Named("kado-retrofit") - fun provideKadoRetrofit(okHttpClient: OkHttpClient): Retrofit = Retrofit.Builder() - .addConverterFactory(GsonConverterFactory.create()) - .baseUrl("https://api.kado.money/") - .client(okHttpClient) - .build() - - @Singleton - @Provides - fun providesKadoApi( - @Named("kado-retrofit") - retrofit: Retrofit - ): KadoApi = retrofit.create(KadoApi::class.java) - - @Singleton - @Provides - fun providesCurrencyProvider( - prefRepository: PrefRepository, - currencyUtils: CurrencyUtils, - locale: LocaleHelper, - resources: ResourceHelper, - ): CurrencyProvider = object : CurrencyProvider { - override suspend fun preferredCurrency(): Currency? { - val preferredCurrencyCode = prefRepository.get( - PrefsString.KEY_LOCAL_CURRENCY, - "" - ).takeIf { it.isNotEmpty() } - val preferredCurrency = preferredCurrencyCode?.let { currencyUtils.getCurrency(it) } - return preferredCurrency ?: locale.getDefaultCurrencyName().let { currencyUtils.getCurrency(it) } - } - - override suspend fun defaultCurrency(): Currency? = currencyUtils.getCurrency(locale.getDefaultCurrencyName()) - - override fun suffix(currency: Currency?): String { - return if (currency?.code == CurrencyCode.KIN.name) { - "" - } else { - resources.getString(R.string.core_ofKin) - } - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/inject/AppModule.kt b/apps/codeApp/src/main/java/com/getcode/inject/AppModule.kt deleted file mode 100644 index a552c6c84..000000000 --- a/apps/codeApp/src/main/java/com/getcode/inject/AppModule.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.getcode.inject - -import android.annotation.SuppressLint -import android.content.ClipboardManager -import android.content.Context -import android.net.ConnectivityManager -import android.net.wifi.WifiManager -import android.os.Build -import android.os.VibratorManager -import android.telephony.TelephonyManager -import androidx.biometric.BiometricManager -import androidx.core.app.NotificationManagerCompat -import com.getcode.analytics.CodeAnalyticsService -import com.getcode.media.StaticImageAnalyzer -import com.getcode.media.StaticImageHelper -import com.getcode.util.AndroidLocale -import com.getcode.util.AndroidPermissions -import com.getcode.util.vibration.Api25Vibrator -import com.getcode.util.vibration.Api26Vibrator -import com.getcode.util.vibration.Api31Vibrator -import com.getcode.util.locale.LocaleHelper -import com.getcode.util.permissions.PermissionChecker -import com.getcode.util.resources.AndroidResources -import com.getcode.util.resources.ResourceHelper -import com.getcode.util.vibration.Vibrator -import com.getcode.utils.CurrencyUtils -import com.getcode.utils.network.Api24NetworkObserver -import com.getcode.utils.network.Api29NetworkObserver -import com.getcode.utils.network.NetworkConnectivityListener -import com.kik.kikx.kikcodes.KikCodeScanner -import com.kik.kikx.kikcodes.implementation.KikCodeScannerImpl -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object AppModule { - @Provides - fun providesResourceHelper( - @ApplicationContext context: Context, - ): ResourceHelper = AndroidResources(context) - - @Provides - fun providesLocaleHelper( - @ApplicationContext context: Context, - currencyUtils: CurrencyUtils, - ): LocaleHelper = AndroidLocale(context, currencyUtils) - - @Provides - fun providesWifiManager( - @ApplicationContext context: Context, - ): WifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager - - @Provides - fun providesConnectivityManager( - @ApplicationContext context: Context, - ): ConnectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - - @Provides - fun providesTelephonyManager( - @ApplicationContext context: Context, - ): TelephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager - - @Provides - @SuppressLint("NewApi") - @Singleton - fun providesNetworkObserver( - connectivityManager: ConnectivityManager, - telephonyManager: TelephonyManager, - wifiManager: WifiManager - ): NetworkConnectivityListener = when (Build.VERSION.SDK_INT) { - in Build.VERSION_CODES.N .. Build.VERSION_CODES.P -> { - Api24NetworkObserver( - wifiManager, - connectivityManager, - telephonyManager - ) - } - else -> Api29NetworkObserver( - connectivityManager, - telephonyManager - ) - } - - @Provides - fun providesPermissionChecker( - @ApplicationContext context: Context, - ): PermissionChecker = AndroidPermissions(context) - - @Provides - fun providesClipboard( - @ApplicationContext context: Context - ): ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - - - @Provides - fun providesNotificationManager( - @ApplicationContext context: Context - ): NotificationManagerCompat = NotificationManagerCompat.from(context) - - @SuppressLint("NewApi") - @Provides - @Singleton - fun providesVibrator( - @ApplicationContext context: Context - ): Vibrator = when (val apiLevel = Build.VERSION.SDK_INT) { - in Build.VERSION_CODES.BASE..Build.VERSION_CODES.R -> { - val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as android.os.Vibrator - if (apiLevel >= Build.VERSION_CODES.O) { - Api26Vibrator(vibrator) - } else { - Api25Vibrator(vibrator) - } - } - else -> Api31Vibrator(context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager) - } - - @Provides - @Singleton - fun providesBiometricsManager( - @ApplicationContext context: Context - ): BiometricManager = BiometricManager.from(context) - - @Provides - @Singleton - fun providesCodeScanner(): KikCodeScanner = KikCodeScannerImpl() - - @Provides - fun providesStaticImageAnalysis( - @ApplicationContext context: Context, - scanner: KikCodeScanner, - analyticsService: CodeAnalyticsService - ): StaticImageAnalyzer = StaticImageHelper(context, scanner, analyticsService) -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/inject/TipModule.kt b/apps/codeApp/src/main/java/com/getcode/inject/TipModule.kt deleted file mode 100644 index 4c14c72ce..000000000 --- a/apps/codeApp/src/main/java/com/getcode/inject/TipModule.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.getcode.inject - -import android.content.Context -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import dev.bmcreations.tipkit.engines.EventEngine -import dev.bmcreations.tipkit.engines.TipsEngine -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object TipModule { - @Provides - @Singleton - fun providesTipEngine( - eventEngine: EventEngine - ) = TipsEngine(eventEngine) - - @Singleton - @Provides - fun providesEventEngine( - @ApplicationContext context: Context - ) = EventEngine(context) -} diff --git a/apps/codeApp/src/main/java/com/getcode/manager/AccountManager.kt b/apps/codeApp/src/main/java/com/getcode/manager/AccountManager.kt deleted file mode 100644 index eec72539f..000000000 --- a/apps/codeApp/src/main/java/com/getcode/manager/AccountManager.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.getcode.manager - -import android.content.Context -import com.getcode.util.AccountUtils -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject - -class AccountManager @Inject constructor( - @ApplicationContext private val context: Context -) { - suspend fun getToken(): String? { - return AccountUtils.getToken(context) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/manager/AuthManager.kt b/apps/codeApp/src/main/java/com/getcode/manager/AuthManager.kt deleted file mode 100644 index b222a2fc5..000000000 --- a/apps/codeApp/src/main/java/com/getcode/manager/AuthManager.kt +++ /dev/null @@ -1,367 +0,0 @@ -package com.getcode.manager - -import android.annotation.SuppressLint -import android.content.Context -import com.bugsnag.android.Bugsnag -import com.getcode.BuildConfig -import com.getcode.analytics.CodeAnalyticsService -import com.getcode.crypt.MnemonicPhrase -import com.getcode.db.CodeAppDatabase -import com.getcode.db.InMemoryDao -import com.getcode.model.AirdropType -import com.getcode.services.model.PrefsBool -import com.getcode.services.model.PrefsString -import com.getcode.model.description -import com.getcode.network.BalanceController -import com.getcode.network.NotificationCollectionHistoryController -import com.getcode.network.exchange.Exchange -import com.getcode.network.repository.BetaFlagsRepository -import com.getcode.network.repository.IdentityRepository -import com.getcode.network.repository.PhoneRepository -import com.getcode.network.repository.PrefRepository -import com.getcode.network.repository.PushRepository -import com.getcode.network.repository.isMock -import com.getcode.services.db.Database -import com.getcode.services.utils.installationId -import com.getcode.services.utils.makeE164 -import com.getcode.services.utils.token -import com.getcode.util.AccountUtils -import com.getcode.utils.ErrorUtils -import com.getcode.utils.TraceType -import com.getcode.utils.encodeBase64 -import com.getcode.utils.getPublicKeyBase58 -import com.getcode.utils.trace -import com.google.firebase.Firebase -import com.google.firebase.installations.installations -import com.google.firebase.messaging.FirebaseMessaging -import com.google.firebase.messaging.messaging -import com.ionspin.kotlin.crypto.LibsodiumInitializer -import com.mixpanel.android.mpmetrics.MixpanelAPI -import dagger.hilt.android.qualifiers.ApplicationContext -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class AuthManager @Inject constructor( - @ApplicationContext private val context: Context, - private val sessionManager: SessionManager, - private val phoneRepository: PhoneRepository, - private val identityRepository: IdentityRepository, - private val pushRepository: PushRepository, - private val prefRepository: PrefRepository, - private val betaFlags: BetaFlagsRepository, - private val exchange: Exchange, - private val balanceController: BalanceController, - private val notificationCollectionHistory: NotificationCollectionHistoryController, - private val inMemoryDao: InMemoryDao, - private val analytics: CodeAnalyticsService, - private val mnemonicManager: com.getcode.services.manager.MnemonicManager, - private val mixpanelAPI: MixpanelAPI -) : CoroutineScope by CoroutineScope(Dispatchers.IO) { - private var softLoginDisabled: Boolean = false - - companion object { - private const val TAG = "AuthManager" - internal fun taggedTrace(message: String, type: TraceType = TraceType.Log) { - trace(message = message, type = type, tag = TAG) - } - } - - @SuppressLint("CheckResult") - fun init(onInitialized: () -> Unit = { }) { - launch { - val token = AccountUtils.getToken(context) - softLogin(token.orEmpty()) - .subscribeOn(Schedulers.computation()) - .doOnComplete { LibsodiumInitializer.initializeWithCallback(onInitialized) } - .subscribe({ }, ErrorUtils::handleError) - } - } - - private fun softLogin(entropyB64: String): Completable { - if (softLoginDisabled) return Completable.complete() - return login(entropyB64, isSoftLogin = true) - } - - fun createAccount( - entropyB64: String, - rollbackOnError: Boolean = false, - ): Completable { - if (entropyB64.isEmpty()) { - taggedTrace("provided entropy was empty", type = TraceType.Error) - sessionManager.clear() - return Completable.complete() - } - - return Single.create { - softLoginDisabled = true - - if (!CodeAppDatabase.isOpen()) { - CodeAppDatabase.init(context, entropyB64) - Database.register(CodeAppDatabase.requireInstance()) - } - - val originalSessionState = SessionManager.authState.value - sessionManager.set(entropyB64) - - it.onSuccess(originalSessionState) - }.flatMapCompletable { - fetchAdditionalAccountData(context, entropyB64, - isSoftLogin = false, - rollbackOnError = rollbackOnError, - originalSessionState = it - ) - }.doOnError { softLoginDisabled = false } - } - - fun login( - entropyB64: String, - isSoftLogin: Boolean = false, - rollbackOnError: Boolean = false - ): Completable { - taggedTrace("Login: isSoftLogin: $isSoftLogin, rollbackOnError: $rollbackOnError") - - if (entropyB64.isEmpty()) { - taggedTrace("provided entropy was empty", type = TraceType.Error) - sessionManager.clear() - return Completable.complete() - } - - return Single.create { - if (!isSoftLogin) softLoginDisabled = true - - if (!CodeAppDatabase.isOpen()) { - CodeAppDatabase.init(context, entropyB64) - Database.register(CodeAppDatabase.requireInstance()) - } - - val originalSessionState = SessionManager.authState.value - sessionManager.set(entropyB64) - - if (!isSoftLogin) { - loginAnalytics(entropyB64) - } - it.onSuccess(originalSessionState) - }.flatMapCompletable { - val fetchData = - fetchAdditionalAccountData(context, entropyB64, isSoftLogin, rollbackOnError, it) - if (isSoftLogin) { - fetchData.onErrorComplete { - ErrorUtils.handleError(it) - true - } - } else { - fetchData - } - }.doOnError { softLoginDisabled = false } - } - - private fun fetchAdditionalAccountData( - context: Context, - entropyB64: String, - isSoftLogin: Boolean, - rollbackOnError: Boolean = false, - originalSessionState: SessionManager.SessionState? - ): Completable { - return fetchData(entropyB64) - .doOnSuccess { - if (!isSoftLogin) { - if (SessionManager.getOrganizer()?.primaryVault == null) { - throw AuthManagerException.PhoneInvalidException() - } - val (phone, _) = it - - AccountUtils.addAccount( - context = context, - name = phone.phoneNumber, - password = entropyB64, - token = entropyB64 - ) - } - } - .doOnError { - val isTimelockUnlockedException = - it is AuthManagerException.TimelockUnlockedException - if (!isSoftLogin) { - if (rollbackOnError) { - login( - originalSessionState?.entropyB64.orEmpty(), - isSoftLogin, - rollbackOnError = false - ) - } else { - clearToken() - } - } else { - if (isTimelockUnlockedException) { - SessionManager.update { state -> state.copy(isTimelockUnlocked = true) } - } - } - } - .ignoreElement() - } - - fun deleteAndLogout(context: Context, onComplete: () -> Unit = {}) { - //todo: add account deletion - logout(context, onComplete) - } - - fun logout(context: Context, onComplete: () -> Unit = {}) { - launch { - AccountUtils.removeAccounts(context) - .doOnSuccess { res: Boolean -> - if (res) { - clearToken() - onComplete() - } - } - .subscribe() - } - } - - suspend fun logout(context: Context): Result { - return AccountUtils.removeAccounts(context).toFlowable() - .to { - runCatching { it.firstOrError().blockingGet() } - }.onSuccess { - clearToken() - }.map { Result.success(Unit) } - } - - - private fun fetchData(entropyB64: String): - Single> { - - taggedTrace("fetching account data") - - var owner = SessionManager.authState.value.keyPair - if (owner == null || SessionManager.authState.value.entropyB64 != entropyB64) { - owner = MnemonicPhrase.fromEntropyB64(entropyB64).getSolanaKeyPair() - } - - var phone: PhoneRepository.GetAssociatedPhoneNumberResponse? = null - var user: IdentityRepository.GetUserResponse? = null - - return phoneRepository.fetchAssociatedPhoneNumber(owner) - .firstOrError() - .subscribeOn(Schedulers.computation()) - .map { - if (it.isUnlocked) throw AuthManagerException.TimelockUnlockedException() - if (!it.isSuccess) throw AuthManagerException.PhoneInvalidException() - it - } - .flatMap { - phone = it - identityRepository.getUser(owner, it.phoneNumber) - .firstOrError() - } - .flatMap { - user = it - if (SessionManager.authState.value.entropyB64 != entropyB64) { - sessionManager.set(entropyB64) - } - balanceController.fetchBalance() - .toSingleDefault(Pair(phone!!, user!!)) - } - .doOnSuccess { - taggedTrace("account data fetched successfully") - - val distinctId = user?.userId?.description - val phoneNumber = phone?.phoneNumber?.makeE164() - - if (!BuildConfig.DEBUG) { - // BugSnag - if (Bugsnag.isStarted()) { - Bugsnag.setUser(distinctId, phoneNumber, null) - } - - // Mixpanel - mixpanelAPI.identify(distinctId) - - if (phone?.phoneNumber != null) { - mixpanelAPI.people.set("\$email", phoneNumber) - } - } - launch { savePrefs(phone!!, user!!) } - launch { exchange.fetchRatesIfNeeded() } - launch { notificationCollectionHistory.fetch() } - } - } - - private fun loginAnalytics(entropyB64: String) { - val owner = mnemonicManager.getKeyPair(entropyB64) - taggedTrace("analytics login event") - analytics.login( - ownerPublicKey = owner.getPublicKeyBase58(), - autoCompleteCount = 0, - inputChangeCount = 0 - ) - } - - private fun clearToken() { - FirebaseMessaging.getInstance().deleteToken() - analytics.logout() - sessionManager.clear() - Database.close() - notificationCollectionHistory.reset() - inMemoryDao.clear() - launch { Database.delete(context) } - if (!BuildConfig.DEBUG) Bugsnag.setUser(null, null, null) - } - - private suspend fun savePrefs( - phone: PhoneRepository.GetAssociatedPhoneNumberResponse, - user: IdentityRepository.GetUserResponse - ) { - Timber.d("saving prefs") - phoneRepository.phoneNumber = phone.phoneNumber - - prefRepository.set( - PrefsString.KEY_USER_ID to user.userId.toByteArray().encodeBase64(), - PrefsString.KEY_DATA_CONTAINER_ID to user.dataContainerId.toByteArray().encodeBase64(), - ) - phoneRepository.phoneLinked.value = phone.isLinked - - betaFlags.enableBeta(user.enableDebugOptions) - - Timber.d("airdrops eligible = ${user.eligibleAirdrops.joinToString { it.name }}") - prefRepository.set( - PrefsBool.IS_ELIGIBLE_GET_FIRST_KIN_AIRDROP, - user.eligibleAirdrops.contains(AirdropType.GetFirstKin), - ) - prefRepository.set( - PrefsBool.IS_ELIGIBLE_GIVE_FIRST_KIN_AIRDROP, - user.eligibleAirdrops.contains(AirdropType.GiveFirstKin), - ) - - updateFcmToken() - sessionManager.comeAlive() - } - - @SuppressLint("CheckResult") - private suspend fun updateFcmToken() { - if (isMock()) return - - val installationId = Firebase.installations.installationId() - val pushToken = Firebase.messaging.token() ?: return - - pushRepository.updateToken(pushToken, installationId) - .onSuccess { - Timber.d("push token updated") - }.onFailure { - Timber.e(t = it, message = "Failure updating push token") - } - } - - sealed class AuthManagerException : Exception() { - class PhoneInvalidException : AuthManagerException() - class TimelockUnlockedException : AuthManagerException() - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/mapper/AppSettingsMapper.kt b/apps/codeApp/src/main/java/com/getcode/mapper/AppSettingsMapper.kt deleted file mode 100644 index e485a2bb1..000000000 --- a/apps/codeApp/src/main/java/com/getcode/mapper/AppSettingsMapper.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.getcode.mapper - -import androidx.biometric.BiometricManager -import com.getcode.R -import com.getcode.libs.biometrics.Biometrics -import com.getcode.services.model.APP_SETTINGS -import com.getcode.services.model.PrefsBool -import com.getcode.models.SettingItem -import com.getcode.network.repository.AppSettings -import com.getcode.services.mapper.SuspendMapper -import javax.inject.Inject - -class AppSettingsMapper @Inject constructor( - private val biometricManager: BiometricManager, -) : SuspendMapper> { - override suspend fun map(from: AppSettings): List { - - return APP_SETTINGS.map { setting -> - when (setting) { - PrefsBool.CAMERA_START_BY_DEFAULT -> SettingItem( - type = setting, - name = R.string.title_autoStartCamera, - icon = R.drawable.ic_camera_outline, - enabled = from.cameraStartByDefault - ) - - PrefsBool.REQUIRE_BIOMETRICS -> { - val biometricsState = biometricManager.canAuthenticate(Biometrics.TEST_AUTH) - - val canUseBiometrics = - !(biometricsState == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE || - biometricsState == BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE || - biometricsState == BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED || - biometricsState == BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED) - - val noBiometricsEnrolled = - biometricsState == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED - - SettingItem( - type = setting, - name = R.string.title_requireBiometrics, - description = if (noBiometricsEnrolled) R.string.description_requireBiometricsNoneEnrolled else null, - icon = R.drawable.ic_biometrics, - enabled = from.requireBiometrics, - available = !noBiometricsEnrolled, - visible = canUseBiometrics - ) - } - } - } - } - -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/media/MediaScanner.kt b/apps/codeApp/src/main/java/com/getcode/media/MediaScanner.kt deleted file mode 100644 index 90f03ab19..000000000 --- a/apps/codeApp/src/main/java/com/getcode/media/MediaScanner.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.getcode.media - -import android.content.Context -import android.media.MediaScannerConnection -import dagger.hilt.android.qualifiers.ApplicationContext -import java.io.File -import javax.inject.Inject - -class MediaScanner @Inject constructor( - @ApplicationContext private val context: Context -) { - fun scan(directory: File) { - MediaScannerConnection.scanFile(context, arrayOf(directory.toString()), null, null) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/media/StaticImageHelper.kt b/apps/codeApp/src/main/java/com/getcode/media/StaticImageHelper.kt deleted file mode 100644 index cba6b65dd..000000000 --- a/apps/codeApp/src/main/java/com/getcode/media/StaticImageHelper.kt +++ /dev/null @@ -1,244 +0,0 @@ -package com.getcode.media - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.Rect -import android.icu.text.DateFormat -import android.icu.text.SimpleDateFormat -import android.net.Uri -import android.os.Environment -import com.getcode.BuildConfig -import com.getcode.analytics.CodeAnalyticsService -import com.getcode.util.save -import com.getcode.util.toByteArray -import com.getcode.util.uriToBitmap -import com.getcode.utils.TraceType -import com.getcode.utils.timedTraceSuspend -import com.kik.kikx.kikcodes.KikCodeScanner -import com.kik.kikx.kikcodes.ScanQuality -import com.kik.kikx.models.ScannableKikCode -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.io.File -import java.util.Date -import java.util.Locale -import javax.inject.Inject -import androidx.core.graphics.scale - -class StaticImageHelper @Inject constructor( - @ApplicationContext - private val context: Context, - private val scanner: KikCodeScanner, - private val analytics: CodeAnalyticsService, -): StaticImageAnalyzer { - override suspend fun analyze(uri: Uri): Result { - val bitmap = context.uriToBitmap(uri) - return if (bitmap != null) { - detectCodeInImage(bitmap) { image, quality -> - scanner.scanKikCode( - image.toByteArray(), - image.width, - image.height, - quality - ) - } - } else { - Result.failure(KikCodeScanner.NoKikCodeFoundException()) - } - } - - private suspend fun detectCodeInImage( - bitmap: Bitmap, - scan: suspend (Bitmap, ScanQuality) -> Result - ): Result = withContext(Dispatchers.Default) { - val destinationRoot = - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) - val date: DateFormat = SimpleDateFormat("yyyy-MM-dd-H-mm", Locale.CANADA) - val destination = File(destinationRoot, date.format(Date())) - if (!destination.exists()) { - destination.mkdirs() - } - - // Start searching - return@withContext search(bitmap, destination, scan) - } - - private suspend fun search( - bitmap: Bitmap, - destination: File, - scan: suspend (Bitmap, ScanQuality) -> Result, - ): Result { - return timedTraceSuspend( - message = "analyzing image", - tag = "Image Analysis", - type = TraceType.Process, - onComplete = { result, time -> - analytics.photoScanned(result.isSuccess, time.inWholeMilliseconds) - } - ) { - // try scanning raw at various scan qualities - for (quality in ScanQuality.iterator()) { - val raw = scan(bitmap, quality) - if (raw.isSuccess) { - debugPrint("Code found raw using $quality") - bitmap.recycle() - return@timedTraceSuspend raw - } else { - debugPrint("No Code found via raw using $quality") - } - } - - val zoomLevels = listOf(1.0) - val result = slidingWindowSearch( - bitmap = bitmap, - destination = destination, - zoomLevels = zoomLevels, - scan = { scan(it, ScanQuality.Medium) }, - ) - - if (result.isSuccess) { - debugPrint("Code found via sliding window") - } else { - debugPrint("No Code found via sliding window") - } - - bitmap.recycle() - return@timedTraceSuspend result - } - } - - private suspend fun slidingWindowSearch( - bitmap: Bitmap, - destination: File, - zoomLevels: List, - scan: suspend (Bitmap) -> Result - ): Result { - return timedTraceSuspend( - message = "slidingWindowSearch", - tag = "Image Analysis", - type = TraceType.Process - ) { - val windows = generateGrid(bitmap.width, bitmap.height) - - // attempt at various zoom levels - for (zoomLevel in zoomLevels) { - // Process windows starting from center out - for ((index, windowRect) in windows.iterateCenterOut().withIndex()) { - // Crop the image for the current window - val windowBitmap = Bitmap.createBitmap( - bitmap, - windowRect.left, - windowRect.top, - windowRect.width(), - windowRect.height() - ) - - val zoomedBitmap = zoomBitmap(windowBitmap, zoomLevel) - - // Ensure the bitmap is copied to avoid memory issues - val processedBitmap = windowBitmap.copy(windowBitmap.config ?: Bitmap.Config.RGB_565, false) - - saveSegment(processedBitmap, destination) { - "${index}@${zoomLevel}_${windowRect}.png" - } - - - val result = scan(processedBitmap) - - // Recycle bitmaps to avoid memory leaks - zoomedBitmap.recycle() - processedBitmap.recycle() - windowBitmap.recycle() - - if (result.isSuccess) { - bitmap.recycle() - return@timedTraceSuspend result - } - } - } - - return@timedTraceSuspend Result.failure(KikCodeScanner.NoKikCodeFoundException()) - } - } - - private fun generateGrid( - width: Int, - height: Int, - rows: Int = 6, - columns: Int = 6 - ): List { - val windows = mutableListOf() - - // Calculate window size for a rows x columns grid - val windowWidth = width / columns - val windowHeight = height / rows - - // Define overlap as 50% of the window size - val stepX = windowWidth / 2 - val stepY = windowHeight / 2 - - // Iterate through the grid top to bottom, left to right - val rowCount = height / stepY - for (r in 0 until rowCount) { - val columnCount = width / stepX - for (c in 0 until columnCount) { - val x = c * stepX - val y = r * stepY - - // Ensure right and bottom do not exceed bitmap bounds - val right = (x + windowWidth).coerceAtMost(width) - val bottom = (y + windowHeight).coerceAtMost(height) - - val windowRect = Rect(x, y, right, bottom) - windows.add(windowRect) - } - } - - return windows - } - - // Extension function to iterate through windows center out - private fun List.iterateCenterOut(): List { - val center = size / 2 - val ordered = mutableListOf() - - for (i in 0 until center) { - ordered.add(this[center - i]) - if (center + i < size) { - ordered.add(this[center + i]) - } - } - return ordered - } - - private fun zoomBitmap(bitmap: Bitmap, zoomLevel: Double): Bitmap { - // If zoomLevel is 1.0, just return a copy of the original bitmap (to prevent recycling issues) - if (zoomLevel == 1.0) return Bitmap.createBitmap(bitmap) - - val cropWidth = (bitmap.width / zoomLevel).toInt() - val cropHeight = (bitmap.height / zoomLevel).toInt() - val xOffset = (bitmap.width - cropWidth) / 2 - val yOffset = (bitmap.height - cropHeight) / 2 - - val croppedBitmap = Bitmap.createBitmap(bitmap, xOffset, yOffset, cropWidth, cropHeight) - val scaledBitmap = croppedBitmap.scale(bitmap.width, bitmap.height) - - croppedBitmap.recycle() - return scaledBitmap - } - - private fun saveSegment(bitmap: Bitmap, destination: File, name: () -> String) { - debugPrint("Scanning ${name().substringBeforeLast(".")}") - if (SAVE_IMAGES) { - bitmap.save(destination, name) - } - } -} - -private fun debugPrint(message: String) { - if (DEBUG) println(message) -} - -private val DEBUG = BuildConfig.DEBUG -private const val SAVE_IMAGES = false \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/models/SettingsItem.kt b/apps/codeApp/src/main/java/com/getcode/models/SettingsItem.kt deleted file mode 100644 index b794316a4..000000000 --- a/apps/codeApp/src/main/java/com/getcode/models/SettingsItem.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.getcode.models - -import com.getcode.services.model.AppSetting - -data class SettingItem( - val type: AppSetting, - val name: Int, - val description: Int? = null, - val icon: Int, - val enabled: Boolean, - val visible: Boolean = true, - val available: Boolean = true -) \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/navigation/screens/Graphs.kt b/apps/codeApp/src/main/java/com/getcode/navigation/screens/Graphs.kt deleted file mode 100644 index 5e26cb156..000000000 --- a/apps/codeApp/src/main/java/com/getcode/navigation/screens/Graphs.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.getcode.navigation.screens - -import android.os.Parcelable -import cafe.adriel.voyager.core.screen.Screen -import kotlinx.parcelize.Parcelize - -/** - * Main graph for the app - */ -@Parcelize -internal sealed interface MainGraph : Screen, Parcelable, NamedScreen { - - fun readResolve(): Any = this -} - - -/** - * Login based graph prior to authentication - */ -@Parcelize -internal sealed interface LoginGraph : Screen, Parcelable, NamedScreen { - fun readResolve(): Any = this -} - -@Parcelize -data class LoginArgs( - val signInEntropy: String? = null, - val isPhoneLinking: Boolean = false, - val isNewAccount: Boolean = false, - val phoneNumber: String? = null -): Parcelable - -/** - * Nested graph for the withdrawal flow within settings - */ -@Parcelize -internal sealed interface WithdrawalGraph : Screen, NamedScreen, Parcelable { - val arguments: WithdrawalArgs - fun readResolve(): Any = this -} - -@Parcelize -data class WithdrawalArgs( - val amountFiat: Double? = null, - val amountKinQuarks: Long? = null, - val amountText: String? = null, - val currencyCode: String? = null, - val currencyResId: Int? = null, - val currencyRate: Double? = null, - val resolvedDestination: String? = null, -): Parcelable - -/** - * Nested graph for the on-chain messaging screens (balance) - */ -@Parcelize -internal sealed interface ChatGraph : Screen, Parcelable, NamedScreen { - - fun readResolve(): Any = this -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/navigation/screens/LoginScreen.kt b/apps/codeApp/src/main/java/com/getcode/navigation/screens/LoginScreen.kt deleted file mode 100644 index a423f0eae..000000000 --- a/apps/codeApp/src/main/java/com/getcode/navigation/screens/LoginScreen.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.getcode.navigation.screens - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.hilt.getViewModel -import com.getcode.R -import com.getcode.analytics.Action -import com.getcode.libs.analytics.LocalAnalytics -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.view.login.LoginHome -import com.getcode.view.login.SeedDeepLink -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class LoginScreen(val seed: String? = null) : LoginGraph { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(id = R.string.action_logIn) - - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - val analytics = LocalAnalytics.current - - if (seed != null) { - SeedDeepLink(getViewModel(), seed) - } else { - LoginHome( - createAccount = { - analytics.action(Action.CreateAccount) - navigator.push(LoginPhoneVerificationScreen(isNewAccount = true)) - }, - login = { - navigator.push(AccessKeyLoginScreen()) - } - ) - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/navigation/screens/LoginScreens.kt b/apps/codeApp/src/main/java/com/getcode/navigation/screens/LoginScreens.kt deleted file mode 100644 index ef730ac11..000000000 --- a/apps/codeApp/src/main/java/com/getcode/navigation/screens/LoginScreens.kt +++ /dev/null @@ -1,159 +0,0 @@ -package com.getcode.navigation.screens - -import android.os.Parcelable -import androidx.activity.compose.BackHandler -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.hilt.getViewModel -import com.getcode.R -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.extensions.getStackScopedViewModel -import com.getcode.view.login.AccessKey -import com.getcode.view.login.AccessKeyViewModel -import com.getcode.view.login.CameraPermission -import com.getcode.view.login.NotificationPermission -import com.getcode.view.login.PhoneConfirm -import com.getcode.view.login.PhoneVerify -import com.getcode.view.login.PhoneVerifyViewModel -import com.getcode.view.login.SeedInput -import com.getcode.view.login.SeedInputViewModel -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class LoginPhoneVerificationScreen( - val arguments: LoginArgs = LoginArgs() -) : LoginGraph { - constructor( - signInEntropy: String? = null, - isPhoneLinking: Boolean = false, - isNewAccount: Boolean = false, - phoneNumber: String? = null, - ) : this(LoginArgs(signInEntropy, isPhoneLinking, isNewAccount, phoneNumber)) - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(R.string.title_enterPhoneNumber) - - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - val viewModel = getStackScopedViewModel(key) - PhoneVerify(viewModel, arguments) { - navigator.show(PhoneAreaSelectionModal(key)) - } - } -} - -@Parcelize -data class LoginPhoneConfirmationScreen( - val arguments: LoginArgs = LoginArgs() -) : LoginGraph { - constructor( - signInEntropy: String? = null, - isPhoneLinking: Boolean = false, - isNewAccount: Boolean = false, - phoneNumber: String? = null, - ) : this(LoginArgs(signInEntropy, isPhoneLinking, isNewAccount, phoneNumber)) - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(R.string.title_verifyPhoneNumber) - - @Composable - override fun Content() { - PhoneConfirm( - getViewModel(), - arguments = arguments, - ) - BackHandler { /* intercept */ } - } -} - -@Parcelize -data class AccessKeyLoginScreen( - val arguments: LoginArgs = LoginArgs() -) : LoginGraph { - - constructor( - signInEntropy: String? = null, - isPhoneLinking: Boolean = false, - isNewAccount: Boolean = false, - phoneNumber: String? = null, - ) : this(LoginArgs(signInEntropy, isPhoneLinking, isNewAccount, phoneNumber)) - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(R.string.title_enterAccessKeyWords) - - @Composable - override fun Content() { - val viewModel: SeedInputViewModel = getViewModel() - SeedInput(viewModel, arguments) - } -} - - -@Parcelize -data class AccessKeyScreen( - val arguments: LoginArgs = LoginArgs() -) : LoginGraph { - - constructor( - signInEntropy: String? = null, - isPhoneLinking: Boolean = false, - isNewAccount: Boolean = false, - phoneNumber: String? = null, - ) : this(LoginArgs(signInEntropy, isPhoneLinking, isNewAccount, phoneNumber)) - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(id = R.string.title_accessKey) - - @Composable - override fun Content() { - val viewModel = getViewModel() - AccessKey(viewModel, arguments) - BackHandler { /* intercept */ } - } -} - -@Parcelize -sealed interface CodeLoginPermission: Parcelable { - @Parcelize - data object Camera : CodeLoginPermission - @Parcelize - data object Notifications : CodeLoginPermission -} - -@Parcelize -data class PermissionRequestScreen(val permission: CodeLoginPermission, val fromOnboarding: Boolean = false) : LoginGraph { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @Composable - override fun Content() { - when (permission) { - CodeLoginPermission.Camera -> { - CameraPermission(fromOnboarding = fromOnboarding) - } - - CodeLoginPermission.Notifications -> { - NotificationPermission(fromOnboarding = fromOnboarding) - } - } - - BackHandler { /* intercept */ } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/navigation/screens/MainScreens.kt b/apps/codeApp/src/main/java/com/getcode/navigation/screens/MainScreens.kt deleted file mode 100644 index cc86b585b..000000000 --- a/apps/codeApp/src/main/java/com/getcode/navigation/screens/MainScreens.kt +++ /dev/null @@ -1,292 +0,0 @@ -package com.getcode.navigation.screens - -import androidx.compose.material.Icon -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.BubbleChart -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.lifecycle.Lifecycle -import androidx.paging.compose.collectAsLazyPagingItems -import cafe.adriel.voyager.core.lifecycle.LifecycleEffect -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.hilt.getViewModel -import cafe.adriel.voyager.navigator.currentOrThrow -import com.getcode.LocalSession -import com.getcode.R -import com.getcode.model.ID -import com.getcode.models.DeepLinkRequest -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.ui.components.SheetTitleDefaults -import com.getcode.ui.components.chat.utils.localized -import com.getcode.ui.utils.RepeatOnLifecycle -import com.getcode.navigation.extensions.getActivityScopedViewModel -import com.getcode.utils.trace -import com.getcode.view.download.ShareDownloadScreen -import com.getcode.view.main.account.AccountHome -import com.getcode.view.main.account.AccountSheetViewModel -import com.getcode.view.main.balance.BalanceScreen -import com.getcode.view.main.balance.BalanceSheetViewModel -import com.getcode.view.main.chat.ChatScreen -import com.getcode.view.main.chat.NotificationCollectionViewModel -import com.getcode.view.main.giveKin.GiveKinScreen -import com.getcode.view.main.requestKin.RequestKinScreen -import com.getcode.view.main.scanner.ScanScreen -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class ScanScreen( - override val seed: String? = null, - val cashLink: String? = null, - @IgnoredOnParcel - val request: DeepLinkRequest? = null, -) : AppScreen(), MainScreen, MainGraph { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @Composable - override fun Content() { - trace("home rendered") - val session = LocalSession.currentOrThrow - - ScanScreen(session, cashLink, request) - } -} - -@Parcelize -data object GiveKinModal : AppScreen(), MainGraph, ModalRoot { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - - override val name: String - @Composable get() = stringResource(id = R.string.title_giveCash) - - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - ModalContainer( - closeButtonEnabled = { - if (navigator.isVisible) { - it is GiveKinModal - } else { - navigator.progress > 0f - } - }, - ) { - GiveKinScreen(getViewModel()) - } - } -} - -@Parcelize -data class RequestKinModal( - val showClose: Boolean = false, -) : AppScreen(), MainGraph, ModalRoot { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - - override val name: String - @Composable get() = stringResource(id = R.string.title_requestKin) - - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - - val content = @Composable { - RequestKinScreen(getViewModel()) - } - - if (showClose) { - ModalContainer( - closeButtonEnabled = { - if (navigator.isVisible) { - it is RequestKinModal - } else { - navigator.progress > 0f - } - } - ) { - content() - } - } else { - ModalContainer( - backButtonEnabled = { - if (navigator.isVisible) { - it is RequestKinModal - } else { - navigator.progress > 0f - } - } - ) { - content() - } - } - } -} - -@Parcelize -data object AccountModal : MainGraph, ModalRoot { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - val viewModel = getActivityScopedViewModel() - ModalContainer( - displayLogo = true, - onLogoClicked = { viewModel.dispatchEvent(AccountSheetViewModel.Event.LogoClicked) }, - closeButtonEnabled = { - if (navigator.isVisible) { - it is AccountModal - } else { - navigator.progress > 0f - } - } - ) { - AccountHome(viewModel) - } - } -} - -@Parcelize -data object ShareDownloadLinkModal : MainGraph, ModalRoot { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @Composable - override fun Content() { - ModalContainer( - closeButtonEnabled = { it is ShareDownloadLinkModal } - ) { - ShareDownloadScreen() - } - } -} - -@Parcelize -data object BalanceModal : ChatGraph, ModalRoot { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - - override val name: String - @Composable get() = stringResource(id = R.string.title_balance) - - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - - val viewModel = getActivityScopedViewModel() - val state by viewModel.stateFlow.collectAsState() - val isViewingBuckets by remember(state.isBucketDebuggerVisible) { - derivedStateOf { state.isBucketDebuggerVisible } - } - - val backButton = @Composable { - when { - isViewingBuckets -> SheetTitleDefaults.BackButton() - !isViewingBuckets && state.isBucketDebuggerEnabled -> { - Icon( - imageVector = Icons.Rounded.BubbleChart, - contentDescription = "", - tint = Color.White, - ) - } - - else -> Unit - } - } - - ModalContainer( - navigator = navigator, - onLogoClicked = {}, - backButton = backButton, - backButtonEnabled = { isViewingBuckets || state.isBucketDebuggerEnabled }, - onBackClicked = when { - isViewingBuckets -> { - { - viewModel.dispatchEvent( - BalanceSheetViewModel.Event.OnDebugBucketsVisible(false) - ) - } - } - - state.isBucketDebuggerEnabled -> { - { - viewModel.dispatchEvent( - BalanceSheetViewModel.Event.OnDebugBucketsVisible(true) - ) - } - } - - else -> null - }, - closeButtonEnabled = close@{ - if (viewModel.stateFlow.value.isBucketDebuggerVisible) return@close false - if (navigator.isVisible) { - it is BalanceModal - } else { - navigator.progress > 0f - } - }, - onCloseClicked = null, - ) { - BalanceScreen(state = state, dispatch = viewModel::dispatchEvent) - } - - LifecycleEffect( - onStarted = { - val disposedScreen = navigator.lastItem - if (disposedScreen !is BalanceModal) { - viewModel.dispatchEvent(BalanceSheetViewModel.Event.OnOpened) - } - }, - onDisposed = { - val disposedScreen = navigator.lastItem - if (disposedScreen !is BalanceModal) { - viewModel.dispatchEvent( - BalanceSheetViewModel.Event.OnDebugBucketsVisible(false) - ) - } - } - ) - } -} - -@Parcelize -data class NotificationCollectionScreen(val collectionId: ID) : MainGraph, ModalContent { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @Composable - override fun Content() { - val vm = getViewModel() - val state by vm.stateFlow.collectAsState() - - ModalContainer( - titleString = { state.title.localized }, - backButtonEnabled = { it is NotificationCollectionScreen }, - ) { - val messages = vm.chatMessages.collectAsLazyPagingItems() - ChatScreen(state = state, messages = messages, dispatch = vm::dispatchEvent) - } - - LaunchedEffect(collectionId) { - vm.dispatchEvent(NotificationCollectionViewModel.Event.OnChatIdChanged(collectionId)) - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/navigation/screens/ModalContainerMessage.kt b/apps/codeApp/src/main/java/com/getcode/navigation/screens/ModalContainerMessage.kt deleted file mode 100644 index 9c1c3af29..000000000 --- a/apps/codeApp/src/main/java/com/getcode/navigation/screens/ModalContainerMessage.kt +++ /dev/null @@ -1,131 +0,0 @@ -package com.getcode.navigation.screens - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import cafe.adriel.voyager.core.screen.Screen -import com.getcode.navigation.modal.ModalHeightMetric -import com.getcode.services.manager.ModalManager -import com.getcode.theme.CodeTheme -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.core.addIf - -fun buildMessageContent( - message: ModalManager.Message, - onClose: (ModalManager.ActionType?) -> Unit -): Screen { - return ModalContainerMessage(message, onClose) -} - -private data class ModalContainerMessage( - val message: ModalManager.Message, - val onClose: (ModalManager.ActionType?) -> Unit, -) : Screen, NamedScreen, ModalRoot { - - @Composable - override fun Content() { - ModalContainer( - modalHeightMetric = ModalHeightMetric.WrapContent, - closeButtonEnabled = { it is ModalContainerMessage }, - onCloseClicked = { - onClose(null) - } - ) { - Column( - modifier = Modifier.padding(horizontal = CodeTheme.dimens.inset), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2) - ) { - message.icon?.let { imageResId -> - Box( - modifier = Modifier - .background(CodeTheme.colors.brandLight, CircleShape), - contentAlignment = Alignment.Center - ) { - Image( - modifier = Modifier.padding(CodeTheme.dimens.grid.x3), - painter = painterResource(imageResId), - contentDescription = null, - ) - } - } - Text( - modifier = Modifier - .fillMaxWidth() - .addIf(message.icon != null) { - Modifier.padding(top = CodeTheme.dimens.grid.x2) - }, - text = message.title, - style = CodeTheme.typography.displaySmall, - color = CodeTheme.colors.onBackground, - ) - - if (message.subtitle.isNotEmpty()) { - Text( - modifier = Modifier.fillMaxWidth(), - text = message.subtitle, - style = CodeTheme.typography.textSmall, - color = CodeTheme.colors.onBackground, - ) - } - - - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(top = CodeTheme.dimens.grid.x2), - buttonState = ButtonState.Filled, - text = message.positiveText, - onClick = { - message.onPositive() - onClose(ModalManager.ActionType.Positive) - } - ) - - message.negativeText?.let { negativeText -> - if (negativeText.isNotEmpty()) { - CodeButton( - modifier = Modifier.fillMaxWidth(), - buttonState = ButtonState.Filled50, - text = negativeText, - onClick = { - message.onNegative() - onClose(ModalManager.ActionType.Negative) - } - ) - } - } - - message.tertiaryText?.let { tertiaryText -> - if (tertiaryText.isNotEmpty()) { - CodeButton( - modifier = Modifier.fillMaxWidth(), - buttonState = ButtonState.Bordered, - text = tertiaryText, - onClick = { - message.onTertiary() - onClose(ModalManager.ActionType.Tertiary) - } - ) - } - } - } - } - - BackHandler(message.isDismissibleByBackButton) { - onClose(null) - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/navigation/screens/ModalScreens.kt b/apps/codeApp/src/main/java/com/getcode/navigation/screens/ModalScreens.kt deleted file mode 100644 index 38b9f6d7f..000000000 --- a/apps/codeApp/src/main/java/com/getcode/navigation/screens/ModalScreens.kt +++ /dev/null @@ -1,548 +0,0 @@ -package com.getcode.navigation.screens - -import android.webkit.JavascriptInterface -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.hilt.getViewModel -import cafe.adriel.voyager.navigator.currentOrThrow -import com.getcode.LocalSession -import com.getcode.R -import com.getcode.analytics.Action -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.SheetTitleDefaults -import com.getcode.navigation.extensions.getActivityScopedViewModel -import com.getcode.navigation.extensions.getStackScopedViewModel -import com.getcode.ui.analytics.AnalyticsScreenWatcher -import com.getcode.view.login.PhoneConfirm -import com.getcode.view.login.PhoneVerify -import com.getcode.view.login.PhoneVerifyViewModel -import com.getcode.view.main.account.AccountDeposit -import com.getcode.view.main.account.AccountDetails -import com.getcode.view.main.account.AccountFaq -import com.getcode.view.main.account.AccountPhone -import com.getcode.view.main.account.AppSettingsScreen -import com.getcode.view.main.account.BackupKey -import com.getcode.view.main.account.BetaFlagsScreen -import com.getcode.view.main.account.ConfirmDeleteAccount -import com.getcode.view.main.account.DeleteCodeAccount -import com.getcode.view.main.currency.CurrencySelectKind -import com.getcode.view.main.currency.CurrencySelectionSheet -import com.getcode.view.main.currency.CurrencyViewModel -import com.getcode.view.main.getKin.BuyAndSellKin -import com.getcode.view.main.getKin.BuyKinScreen -import com.getcode.view.main.getKin.GetKinSheet -import com.getcode.view.main.getKin.GetKinSheetViewModel -import com.getcode.view.main.getKin.KadoWebScreen -import com.getcode.view.main.tip.ConnectAccountScreen -import com.getcode.view.main.tip.EnterTipScreen -import com.getcode.view.main.tip.IdentityConnectionReason -import com.getcode.view.main.tip.ConnectAccountViewModel -import com.kevinnzou.web.rememberWebViewNavigator -import com.kevinnzou.web.rememberWebViewState -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - - -@Parcelize -data object DepositKinScreen : MainGraph, ModalContent { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(id = R.string.title_depositKin) - - @Composable - override fun Content() { - ModalContainer(backButtonEnabled = { it is DepositKinScreen }) { - AccountDeposit() - } - } -} - -@Parcelize -data object FaqScreen : MainGraph, ModalContent { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(id = R.string.title_faq) - - @Composable - override fun Content() { - ModalContainer(backButtonEnabled = { it is FaqScreen }) { - AccountFaq(getViewModel()) - } - } -} - -@Parcelize -data object AccountDebugOptionsScreen : MainGraph, ModalContent { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(id = R.string.title_betaFlags) - - @Composable - override fun Content() { - ModalContainer(backButtonEnabled = { it is AccountDebugOptionsScreen }) { - BetaFlagsScreen(getViewModel()) - } - } -} - -@Parcelize -data object AppSettingsScreen : MainGraph, ModalContent { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(id = R.string.title_appSettings) - - @Composable - override fun Content() { - ModalContainer(backButtonEnabled = { it is AppSettingsScreen }) { - AppSettingsScreen(getViewModel()) - } - } -} - -@Parcelize -data object AccountDetailsScreen : MainGraph, ModalContent { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(id = R.string.title_myAccount) - - @Composable - override fun Content() { - ModalContainer(backButtonEnabled = { it is AccountDetailsScreen }) { - AccountDetails(getActivityScopedViewModel()) - } - } -} - -@Parcelize -data object BackupScreen : MainGraph, ModalContent { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(id = R.string.title_accessKey) - - @Composable - override fun Content() { - ModalContainer(backButtonEnabled = { it is BackupScreen }) { - BackupKey(getViewModel()) - } - } -} - -@Parcelize -data object PhoneNumberScreen : MainGraph, ModalContent { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(id = R.string.title_phoneNumber) - - @Composable - override fun Content() { - ModalContainer(backButtonEnabled = { it is PhoneNumberScreen }) { - AccountPhone(getViewModel()) - } - } -} - -@Parcelize -data class PhoneVerificationScreen( - val arguments: LoginArgs = LoginArgs() -) : MainGraph, ModalContent { - constructor( - signInEntropy: String? = null, - isPhoneLinking: Boolean = false, - isNewAccount: Boolean = false, - phoneNumber: String? = null, - ) : this(LoginArgs(signInEntropy, isPhoneLinking, isNewAccount, phoneNumber)) - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(R.string.title_enterPhoneNumber) - - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - val viewModel = getStackScopedViewModel(key) - ModalContainer(backButtonEnabled = { it is PhoneVerificationScreen }) { - PhoneVerify(viewModel, arguments) { - navigator.show(PhoneAreaSelectionModal(key)) - } - } - } -} - -@Parcelize -data class PhoneAreaSelectionModal(val providedKey: String) : MainGraph, ModalContent { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(R.string.title_selectCountry) - - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - val vm = getStackScopedViewModel(providedKey) - - ModalContainer(closeButtonEnabled = { it is PhoneAreaSelectionModal }) { - PhoneCountrySelection(viewModel = vm) { - navigator.hide() - } - } - } -} - -@Parcelize -data class PhoneConfirmationScreen( - val arguments: LoginArgs = LoginArgs() -) : MainGraph, ModalContent { - constructor( - signInEntropy: String? = null, - isPhoneLinking: Boolean = false, - isNewAccount: Boolean = false, - phoneNumber: String? = null, - ) : this(LoginArgs(signInEntropy, isPhoneLinking, isNewAccount, phoneNumber)) - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(R.string.title_verifyPhoneNumber) - - @Composable - override fun Content() { - ModalContainer(backButtonEnabled = { it is PhoneConfirmationScreen }) { - PhoneConfirm( - getViewModel(), - arguments = arguments, - ) - } - } -} - - -@Parcelize -data object DeleteCodeScreen : MainGraph, ModalContent { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(id = R.string.action_deleteAccount) - - @Composable - override fun Content() { - ModalContainer(backButtonEnabled = { it is DeleteCodeScreen }) { - DeleteCodeAccount() - } - } -} - -@Parcelize -data object DeleteConfirmationScreen : MainGraph, ModalContent { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(id = R.string.action_deleteAccount) - - @Composable - override fun Content() { - ModalContainer(backButtonEnabled = { it is DeleteConfirmationScreen }) { - ConfirmDeleteAccount(getViewModel()) - } - } -} - -@Parcelize -data class CurrencySelectionModal(val kind: CurrencySelectKind = CurrencySelectKind.Entry) : - MainGraph, ModalContent { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - - override val name: String - @Composable get() = stringResource(id = R.string.title_selectCurrency) - - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - val viewModel = getActivityScopedViewModel() - ModalContainer( - backButtonEnabled = { - if (navigator.isVisible) { - it is CurrencySelectionModal - } else { - navigator.progress > 0f - } - } - ) { - CurrencySelectionSheet(viewModel = viewModel) - } - - LaunchedEffect(viewModel, kind) { - viewModel.dispatchEvent(CurrencyViewModel.Event.OnKindChanged(kind)) - } - } -} - -@Parcelize -data class BuyMoreKinModal( - val showClose: Boolean = false, -) : MainGraph, ModalRoot { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(id = R.string.action_addCash) - - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - val content = @Composable { - BuyKinScreen( - viewModel = getViewModel(), - onRedirected = { - navigator.hide() - } - ) - } - - if (showClose) { - ModalContainer( - closeButtonEnabled = { - if (navigator.isVisible) { - it is BuyMoreKinModal - } else { - navigator.progress > 0f - } - } - ) { - content() - } - } else { - ModalContainer( - backButtonEnabled = { - if (navigator.isVisible) { - it is BuyMoreKinModal - } else { - navigator.progress > 0f - } - } - ) { - content() - } - } - } -} - -@Parcelize -data class KadoWebScreen(val url: String) : MainGraph, ModalContent { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(id = R.string.action_addCash) - - @Composable - override fun Content() { - val state = rememberWebViewState(url = url) - val navigator = LocalCodeNavigator.current - val webNavigator = rememberWebViewNavigator() - ModalContainer( - modalColor = if (isSystemInDarkTheme()) { - Color(0xFF0A121F) - } else { - CodeTheme.colors.background - }, - backButtonEnabled = { true }, - backButton = { SheetTitleDefaults.CloseButton() }, - onBackClicked = { navigator.hide() }, - closeButtonEnabled = { true }, - closeButton = { SheetTitleDefaults.RefreshButton() }, - onCloseClicked = { webNavigator.reload() } - ) { - KadoWebScreen(viewModel = getViewModel(), state = state, webNavigator = webNavigator) - } - } - - class BuyKinWebInterface { - - @JavascriptInterface - fun handleMessage(message: String) { - println("KADO BUY KIN MESSAGE :: $message") - } - } -} - -@Parcelize -data class EnterTipModal(val isInChat: Boolean = false) : MainGraph, ModalRoot { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - - override val name: String - @Composable get() = - if (isInChat) stringResource(R.string.title_sendKin) - else stringResource(id = R.string.title_tipKin) - - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - val session = LocalSession.currentOrThrow - - if (isInChat) { - ModalContainer( - backButtonEnabled = { - if (navigator.isVisible) { - it is EnterTipModal - } else { - navigator.progress > 0f - } - } - ) { - EnterTipScreen(getViewModel()) { - navigator.pop() - } - } - } else { - ModalContainer( - closeButtonEnabled = { - if (navigator.isVisible) { - it is EnterTipModal - } else { - navigator.progress > 0f - } - }, - onCloseClicked = { - session.cancelTipEntry() - navigator.hide() - } - ) { - EnterTipScreen(getViewModel()) { - navigator.hide() - } - } - } - - BackHandler { - session.cancelTipEntry() - navigator.hide() - } - } - -} - -@Parcelize -data class ConnectAccount( - val reason: IdentityConnectionReason = IdentityConnectionReason.TipCard, -) : MainGraph, ModalContent { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - val viewModel = getViewModel() - when (reason) { - IdentityConnectionReason.TipCard -> { - ModalContainer( - closeButtonEnabled = { - if (navigator.isVisible) { - it is ConnectAccount - } else { - navigator.progress > 0f - } - } - ) { - ConnectAccountScreen(viewModel) - } - } - IdentityConnectionReason.IdentityReveal -> { - ModalContainer( - backButtonEnabled = { - if (navigator.isVisible) { - it is ConnectAccount - } else { - navigator.progress > 0f - } - } - ) { - ConnectAccountScreen(viewModel) - } - } - - IdentityConnectionReason.Login -> { - ConnectAccountScreen(viewModel) - } - } - - LaunchedEffect(viewModel, reason) { - viewModel.dispatchEvent(ConnectAccountViewModel.Event.OnReasonChanged(reason)) - } - - if (reason == IdentityConnectionReason.TipCard || reason == IdentityConnectionReason.Login) { - AnalyticsScreenWatcher(action = Action.OpenConnectAccount) - } - } -} - -@Parcelize -data object GetKinModal : MainGraph, ModalRoot { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - - val viewModel = getViewModel() - ModalContainer( - closeButtonEnabled = { - if (navigator.isVisible) { - it is GetKinModal - } else { - navigator.progress > 0f - } - }, - ) { - GetKinSheet(viewModel) - } - } -} - -@Parcelize -data object BuySellScreen : MainGraph, ModalContent { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @Composable - override fun Content() { - ModalContainer(backButtonEnabled = { it is BuySellScreen }) { - BuyAndSellKin(getViewModel()) - } - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/navigation/screens/Modals.kt b/apps/codeApp/src/main/java/com/getcode/navigation/screens/Modals.kt deleted file mode 100644 index 5b30badf0..000000000 --- a/apps/codeApp/src/main/java/com/getcode/navigation/screens/Modals.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.getcode.navigation.screens - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.LocalOverscrollConfiguration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import cafe.adriel.voyager.core.screen.Screen -import com.getcode.LocalBetaFlags -import com.getcode.TopLevelViewModel -import com.getcode.navigation.core.CodeNavigator -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.SheetTitle -import com.getcode.ui.components.SheetTitleDefaults -import com.getcode.ui.components.SheetTitleText -import com.getcode.navigation.extensions.getActivityScopedViewModel -import com.getcode.navigation.modal.ModalHeightMetric -import com.getcode.ui.utils.keyboardAsState -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - - -@OptIn(ExperimentalFoundationApi::class) -@Composable -internal fun NamedScreen.ModalContainer( - navigator: CodeNavigator = LocalCodeNavigator.current, - modalColor: Color = CodeTheme.colors.background, - modalHeightMetric: ModalHeightMetric = ModalHeightMetric.Weight(CodeTheme.dimens.modalHeightRatio), - displayLogo: Boolean = false, - titleString: @Composable (NamedScreen?) -> String? = { name }, - title: @Composable BoxScope.() -> Unit = { }, - backButton: @Composable () -> Unit = { SheetTitleDefaults.BackButton() }, - backButtonEnabled: (Screen?) -> Boolean = { false }, - onBackClicked: (() -> Unit)? = null, - closeButton: @Composable () -> Unit = { SheetTitleDefaults.CloseButton() }, - closeButtonEnabled: (Screen?) -> Boolean = { false }, - onCloseClicked: (() -> Unit)? = null, - onLogoClicked: () -> Unit = { }, - screenContent: @Composable BoxScope.() -> Unit -) { - Column( - modifier = Modifier - .fillMaxWidth() - .then( - when (modalHeightMetric) { - is ModalHeightMetric.Weight -> Modifier.fillMaxHeight(modalHeightMetric.weight) - ModalHeightMetric.WrapContent -> Modifier.wrapContentHeight() - } - ) - .background(modalColor) - ) { - val lastItem by remember(navigator.lastModalItem) { - derivedStateOf { navigator.lastModalItem } - } - - val isBackEnabled by remember(backButtonEnabled, lastItem) { - derivedStateOf { backButtonEnabled(lastItem) } - } - - val isCloseEnabled by remember(closeButtonEnabled, lastItem) { - derivedStateOf { closeButtonEnabled(lastItem) } - } - - val keyboardVisible by keyboardAsState() - val keyboardController = LocalSoftwareKeyboardController.current - val composeScope = rememberCoroutineScope() - - val hideSheet = { callback: () -> Unit -> - composeScope.launch { - if (keyboardVisible) { - keyboardController?.hide() - delay(500) - } - callback() - } - Unit - } - SheetTitle( - modifier = Modifier, - color = modalColor, - title = { - titleString(this@ModalContainer)?.let { - SheetTitleText(text = it) - } ?: title() - }, - displayLogo = displayLogo, - onLogoClicked = onLogoClicked, - // hide while transitioning to/from other destinations - backButton = backButton, - closeButton = closeButton, - backButtonEnabled = isBackEnabled, - closeButtonEnabled = isCloseEnabled, - onBackIconClicked = onBackClicked?.let { { hideSheet { it() } } } - ?: { hideSheet { navigator.pop() } }, - onCloseIconClicked = onCloseClicked?.let { { hideSheet { it() } } } - ?: { hideSheet { navigator.hide() } } - ) - Box( - modifier = Modifier - .windowInsetsPadding(WindowInsets.navigationBars) - ) { - val tlvm = getActivityScopedViewModel() - val state by tlvm.state.collectAsState() - CompositionLocalProvider( - LocalOverscrollConfiguration provides null, - LocalBetaFlags provides state.betaFlags, - ) { - screenContent() - } - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/navigation/screens/PhoneCountrySelection.kt b/apps/codeApp/src/main/java/com/getcode/navigation/screens/PhoneCountrySelection.kt deleted file mode 100644 index 91a43f340..000000000 --- a/apps/codeApp/src/main/java/com/getcode/navigation/screens/PhoneCountrySelection.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.getcode.navigation.screens - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.Divider -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.getcode.theme.CodeTheme -import com.getcode.util.PhoneUtils -import com.getcode.ui.core.rememberedClickable -import com.getcode.view.login.PhoneVerifyUiModel -import com.getcode.view.login.PhoneVerifyViewModel - -@Composable -fun PhoneCountrySelection( - viewModel: PhoneVerifyViewModel, - onSelection: () -> Unit = { }, -) { - val state by viewModel.uiFlow.collectAsState() - PhoneCountrySelection(state = state) { - viewModel.setCountryCode(it) - onSelection() - } -} - -@Composable -private fun PhoneCountrySelection( - state: PhoneVerifyUiModel, - onSelection: (PhoneUtils.CountryLocale) -> Unit -) { - LazyColumn(Modifier.fillMaxSize()) { - items(state.countryLocalesFiltered) { countryCode -> - Row( - modifier = Modifier - .fillMaxWidth() - .rememberedClickable { onSelection(countryCode) }, - ) { - countryCode.resId?.let { resId -> - Image( - modifier = Modifier - .align(Alignment.CenterVertically) - .padding(start = CodeTheme.dimens.inset) - .size(CodeTheme.dimens.grid.x5) - .clip(CodeTheme.shapes.large), - painter = painterResource(id = resId), - contentDescription = "" - ) - } - Text( - modifier = Modifier - .weight(1f) - .padding(vertical = CodeTheme.dimens.inset) - .padding(start = CodeTheme.dimens.inset) - .align(Alignment.CenterVertically), - text = countryCode.name, - style = CodeTheme.typography.textMedium.copy(fontWeight = FontWeight.Bold) - ) - Text( - modifier = Modifier - .padding(CodeTheme.dimens.inset) - .align(Alignment.CenterVertically), - color = CodeTheme.colors.textSecondary, - text = "+${countryCode.phoneCode}", - style = CodeTheme.typography.textMedium.copy(fontWeight = FontWeight.Bold) - ) - } - Divider( - color = CodeTheme.colors.dividerVariant, - modifier = Modifier - .fillMaxWidth() - .height(1.dp) - ) - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/navigation/screens/Screens.kt b/apps/codeApp/src/main/java/com/getcode/navigation/screens/Screens.kt deleted file mode 100644 index 3a704229c..000000000 --- a/apps/codeApp/src/main/java/com/getcode/navigation/screens/Screens.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.getcode.navigation.screens - -import androidx.compose.runtime.Composable -import cafe.adriel.voyager.core.screen.Screen -import kotlinx.coroutines.flow.MutableStateFlow -import timber.log.Timber - -interface MainScreen { - val seed: String? -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/navigation/screens/WithdrawalScreens.kt b/apps/codeApp/src/main/java/com/getcode/navigation/screens/WithdrawalScreens.kt deleted file mode 100644 index cf16f0266..000000000 --- a/apps/codeApp/src/main/java/com/getcode/navigation/screens/WithdrawalScreens.kt +++ /dev/null @@ -1,109 +0,0 @@ -package com.getcode.navigation.screens - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.hilt.getViewModel -import com.getcode.R -import com.getcode.view.main.account.withdraw.AccountWithdrawAddress -import com.getcode.view.main.account.withdraw.AccountWithdrawAmount -import com.getcode.view.main.account.withdraw.AccountWithdrawSummary -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - - -@Parcelize -internal data object WithdrawalAmountScreen : WithdrawalGraph, ModalContent { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @IgnoredOnParcel - override val arguments: WithdrawalArgs = WithdrawalArgs() - - override val name: String - @Composable get() = stringResource(id = R.string.title_withdrawKin) - - @Composable - override fun Content() { - ModalContainer(backButtonEnabled = { it is WithdrawalAmountScreen }) { - AccountWithdrawAmount(viewModel = getViewModel()) - } - } -} - -@Parcelize -data class WithdrawalAddressScreen(override val arguments: WithdrawalArgs = WithdrawalArgs()) : - WithdrawalGraph, ModalContent { - - constructor( - amountFiat: Double? = null, - amountKinQuarks: Long? = null, - amountText: String? = null, - currencyCode: String? = null, - currencyResId: Int? = null, - currencyRate: Double? = null, - resolvedDestination: String? = null, - ) : this( - WithdrawalArgs( - amountFiat, - amountKinQuarks, - amountText, - currencyCode, - currencyResId, - currencyRate, - resolvedDestination - ) - ) - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(id = R.string.title_withdrawKin) - - @Composable - override fun Content() { - ModalContainer(backButtonEnabled = { it is WithdrawalAddressScreen }) { - AccountWithdrawAddress(getViewModel(), arguments) - } - } -} - -@Parcelize -data class WithdrawalSummaryScreen(override val arguments: WithdrawalArgs = WithdrawalArgs()) : - WithdrawalGraph, ModalContent { - - constructor( - amountFiat: Double? = null, - amountKinQuarks: Long? = null, - amountText: String? = null, - currencyCode: String? = null, - currencyResId: Int? = null, - currencyRate: Double? = null, - resolvedDestination: String? = null, - ) : this( - WithdrawalArgs( - amountFiat, - amountKinQuarks, - amountText, - currencyCode, - currencyResId, - currencyRate, - resolvedDestination - ) - ) - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(id = R.string.title_withdrawKin) - - @Composable - override fun Content() { - ModalContainer(backButtonEnabled = { it is WithdrawalSummaryScreen }) { - AccountWithdrawSummary(getViewModel(), arguments) - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/notifications/CodePushMessagingService.kt b/apps/codeApp/src/main/java/com/getcode/notifications/CodePushMessagingService.kt deleted file mode 100644 index 3530c5b66..000000000 --- a/apps/codeApp/src/main/java/com/getcode/notifications/CodePushMessagingService.kt +++ /dev/null @@ -1,260 +0,0 @@ -package com.getcode.notifications - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.media.RingtoneManager -import android.os.Build -import androidx.core.app.NotificationCompat -import androidx.core.app.Person -import com.getcode.R -import com.getcode.analytics.CodeAnalyticsService -import com.getcode.manager.AuthManager -import com.getcode.manager.SessionManager -import com.getcode.model.notifications.NotificationType -import com.getcode.model.notifications.parse -import com.getcode.network.BalanceController -import com.getcode.network.NotificationCollectionHistoryController -import com.getcode.network.TipController -import com.getcode.network.repository.AccountRepository -import com.getcode.network.repository.PushRepository -import com.getcode.network.repository.TransactionRepository -import com.getcode.services.utils.installationId -import com.getcode.ui.components.chat.utils.localizedText -import com.getcode.util.resources.ResourceHelper -import com.getcode.util.resources.ResourceType -import com.getcode.utils.CurrencyUtils -import com.getcode.utils.ErrorUtils -import com.getcode.utils.TraceType -import com.getcode.utils.trace -import com.getcode.view.MainActivity -import com.google.firebase.Firebase -import com.google.firebase.installations.installations -import com.google.firebase.messaging.FirebaseMessagingService -import com.google.firebase.messaging.RemoteMessage -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlin.time.Clock -import timber.log.Timber -import javax.inject.Inject - - -@AndroidEntryPoint -class CodePushMessagingService : FirebaseMessagingService(), - CoroutineScope by CoroutineScope(Dispatchers.IO) { - - @Inject - lateinit var analyticsService: CodeAnalyticsService - - @Inject - lateinit var pushRepository: PushRepository - - @Inject - lateinit var transactionRepository: TransactionRepository - - @Inject - lateinit var accountRepository: AccountRepository - - @Inject - lateinit var resources: ResourceHelper - - @Inject - lateinit var currencyUtils: CurrencyUtils - - @Inject - lateinit var authManager: AuthManager - - @Inject - lateinit var balanceController: BalanceController - - @Inject - lateinit var notificationHistory: NotificationCollectionHistoryController - - @Inject - lateinit var tipController: TipController - - override fun onMessageReceived(remoteMessage: RemoteMessage) { - Timber.d("onMessageReceived") - if (SessionManager.isAuthenticated() == null) { - // sodium initialized internally during init - Timber.d("initializing session") - authManager.init { - handleMessage(remoteMessage) - } - } else { - handleMessage(remoteMessage) - } - } - - private fun handleMessage(remoteMessage: RemoteMessage) { - Timber.d("handling received message") - // Check if message contains a data payload. - if (remoteMessage.data.isNotEmpty()) { - Timber.d("Message data payload: ${remoteMessage.data}") - val notification = remoteMessage.parse() - - if (notification != null) { - val (type, titleKey, messageContent) = notification - if (type.isNotifiable()) { - val title = titleKey.localizedStringByKey(resources) ?: titleKey - val body = messageContent.localizedText( - resources = resources, - currencyUtils = currencyUtils - ) - notify(type, title, body) - } - - when (type) { - NotificationType.ChatMessage -> { - launch { notificationHistory.fetch() } - launch { balanceController.fetchBalanceSuspend() } - } - - NotificationType.ExecuteSwap -> { - analyticsService.backgroundSwapInitiated() - updateOrganizerAndSwap() - } - - NotificationType.Twitter -> { - launch { tipController.checkForConnection() } - } - - NotificationType.Unknown -> Unit - } - } else { - notify( - NotificationType.Unknown, - resources.getString(R.string.app_name), - "You have a new message." - ) - } - } - } - - override fun onNewToken(token: String) { - super.onNewToken(token) - launch { - if (SessionManager.isAuthenticated() == true) { - val installationId = Firebase.installations.installationId() - pushRepository.updateToken(token, installationId) - .onSuccess { - Timber.d("push token updated") - }.onFailure { - ErrorUtils.handleError(it) - Timber.e(t = it, message = "Failure updating push token") - } - } - } - } - - private fun notify( - type: NotificationType, - title: String, - content: String, - ) { - val notificationManager = - getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - notificationManager.createNotificationChannel( - NotificationChannel( - type.name, - type.name, - NotificationManager.IMPORTANCE_DEFAULT - ) - ) - } - - val person = Person.Builder() - .setName(title) - .build() - - val message = NotificationCompat.MessagingStyle.Message( - content, - Clock.System.now().toEpochMilliseconds(), - person - ) - - val style = notificationManager.getActiveNotification(title.hashCode())?.let { - NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(it) - } ?: NotificationCompat.MessagingStyle(person) - - val notificationBuilder: NotificationCompat.Builder = - NotificationCompat.Builder(this, type.name) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setStyle(style.addMessage(message)) - .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)) - .setSmallIcon(R.drawable.ic_code_logo_outline) - .setAutoCancel(true) - .setContentIntent(buildContentIntent(type)) - - notificationManager.notify(title.hashCode(), notificationBuilder.build()) - - trace( - tag = "Push", - message = "Push notification shown", - metadata = { - "category" to type.name - }, - type = TraceType.Process - ) - } - - private fun updateOrganizerAndSwap() = launch { - val owner = SessionManager.getKeyPair() - if (owner == null) { - ErrorUtils.handleError(Throwable("ExecuteSwap:: Missing owner")) - return@launch - } - - val organizer = SessionManager.getOrganizer() - if (organizer == null) { - ErrorUtils.handleError(Throwable("ExecuteSwap:: Missing organizer")) - return@launch - } - - val accountInfo = accountRepository.getTokenAccountInfos(owner).blockingGet() - organizer.setAccountInfo(accountInfo) - SessionManager.update { it.copy(organizer = organizer) } - transactionRepository.swapIfNeeded(organizer) - } -} - -private fun Context.buildContentIntent(type: NotificationType): PendingIntent { - val launchIntent = Intent(this, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - - return PendingIntent.getActivity( - this, - type.ordinal, - launchIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) -} - - -private fun NotificationManager.getActiveNotification(notificationId: Int): Notification? { - val barNotifications = getActiveNotifications() - for (notification in barNotifications) { - if (notification.id == notificationId) { - return notification.notification - } - } - return null -} - -private fun String.localizedStringByKey(resources: ResourceHelper): String? { - val name = this.replace(".", "_") - val resId = resources.getIdentifier( - name, - ResourceType.String, - ).let { if (it == 0) null else it } - - return resId?.let { resources.getString(it) } -} diff --git a/apps/codeApp/src/main/java/com/getcode/ui/components/AuthCheck.kt b/apps/codeApp/src/main/java/com/getcode/ui/components/AuthCheck.kt deleted file mode 100644 index 1224add2b..000000000 --- a/apps/codeApp/src/main/java/com/getcode/ui/components/AuthCheck.kt +++ /dev/null @@ -1,215 +0,0 @@ -package com.getcode.ui.components - -import android.content.Context -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalContext -import cafe.adriel.voyager.core.screen.Screen -import com.getcode.AppHomeScreen -import com.getcode.LocalDeeplinks -import com.getcode.R -import com.getcode.manager.BottomBarManager -import com.getcode.manager.SessionManager -import com.getcode.navigation.core.CodeNavigator -import com.getcode.navigation.screens.AccessKeyLoginScreen -import com.getcode.navigation.screens.LoginGraph -import com.getcode.navigation.screens.LoginScreen -import com.getcode.navigation.screens.MainScreen -import com.getcode.util.DeeplinkHandler -import com.getcode.util.DeeplinkResult -import com.getcode.ui.utils.getActivity -import com.getcode.utils.trace -import dev.bmcreations.tipkit.engines.LocalTipsEngine -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch - -private typealias DeeplinkFlowState = Pair - -@Composable -fun AuthCheck( - navigator: CodeNavigator, - onNavigate: (List) -> Unit, - onSwitchAccounts: (String) -> Unit, -) { - val deeplinkHandler = LocalDeeplinks.current - val tipsEngine = LocalTipsEngine.current - val dataState by SessionManager.authState.collectAsState() - - val isAuthenticated = dataState.isAuthenticated - val currentRoute = navigator.lastItem - - var deeplinkRouted by remember { - mutableStateOf(false) - } - - val context = LocalContext.current - deeplinkHandler ?: return - - LaunchedEffect(deeplinkHandler) { - val scope = this - deeplinkHandler.intent - .flatMapLatest { - combine( - flowOf(deeplinkHandler.handle(it)), - SessionManager.authState - ) { a, b -> a to b } - } - .filter { (result, authState) -> - if (result == null) return@filter false - // wait for authentication - trace("checking auth state=${authState.isAuthenticated}") - if (authState.isAuthenticated == null) { - trace("awaiting auth state confirmation") - return@filter false - } - return@filter true - }.mapNotNull { (result, state) -> - result ?: return@mapNotNull null - result to state - } - .mapSeedToHome() - .filter { (result, state) -> - when (result.type) { - is DeeplinkHandler.Type.Login -> true - is DeeplinkHandler.Type.Cash, - is DeeplinkHandler.Type.Tip, - is DeeplinkHandler.Type.Image, - is DeeplinkHandler.Type.Sdk -> { - val hasAuth = state.isAuthenticated == true - if (!hasAuth) { - // drop deeplink if not authenticated - deeplinkHandler.debounceIntent = null - context.getActivity()?.intent = null - } - hasAuth - } - - is DeeplinkHandler.Type.Unknown -> false - } - } - .map { it.first } - .onEach { (_, screens) -> - deeplinkRouted = true - trace("navigating from deep link") - onNavigate(screens) - deeplinkHandler.debounceIntent = null - context.getActivity()?.intent = null - } - .showLogoutConfirmationIfNeeded( - context = context, - scope = scope, - onSwitchAccounts = { - onSwitchAccounts(it) - deeplinkRouted = false - }, - onCancel = { - deeplinkRouted = false - } - ) - .launchIn(this) - } - - LaunchedEffect(isAuthenticated) { - trace("isauth=$isAuthenticated") - isAuthenticated?.let { authenticated -> - // Allow the seed input screen to complete and avoid - // premature navigation - if (currentRoute is AccessKeyLoginScreen) { - trace("No navigation within seed input") - return@LaunchedEffect - } - if (currentRoute is LoginGraph) { - trace("No navigation within account creation and onboarding") - } else { - if (authenticated) { - if (!deeplinkRouted) { - trace("Navigating to home") - onNavigate(listOf(AppHomeScreen())) - } - } else { - tipsEngine?.invalidateAllTips() - if (!deeplinkRouted) { - trace("Navigating to login") - onNavigate(listOf(LoginScreen())) - } - } - } - } - } -} - -private fun Flow.mapSeedToHome(): Flow = - map { (data, auth) -> - trace("checking type") - val (type, screens) = data - if (type is DeeplinkHandler.Type.Login && auth.isAuthenticated == true) { - trace("mapping entropy to home screen") - // send the user to home screen - val entropy = (screens.first() as? LoginScreen)?.seed - val updatedData = data.copy(stack = listOf(AppHomeScreen(seed = entropy))) - updatedData to auth - } else { - data to auth - } - } - - -private fun Flow.showLogoutConfirmationIfNeeded( - context: Context, - scope: CoroutineScope, - onSwitchAccounts: (String) -> Unit, - onCancel: () -> Unit -): Flow = onEach { (type, screens) -> - if (type is DeeplinkHandler.Type.Login) { - val entropy = (screens.first() as? MainScreen)?.seed - if (entropy != null) { - trace("showing logout confirm") - showLogoutMessage( - context = context, - entropyB64 = entropy, - onSwitchAccounts = { - scope.launch { - delay(300) // wait for dismiss - onSwitchAccounts(it) - } - }, - onCancel = onCancel - ) - } - } -} - -private fun showLogoutMessage( - context: Context, - entropyB64: String, - onSwitchAccounts: (String) -> Unit, - onCancel: () -> Unit, -) { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = context.getString(R.string.subtitle_logoutAndLoginConfirmation), - positiveText = context.getString(R.string.action_logIn), - negativeText = context.getString(R.string.action_cancel), - isDismissible = false, - onPositive = { - onSwitchAccounts(entropyB64) - }, - onNegative = { onCancel() } - ) - ) -} diff --git a/apps/codeApp/src/main/java/com/getcode/ui/components/FullScreenProgressSpinner.kt b/apps/codeApp/src/main/java/com/getcode/ui/components/FullScreenProgressSpinner.kt deleted file mode 100644 index 90951f9a2..000000000 --- a/apps/codeApp/src/main/java/com/getcode/ui/components/FullScreenProgressSpinner.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.getcode.ui.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.getcode.theme.CodeTheme -import com.getcode.ui.theme.CodeCircularProgressIndicator -import com.getcode.ui.core.swallowClicks - -@Composable -fun FullScreenProgressSpinner(isLoading: Boolean, modifier: Modifier = Modifier) { - if (isLoading) { - Box( - modifier = modifier - .fillMaxSize() - .background(CodeTheme.colors.surface.copy(alpha = 0.32f)) - .swallowClicks() - ) { - CodeCircularProgressIndicator( - modifier = Modifier - .size(100.dp) - .align(Alignment.Center) - ) - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/ui/components/ImageWithBackground.kt b/apps/codeApp/src/main/java/com/getcode/ui/components/ImageWithBackground.kt deleted file mode 100644 index 08405ac2c..000000000 --- a/apps/codeApp/src/main/java/com/getcode/ui/components/ImageWithBackground.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.getcode.ui.components - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.DefaultAlpha -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource - -@Composable -fun ImageWithBackground( - modifier: Modifier = Modifier, - painter: Painter, - @DrawableRes backgroundDrawableResId: Int, - contentDescription: String?, - shape: Shape = RectangleShape, - alignment: Alignment = Alignment.Center, - contentScale: ContentScale = ContentScale.Fit, - imageScale: Float = 1f, - alpha: Float = DefaultAlpha, - colorFilter: ColorFilter? = null -) { - Box( - modifier = modifier.background(color = Color.Unspecified, shape = shape), - ) { - Image( - painter = painter, - contentDescription = contentDescription, - alignment = alignment, - contentScale = contentScale, - alpha = alpha, - colorFilter = colorFilter, - modifier = Modifier.scale(imageScale), - ) - Image( - modifier = Modifier - .matchParentSize() - .clip(shape), - alignment = alignment, - contentScale = contentScale, - alpha = alpha, - painter = painterResource(backgroundDrawableResId), - contentDescription = null, - ) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/ui/components/MarkdownText.kt b/apps/codeApp/src/main/java/com/getcode/ui/components/MarkdownText.kt deleted file mode 100644 index 9ea5422ad..000000000 --- a/apps/codeApp/src/main/java/com/getcode/ui/components/MarkdownText.kt +++ /dev/null @@ -1,124 +0,0 @@ -package com.getcode.ui.components - -import android.content.Context -import android.os.Build -import android.util.TypedValue -import android.view.View -import android.widget.TextView -import androidx.annotation.FontRes -import androidx.annotation.IdRes -import androidx.compose.material.LocalContentAlpha -import androidx.compose.material.LocalContentColor -import androidx.compose.material.LocalTextStyle -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.takeOrElse -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.content.res.ResourcesCompat -import io.noties.markwon.Markwon -import io.noties.markwon.ext.strikethrough.StrikethroughPlugin -import io.noties.markwon.linkify.LinkifyPlugin - -@Composable -fun MarkdownText( - markdown: String, - modifier: Modifier = Modifier, - color: Color = Color.Unspecified, - fontSize: TextUnit = TextUnit.Unspecified, - textAlign: TextAlign = TextAlign.Unspecified, - maxLines: Int = Int.MAX_VALUE, - @FontRes fontResource: Int? = null, - style: TextStyle = LocalTextStyle.current, - @IdRes viewId: Int? = null, - onClick: (() -> Unit)? = null, - // this option will disable all clicks on links, inside the markdown text - // it also enable the parent view to receive the click event - disableLinkMovementMethod: Boolean = false, -) { - val defaultColor: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current) - val context: Context = LocalContext.current - val markdownRender: Markwon = remember { createMarkdownRender(context) } - AndroidView( - modifier = modifier, - factory = { ctx -> - createTextView( - context = ctx, - color = color, - defaultColor = defaultColor, - fontSize = fontSize, - fontResource = fontResource, - maxLines = maxLines, - style = style, - textAlign = textAlign, - viewId = viewId, - onClick = onClick, - ) - }, - update = { textView -> - markdownRender.setMarkdown(textView, markdown) - if (disableLinkMovementMethod) { - textView.movementMethod = null - } - } - ) -} - -private fun createTextView( - context: Context, - color: Color = Color.Unspecified, - defaultColor: Color, - fontSize: TextUnit = TextUnit.Unspecified, - textAlign: TextAlign = TextAlign.Unspecified, - maxLines: Int = Int.MAX_VALUE, - @FontRes fontResource: Int? = null, - style: TextStyle, - @IdRes viewId: Int? = null, - onClick: (() -> Unit)? = null -): TextView { - - val textColor = color.takeOrElse { style.color.takeOrElse { defaultColor } } - val mergedStyle = style.merge( - TextStyle( - color = textColor, - fontSize = fontSize, - textAlign = textAlign - ) - ) - return TextView(context).apply { - onClick?.let { setOnClickListener { onClick() } } - setTextColor(textColor.toArgb()) - setMaxLines(maxLines) - setTextSize(TypedValue.COMPLEX_UNIT_DIP, mergedStyle.fontSize.value) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - lineHeight = 70 - } - - viewId?.let { id = viewId } - textAlign?.let { align -> - textAlignment = when (align) { - TextAlign.Left, TextAlign.Start -> View.TEXT_ALIGNMENT_TEXT_START - TextAlign.Right, TextAlign.End -> View.TEXT_ALIGNMENT_TEXT_END - TextAlign.Center -> View.TEXT_ALIGNMENT_CENTER - else -> View.TEXT_ALIGNMENT_TEXT_START - } - } - - fontResource?.let { font -> - typeface = ResourcesCompat.getFont(context, font) - } - } -} - -private fun createMarkdownRender(context: Context): Markwon { - return Markwon.builder(context) - .usePlugin(StrikethroughPlugin.create()) - .usePlugin(LinkifyPlugin.create()) - .build() -} diff --git a/apps/codeApp/src/main/java/com/getcode/ui/components/MiddleEllipsisText.kt b/apps/codeApp/src/main/java/com/getcode/ui/components/MiddleEllipsisText.kt deleted file mode 100644 index 05339b0dd..000000000 --- a/apps/codeApp/src/main/java/com/getcode/ui/components/MiddleEllipsisText.kt +++ /dev/null @@ -1,170 +0,0 @@ -package com.getcode.ui.components - -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.SubcomposeLayout -import androidx.compose.ui.text.TextLayoutResult -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.unit.Constraints -import androidx.compose.ui.unit.TextUnit - -@Composable -fun MiddleEllipsisText( - text: String, - modifier: Modifier = Modifier, - color: Color = Color.Unspecified, - fontSize: TextUnit = TextUnit.Unspecified, - fontStyle: FontStyle? = null, - fontWeight: FontWeight? = null, - fontFamily: FontFamily? = null, - letterSpacing: TextUnit = TextUnit.Unspecified, - textDecoration: TextDecoration? = null, - textAlign: TextAlign? = null, - lineHeight: TextUnit = TextUnit.Unspecified, - softWrap: Boolean = true, - onTextLayout: (TextLayoutResult) -> Unit = {}, - style: TextStyle = LocalTextStyle.current, -) { - // some letters, like "r", will have less width when placed right before "." - // adding a space to prevent such case - val layoutText = remember(text) { "$text $ellipsisText" } - val textLayoutResultState = remember(layoutText) { - mutableStateOf(null) - } - SubcomposeLayout(modifier) { constraints -> - // result is ignored - we only need to fill our textLayoutResult - subcompose("measure") { - Text( - text = layoutText, - color = color, - fontSize = fontSize, - fontStyle = fontStyle, - fontWeight = fontWeight, - fontFamily = fontFamily, - letterSpacing = letterSpacing, - textDecoration = textDecoration, - textAlign = textAlign, - lineHeight = lineHeight, - softWrap = softWrap, - maxLines = 1, - onTextLayout = { textLayoutResultState.value = it }, - style = style, - ) - }.first().measure(Constraints()) - // to allow smart cast - val textLayoutResult = textLayoutResultState.value - ?: // shouldn't happen - onTextLayout is called before subcompose finishes - return@SubcomposeLayout layout(0, 0) {} - val placeable = subcompose("visible") { - val finalText = remember(text, textLayoutResult) { - if (text.isEmpty() || textLayoutResult.getBoundingBox(text.indices.last).right <= constraints.maxWidth) { - // text not including ellipsis fits on the first line. - return@remember text - } - - val ellipsisWidth = layoutText.indices.toList() - .takeLast(ellipsisCharactersCount) - .let widthLet@{ indices -> - // fix this bug: https://issuetracker.google.com/issues/197146630 - // in this case width is invalid - for (i in indices) { - val width = textLayoutResult.getBoundingBox(i).width - if (width > 0) { - return@widthLet width * ellipsisCharactersCount - } - } - // this should not happen, because - // this error occurs only for the last character in the string - throw IllegalStateException("all ellipsis chars have invalid width") - } - val availableWidth = constraints.maxWidth - ellipsisWidth - val startCounter = BoundCounter(text, textLayoutResult) { it } - val endCounter = BoundCounter(text, textLayoutResult) { text.indices.last - it } - - while (availableWidth - startCounter.width - endCounter.width > 0) { - val possibleEndWidth = endCounter.widthWithNextChar() - if ( - startCounter.width >= possibleEndWidth - && availableWidth - startCounter.width - possibleEndWidth >= 0 - ) { - endCounter.addNextChar() - } else if (availableWidth - startCounter.widthWithNextChar() - endCounter.width >= 0) { - startCounter.addNextChar() - } else { - break - } - } - startCounter.string.trimEnd() + ellipsisText + endCounter.string.reversed().trimStart() - } - Text( - text = finalText, - color = color, - fontSize = fontSize, - fontStyle = fontStyle, - fontWeight = fontWeight, - fontFamily = fontFamily, - letterSpacing = letterSpacing, - textDecoration = textDecoration, - textAlign = textAlign, - lineHeight = lineHeight, - softWrap = softWrap, - onTextLayout = onTextLayout, - style = style, - ) - }[0].measure(constraints) - layout(placeable.width, placeable.height) { - placeable.place(0, 0) - } - } -} - -private const val ellipsisCharactersCount = 3 -private const val ellipsisCharacter = '.' -private val ellipsisText = List(ellipsisCharactersCount) { ellipsisCharacter }.joinToString(separator = "") - -private class BoundCounter( - private val text: String, - private val textLayoutResult: TextLayoutResult, - private val charPosition: (Int) -> Int, -) { - var string = "" - private set - var width = 0f - private set - - private var _nextCharWidth: Float? = null - private var invalidCharsCount = 0 - - fun widthWithNextChar(): Float = - width + nextCharWidth() - - private fun nextCharWidth(): Float = - _nextCharWidth ?: run { - var boundingBox: Rect - // invalidCharsCount fixes this bug: https://issuetracker.google.com/issues/197146630 - invalidCharsCount-- - do { - boundingBox = textLayoutResult - .getBoundingBox(charPosition(string.count() + ++invalidCharsCount)) - } while (boundingBox.right == 0f) - _nextCharWidth = boundingBox.width - boundingBox.width - } - - fun addNextChar() { - string += text[charPosition(string.count())] - width += nextCharWidth() - _nextCharWidth = null - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/ui/components/ModalContainer.kt b/apps/codeApp/src/main/java/com/getcode/ui/components/ModalContainer.kt deleted file mode 100644 index 8a0bc8270..000000000 --- a/apps/codeApp/src/main/java/com/getcode/ui/components/ModalContainer.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.getcode.ui.components - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableLongStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow -import cafe.adriel.voyager.core.stack.StackEvent -import com.getcode.CodeAppState -import com.getcode.services.manager.ModalManager -import com.getcode.navigation.core.CodeNavigator -import com.getcode.navigation.screens.buildMessageContent -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch - -@Composable -fun ModalContainer( - navigator: CodeNavigator, - appState: CodeAppState -) { - val modalMessage by appState.modalMessage.collectAsState() - var modalMessageId by remember { mutableLongStateOf(0L) } - val onClose: suspend (actionType: ModalManager.ActionType?) -> Unit = { - modalMessageId = modalMessage?.id ?: 0 - modalMessage?.onClose?.invoke(it) - - delay(100) - ModalManager.setMessageShown(modalMessageId) - } - - // handle changes in visible state - LaunchedEffect(navigator) { - snapshotFlow { navigator.lastEvent } - .filter { it == StackEvent.Pop } - .onEach { delay(50) } - .onEach { - if (modalMessageId == modalMessage?.id) { - modalMessageId = 0 - } - }.launchIn(this) - } - - // handle provided timeout duration; triggering onClose with no action - LaunchedEffect(modalMessage) { - modalMessage?.timeoutSeconds?.let { - delay(it * 1000L) - onClose(null) - } - } - - val scope = rememberCoroutineScope() - val closeWith: (ModalManager.ActionType?) -> Unit = { type -> - scope.launch { - onClose(type) - } - navigator.hide() - } - - modalMessage?.let { message -> - navigator.show(buildMessageContent(message, onClose = closeWith)) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/ui/components/OtpBox.kt b/apps/codeApp/src/main/java/com/getcode/ui/components/OtpBox.kt deleted file mode 100644 index 66edc93d2..000000000 --- a/apps/codeApp/src/main/java/com/getcode/ui/components/OtpBox.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.getcode.ui.components - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import com.getcode.theme.CodeTheme -import com.getcode.theme.WindowSizeClass -import com.getcode.ui.core.rememberedClickable - -@Composable -fun OtpBox( - character: String, - modifier: Modifier = Modifier, - isHighlighted: Boolean = false, - onClick: () -> Unit, -) { - - val height = when (CodeTheme.dimens.heightWindowSizeClass) { - WindowSizeClass.COMPACT -> CodeTheme.dimens.grid.x9 - else -> CodeTheme.dimens.grid.x11 - } - - Box( - modifier = modifier - .padding(CodeTheme.dimens.grid.x1) - .height(height) - .width(CodeTheme.dimens.grid.x7) - .clip(CodeTheme.shapes.small) - .rememberedClickable(onClick = onClick) - .border( - border = if (isHighlighted) - BorderStroke(CodeTheme.dimens.thickBorder, color = CodeTheme.colors.brandLight.copy(alpha = 0.7f)) - else - BorderStroke(CodeTheme.dimens.border, color = CodeTheme.colors.brandLight.copy(alpha = 0.3f)), - shape = CodeTheme.shapes.small - ) - .background(Color.White.copy(alpha = 0.1f)), - ) { - Text( - text = character, - modifier = Modifier - .align(Alignment.Center), - style = CodeTheme.typography.displayExtraSmall, - color = Color.White, - ) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/ui/components/OtpRow.kt b/apps/codeApp/src/main/java/com/getcode/ui/components/OtpRow.kt deleted file mode 100644 index 3c9f80427..000000000 --- a/apps/codeApp/src/main/java/com/getcode/ui/components/OtpRow.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.getcode.ui.components - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview - - -@Preview -@Composable -fun OtpRow( - modifier: Modifier = Modifier, - length: Int = 4, - values: CharArray = charArrayOf(), - onClick: () -> Unit = {} -) { - Row( - modifier = modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - for (i in 0 until length) { - val text = if (i < values.size) values[i] else ' ' - val isHighlighted = values.size == i - OtpBox( - character = text.toString(), - onClick = onClick, - isHighlighted = isHighlighted - ) - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/ui/components/Row.kt b/apps/codeApp/src/main/java/com/getcode/ui/components/Row.kt deleted file mode 100644 index 6612c63a2..000000000 --- a/apps/codeApp/src/main/java/com/getcode/ui/components/Row.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.getcode.ui.components - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.getcode.ui.utils.calculateEndPadding -import com.getcode.ui.utils.calculateStartPadding -import com.getcode.ui.utils.calculateVerticalPadding - -@Composable -inline fun Row( - modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(0.dp), - horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, - verticalAlignment: Alignment.Vertical = Alignment.Top, - content: @Composable RowScope.() -> Unit, -) { - Box(modifier = modifier) { - androidx.compose.foundation.layout.Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = contentPadding.calculateVerticalPadding()), - horizontalArrangement = horizontalArrangement, - verticalAlignment = verticalAlignment, - ) { - Spacer(modifier = Modifier.requiredWidth(contentPadding.calculateStartPadding())) - content() - Spacer(modifier = Modifier.requiredWidth(contentPadding.calculateEndPadding())) - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/ui/components/SheetTitle.kt b/apps/codeApp/src/main/java/com/getcode/ui/components/SheetTitle.kt deleted file mode 100644 index 0e0906461..000000000 --- a/apps/codeApp/src/main/java/com/getcode/ui/components/SheetTitle.kt +++ /dev/null @@ -1,145 +0,0 @@ -package com.getcode.ui.components - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.ArrowBack -import androidx.compose.material.icons.outlined.Close -import androidx.compose.material.icons.outlined.Refresh -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import com.getcode.R -import com.getcode.theme.CodeTheme -import com.getcode.theme.topBarHeight -import com.getcode.ui.core.rememberedClickable -import com.getcode.ui.core.unboundedClickable - -@Composable -fun BoxScope.SheetTitleText(modifier: Modifier = Modifier, text: String) { - Text( - text = text, - color = Color.White, - style = CodeTheme.typography.screenTitle, - modifier = modifier.align(Alignment.Center) - ) -} - -object SheetTitleDefaults { - @Composable - fun BackButton() { - Icon( - imageVector = Icons.AutoMirrored.Outlined.ArrowBack, - contentDescription = "", - tint = Color.White, - ) - } - - @Composable - fun CloseButton() { - Icon( - imageVector = Icons.Outlined.Close, - contentDescription = "", - tint = Color.White, - ) - } - - @Composable - fun RefreshButton() { - Icon( - imageVector = Icons.Outlined.Refresh, - contentDescription = "", - tint = Color.White, - ) - } -} - -@Composable -fun SheetTitle( - modifier: Modifier = Modifier, - color: Color = CodeTheme.colors.background, - title: @Composable BoxScope.() -> Unit = { }, - displayLogo: Boolean = false, - onLogoClicked: () -> Unit = { }, - backButton: @Composable () -> Unit = { SheetTitleDefaults.BackButton() }, - backButtonEnabled: Boolean = false, - onBackIconClicked: () -> Unit = {}, - closeButton: @Composable () -> Unit = { SheetTitleDefaults.CloseButton() }, - closeButtonEnabled: Boolean = !backButtonEnabled, - onCloseIconClicked: () -> Unit = {}, -) { - Surface( - modifier = modifier, - color = color, - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .background(color) - .padding(vertical = CodeTheme.dimens.grid.x2) - .fillMaxWidth() - .height(topBarHeight), - ) { - if (closeButtonEnabled) { - Box( - modifier = Modifier - .align(Alignment.CenterEnd) - .padding(end = CodeTheme.dimens.inset) - .wrapContentWidth() - .size(CodeTheme.dimens.staticGrid.x6) - .unboundedClickable { onCloseIconClicked() } - ) { - closeButton() - } - } - - if (backButtonEnabled) { - Box( - modifier = Modifier - .align(Alignment.CenterStart) - .padding(start = CodeTheme.dimens.inset) - .wrapContentWidth() - .size(CodeTheme.dimens.staticGrid.x6) - .unboundedClickable { onBackIconClicked() } - ) { - backButton() - } - } - - if (displayLogo) { - Image( - painterResource( - R.drawable.ic_code_logo_near_white - ), - contentDescription = "", - modifier = Modifier - .requiredHeight(CodeTheme.dimens.staticGrid.x8) - .align(Alignment.Center) - .rememberedClickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { onLogoClicked() } - ) - } else { - title() - } - } - } -} - -@Preview -@Composable -fun TitlePreview() { - SheetTitle( - title = { - SheetTitleText(text = "Sheet Title") - } - ) -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/ui/components/TextSection.kt b/apps/codeApp/src/main/java/com/getcode/ui/components/TextSection.kt deleted file mode 100644 index ef1f82f03..000000000 --- a/apps/codeApp/src/main/java/com/getcode/ui/components/TextSection.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.getcode.ui.components - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import com.getcode.theme.CodeTheme - -@Composable -fun TextSection(title: String, description: String) { - Column(verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2)) { - Text( - text = title, - style = CodeTheme.typography.textLarge - ) - Text( - text = description, - style = CodeTheme.typography.textSmall - ) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/ui/components/TwitterUsernameDisplay.kt b/apps/codeApp/src/main/java/com/getcode/ui/components/TwitterUsernameDisplay.kt deleted file mode 100644 index 9cacaf2ab..000000000 --- a/apps/codeApp/src/main/java/com/getcode/ui/components/TwitterUsernameDisplay.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.getcode.ui.components - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.res.vectorResource -import com.getcode.model.TwitterUser -import com.getcode.theme.CodeTheme - -@Composable -fun TwitterUsernameDisplay( - modifier: Modifier = Modifier, - username: String, - verificationStatus: TwitterUser.VerificationStatus? = null -) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy( - CodeTheme.dimens.grid.x2, - Alignment.CenterHorizontally - ), - verticalAlignment = Alignment.CenterVertically, - ) { - Image( - painter = rememberVectorPainter(image = ImageVector.vectorResource(id = R.drawable.ic_twitter_x)), - contentDescription = null - ) - Text(text = username, style = CodeTheme.typography.textLarge, color = CodeTheme.colors.textMain) - verificationStatus?.let { status -> - status.checkmark()?.let { asset -> - Image( - painter = rememberVectorPainter(image = asset), - contentDescription = null - ) - } - } - } -} - -@Composable -fun TwitterUser.VerificationStatus.checkmark(): ImageVector? { - return when (this) { - TwitterUser.VerificationStatus.blue -> ImageVector.vectorResource(id = R.drawable.ic_twitter_verified_badge) - TwitterUser.VerificationStatus.business -> ImageVector.vectorResource(id = R.drawable.ic_twitter_verified_badge_gold) - TwitterUser.VerificationStatus.government -> ImageVector.vectorResource(id = R.drawable.ic_twitter_verified_badge_gray) - else -> null - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/ui/modals/Confirmations.kt b/apps/codeApp/src/main/java/com/getcode/ui/modals/Confirmations.kt deleted file mode 100644 index 0a4944d29..000000000 --- a/apps/codeApp/src/main/java/com/getcode/ui/modals/Confirmations.kt +++ /dev/null @@ -1,156 +0,0 @@ -package com.getcode.ui.modals - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Alignment.Companion.BottomCenter -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.platform.LocalContext -import cafe.adriel.voyager.navigator.currentOrThrow -import com.getcode.LocalSession -import com.getcode.R -import com.getcode.manager.TopBarManager -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.BuyMoreKinModal -import com.getcode.navigation.screens.BuySellScreen -import com.getcode.theme.Black40 -import com.getcode.ui.utils.AnimationUtils -import com.getcode.ui.utils.ModalAnimationSpeed -import com.getcode.ui.core.rememberedClickable - -@Composable -fun ConfirmationModals( - modifier: Modifier = Modifier, -) { - val navigator = LocalCodeNavigator.current - val context = LocalContext.current - val session = LocalSession.currentOrThrow - val sessionState by session.state.collectAsState() - Box(modifier = modifier) { - val billState by rememberUpdatedState(sessionState.billState) - - val showScrim by remember(billState) { - derivedStateOf { - val loginConfirmation = billState.loginConfirmation - val paymentConfirmation = billState.privatePaymentConfirmation - val socialPaymentConfirmation = billState.socialUserPaymentConfirmation - - listOf(loginConfirmation, paymentConfirmation, socialPaymentConfirmation).any { - it?.showScrim == true - } - } - } - - val scrimAlpha by animateFloatAsState(if (showScrim) 1f else 0f, label = "scrim visibility") - - if (showScrim) { - Box( - modifier = Modifier - .fillMaxSize() - .alpha(scrimAlpha) - .background(Black40) - .rememberedClickable(indication = null, - interactionSource = remember { MutableInteractionSource() }) {} - ) - } - - // Payment Confirmation container - AnimatedContent( - modifier = Modifier.align(BottomCenter), - targetState = sessionState.billState.privatePaymentConfirmation?.payload, // payload is constant across state changes - transitionSpec = AnimationUtils.modalAnimationSpec(), - label = "payment confirmation", - ) { - if (it != null) { - Box( - contentAlignment = BottomCenter - ) { - PaymentConfirmation( - confirmation = sessionState.billState.privatePaymentConfirmation, - balance = sessionState.balance, - onAddKin = { - session.rejectPayment() - if (sessionState.buyModule.enabled) { - if (sessionState.buyModule.available) { - navigator.show(BuyMoreKinModal(showClose = true)) - } else { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = context.getString(R.string.error_title_buyModuleUnavailable), - message = context.getString(R.string.error_description_buyModuleUnavailable), - type = TopBarManager.TopBarMessageType.ERROR - ) - ) - } - } else { - navigator.show(BuySellScreen) - } - }, - onSend = { session.completePayment() }, - onCancel = { - session.rejectPayment() - } - ) - } - } - } - - // Login Confirmation container - AnimatedContent( - modifier = Modifier.align(BottomCenter), - targetState = sessionState.billState.loginConfirmation?.payload, // payload is constant across state changes - transitionSpec = AnimationUtils.modalAnimationSpec(), - label = "login confirmation", - ) { - if (it != null) { - Box( - contentAlignment = BottomCenter - ) { - LoginConfirmation( - confirmation = sessionState.billState.loginConfirmation, - onSend = { session.completeLogin() }, - onCancel = { - session.rejectLogin() - } - ) - } - } - } - - // Social Payment Confirmation container - AnimatedContent( - modifier = Modifier.align(BottomCenter), - targetState = sessionState.billState.socialUserPaymentConfirmation?.payload, // payload is constant across state changes - transitionSpec = AnimationUtils.modalAnimationSpec(speed = ModalAnimationSpeed.Fast), - label = "tip confirmation", - ) { - if (it != null) { - Box( - contentAlignment = BottomCenter - ) { - TipConfirmation( - confirmation = sessionState.billState.socialUserPaymentConfirmation, - onSend = { - if (sessionState.billState.socialUserPaymentConfirmation?.isPrivate == true) { - session.completePrivatePayment() - } else { - session.completeTipPayment() - } - }, - onCancel = { session.cancelTip() } - ) - } - } - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/ui/modals/LoginConfirmation.kt b/apps/codeApp/src/main/java/com/getcode/ui/modals/LoginConfirmation.kt deleted file mode 100644 index 000b7318e..000000000 --- a/apps/codeApp/src/main/java/com/getcode/ui/modals/LoginConfirmation.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.getcode.ui.modals - -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import com.getcode.R -import com.getcode.models.ConfirmationState -import com.getcode.models.LoginConfirmation -import com.getcode.theme.CodeTheme -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.components.Modal -import com.getcode.ui.components.SlideToConfirm -import com.getcode.ui.components.SlideToConfirmDefaults - -@Composable -internal fun LoginConfirmation( - modifier: Modifier = Modifier, - confirmation: LoginConfirmation?, - onSend: () -> Unit, - onCancel: () -> Unit, -) { - val state by remember(confirmation?.state) { - derivedStateOf { confirmation?.state } - } - - val isSending by remember(state) { - derivedStateOf { state is ConfirmationState.Sending } - } - - val domain by remember(confirmation?.domain) { - derivedStateOf { - confirmation?.domain?.urlString?.replaceFirstChar { - if (it.isLowerCase()) it.titlecase() else it.toString() - } - } - } - - Modal(modifier, backgroundColor = Color.Black) { - domain?.let { - Text( - text = it, - color = Color.White, - style = CodeTheme.typography.displaySmall - ) - SlideToConfirm( - isLoading = isSending, - trackColor = SlideToConfirmDefaults.BlackTrackColor, - isSuccess = state is ConfirmationState.Sent, - onConfirm = { onSend() }, - label = stringResource(R.string.action_swipeToLogin) - ) - } - - val enabled = !isSending && state !is ConfirmationState.Sent - val alpha by animateFloatAsState(targetValue = if (enabled) 1f else 0f, label = "alpha") - CodeButton( - modifier = Modifier.fillMaxWidth().alpha(alpha), - enabled = enabled, - buttonState = ButtonState.Subtle, - onClick = onCancel, - text = stringResource(id = android.R.string.cancel), - ) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/ui/modals/PaymentConfirmation.kt b/apps/codeApp/src/main/java/com/getcode/ui/modals/PaymentConfirmation.kt deleted file mode 100644 index 80be81329..000000000 --- a/apps/codeApp/src/main/java/com/getcode/ui/modals/PaymentConfirmation.kt +++ /dev/null @@ -1,302 +0,0 @@ -package com.getcode.ui.modals - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.getcode.R -import com.getcode.services.model.CodePayload -import com.getcode.model.CurrencyCode -import com.getcode.model.Fiat -import com.getcode.model.Kin.Companion.fromFiat -import com.getcode.model.KinAmount -import com.getcode.services.model.Kind -import com.getcode.model.Rate -import com.getcode.model.fromFiatAmount -import com.getcode.models.PrivatePaymentConfirmation -import com.getcode.models.ConfirmationState -import com.getcode.theme.CodeTheme -import com.getcode.theme.DesignSystem -import com.getcode.theme.bolded -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.components.Modal -import com.getcode.ui.components.PriceWithFlag -import com.getcode.ui.components.SlideToConfirm -import com.getcode.ui.components.SlideToConfirmDefaults -import kotlinx.coroutines.delay - -@Composable -internal fun PaymentConfirmation( - modifier: Modifier = Modifier, - balance: KinAmount?, - confirmation: PrivatePaymentConfirmation?, - onAddKin: () -> Unit = { }, - onSend: () -> Unit, - onCancel: () -> Unit, -) { - val state by remember(confirmation?.state) { - derivedStateOf { confirmation?.state } - } - - val isSending by remember(state) { - derivedStateOf { state is ConfirmationState.Sending } - } - - val requestedAmount by remember(confirmation?.localAmount?.kin?.quarks) { - derivedStateOf { confirmation?.localAmount } - } - - Modal(modifier) { - val amount = requestedAmount - if (state != null && amount != null && balance != null) { - val balanceAmount = remember { - balance.kin - } - - if (balanceAmount >= amount.kin) { - PaymentConfirmationContent( - amount = amount, - isSending = isSending, - state = state, - onApproved = onSend - ) - } else { - InsufficientFundsModalContent(onAddKin) - } - val enabled = !isSending && state !is ConfirmationState.Sent - val alpha by animateFloatAsState(targetValue = if (enabled) 1f else 0f, label = "alpha") - CodeButton( - modifier = Modifier.fillMaxWidth().alpha(alpha), - enabled = enabled, - buttonState = ButtonState.Subtle, - onClick = onCancel, - text = stringResource(id = android.R.string.cancel), - ) - } - } -} - -private val usd_fx = 0.00001585 -private val USD_Rate = Rate(usd_fx, CurrencyCode.USD) - -private val payload = CodePayload( - Kind.RequestPayment, - value = Fiat(CurrencyCode.USD, 0.25), - nonce = listOf( - -85, -37, -27, -38, 37, -1, -4, -128, 102, 123, -35 - ).map { it.toByte() } -) - -private fun confirmationWithState(state: ConfirmationState) = PrivatePaymentConfirmation( - state = state, - payload = payload, - requestedAmount = KinAmount.fromFiatAmount( - fiat = 0.25, - fx = usd_fx, - CurrencyCode.USD - ), - localAmount = KinAmount.fromFiatAmount( - fiat = 0.25, - fx = usd_fx, - CurrencyCode.USD - ), -) - -@Preview(showBackground = true) -@Composable -fun Preview_PaymentConfirmModal_Awaiting() { - DesignSystem { - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.White) - ) { - PaymentConfirmation( - modifier = Modifier.align(Alignment.BottomCenter), - confirmation = confirmationWithState(ConfirmationState.AwaitingConfirmation), - balance = KinAmount.newInstance(fromFiat(1_000.0, usd_fx), USD_Rate), - onSend = { } - ) { - - } - } - } -} - -@Preview(showBackground = true) -@Composable -fun Preview_PaymentConfirmModal_Sending() { - DesignSystem { - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.White) - ) { - PaymentConfirmation( - modifier = Modifier.align(Alignment.BottomCenter), - confirmation = confirmationWithState(ConfirmationState.Sending), - balance = KinAmount.newInstance(fromFiat(1_000.0, usd_fx), USD_Rate), - onSend = { } - ) { - - } - } - } -} - -@Preview(showBackground = true) -@Composable -fun Preview_PaymentConfirmModal_Sent() { - DesignSystem { - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.White) - ) { - PaymentConfirmation( - modifier = Modifier.align(Alignment.BottomCenter), - confirmation = confirmationWithState(ConfirmationState.Sent), - balance = KinAmount.newInstance(fromFiat(1_000.0, usd_fx), USD_Rate), - onSend = { } - ) { - - } - } - } -} - -@Preview(showBackground = true) -@Composable -fun Preview_PaymentConfirmModal_Interactive() { - DesignSystem { - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.White) - ) { - var confirmation by remember { - mutableStateOf( - PrivatePaymentConfirmation( - state = ConfirmationState.AwaitingConfirmation, - payload = payload, - requestedAmount = KinAmount.fromFiatAmount( - fiat = 0.25, - fx = usd_fx, - CurrencyCode.USD - ), - localAmount = KinAmount.fromFiatAmount( - fiat = 0.25, - fx = usd_fx, - CurrencyCode.USD - ), - ) - ) - } - - AnimatedContent( - modifier = Modifier.align(Alignment.BottomCenter), - targetState = confirmation?.payload, - transitionSpec = { - slideIntoContainer( - towards = AnimatedContentTransitionScope.SlideDirection.Up, - animationSpec = tween(durationMillis = 600, delayMillis = 450) - ) togetherWith slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Down) - }, - label = "payment confirmation", - ) { - // uses static payload for animation criteria; renders off state - if (it != null) { - Box( - contentAlignment = Alignment.BottomCenter - ) { - PaymentConfirmation( - confirmation = confirmation, - balance = KinAmount.newInstance(fromFiat(1_000.0, usd_fx), USD_Rate), - onSend = { - confirmation = confirmation?.copy(state = ConfirmationState.Sending) - }, - onCancel = { confirmation = null } - ) - } - } - } - - LaunchedEffect(confirmation?.state) { - val state = confirmation?.state - if (state is ConfirmationState.Sending) { - delay(1_500) - confirmation = confirmation?.copy(state = ConfirmationState.Sent) - } else if (state is ConfirmationState.Sent) { - delay(500) - confirmation = null - } - } - } - } -} - -@Composable -private fun PaymentConfirmationContent( - amount: KinAmount, - isSending: Boolean, - state: ConfirmationState?, - onApproved: () -> Unit -) { - PriceWithFlag( - currencyCode = amount.rate.currency, - amount = amount, - iconSize = 24.dp - ) { - Text( - text = it, - color = Color.White, - style = CodeTheme.typography.displayMedium.bolded() - ) - } - SlideToConfirm( - isLoading = isSending, - isSuccess = state is ConfirmationState.Sent, - onConfirm = { onApproved() }, - ) -} - -@Composable -private fun InsufficientFundsModalContent(onClick: () -> Unit) { - Text( - text = stringResource(R.string.title_insufficientFunds), - color = Color.White, - style = CodeTheme.typography.displaySmall - ) - Text( - text = stringResource(R.string.subtitle_insufficientFundsDescription), - color = Color.White, - style = CodeTheme.typography.textSmall - ) - CodeButton( - modifier = Modifier.fillMaxWidth(), - onClick = onClick, - text = stringResource(R.string.title_getMoreKin), - buttonState = ButtonState.Filled - ) -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/ui/modals/ReceivedKinConfirmation.kt b/apps/codeApp/src/main/java/com/getcode/ui/modals/ReceivedKinConfirmation.kt deleted file mode 100644 index 1c3246bae..000000000 --- a/apps/codeApp/src/main/java/com/getcode/ui/modals/ReceivedKinConfirmation.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.getcode.ui.modals - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import com.getcode.R -import com.getcode.models.Bill -import com.getcode.theme.Brand -import com.getcode.theme.CodeTheme -import com.getcode.theme.White -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.components.Modal -import com.getcode.utils.flagResId -import com.getcode.extensions.formatted -import com.getcode.ui.components.text.AmountArea - -@Composable -internal fun ReceivedKinConfirmation( - bill: Bill.Cash, - onClaim: () -> Unit, -) { - Modal { - Text( - modifier = Modifier.padding(top = CodeTheme.dimens.grid.x3), - style = CodeTheme.typography.textLarge.copy( - fontWeight = FontWeight.Bold - ), - color = White, - text = stringResource(id = R.string.subtitle_youReceived) - ) - - Row { - AmountArea( - amountText = bill.amount.formatted(), - currencyResId = bill.amount.rate.currency.flagResId, - isClickable = false - ) - - } - CodeButton( - modifier = Modifier.fillMaxWidth(), - onClick = onClaim, - buttonState = ButtonState.Filled, - text = stringResource(id = R.string.action_putInWallet) - ) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/ui/modals/TipConfirmation.kt b/apps/codeApp/src/main/java/com/getcode/ui/modals/TipConfirmation.kt deleted file mode 100644 index 1631f5e8d..000000000 --- a/apps/codeApp/src/main/java/com/getcode/ui/modals/TipConfirmation.kt +++ /dev/null @@ -1,124 +0,0 @@ -package com.getcode.ui.modals - -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Divider -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImage -import coil3.compose.LocalPlatformContext -import coil3.request.ImageRequest -import coil3.request.error -import com.getcode.model.TwitterUser -import com.getcode.models.ConfirmationState -import com.getcode.models.SocialUserPaymentConfirmation -import com.getcode.theme.CodeTheme -import com.getcode.theme.bolded -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.components.Modal -import com.getcode.ui.components.PriceWithFlag -import com.getcode.ui.components.R -import com.getcode.ui.components.SlideToConfirm -import com.getcode.ui.components.SlideToConfirmDefaults -import com.getcode.ui.components.TwitterUsernameDisplay - -@Composable -fun TipConfirmation( - modifier: Modifier = Modifier, - confirmation: SocialUserPaymentConfirmation?, - trackColor: Color = SlideToConfirmDefaults.ThemedColor, - onSend: () -> Unit, - onCancel: () -> Unit, -) { - val state by remember(confirmation?.state) { - derivedStateOf { confirmation?.state } - } - - val isSending by remember(state) { - derivedStateOf { state is ConfirmationState.Sending } - } - - Modal(modifier) { - AsyncImage( - modifier = Modifier - .padding(top = CodeTheme.dimens.grid.x12) - .size(72.dp) - .clip(CircleShape), - model = ImageRequest.Builder(LocalPlatformContext.current) - .data(confirmation?.imageUrl) - .error(R.drawable.ic_placeholder_user) - .placeholderMemoryCacheKey(confirmation?.metadata?.username) - .build(), - contentDescription = null, - ) - - TwitterUsernameDisplay( - modifier = Modifier.fillMaxWidth(), - username = confirmation?.metadata?.username.orEmpty(), - verificationStatus = (confirmation?.metadata as? TwitterUser)?.verificationStatus - ) - if (confirmation?.followerCountFormatted != null) { - Text( - text = "${confirmation.followerCountFormatted} Followers", - color = CodeTheme.colors.textSecondary, - style = CodeTheme.typography.textSmall - ) - } - - Divider( - modifier = Modifier.padding(vertical = CodeTheme.dimens.grid.x8), - color = CodeTheme.colors.divider, - ) - - val amount by remember(confirmation?.amount) { - derivedStateOf { confirmation?.amount } - } - - amount?.let { - PriceWithFlag( - currencyCode = it.rate.currency, - amount = it, - iconSize = 24.dp - ) { text -> - Text( - text = text, - color = Color.White, - style = CodeTheme.typography.displayMedium.bolded() - ) - } - } - - SlideToConfirm( - isLoading = isSending, - isSuccess = state is ConfirmationState.Sent, - trackColor = trackColor, - onConfirm = { onSend() }, - label = stringResource(R.string.action_swipeToTip) - ) - - val enabled = !isSending && state !is ConfirmationState.Sent - val alpha by animateFloatAsState(targetValue = if (enabled) 1f else 0f, label = "alpha") - CodeButton( - modifier = Modifier - .fillMaxWidth() - .alpha(alpha), - enabled = enabled, - buttonState = ButtonState.Subtle, - onClick = onCancel, - text = stringResource(id = android.R.string.cancel), - ) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/ui/theme/CodeTheme.kt b/apps/codeApp/src/main/java/com/getcode/ui/theme/CodeTheme.kt deleted file mode 100644 index 09a3a2094..000000000 --- a/apps/codeApp/src/main/java/com/getcode/ui/theme/CodeTheme.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.getcode.ui.theme - -import androidx.compose.runtime.Composable -import com.getcode.theme.DesignSystem - -@Composable -fun CodeTheme(content: @Composable () -> Unit) { - DesignSystem(content = content) -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/ui/tips/DefinedTips.kt b/apps/codeApp/src/main/java/com/getcode/ui/tips/DefinedTips.kt deleted file mode 100644 index c50d5bf37..000000000 --- a/apps/codeApp/src/main/java/com/getcode/ui/tips/DefinedTips.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.getcode.ui.tips - -import com.getcode.ui.tips.definitions.DownloadCodeTip -import dev.bmcreations.tipkit.engines.TipInterface -import javax.inject.Inject - -class DefinedTips @Inject constructor( - val downloadCodeTip: DownloadCodeTip -): TipInterface - - diff --git a/apps/codeApp/src/main/java/com/getcode/ui/tips/definitions/DownloadCodeTip.kt b/apps/codeApp/src/main/java/com/getcode/ui/tips/definitions/DownloadCodeTip.kt deleted file mode 100644 index 7b83f316e..000000000 --- a/apps/codeApp/src/main/java/com/getcode/ui/tips/definitions/DownloadCodeTip.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.getcode.ui.tips.definitions - -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.getcode.R -import com.getcode.manager.SessionManager -import com.getcode.theme.CodeTheme -import dev.bmcreations.tipkit.Tip -import dev.bmcreations.tipkit.engines.EligibilityCriteria -import dev.bmcreations.tipkit.engines.EventEngine -import dev.bmcreations.tipkit.engines.TipsEngine -import dev.bmcreations.tipkit.engines.Trigger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.firstOrNull -import kotlin.time.Clock -import kotlinx.datetime.Instant -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.time.Duration.Companion.days - - -@Singleton -class DownloadCodeTip @Inject constructor( - eventEngine: EventEngine, - tipsEngine: TipsEngine, -): Tip(eventEngine, tipsEngine), CoroutineScope by CoroutineScope(Dispatchers.IO) { - - val homeOpen = Trigger( - id = "home-open", - engine = eventEngine - ).also { await(it) } - - override fun showClose(): Boolean = false - - override fun message(): @Composable () -> Unit { - return { - Text( - text = stringResource(R.string.tooltip_tapLogo), - style = CodeTheme.typography.textMedium - ) - } - } - - override suspend fun criteria(): List { - val homeOpenCount = homeOpen.events.firstOrNull().orEmpty().count() - - - val now = Clock.System.now() - return listOf( - { homeOpenCount >= 1 }, - { - val accountCreatedAtMillis = SessionManager.getOrganizer()?.createdAtMillis - val millis = accountCreatedAtMillis ?: return@listOf false - val createdAt = Instant.fromEpochMilliseconds(millis) - createdAt.plus(1.days) < now - } - ) - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/util/AccountAuthenticator.kt b/apps/codeApp/src/main/java/com/getcode/util/AccountAuthenticator.kt deleted file mode 100644 index 4327adac6..000000000 --- a/apps/codeApp/src/main/java/com/getcode/util/AccountAuthenticator.kt +++ /dev/null @@ -1,91 +0,0 @@ -package com.getcode.util - -import android.accounts.* -import android.content.Context -import android.os.Bundle -import androidx.core.os.bundleOf -import com.getcode.utils.trace - - -class AccountAuthenticator( - private val context: Context, -) : AbstractAccountAuthenticator(context) { - @Throws(NetworkErrorException::class) - override fun addAccount( - response: AccountAuthenticatorResponse, - accountType: String, - authTokenType: String, - requiredFeatures: Array, - options: Bundle - ): Bundle = Bundle() - - @Throws(NetworkErrorException::class) - override fun confirmCredentials( - arg0: AccountAuthenticatorResponse, - arg1: Account, arg2: Bundle - ): Bundle? = null - - override fun editProperties(arg0: AccountAuthenticatorResponse, arg1: String): Bundle? = null - - @Throws(NetworkErrorException::class) - override fun getAuthToken( - response: AccountAuthenticatorResponse, - account: Account, - authTokenType: String, - options: Bundle - ): Bundle { - // Extract the username and password from the Account Manager, then, generate token - val am = AccountManager.get(context) - var authToken = am.peekAuthToken(account, authTokenType) - trace("authenticator: authToken ${authToken != null}, $authTokenType") - // Lets give another try to authenticate the user - if (null != authToken) { - if (authToken.isEmpty()) { - val password = am.getPassword(account) - if (password != null) { - authToken = "test" - } - } - } - - // If we get an authToken - we return it - if (null != authToken) { - if (authToken.isNotEmpty()) { - val result = Bundle() - result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name) - result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type) - result.putString(AccountManager.KEY_AUTHTOKEN, authToken) - return result - } - } - - trace( - message = "authenticator failure", - error = Throwable("Failed to retrieve authToken from AccountManager") - ) - // If we get here, then we couldn't access the user's password - return Bundle() - } - - override fun getAuthTokenLabel(arg0: String): String? { - return "entropy" - } - - @Throws(NetworkErrorException::class) - override fun hasFeatures( - arg0: AccountAuthenticatorResponse, arg1: Account, - arg2: Array - ): Bundle { - // This call is used to query whether the Authenticator supports - // specific features. We don't expect to get called, so we always - // return false (no) for any queries. - val result = bundleOf(AccountManager.KEY_BOOLEAN_RESULT to false) - return result - } - - @Throws(NetworkErrorException::class) - override fun updateCredentials( - arg0: AccountAuthenticatorResponse, - arg1: Account, arg2: String, arg3: Bundle - ): Bundle? = null -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/util/AccountUtils.kt b/apps/codeApp/src/main/java/com/getcode/util/AccountUtils.kt deleted file mode 100644 index 642fbace3..000000000 --- a/apps/codeApp/src/main/java/com/getcode/util/AccountUtils.kt +++ /dev/null @@ -1,147 +0,0 @@ -package com.getcode.util - -import android.accounts.Account -import android.accounts.AccountManager -import android.accounts.AuthenticatorException -import android.content.Context -import android.os.Handler -import android.os.HandlerThread -import androidx.core.os.bundleOf -import com.getcode.BuildConfig -import com.getcode.utils.TraceType -import com.getcode.utils.network.retryable -import com.getcode.utils.trace -import io.reactivex.rxjava3.annotations.NonNull -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.subjects.SingleSubject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlin.time.Clock -import kotlin.coroutines.resume - - -object AccountUtils { - private const val ACCOUNT_TYPE = BuildConfig.APPLICATION_ID - - fun addAccount( - context: Context, - name: String, - password: String, - token: String - ) { - val accountManager: AccountManager = AccountManager.get(context) - val account = Account(name, ACCOUNT_TYPE) - - val data = bundleOf(AccountManager.KEY_AUTH_TOKEN_LABEL to "entropy") - accountManager.addAccountExplicitly(account, password, data) - accountManager.setAuthToken(account, ACCOUNT_TYPE, token) - } - - suspend fun removeAccounts(context: Context): @NonNull Single { - return getAccount(context) - .map { - if (it.second == null) return@map false - val am: AccountManager = AccountManager.get(context) - am.removeAccountExplicitly(it.second) - } - } - - private suspend fun getAccount(context: Context): @NonNull Single> { - val subject = SingleSubject.create>() - return subject.doOnSubscribe { - CoroutineScope(Dispatchers.IO).launch { - val result = retryable( - call = { getAccountNoActivity(context) }, - onRetry = { currentAttempt -> - trace( - tag = "Account", - message = "Retrying call", - metadata = { - "count" to currentAttempt - }, - type = TraceType.Process, - ) - } - ) - subject.onSuccess(result ?: (null to null)) - } - } - } - - private val handler: Handler by lazy { Handler(handlerThread.looper) } - - private val handlerThread: HandlerThread by lazy { - HandlerThread("RenetikBackgroundThread").apply { - setUncaughtExceptionHandler { _, e -> run { throw RuntimeException(e) } } - start() - } - } - - private suspend fun getAccountNoActivity( - context: Context - ): Pair? = suspendCancellableCoroutine { cont -> - trace("getAuthToken", type = TraceType.Silent) - val am: AccountManager = AccountManager.get(context) - val account = am.getAccountsByType(ACCOUNT_TYPE).firstOrNull() - if (account == null) { - trace( - "no associated account found", - type = TraceType.Error - ) - cont.resume(null) - return@suspendCancellableCoroutine - } - val start = Clock.System.now() - am.getAuthToken( - account, ACCOUNT_TYPE, null, false, - { future -> - try { - val bundle = future?.result - val authToken = bundle?.getString(AccountManager.KEY_AUTHTOKEN) - - val end = Clock.System.now() - trace("auth token fetch took ${end.toEpochMilliseconds() - start.toEpochMilliseconds()} ms") - - cont.resume(authToken.orEmpty() to account) - } catch (e: AuthenticatorException) { - trace( - message = "failed to read account", - error = e, - type = TraceType.Error - ) - cont.resume(null) - } - }, handler - ) - } - - suspend fun getToken(context: Context): String? { - val accountManager = AccountManager.get(context) - val account = accountManager.accounts.firstOrNull() - if (account != null) { - val token = runCatching { accountManager.peekAuthToken(account, ACCOUNT_TYPE) } - .getOrNull()?.takeIf { it.isNotEmpty() } - - if (token != null) { - return token - } - } - - return retryable( - call = { getAccountNoActivity(context) }, - onRetry = { currentAttempt -> - trace( - tag = "Account", - message = "Retrying call", - metadata = { - "count" to currentAttempt - }, - type = TraceType.Process, - ) - } - - )?.first - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/util/AndroidLocale.kt b/apps/codeApp/src/main/java/com/getcode/util/AndroidLocale.kt deleted file mode 100644 index 20037ba24..000000000 --- a/apps/codeApp/src/main/java/com/getcode/util/AndroidLocale.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.getcode.util - -import android.content.Context -import com.getcode.model.Currency -import com.getcode.util.locale.LocaleHelper -import com.getcode.util.locale.LocaleUtils -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject - -class AndroidLocale @Inject constructor( - @ApplicationContext private val context: Context, - private val currencyUtils: com.getcode.utils.CurrencyUtils, -): LocaleHelper { - override fun getDefaultCurrencyName(): String { - return LocaleUtils.getDefaultCurrency(context) - } - - override fun getDefaultCountry(): String { - return LocaleUtils.getDefaultCountry(context) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/util/AndroidPermissions.kt b/apps/codeApp/src/main/java/com/getcode/util/AndroidPermissions.kt deleted file mode 100644 index 02e2fc6a9..000000000 --- a/apps/codeApp/src/main/java/com/getcode/util/AndroidPermissions.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.getcode.util - -import android.content.Context -import android.content.pm.PackageManager -import androidx.core.content.ContextCompat -import com.getcode.util.permissions.PermissionChecker -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject - -class AndroidPermissions @Inject constructor( - @ApplicationContext private val context: Context -) : PermissionChecker { - override fun isGranted(permission: String): Boolean { - return check(permission) == PackageManager.PERMISSION_GRANTED - } - - override fun isDenied(permission: String): Boolean { - return check(permission) == PackageManager.PERMISSION_DENIED - } - - override fun check(permission: String): Int { - return ContextCompat.checkSelfPermission( - context, - permission, - ) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/util/AuthenticatorService.kt b/apps/codeApp/src/main/java/com/getcode/util/AuthenticatorService.kt deleted file mode 100644 index b53df149b..000000000 --- a/apps/codeApp/src/main/java/com/getcode/util/AuthenticatorService.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.getcode.util - -import android.accounts.AccountManager -import android.app.Service -import android.content.Intent -import android.os.IBinder -import dagger.hilt.android.AndroidEntryPoint - - -@AndroidEntryPoint -class AuthenticatorService : Service() { - private val accountAuthenticator: AccountAuthenticator by lazy { - AccountAuthenticator(this) - } - - override fun onBind(intent: Intent): IBinder? { - var binder: IBinder? = null - if (intent.action == AccountManager.ACTION_AUTHENTICATOR_INTENT) { - binder = accountAuthenticator.iBinder - } - return binder - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/util/Bitmap.kt b/apps/codeApp/src/main/java/com/getcode/util/Bitmap.kt deleted file mode 100644 index db99825d2..000000000 --- a/apps/codeApp/src/main/java/com/getcode/util/Bitmap.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.getcode.util - -import android.graphics.Bitmap -import com.getcode.utils.ErrorUtils -import java.io.File -import java.io.FileOutputStream - -fun Bitmap.toByteArray(): ByteArray = getLuminanceData() - -private fun Bitmap.getLuminanceData(): ByteArray { - val width = this.width - val height = this.height - val pixelData = IntArray(width * height) - this.getPixels(pixelData, 0, width, 0, 0, width, height) - - val luminanceData = ByteArray(width * height) - - for (y in 0 until height) { - for (x in 0 until width) { - val pixel = pixelData[y * width + x] - - val r = (pixel shr 16) and 0xFF - val g = (pixel shr 8) and 0xFF - val b = pixel and 0xFF - - val luminance = (0.299 * r + 0.587 * g + 0.114 * b).toInt() - luminanceData[y * width + x] = luminance.toByte() - } - } - - return luminanceData -} - -internal fun Bitmap.save(destination: File, name: () -> String): Boolean { - val filename = name() - if (!destination.exists()) { - destination.mkdirs() - } - val dest = File(destination, filename) - - try { - val out = FileOutputStream(dest) - compress(Bitmap.CompressFormat.PNG, 90, out) - out.flush() - out.close() - } catch (e: Exception) { - e.printStackTrace() - ErrorUtils.handleError(e) - return false - } - return true -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/util/Build.kt b/apps/codeApp/src/main/java/com/getcode/util/Build.kt deleted file mode 100644 index 1cd723f34..000000000 --- a/apps/codeApp/src/main/java/com/getcode/util/Build.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.getcode.util - -import android.os.Build - -val isEmulator: Boolean - get() = (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) - || Build.FINGERPRINT.startsWith("generic") - || Build.FINGERPRINT.startsWith("unknown") - || Build.HARDWARE.contains("goldfish") - || Build.HARDWARE.contains("ranchu") - || Build.MODEL.contains("google_sdk") - || Build.MODEL.contains("Emulator") - || Build.MODEL.contains("Android SDK built for x86") - || Build.MANUFACTURER.contains("Genymotion") - || Build.PRODUCT.contains("sdk_google") - || Build.PRODUCT.contains("google_sdk") - || Build.PRODUCT.contains("sdk") - || Build.PRODUCT.contains("sdk_x86") - || Build.PRODUCT.contains("sdk_gphone64_arm64") - || Build.PRODUCT.contains("vbox86p") - || Build.PRODUCT.contains("emulator") - || Build.PRODUCT.contains("simulator") \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/util/ChromeTabsUtils.kt b/apps/codeApp/src/main/java/com/getcode/util/ChromeTabsUtils.kt deleted file mode 100644 index 4d68cee16..000000000 --- a/apps/codeApp/src/main/java/com/getcode/util/ChromeTabsUtils.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.getcode.util - -import android.content.ComponentName -import android.content.Context -import android.net.Uri -import androidx.browser.customtabs.CustomTabColorSchemeParams -import androidx.browser.customtabs.CustomTabsClient -import androidx.browser.customtabs.CustomTabsIntent -import androidx.browser.customtabs.CustomTabsServiceConnection -import androidx.browser.customtabs.CustomTabsSession -import androidx.compose.ui.graphics.Color -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.toBitmap -import com.getcode.R -import com.getcode.theme.Brand -import com.getcode.ui.utils.toAGColor - -object ChromeTabsUtils { - fun launchUrl( - context: Context, - url: String, - showBack: Boolean = false - ) { - val mCustomTabsServiceConnection: CustomTabsServiceConnection? - var mClient: CustomTabsClient? - var mCustomTabsSession: CustomTabsSession? = null - mCustomTabsServiceConnection = object : CustomTabsServiceConnection() { - override fun onCustomTabsServiceConnected( - componentName: ComponentName, - customTabsClient: CustomTabsClient - ) { - mClient = customTabsClient - mClient?.warmup(0L) - mCustomTabsSession = mClient?.newSession(null) - } - - override fun onServiceDisconnected(name: ComponentName) { - mClient = null - } - } - CustomTabsClient.bindCustomTabsService( - context, - "com.android.chrome", - mCustomTabsServiceConnection - ) - val builder: CustomTabsIntent.Builder = CustomTabsIntent.Builder(mCustomTabsSession) - .setShowTitle(false) - .setShareState(CustomTabsIntent.SHARE_STATE_OFF) - .setInstantAppsEnabled(false) - .setColorSchemeParams( - CustomTabsIntent.COLOR_SCHEME_DARK, - CustomTabColorSchemeParams.Builder() - .setToolbarColor(Brand.toAGColor()) - .setNavigationBarDividerColor(Color.Transparent.toAGColor()) - .setNavigationBarColor(Color.Transparent.toAGColor()) - .build() - ) - - if (showBack) { - ContextCompat.getDrawable( - context, - R.drawable.ic_arrow_back - )?.toBitmap()?.let { backArrow -> - builder.setCloseButtonIcon(backArrow) - } - } - - val customTabsIntent = builder.build() - - customTabsIntent.launchUrl(context, Uri.parse(url)) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/util/Context.kt b/apps/codeApp/src/main/java/com/getcode/util/Context.kt deleted file mode 100644 index 84c66273e..000000000 --- a/apps/codeApp/src/main/java/com/getcode/util/Context.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.getcode.util - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.ImageDecoder -import android.net.Uri -import android.os.Build -import android.provider.MediaStore.Images.Media -import androidx.core.content.ContextCompat -import com.getcode.R - - -fun Context.launchAppSettings() { - val intent = IntentUtils.appSettings() - ContextCompat.startActivity(this, intent, null) -} - -fun Context.launchSmsIntent(phoneValue: String, message: String) { - val intent = IntentUtils.sendSms(phoneValue, message) - ContextCompat.startActivity(this, intent, null) -} - -fun Context.shareDownloadLink() { - val shareRef = getString(R.string.app_download_link_share_ref) - val url = getString(R.string.app_download_link_with_ref, shareRef) - val intent = IntentUtils.share(url) - ContextCompat.startActivity(this, intent, null) -} - -fun Context.uriToBitmap(uri: Uri): Bitmap? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - val source = ImageDecoder.createSource(contentResolver, uri) - ImageDecoder.decodeBitmap(source) { decoder, _, _ -> - decoder.isMutableRequired = true - } - } else { - Media.getBitmap(contentResolver, uri) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/util/DeeplinkHandler.kt b/apps/codeApp/src/main/java/com/getcode/util/DeeplinkHandler.kt deleted file mode 100644 index 9b2e94eec..000000000 --- a/apps/codeApp/src/main/java/com/getcode/util/DeeplinkHandler.kt +++ /dev/null @@ -1,244 +0,0 @@ -package com.getcode.util - -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Parcelable -import androidx.core.net.toUri -import cafe.adriel.voyager.core.screen.Screen -import com.getcode.R -import com.getcode.services.model.PrefsBool -import com.getcode.models.DeepLinkRequest -import com.getcode.navigation.screens.ScanScreen -import com.getcode.navigation.screens.LoginScreen -import com.getcode.network.repository.BetaFlagsRepository -import com.getcode.services.utils.base64EncodedData -import com.getcode.utils.TraceType -import com.getcode.utils.trace -import com.getcode.utils.urlDecode -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.MutableStateFlow -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -data class DeeplinkResult( - val type: DeeplinkHandler.Type, - val stack: List, -) - -/** - * This class is used to manage intent state across navigation. - * - * This hack is in place because determining the users authenticated - * state takes a long time. The app will try to handle incoming intents - * before the authentication state is determined. - * This class caches the incoming deeplink intent so the navigation controller - * does not override the intent sent by handleDeepLink(intent) in the main activity. - * - * If this was not in place the app would try to handle the deeplink before the - * authentication state is complete and override navigation - dropping the intent - * in favour of the latest request in the navigation graph. - */ -@Singleton -class DeeplinkHandler @Inject constructor( - @ApplicationContext private val context: Context, - private val betaFlags: BetaFlagsRepository -) { - var debounceIntent: Intent? = null - set(value) { - intent.value = value - field = value - trace( - "debounced intent data=${value?.data}", - type = TraceType.Silent - ) - } - - val intent = MutableStateFlow(debounceIntent) - - suspend fun handle(intent: Intent? = debounceIntent): DeeplinkResult? { - var uri = when { - intent?.data != null -> intent.data - intent?.getStringExtra(Intent.EXTRA_TEXT) != null -> { - val sharedLink = intent.getStringExtra(Intent.EXTRA_TEXT)?.toUri() ?: return null - sharedLink.resolveSharedEntity() - } - (intent?.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri != null) -> { - intent.getParcelableExtra(Intent.EXTRA_STREAM) as Uri - } - - else -> null - } ?: return null - - // check for jump - if (uri.host == context.getString(R.string.root_url_jump_no_protocol)) { - val regex = "#source=([^&]*)".toRegex() - val source = regex.find(uri.toString())?.groupValues?.get(1) - if (source != null) { - uri = Uri.parse(source) - } - } - - return when (val type = uri.deeplinkType) { - is Type.Login -> { - DeeplinkResult( - type, - listOf(LoginScreen(type.link)), - ) - } - - is Type.Cash -> { - Timber.d("cash=${type.link}") - DeeplinkResult( - type, - listOf(ScanScreen(cashLink = type.link)), - ) - } - - is Type.Sdk -> { - Timber.d("sdk=${type.payload}") - val request = type.payload?.base64EncodedData()?.let { DeepLinkRequest.from(it) } - DeeplinkResult( - type, - listOf(ScanScreen(request = request)), - ) - } - - is Type.Image -> { - DeeplinkResult( - type, - listOf( - ScanScreen( - request = DeepLinkRequest.fromImage(type.uri) - ) - ) - ) - } - - is Type.Tip -> { - Timber.d("tipcard for ${type.username} on ${type.platform}") - DeeplinkResult( - type, - listOf( - ScanScreen( - request = DeepLinkRequest.fromTipCardUsername( - type.platform, - type.username - ) - ) - ), - ) - } - - is Type.Unknown -> null - } - } - - /** - * Handles converting inbound shared content with possible deeplinks - * e.g sharing a tweet to trigger a tipcard flow - */ - private suspend fun Uri.resolveSharedEntity(): Uri { - when { - this.host == "x.com" || this.host == "twitter.com" -> { - // https://x.com//status/ - if (betaFlags.isEnabled(PrefsBool.SHARE_TWEET_TO_TIP)) { - // convert shared tweets to owner's tip card - val username = pathSegments.firstOrNull() ?: return this - return Uri.parse(Linkify.tipCard(username, "x")) - } - } - } - return this - } - - private val Uri.deeplinkType: Type - get() { - // check if image - val isImage = context.contentResolver.getType(this)?.startsWith("image/") == true - if (isImage) { - return Type.Image(uri = this) - } - - // check for tipcard URLs - val components = pathSegments - if (components.count() >= 2 && components[0] == "x" && components[1].isNotEmpty()) { - return Type.Tip(components[0], components[1]) - } - - return when (val segment = lastPathSegment) { - "login" -> { - var entropy = fragments[Key.entropy] - if (entropy == null) { - entropy = this.getQueryParameter("data") - } - - Type.Login(entropy.also { Timber.d("entropy=$it") }) - } - - "cash", "c" -> Type.Cash(fragments[Key.entropy]) - - // support all variations of SDK request triggers - in Type.Sdk.regex -> Type.Sdk(fragments[Key.payload]?.urlDecode()) - else -> Type.Unknown(path = segment) - } - } - - private val Uri.fragments: Map - get() { - return this.toString().split("/") - .mapNotNull { fragment -> - val data = Key.entries - .map { key -> key to "${key.value}=" } - .filter { (key, prefix) -> fragment.startsWith(prefix) } - .firstNotNullOfOrNull { (key, prefix) -> key to fragment.removePrefix(prefix) } - - data ?: return@mapNotNull null - }.associate { (key, value) -> key to value } - } - - sealed interface Type { - data class Login(val link: String?) : Type - data class Cash(val link: String?) : Type - data class Tip(val platform: String, val username: String) : Type - data class Sdk(val payload: String?) : Type { - companion object { - val regex = Regex("^(login|payment|tip)?-?request-(modal|page)-(mobile|desktop)\$") - } - } - data class Image(val uri: Uri): Type - - data class Unknown(val path: String?) : Type - } - - @Suppress("ClassName") - sealed interface Key { - val value: String - - data object entropy : Key { - override val value: String = "e" - } - - data object payload : Key { - override val value: String = "p" - } - - // unused - data object key : Key { - override val value: String = "k" - } - - // unused - data object data : Key { - override val value: String = "d" - } - - companion object { - val entries = listOf(entropy, payload, key, data) - } - } -} - -private operator fun Regex.contains(text: String?): Boolean = - text?.let { this.matches(it) } ?: false diff --git a/apps/codeApp/src/main/java/com/getcode/util/ErrorUtils.kt b/apps/codeApp/src/main/java/com/getcode/util/ErrorUtils.kt deleted file mode 100644 index d39d152b2..000000000 --- a/apps/codeApp/src/main/java/com/getcode/util/ErrorUtils.kt +++ /dev/null @@ -1,22 +0,0 @@ -@file:Suppress("UnusedReceiverParameter") - -package com.getcode.util - -import android.content.Context -import com.getcode.R -import com.getcode.manager.TopBarManager -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.ErrorUtils - -fun ErrorUtils.showNetworkError(context: Context) = TopBarManager.TopBarMessage( - title = context.getString(R.string.error_title_noInternet), - message = context.getString(R.string.error_description_noInternet), - type = TopBarManager.TopBarMessageType.ERROR_NETWORK -).let { TopBarManager.showMessage(it) } - -fun ErrorUtils.showNetworkError(resources: ResourceHelper) = TopBarManager.TopBarMessage( - title = resources.getString(R.string.error_title_noInternet), - message =resources.getString(R.string.error_description_noInternet), - type = TopBarManager.TopBarMessageType.ERROR_NETWORK -).let { TopBarManager.showMessage(it) } - diff --git a/apps/codeApp/src/main/java/com/getcode/util/IntentLauncher.kt b/apps/codeApp/src/main/java/com/getcode/util/IntentLauncher.kt deleted file mode 100644 index 730a2fb68..000000000 --- a/apps/codeApp/src/main/java/com/getcode/util/IntentLauncher.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.getcode.util - -import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject - -class IntentLauncher @Inject constructor( - @ApplicationContext private val context: Context -) { - fun launchAppSettings() { - context.launchAppSettings() - } - - fun launchSmsIntent(phoneValue: String, message: String) { - context.launchSmsIntent(phoneValue, message) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/util/IntentUtils.kt b/apps/codeApp/src/main/java/com/getcode/util/IntentUtils.kt deleted file mode 100644 index e5b02111c..000000000 --- a/apps/codeApp/src/main/java/com/getcode/util/IntentUtils.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.getcode.util - -import android.content.Intent -import android.net.Uri -import android.provider.Settings -import com.getcode.BuildConfig -import com.getcode.services.utils.makeE164 - - -object IntentUtils { - - fun appSettings() = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { - data = Uri.fromParts("package", BuildConfig.APPLICATION_ID, null) - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - - fun sendSms(phoneValue: String, message: String) = Intent( - Intent.ACTION_SENDTO, - Uri.parse("smsto:${phoneValue.makeE164()}") - ).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - putExtra("sms_body", message) - } - - fun tweet(message: String) = Intent(Intent.ACTION_VIEW).apply { - val url = Linkify.tweet(message) - setData(Uri.parse(url)) - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - - fun share(text: String): Intent { - val sendIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, text) - type = "text/plain" - } - val shareIntent = Intent.createChooser(sendIntent, null) - shareIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - - return shareIntent - } - - fun tipCard(username: String, platform: String): Intent { - val url = Linkify.tipCard(username, platform) - - val sendIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, url) - type = "text/plain" - } - - val shareIntent = Intent.createChooser(sendIntent, null).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - - return shareIntent - } - - fun cashLink( - entropy: String, - formattedAmount: String, - ): Intent { - val url = Linkify.cashLink(entropy) - val text = "$formattedAmount $url" - - val sendIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, text) - type = "text/plain" - } - val shareIntent = Intent.createChooser(sendIntent, null).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - - return shareIntent - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/util/Linkify.kt b/apps/codeApp/src/main/java/com/getcode/util/Linkify.kt deleted file mode 100644 index 525576eba..000000000 --- a/apps/codeApp/src/main/java/com/getcode/util/Linkify.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.getcode.util - -import com.getcode.utils.urlEncode - -object Linkify { - fun cashLink(entropy: String): String = "https://cash.getcode.com/c/#/e=${entropy}" - fun tipCard(username: String, platform: String): String = "https://tipcard.getcode.com/${platform}/${username}" - fun tweet(message: String): String = "https://www.twitter.com/intent/tweet?text=${message.urlEncode()}" -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/util/OtpSmsBroadcastReceiver.kt b/apps/codeApp/src/main/java/com/getcode/util/OtpSmsBroadcastReceiver.kt deleted file mode 100644 index be3ef399c..000000000 --- a/apps/codeApp/src/main/java/com/getcode/util/OtpSmsBroadcastReceiver.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.getcode.util - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.util.Log -import com.google.android.gms.auth.api.phone.SmsRetriever -import com.google.android.gms.common.api.CommonStatusCodes -import com.google.android.gms.common.api.Status -import io.reactivex.rxjava3.core.BackpressureStrategy -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.subjects.BehaviorSubject -import timber.log.Timber -import java.util.concurrent.TimeUnit - -class OtpSmsBroadcastReceiver : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent) { - if (SmsRetriever.SMS_RETRIEVED_ACTION == intent.action) { - val extras: Bundle = intent.extras ?: return - val status: Status? = extras[SmsRetriever.EXTRA_STATUS] as Status? - when (status?.statusCode) { - CommonStatusCodes.SUCCESS -> { - val message: String = - extras[SmsRetriever.EXTRA_SMS_MESSAGE] as String? ?: return - val otpCode = "[0-9]{6}".toRegex().find(message)?.value ?: return - otpCodeSubject.onNext(otpCode) - - Timber.i("success: $message. OTP: $otpCode") - } - CommonStatusCodes.TIMEOUT -> { - Timber.i("timeout") - } - } - } - } - - companion object { - private val otpCodeSubject = BehaviorSubject.create() - val otpCode: Flowable = otpCodeSubject.toFlowable(BackpressureStrategy.DROP).delay(1500L, TimeUnit.MILLISECONDS) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/util/Pacman.kt b/apps/codeApp/src/main/java/com/getcode/util/Pacman.kt deleted file mode 100644 index db2500529..000000000 --- a/apps/codeApp/src/main/java/com/getcode/util/Pacman.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.getcode.util - -import android.content.ComponentName -import android.content.Context -import android.content.pm.PackageManager -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject - - -class Pacman @Inject constructor( - @ApplicationContext private val context: Context -) { - private val packageManager = context.packageManager - - fun enableTweetShare(enable: Boolean) { - val component = ComponentName(context.packageName, "com.getcode.view.TweetShareHandler") - if (enable) { - packageManager.setComponentEnabledSetting( - component, - PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP - ) - } else { - packageManager.setComponentEnabledSetting( - component, - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP - ) - } - } - -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/util/Pair.kt b/apps/codeApp/src/main/java/com/getcode/util/Pair.kt deleted file mode 100644 index 77117f968..000000000 --- a/apps/codeApp/src/main/java/com/getcode/util/Pair.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.getcode.util - -infix fun Pair.to(that: C): Triple = Triple(first, second, that) \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/util/PhoneUtils.kt b/apps/codeApp/src/main/java/com/getcode/util/PhoneUtils.kt deleted file mode 100644 index f7e02de7b..000000000 --- a/apps/codeApp/src/main/java/com/getcode/util/PhoneUtils.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.getcode.util - -import android.content.Context -import android.telephony.PhoneNumberUtils -import com.getcode.utils.CurrencyUtils -import dagger.hilt.android.qualifiers.ApplicationContext -import io.michaelrocks.libphonenumber.android.NumberParseException -import io.michaelrocks.libphonenumber.android.PhoneNumberUtil -import java.util.* -import javax.inject.Inject -import javax.inject.Singleton - - -@Singleton -class PhoneUtils @Inject constructor( - @ApplicationContext private val context: Context, - currencyUtils: CurrencyUtils -) { - var countryLocales: List = listOf() - private var countryCodesMap: Map = mapOf() - var defaultCountryLocale: CountryLocale - - private val phoneNumberUtil = PhoneNumberUtil.createInstance(context) - - init { - countryLocales = phoneNumberUtil.supportedRegions.map { region -> - val countryCode = phoneNumberUtil.getCountryCodeForRegion(region) - val resId: Int? = currencyUtils.getFlag(region) - val displayCountry = Locale(Locale.getDefault().language, region).displayCountry - - CountryLocale( - name = displayCountry, - phoneCode = countryCode, - countryCode = region, - resId = resId - ) - } - .sortedBy { it.name } - .filter { it.resId != null } - - countryCodesMap = countryLocales.map { it }.associateBy { it.phoneCode } - val isoCountry = Locale.getDefault().country - defaultCountryLocale = - countryLocales.find { it.countryCode == isoCountry } ?: countryLocales.first() - } - - data class CountryLocale( - val name: String, - val phoneCode: Int, - val countryCode: String, - val resId: Int? = null - ) - - - fun getCountryCode(number: String): String { - val map = countryCodesMap - for (k in map.keys) { - if (number.startsWith(k.toString())) return map[k]!!.countryCode - } - - return Locale.getDefault().country - } - - fun isPhoneNumberValid(number: String, countryCode: String): Boolean { - var isValid = false - var numberType: PhoneNumberUtil.PhoneNumberType? = null - - try { - //val countryCallingCode: Int = phoneNumberUtil.getCountryCodeForRegion(countryCode) - //val countryCode: String = phoneNumberUtil.getRegionCodeForCountryCode(countryCallingCode) - val phoneNumber = phoneNumberUtil.parse(number, countryCode) - isValid = phoneNumberUtil.isValidNumber(phoneNumber) - numberType = phoneNumberUtil.getNumberType(phoneNumber) - } catch (e: NumberParseException) { - //e.printStackTrace() - } catch (e: NullPointerException) { - //e.printStackTrace() - } catch (e: NumberFormatException) { - //e.printStackTrace() - } - - return isValid && (PhoneNumberUtil.PhoneNumberType.UNKNOWN !== numberType) - } - - fun formatNumber( - number: String - ): String { - val countryCode = getCountryCode(number) - return formatNumber(number, countryCode) - } - - fun formatNumber(number: String, countryCode: String, plus: Boolean = true): String { - val numberFormatted = (PhoneNumberUtils.formatNumber(number, countryCode) ?: number) - return if (plus && !numberFormatted.startsWith("+")) "+$numberFormatted" - else numberFormatted - } - - fun toFlagEmoji(country: String): String { - // 1. It first checks if the string consists of only 2 characters: ISO 3166-1 alpha-2 two-letter country codes (https://en.wikipedia.org/wiki/Regional_Indicator_Symbol). - if (country.length != 2) { - return country - } - - val countryCodeCaps = - country.uppercase(Locale.CANADA) // upper case is important because we are calculating offset - val firstLetter = Character.codePointAt(countryCodeCaps, 0) - 0x41 + 0x1F1E6 - val secondLetter = Character.codePointAt(countryCodeCaps, 1) - 0x41 + 0x1F1E6 - - // 2. It then checks if both characters are alphabet - if (!countryCodeCaps[0].isLetter() || !countryCodeCaps[1].isLetter()) { - return country - } - - return String(Character.toChars(firstLetter)) + String(Character.toChars(secondLetter)) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/MainActivity.kt b/apps/codeApp/src/main/java/com/getcode/view/MainActivity.kt deleted file mode 100644 index fec2471c5..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/MainActivity.kt +++ /dev/null @@ -1,183 +0,0 @@ -package com.getcode.view - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import android.os.Process.killProcess -import android.os.Process.myPid -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.fragment.app.FragmentActivity -import com.getcode.BuildConfig -import com.getcode.CodeApp -import com.getcode.LocalDeeplinks -import com.getcode.LocalDownloadQrCode -import com.getcode.LocalPhoneFormatter -import com.getcode.LocalSession -import com.getcode.R -import com.getcode.SessionController -import com.getcode.analytics.CodeAnalyticsService -import com.getcode.libs.analytics.LocalAnalytics -import com.getcode.network.TipController -import com.getcode.network.client.Client -import com.getcode.network.exchange.Exchange -import com.getcode.network.exchange.LocalExchange -import com.getcode.ui.tips.DefinedTips -import com.getcode.util.DeeplinkHandler -import com.getcode.util.PhoneUtils -import com.getcode.util.resources.LocalResources -import com.getcode.util.resources.ResourceHelper -import com.getcode.util.vibration.LocalVibrator -import com.getcode.util.vibration.Vibrator -import com.getcode.utils.CurrencyUtils -import com.getcode.utils.LocalCurrencyUtils -import com.getcode.utils.network.LocalNetworkObserver -import com.getcode.utils.network.NetworkConnectivityListener -import com.getcode.utils.trace -import com.google.firebase.crashlytics.FirebaseCrashlytics -import dagger.hilt.android.AndroidEntryPoint -import dev.bmcreations.tipkit.engines.LocalTipsEngine -import dev.bmcreations.tipkit.engines.TipsEngine -import timber.log.Timber -import javax.inject.Inject -import kotlin.system.exitProcess - -@AndroidEntryPoint -class MainActivity : FragmentActivity() { - - @Inject - lateinit var resources: ResourceHelper - - @Inject - lateinit var client: Client - - @Inject - lateinit var analyticsManager: CodeAnalyticsService - - @Inject - lateinit var deeplinkHandler: DeeplinkHandler - - @Inject - lateinit var networkObserver: NetworkConnectivityListener - - @Inject - lateinit var phoneUtils: PhoneUtils - - @Inject - lateinit var vibrator: Vibrator - - @Inject - lateinit var currencyUtils: CurrencyUtils - - @Inject - lateinit var exchange: Exchange - - @Inject - lateinit var tipController: TipController - - @Inject - lateinit var tipEngine: TipsEngine - - @Inject - lateinit var tipDefinitions: DefinedTips - - @Inject - lateinit var sessionController: SessionController - - /** - * The compose navigation controller does not play nice with single task activities. - * Invoking the navigation controller here will cause the intent to be fired - * again we want to debounce this once when the activity is started with an intent. - */ - override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - val cachedIntent = deeplinkHandler.debounceIntent - if (cachedIntent != null && cachedIntent.data == intent.data) { - Timber.d("Debouncing Intent " + intent.data) - deeplinkHandler.debounceIntent = null - return - } - deeplinkHandler.debounceIntent = intent - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - analyticsManager.onAppStart() - handleUncaughtException() - setFullscreen() - - tipEngine.configure(tipDefinitions) - - deeplinkHandler.debounceIntent = intent - - setContent { - BoxWithConstraints { - trace("set content") - - val downloadQr = com.getcode.libs.qr.rememberQrBitmapPainter( - content = stringResource( - R.string.app_download_link, - stringResource(id = R.string.app_download_link_qr_ref) - ), - size = maxWidth * 0.60f, - padding = 0.25.dp - ) - - CompositionLocalProvider( - LocalSession provides sessionController, - LocalAnalytics provides analyticsManager, - LocalDeeplinks provides deeplinkHandler, - LocalNetworkObserver provides networkObserver, - LocalPhoneFormatter provides phoneUtils, - LocalVibrator provides vibrator, - LocalCurrencyUtils provides currencyUtils, - LocalExchange provides exchange, - LocalDownloadQrCode provides downloadQr, - LocalTipsEngine provides tipEngine, - LocalResources provides resources - ) { - CodeApp(tipEngine) - } - } - } - } - - private fun setFullscreen() { - enableEdgeToEdge() - } - - override fun onResume() { - super.onResume() - client.startTimer() - tipController.checkForConnection() - } - - override fun onStop() { - super.onStop() - client.stopTimer() - tipController.stopTimer() - } -} - -private fun Activity.handleUncaughtException() { - val crashedKey = "isCrashed" - if (intent.getBooleanExtra(crashedKey, false)) return - Thread.setDefaultUncaughtExceptionHandler { _, throwable -> - if (BuildConfig.DEBUG) throw throwable - - FirebaseCrashlytics.getInstance().recordException(throwable) - - val intent = Intent(this, MainActivity::class.java).apply { - putExtra(crashedKey, true) - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) - } - startActivity(intent) - finish() - killProcess(myPid()) - exitProcess(2) - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/view/download/ShareDownloadScreen.kt b/apps/codeApp/src/main/java/com/getcode/view/download/ShareDownloadScreen.kt deleted file mode 100644 index 5b57b51f9..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/download/ShareDownloadScreen.kt +++ /dev/null @@ -1,148 +0,0 @@ -package com.getcode.view.download - -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.scale -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.layout.boundsInWindow -import androidx.compose.ui.layout.onPlaced -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import com.getcode.LocalDownloadQrCode -import com.getcode.R -import com.getcode.theme.CodeTheme -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.components.Cloudy -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.theme.CodeCircularProgressIndicator -import com.getcode.ui.components.Row -import com.getcode.ui.components.SelectionContainer -import com.getcode.ui.components.rememberSelectionState -import com.getcode.ui.core.rememberedLongClickable -import com.getcode.util.shareDownloadLink - -@Composable -fun ShareDownloadScreen() { - val downloadLink = stringResource( - id = R.string.app_download_link_with_ref, - stringResource(id = R.string.app_download_link_share_ref) - ) - val selectionState = rememberSelectionState(content = downloadLink) - var contentRect: Rect by remember { - mutableStateOf(Rect.Zero) - } - - SelectionContainer( - modifier = Modifier - .fillMaxSize() - .windowInsetsPadding(WindowInsets.navigationBars), - contentRect = contentRect, - state = selectionState, - ) { onClick -> - Cloudy( - modifier = Modifier - .fillMaxSize(), - enabled = selectionState.shown - ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = CodeTheme.dimens.inset) - .padding(bottom = CodeTheme.dimens.grid.x4), - horizontalAlignment = Alignment.CenterHorizontally - ) { - - Spacer(modifier = Modifier.weight(1f)) - - val context = LocalContext.current - CodeButton( - modifier = Modifier.fillMaxWidth(), - buttonState = ButtonState.Filled, - text = stringResource(id = R.string.action_share), - onClick = { context.shareDownloadLink() } - ) - } - } - - Column( - modifier = Modifier - .fillMaxSize() - .align(Alignment.Center) - .padding(bottom = CodeTheme.dimens.grid.x11), - verticalArrangement = Arrangement.spacedBy( - space = CodeTheme.dimens.grid.x7, - alignment = Alignment.CenterVertically - ), - horizontalAlignment = Alignment.CenterHorizontally - ) { - val contentAlpha by animateFloatAsState( - targetValue = if (selectionState.shown) 0f else 1f, - label = "icon alpha" - ) - Text( - modifier = Modifier - .padding(bottom = CodeTheme.dimens.grid.x4) - .alpha(contentAlpha), - text = stringResource(R.string.subtitle_scanToDownload), - style = CodeTheme.typography.textLarge, - textAlign = TextAlign.Center - ) - - val qrCode = LocalDownloadQrCode.current - - - if (qrCode != null) { - Image( - modifier = Modifier - .onPlaced { contentRect = it.boundsInWindow() } - .rememberedLongClickable { - onClick() - } - .scale(selectionState.scale.value), - painter = qrCode, - contentDescription = "qr" - ) - } else { - CodeCircularProgressIndicator() - } - - Row( - modifier = Modifier.alpha(contentAlpha), - horizontalArrangement = Arrangement.spacedBy( - space = CodeTheme.dimens.inset, - alignment = Alignment.CenterHorizontally - ), - verticalAlignment = Alignment.CenterVertically - ) { - Image( - painter = painterResource(id = R.drawable.ic_apple_icon), - contentDescription = null - ) - Image( - painter = painterResource(id = R.drawable.ic_android_icon), - contentDescription = null - ) - } - } - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/view/login/AccessKey.kt b/apps/codeApp/src/main/java/com/getcode/view/login/AccessKey.kt deleted file mode 100644 index 0561ffb75..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/login/AccessKey.kt +++ /dev/null @@ -1,269 +0,0 @@ -package com.getcode.view.login - -import android.Manifest -import android.os.Build -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.MutableTransitionState -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.isSpecified -import androidx.hilt.navigation.compose.hiltViewModel -import com.getcode.R -import com.getcode.manager.BottomBarManager -import com.getcode.manager.TopBarManager -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.LoginArgs -import com.getcode.theme.CodeTheme -import com.getcode.theme.White -import com.getcode.ui.LocalTopBarPadding -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.components.Cloudy -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.components.SelectionContainer -import com.getcode.ui.components.rememberSelectionState -import com.getcode.ui.core.addIf -import com.getcode.ui.core.measured -import com.getcode.util.launchAppSettings -import com.getcode.util.permissions.PermissionResult -import com.getcode.util.permissions.getPermissionLauncher -import com.getcode.util.permissions.rememberPermissionHandler - -@Preview -@Composable -fun AccessKey( - viewModel: AccessKeyViewModel = hiltViewModel(), - arguments: LoginArgs = LoginArgs(), -) { - val navigator = LocalCodeNavigator.current - val context = LocalContext.current - val dataState by viewModel.uiFlow.collectAsState() - val keyboardController = LocalSoftwareKeyboardController.current - keyboardController?.hide() - - var isExportSeedRequested by remember { mutableStateOf(false) } - var isStoragePermissionGranted by remember { mutableStateOf(false) } - val isAccessKeyVisible = remember { MutableTransitionState(false) } - - val onPermissionResult = { result: PermissionResult -> - isStoragePermissionGranted = result == PermissionResult.Granted - - if (!isStoragePermissionGranted) { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = context.getString(R.string.error_title_failedToSave), - message = context.getString(R.string.error_description_failedToSave), - type = TopBarManager.TopBarMessageType.ERROR, - secondaryText = context.getString(R.string.action_openSettings), - secondaryAction = { context.launchAppSettings() } - ) - ) - } - } - - val launcher = getPermissionLauncher(Manifest.permission.WRITE_EXTERNAL_STORAGE, onPermissionResult) - val permissionChecker = rememberPermissionHandler() - - if (isExportSeedRequested && isStoragePermissionGranted) { - viewModel.onSubmit(navigator, true) - isExportSeedRequested = false - } - - val onExportClick = { - isExportSeedRequested = true - - if (Build.VERSION.SDK_INT > 29) { - isStoragePermissionGranted = true - } else { - permissionChecker.request( - permission = Manifest.permission.WRITE_EXTERNAL_STORAGE, - onPermissionResult = onPermissionResult, - launcher = launcher - ) - } - - } - val onSkipClick = { - viewModel.onSubmit(navigator, false) - } - - var buttonHeight by remember { - mutableStateOf(0.dp) - } - - val selectionState = rememberSelectionState( - content = dataState.words.joinToString(" ") - ) - - SelectionContainer( - modifier = Modifier - .fillMaxSize() - .windowInsetsPadding(WindowInsets.navigationBars), - state = selectionState, - ) { - Cloudy( - modifier = Modifier - .fillMaxSize(), - enabled = selectionState.shown - ) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = CodeTheme.dimens.inset) - .padding(vertical = CodeTheme.dimens.grid.x4) - ) { - Column( - modifier = Modifier - .align(Alignment.BottomCenter) - .measured { buttonHeight = it.height }, - ) { - CodeButton( - modifier = Modifier.fillMaxWidth(), - onClick = onExportClick, - text = stringResource(R.string.action_saveAccessKey), - buttonState = ButtonState.Filled, - isLoading = dataState.isLoading, - enabled = dataState.isEnabled, - isSuccess = dataState.isSuccess, - ) - - CodeButton( - modifier = Modifier.fillMaxWidth(), - onClick = { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = context.getString(R.string.prompt_title_wroteThemDown), - subtitle = context - .getString(R.string.prompt_description_wroteThemDown), - positiveText = context - .getString(R.string.action_yesWroteThemDown), - negativeText = context.getString(R.string.action_cancel), - onPositive = { onSkipClick() }, - onNegative = {} - ) - ) - }, - text = stringResource(R.string.action_wroteThemDownInstead), - buttonState = ButtonState.Subtle, - enabled = dataState.isEnabled, - ) - } - } - } - - Column( - modifier = Modifier - .align(Alignment.TopCenter) - .fillMaxHeight() - .padding(LocalTopBarPadding.current) - .addIf(buttonHeight.isSpecified) { Modifier.padding(bottom = buttonHeight + CodeTheme.dimens.grid.x4) }, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Column( - modifier = Modifier - // highly specific aspect ratio from iOS :) - .aspectRatio(0.607f, matchHeightConstraintsFirst = true) - .fillMaxWidth() - .weight(1f), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - AnimatedVisibility( - visibleState = isAccessKeyVisible, - enter = fadeIn(animationSpec = tween(300, 0)), - exit = fadeOut(animationSpec = tween(300, 0)) - ) { - dataState.accessKeyCroppedBitmap?.let { bitmap -> - Image( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .scale(selectionState.scale.value), - bitmap = bitmap.asImageBitmap(), - contentScale = ContentScale.Crop, - contentDescription = dataState.wordsFormatted, - ) - } - } - } - - val textAlpha by animateFloatAsState( - if (selectionState.shown) 0f else 1f, - label = "text alpha" - ) - - Text( - modifier = Modifier - .alpha(textAlpha) - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.grid.x5) - .padding( - top = CodeTheme.dimens.grid.x3, - bottom = CodeTheme.dimens.grid.x6 - ), - style = CodeTheme.typography.textSmall.copy(textAlign = TextAlign.Center), - color = White, - text = stringResource(R.string.subtitle_accessKeyDescription) - ) - } - - - BackHandler { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = context.getString(R.string.prompt_title_exitAccountCreation), - subtitle = context - .getString(R.string.prompt_description_exitAccountCreation), - positiveText = context.getString(R.string.action_exit), - negativeText = context.getString(R.string.action_cancel), - onPositive = { navigator.popAll() }, - onNegative = {} - ) - ) - } - - LaunchedEffect(viewModel) { - arguments.signInEntropy - ?.let { viewModel.initWithEntropy(it) } - } - - LaunchedEffect(dataState.accessKeyCroppedBitmap) { - isAccessKeyVisible.targetState = dataState.accessKeyCroppedBitmap != null - } - } -} - diff --git a/apps/codeApp/src/main/java/com/getcode/view/login/AccessKeyViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/login/AccessKeyViewModel.kt deleted file mode 100644 index 7785a73ab..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/login/AccessKeyViewModel.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.getcode.view.login - -import android.Manifest -import android.annotation.SuppressLint -import android.os.Build -import com.getcode.AppHomeScreen -import com.getcode.analytics.Action -import com.getcode.analytics.ActionSource -import com.getcode.analytics.CodeAnalyticsService -import com.getcode.libs.qr.QRCodeGenerator -import com.getcode.manager.AuthManager -import com.getcode.services.manager.MnemonicManager -import com.getcode.media.MediaScanner -import com.getcode.navigation.core.CodeNavigator -import com.getcode.navigation.screens.CodeLoginPermission -import com.getcode.navigation.screens.LoginScreen -import com.getcode.navigation.screens.PermissionRequestScreen -import com.getcode.util.permissions.PermissionChecker -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.getPublicKeyBase58 -import dagger.hilt.android.lifecycle.HiltViewModel -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.schedulers.Schedulers -import java.util.concurrent.TimeUnit -import javax.inject.Inject - - -@HiltViewModel -class AccessKeyViewModel @Inject constructor( - private val authManager: AuthManager, - private val analytics: CodeAnalyticsService, - private val permissions: PermissionChecker, - private val mnemonicManager: MnemonicManager, - resources: ResourceHelper, - mediaScanner: MediaScanner, - qrCodeGenerator: QRCodeGenerator, -) : BaseAccessKeyViewModel(resources, mnemonicManager, mediaScanner, qrCodeGenerator) { - @SuppressLint("CheckResult") - fun onSubmit(navigator: CodeNavigator, isSaveImage: Boolean, isDeepLink: Boolean = false) { - val entropyB64 = uiFlow.value.entropyB64 ?: return - - authManager.createAccount(entropyB64, rollbackOnError = isDeepLink) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeOn(Schedulers.computation()) - .doOnSubscribe { - uiFlow.value = uiFlow.value.copy(isLoading = true, isEnabled = false) - } - .concatWith( - if (isSaveImage) { - Completable.create { c -> - analytics.action(Action.ConfirmAccessKey, source = ActionSource.AccessKeySaved) - val result = saveBitmapToFile() - if (result) c.onComplete() else c.onError(IllegalStateException()) - }.subscribeOn(Schedulers.computation()) - } else { - analytics.action(Action.ConfirmAccessKey, source = ActionSource.AccessKeyWroteDown) - Completable.complete() - } - ) - .doOnComplete { - val owner = mnemonicManager.getKeyPair(entropyB64) - analytics.createAccount(true, owner.getPublicKeyBase58()) - uiFlow.value = uiFlow.value.copy(isLoading = false, isSuccess = true) - } - .delay(2L, TimeUnit.SECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { - onComplete(navigator, entropyB64) - }, - { - onSubmitError(it) - analytics.createAccount(false, null) - uiFlow.value = uiFlow.value.copy(isLoading = false) - navigator.replaceAll(LoginScreen()) - } - ) - } - - private fun onComplete(navigator: CodeNavigator, entropyB64: String) { - val cameraPermissionDenied = permissions.isDenied(Manifest.permission.CAMERA) - - if (cameraPermissionDenied) { - navigator.push(PermissionRequestScreen(CodeLoginPermission.Camera, true)) - } else { - if (Build.VERSION.SDK_INT < 33) { - analytics.action(Action.CompletedOnboarding) - navigator.replaceAll(AppHomeScreen()) - } else { - val notificationsPermissionDenied = permissions.isDenied( - Manifest.permission.POST_NOTIFICATIONS - ) - - if (notificationsPermissionDenied) { - navigator.push(PermissionRequestScreen(CodeLoginPermission.Notifications, true)) - } else { - analytics.action(Action.CompletedOnboarding) - navigator.replaceAll(AppHomeScreen()) - } - } - } - } - -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/login/BaseAccessKeyViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/login/BaseAccessKeyViewModel.kt deleted file mode 100644 index 7810af0ef..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/login/BaseAccessKeyViewModel.kt +++ /dev/null @@ -1,321 +0,0 @@ -package com.getcode.view.login - -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.Typeface -import android.os.Environment -import androidx.core.graphics.applyCanvas -import androidx.core.graphics.drawable.toBitmap -import androidx.lifecycle.viewModelScope -import com.getcode.R -import com.getcode.libs.qr.QRCodeGenerator -import com.getcode.services.manager.MnemonicManager -import com.getcode.manager.SessionManager -import com.getcode.manager.TopBarManager -import com.getcode.media.MediaScanner -import com.getcode.network.repository.DeniedReason -import com.getcode.network.repository.ErrorSubmitIntent -import com.getcode.network.repository.ErrorSubmitIntentException -import com.getcode.theme.Alert -import com.getcode.theme.Brand -import com.getcode.theme.White -import com.getcode.ui.utils.toAGColor -import com.getcode.util.resources.ResourceHelper -import com.getcode.util.save -import com.getcode.utils.decodeBase64 -import com.getcode.view.BaseViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.distinctUntilChangedBy -import kotlinx.coroutines.launch -import org.kin.sdk.base.tools.Base58 -import timber.log.Timber -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import com.getcode.theme.R as themeR - - -data class AccessKeyUiModel( - val entropyB64: String? = null, - val isLoading: Boolean = false, - val isSuccess: Boolean = false, - val isEnabled: Boolean = true, - val words: List = listOf(), - val wordsFormatted: String = "", - val accessKeyBitmap: Bitmap? = null, - val accessKeyCroppedBitmap: Bitmap? = null, -) - -abstract class BaseAccessKeyViewModel( - private val resources: ResourceHelper, - private val mnemonicManager: MnemonicManager, - private val mediaScanner: MediaScanner, - private val qrCodeGenerator: QRCodeGenerator, -) : BaseViewModel(resources) { - val uiFlow = MutableStateFlow(AccessKeyUiModel()) - - fun init() { - viewModelScope.launch { - SessionManager.authState - .distinctUntilChangedBy { it.entropyB64 } - .collect { it.entropyB64?.let { e -> initWithEntropy(e) } } - } - } - - fun initWithEntropy(entropyB64: String) { - if (uiFlow.value.entropyB64 == entropyB64) return - Timber.d("entropy=$entropyB64") - val words = mnemonicManager.fromEntropyBase64(entropyB64).words - val wordsFormatted = getAccessKeyText(words).joinToString("\n") - - uiFlow.value = uiFlow.value.copy( - entropyB64 = entropyB64, - words = words, - wordsFormatted = wordsFormatted - ) - - CoroutineScope(Dispatchers.IO).launch { - val accessKeyBitmap = createBitmapForExport(words = words, entropyB64 = entropyB64) - val accessKeyBitmapDisplay = - createBitmapForExport(drawBackground = false, words, entropyB64) - val accessKeyCroppedBitmap = - Bitmap.createBitmap(accessKeyBitmapDisplay, 0, 500, 1200, 1450) - - uiFlow.value = uiFlow.value.copy( - accessKeyBitmap = accessKeyBitmap, - accessKeyCroppedBitmap = accessKeyCroppedBitmap - ) - } - } - - private fun getAccessKeyText(words: List): List { - return listOf( - words.subList(0, 6).joinToString(" "), - words.subList(6, 12).joinToString(" ") - ) - } - - private val targetWidth = 1200 - private val targetHeight = 2500 - - private val logoWidth = 270 - private val logoHeight = 88 - private val qrCodeSize = 480 - - private val bgTopOffset = 550 - private val logoTopOffset = 770 - private val qrTopOffset = 980 - private val keyTextTopOffset = 1600 - private val topTextTopOffset = 200 - private val bottomTextTopOffset = 2000 - - - internal fun saveBitmapToFile(): Boolean { - val bitmap = uiFlow.value.accessKeyBitmap ?: return false - val destination = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) - val result = bitmap.save( - destination = destination, - name = { - val date: DateFormat = SimpleDateFormat("yyy-MM-dd-h-mm", Locale.CANADA) - "Code-Recovery-${date.format(Date())}.png" - }, - ) - - if (result) { - mediaScanner.scan(destination) - } - - return result - } - - private fun createBitmapForExport( - drawBackground: Boolean = true, - words: List, - entropyB64: String - ): Bitmap { - val accessKeyText = getAccessKeyText(words) - - val accessKeyBg = resources.getDrawable(R.drawable.ic_access_key_bg) - ?.toBitmap(812, 1353)!! - - val imageLogo = - resources.getDrawable(R.drawable.ic_code_logo_white) - ?.toBitmap(logoWidth, logoHeight)!! - - val imageOut = Bitmap.createBitmap( - targetWidth, targetHeight, - Bitmap.Config.ARGB_8888 - ).applyCanvas { - val accessBgActualWidth = - accessKeyBg.getScaledWidth(resources.displayMetrics) - - if (drawBackground) { - val paintBackground = Paint() - paintBackground.color = Brand.toAGColor() - paintBackground.style = Paint.Style.FILL - drawPaint(paintBackground) - } - - val topTextChunks = getString(R.string.subtitle_accessKeySnapshotWarning) - .split(" ", "\n") - .chunked(7) - .map { it.joinToString(" ") } - - topTextChunks.forEachIndexed { index, text -> - drawText( - canvas = this, - y = topTextTopOffset + (60 * (index + 1)), - sizePx = 40, - color = Alert.toAGColor(), - text = text - ) - } - - - drawBitmap( - accessKeyBg, - (((targetWidth - accessBgActualWidth) / 2)).toFloat(), - bgTopOffset.toFloat(), - null - ) - - drawBitmap( - imageLogo, - ((targetWidth - logoWidth) / 2).toFloat(), - logoTopOffset.toFloat(), - null - ) - - getQrCode(entropyB64)?.let { bitmap -> - drawBitmap( - bitmap, - ((targetWidth - qrCodeSize) / 2).toFloat(), - qrTopOffset.toFloat(), - null - ) - } - - drawText( - canvas = this, - y = keyTextTopOffset, - sizePx = 32, - color = White.toAGColor(), - text = accessKeyText[0] - ) - - drawText( - canvas = this, - y = keyTextTopOffset + 40, - sizePx = 32, - color = White.toAGColor(), - text = accessKeyText[1] - ) - - val bottomTextChunks = getString(R.string.subtitle_accessKeySnapshotDescription) - .split(" ") - .chunked(8) - .map { it.joinToString(" ") } - - bottomTextChunks.forEachIndexed { index, text -> - drawText( - canvas = this, - y = bottomTextTopOffset + (60 * (index + 1)), - sizePx = 40, - color = White.toAGColor(), - text = text - ) - } - - } - return imageOut - } - - private fun getQrCode(entropyB64: String): Bitmap? { - val base58 = Base58.encode(entropyB64.decodeBase64()) - val url = "${resources.getString(R.string.root_url_app)}/login?data=$base58" - - return qrCodeGenerator.generate(url, qrCodeSize) - } - - private fun drawText( - canvas: Canvas, - y: Int, - x: Int? = null, - sizePx: Int, - color: Int, - text: String - ) { - val textPaint = Paint(Paint.ANTI_ALIAS_FLAG) - textPaint.color = color - textPaint.textSize = sizePx.toFloat() - textPaint.typeface = Typeface.create( - resources.getFont(themeR.font.avenir_next_demi), - Typeface.BOLD - ) - - val bounds1 = android.graphics.Rect() - textPaint.getTextBounds(text, 0, text.length, bounds1) - val xV: Int = x ?: ((targetWidth - bounds1.width()) / 2) - canvas.drawText(text, xV.toFloat(), y.toFloat(), textPaint) - } - - private fun getAccessKeySaveError() = TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToSave), - resources.getString(R.string.error_description_failedToSave), - ) - - private fun getSomethingWentWrongError() = TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToCreateAccount), - resources.getString(R.string.error_description_failedToCreateAccount), - ) - - private fun getTooManyAccountsPerPhoneError() = TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_tooManyAccountsPerPhone), - resources.getString(R.string.error_description_tooManyAccountsPerPhone) - ) - - private fun getTooManyAccountsPerDeviceError() = TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_tooManyAccountsPerDevice), - resources.getString(R.string.error_description_tooManyAccountsPerDevice) - ) - - private fun getUnsupportedCountryError() = TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_countryNotSupported), - resources.getString(R.string.error_description_countryNotSupported) - ) - - private fun getUnsupportedDeviceError() = TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_deviceNotSupported), - resources.getString(R.string.error_description_deviceNotSupported) - ) - - internal fun onSubmitError(e: Throwable) { - when (e) { - is ErrorSubmitIntentException -> { - when (val intent = e.errorSubmitIntent) { - is ErrorSubmitIntent.Denied -> { - if (intent.reasons.isEmpty() || intent.reasons.first() == DeniedReason.Unspecified) { - getSomethingWentWrongError() - } else { - val reason = intent.reasons.first() - when (reason) { - DeniedReason.Unspecified -> getSomethingWentWrongError() - DeniedReason.TooManyFreeAccountsForPhoneNumber -> getTooManyAccountsPerPhoneError() - DeniedReason.TooManyFreeAccountsForDevice -> getTooManyAccountsPerDeviceError() - DeniedReason.UnsupportedCountry -> getUnsupportedCountryError() - DeniedReason.UnsupportedDevice -> getUnsupportedDeviceError() - } - } - } - else -> getSomethingWentWrongError() - } - } - is IllegalStateException -> getAccessKeySaveError() - else -> getSomethingWentWrongError() - }.let { m -> TopBarManager.showMessage(m) } - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/view/login/CameraPermission.kt b/apps/codeApp/src/main/java/com/getcode/view/login/CameraPermission.kt deleted file mode 100644 index 914b29b83..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/login/CameraPermission.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.getcode.view.login - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.constraintlayout.compose.ConstraintLayout -import com.getcode.AppHomeScreen -import com.getcode.R -import com.getcode.analytics.Action -import com.getcode.libs.analytics.LocalAnalytics -import com.getcode.navigation.screens.CodeLoginPermission -import com.getcode.navigation.core.CodeNavigator -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.PermissionRequestScreen -import com.getcode.theme.CodeTheme -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.util.permissions.cameraPermissionCheck - -@Composable -fun CameraPermission(navigator: CodeNavigator = LocalCodeNavigator.current, fromOnboarding: Boolean = false) { - var isResultHandled by remember { mutableStateOf(false) } - val analytics = LocalAnalytics.current - val onNotificationResult: (Boolean) -> Unit = { isGranted -> - if (!isResultHandled) { - isResultHandled = true - - if (isGranted) { - if (fromOnboarding) { - analytics.action(Action.CompletedOnboarding) - } - navigator.replaceAll(AppHomeScreen()) - } else { - navigator.push(PermissionRequestScreen(CodeLoginPermission.Notifications, fromOnboarding)) - } - } - } - - val notificationPermissionCheck = - com.getcode.util.permissions.notificationPermissionCheck { onNotificationResult(it) } - - val onCameraResult: (Boolean) -> Unit = { isGranted -> - if (isGranted) { - notificationPermissionCheck(false) - } - } - - val cameraPermissionCheck = cameraPermissionCheck { onCameraResult(it) } - - SideEffect { - cameraPermissionCheck(false) - } - - ConstraintLayout( - modifier = Modifier - .fillMaxSize() - .padding(bottom = CodeTheme.dimens.grid.x4) - .windowInsetsPadding(WindowInsets.navigationBars), - ) { - val (image, caption, button) = createRefs() - - Image( - painter = painterResource(R.drawable.ic_home_bill_image), - contentDescription = "", - modifier = Modifier - .padding(horizontal = CodeTheme.dimens.grid.x8) - .padding(top = CodeTheme.dimens.grid.x10) - .fillMaxHeight(0.6f) - .fillMaxWidth() - .constrainAs(image) { - top.linkTo(parent.top) - bottom.linkTo(caption.top) - start.linkTo(parent.start) - end.linkTo(parent.end) - } - ) - - Text( - modifier = Modifier - .padding(horizontal = CodeTheme.dimens.inset) - .constrainAs(caption) { - top.linkTo(image.bottom) - bottom.linkTo(button.top) - start.linkTo(parent.start) - end.linkTo(parent.end) - }, - text = stringResource(R.string.permissions_description_camera), - style = CodeTheme.typography.textMedium - .copy(textAlign = TextAlign.Center), - ) - - CodeButton( - onClick = { cameraPermissionCheck(true) }, - text = stringResource(R.string.action_allowCameraAccess), - buttonState = ButtonState.Filled, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset) - .constrainAs(button) { - linkTo(button.bottom, parent.bottom, bias = 1.0F) - start.linkTo(parent.start) - end.linkTo(parent.end) - }, - ) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/login/LoginHome.kt b/apps/codeApp/src/main/java/com/getcode/view/login/LoginHome.kt deleted file mode 100644 index d8b72719b..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/login/LoginHome.kt +++ /dev/null @@ -1,159 +0,0 @@ -package com.getcode.view.login - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.text.ClickableText -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.painter.ColorPainter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.text.withStyle -import androidx.constraintlayout.compose.ConstraintLayout -import com.getcode.R -import com.getcode.libs.analytics.LocalAnalytics -import com.getcode.theme.Brand -import com.getcode.theme.CodeTheme -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.components.ImageWithBackground -import com.getcode.util.ChromeTabsUtils - -@Composable -fun LoginHome( - createAccount: () -> Unit, - login: () -> Unit, -) { - val context = LocalContext.current - - Box { - ConstraintLayout( - modifier = Modifier - .fillMaxSize() - .windowInsetsPadding(WindowInsets.navigationBars), - ) { - val (bgImage, logo, buttonCreate, buttonLogin, toc) = createRefs() - - ImageWithBackground( - modifier = Modifier - .constrainAs(bgImage) { - start.linkTo(parent.start) - end.linkTo(parent.end) - top.linkTo(parent.top) - } - .fillMaxSize(), - painter = ColorPainter(Brand), - backgroundDrawableResId = R.drawable.ic_code_splash_bg, - contentDescription = null - ) - - Image( - painter = painterResource( - R.drawable.ic_code_logo_near_white - ), - contentDescription = "", - modifier = Modifier - .constrainAs(logo) { - top.linkTo(parent.top) - bottom.linkTo(buttonCreate.top) - centerHorizontallyTo(parent) - } - .fillMaxWidth(0.65f) - .fillMaxHeight(0.65f) - ) - - CodeButton( - modifier = Modifier - .fillMaxWidth() - .constrainAs(buttonCreate) { - top.linkTo(logo.bottom) //possibly remove!! - bottom.linkTo(buttonLogin.top) - } - .padding(horizontal = CodeTheme.dimens.inset), - onClick = createAccount, - text = stringResource(R.string.action_createAccount), - buttonState = ButtonState.Filled, - ) - CodeButton( - modifier = Modifier - .fillMaxWidth() - .constrainAs(buttonLogin) { - top.linkTo(buttonCreate.bottom) - } - .padding(horizontal = CodeTheme.dimens.inset), - onClick = login, - text = stringResource(R.string.action_logIn), - buttonState = ButtonState.Subtle, - ) - - - val bottomString = buildAnnotatedString { - append(stringResource(R.string.login_description_byTapping)) - append(" ") - append(stringResource(R.string.login_description_agreeToOur)) - append(" ") - pushStringAnnotation(tag = "tos", annotation = stringResource(R.string.app_privacy_policy)) - withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) { - append(stringResource(R.string.title_termsOfService)) - } - pop() - append(" ") - append(stringResource(R.string.core_and)) - append(" ") - pushStringAnnotation( - tag = "policy", - annotation = stringResource(R.string.app_privacy_policy) - ) - withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) { - append(stringResource(R.string.title_privacyPolicy)) - } - pop() - } - - ClickableText( - text = bottomString, - style = CodeTheme.typography.caption.copy( - textAlign = TextAlign.Center, - color = CodeTheme.colors.textSecondary - ), - modifier = Modifier - .constrainAs(toc) { - bottom.linkTo(parent.bottom) - } - .padding(CodeTheme.dimens.grid.x4), - onClick = { offset -> - bottomString.getStringAnnotations(tag = "tos", start = offset, end = offset) - .firstOrNull()?.let { - ChromeTabsUtils.launchUrl(context, it.item) - } - bottomString.getStringAnnotations(tag = "policy", start = offset, end = offset) - .firstOrNull()?.let { - ChromeTabsUtils.launchUrl(context, it.item) - } - } - ) - } - } - - val focusManager = LocalFocusManager.current - val analytics = LocalAnalytics.current - LaunchedEffect(Unit) { - focusManager.clearFocus() - analytics.onAppStarted() - - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/login/NotificationPermission.kt b/apps/codeApp/src/main/java/com/getcode/view/login/NotificationPermission.kt deleted file mode 100644 index 23f68b362..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/login/NotificationPermission.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.getcode.view.login - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.constraintlayout.compose.ConstraintLayout -import com.getcode.AppHomeScreen -import com.getcode.R -import com.getcode.analytics.Action -import com.getcode.libs.analytics.LocalAnalytics -import com.getcode.navigation.core.CodeNavigator -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.theme.CodeTheme -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton - -@Composable -fun NotificationPermission(navigator: CodeNavigator = LocalCodeNavigator.current, fromOnboarding: Boolean = false) { - val analytics = LocalAnalytics.current - val onNotificationResult: (Boolean) -> Unit = { isGranted -> - if (isGranted) { - if (fromOnboarding) { - analytics.action(Action.CompletedOnboarding) - } - navigator.replaceAll(AppHomeScreen()) - } - } - val notificationPermissionCheck = - com.getcode.util.permissions.notificationPermissionCheck(onResult = { - onNotificationResult(it) - }) - - SideEffect { - notificationPermissionCheck(false) - } - - ConstraintLayout( - modifier = Modifier - .fillMaxSize() - .windowInsetsPadding(WindowInsets.navigationBars), - ) { - val (image, caption, button, buttonSkip) = createRefs() - - Image( - painter = painterResource(id = R.drawable.ic_notification_request), - contentDescription = "", - modifier = Modifier - .padding(horizontal = CodeTheme.dimens.grid.x8) - .padding(top = CodeTheme.dimens.grid.x10) - .fillMaxHeight(0.6f) - .fillMaxWidth() - .constrainAs(image) { - top.linkTo(parent.top) - bottom.linkTo(caption.top) - start.linkTo(parent.start) - end.linkTo(parent.end) - } - ) - - Text( - modifier = Modifier - .padding(horizontal = CodeTheme.dimens.inset) - .constrainAs(caption) { - top.linkTo(image.bottom) - bottom.linkTo(button.top) - start.linkTo(parent.start) - end.linkTo(parent.end) - }, - text = stringResource(R.string.permissions_description_push), - style = CodeTheme.typography.textMedium - .copy(textAlign = TextAlign.Center), - ) - - CodeButton( - onClick = { notificationPermissionCheck(true) }, - text = stringResource(R.string.action_allowPushNotifications), - buttonState = ButtonState.Filled, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset) - .constrainAs(button) { - start.linkTo(parent.start) - end.linkTo(parent.end) - linkTo(button.bottom, buttonSkip.top, bias = 1.0F) - }, - ) - - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = CodeTheme.dimens.grid.x2) - .padding(horizontal = CodeTheme.dimens.inset) - .constrainAs(buttonSkip) { - linkTo(buttonSkip.bottom, parent.bottom, bias = 1.0F) - start.linkTo(parent.start) - end.linkTo(parent.end) - }, - onClick = { - onNotificationResult(true) - }, - text = stringResource(R.string.action_notNow), - buttonState = ButtonState.Subtle, - ) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/login/PhoneConfirm.kt b/apps/codeApp/src/main/java/com/getcode/view/login/PhoneConfirm.kt deleted file mode 100644 index 909cbeb23..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/login/PhoneConfirm.kt +++ /dev/null @@ -1,219 +0,0 @@ -package com.getcode.view.login - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.text.ClickableText -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.ProvideTextStyle -import androidx.compose.material.Text -import androidx.compose.material.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel -import com.getcode.LocalPhoneFormatter -import com.getcode.R -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.LoginArgs -import com.getcode.theme.CodeTheme -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.theme.CodeCircularProgressIndicator -import com.getcode.ui.components.OtpRow -import com.getcode.utils.replaceParam - -internal const val OTP_LENGTH = 6 - -@Preview -@Composable -fun PhoneConfirm( - viewModel: PhoneConfirmViewModel = hiltViewModel(), - arguments: LoginArgs = LoginArgs(), -) { - val navigator = LocalCodeNavigator.current - - val dataState by viewModel.uiFlow.collectAsState() - val keyboardController = LocalSoftwareKeyboardController.current - val focusRequester = FocusRequester() - - fun cleanInputString(str: String): String = - (if (str.length <= OTP_LENGTH) str else str.substring( - 0, - OTP_LENGTH - )).filter { it.isDigit() } - - Column( - modifier = Modifier - .fillMaxSize() - .windowInsetsPadding(WindowInsets.navigationBars) - .padding(horizontal = CodeTheme.dimens.inset) - .padding(bottom = CodeTheme.dimens.grid.x4) - .imePadding() - ) { - Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) { - Column( - modifier = Modifier - .align(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Box(modifier = Modifier.fillMaxWidth()) { - TextField( - modifier = Modifier - .alpha(0f) - .focusRequester(focusRequester), - value = dataState.otpInputTextFieldValue, - onValueChange = { - viewModel.onOtpInputChange(cleanInputString(it.text)) - }, - readOnly = dataState.otpInputTextFieldValue.text.length == OTP_LENGTH, - keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number), - ) - - OtpRow( - modifier = Modifier - .padding(top = CodeTheme.dimens.grid.x1), - length = OTP_LENGTH, - values = dataState.otpInput.orEmpty().toCharArray(), - onClick = { - focusRequester.requestFocus() - keyboardController?.show() - } - ) - } - - Column( - Modifier - .padding(top = CodeTheme.dimens.inset) - .wrapContentHeight() - .fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2) - ) { - - ProvideTextStyle( - CodeTheme.typography.textSmall.copy( - textAlign = TextAlign.Center, - color = CodeTheme.colors.textSecondary - ) - ) { - Text( - text = stringResource(id = R.string.subtitle_smsWasSent) - ) - if (dataState.isResendTimerRunning) { - Text( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.subtitle_didntGetCode) - .replaceParam( - LocalPhoneFormatter.current?.formatNumber( - dataState.phoneNumberFormatted.orEmpty() - ) - ) + - "\n" + - stringResource(R.string.subtitle_requestNewOneIn) - .replaceParam( - "0:${if (dataState.resetTimerTime < 10) "0" else ""}${dataState.resetTimerTime}" - ) - ) - } else if (dataState.isResendingCode) { - CodeCircularProgressIndicator( - strokeWidth = CodeTheme.dimens.thickBorder, - color = CodeTheme.colors.brandLight, - modifier = Modifier - .size(CodeTheme.dimens.grid.x4) - .align(Alignment.CenterHorizontally), - ) - } else { - val text = buildAnnotatedString { - append(stringResource(id = R.string.subtitle_didntGetCodeResend)) - append(" ") - pushStringAnnotation( - tag = "resend", - annotation = "resend code trigger" - ) - withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) { - append(stringResource(R.string.subtitle_resend)) - } - pop() - } - - ClickableText( - modifier = Modifier.fillMaxWidth(), - text = text, - style = LocalTextStyle.current - ) { offset -> - text.getStringAnnotations( - tag = "resend", - start = offset, - end = offset - ) - .firstOrNull()?.let { - viewModel.resendCode() - } - } - } - } - } - } - } - - CodeButton( - modifier = Modifier.fillMaxWidth(), - onClick = { - viewModel.onSubmit() - }, - isLoading = dataState.isLoading && !dataState.isResendingCode, - isSuccess = dataState.isSuccess, - enabled = false, - text = stringResource(R.string.action_confirm), - buttonState = ButtonState.Filled, - ) - } - - SideEffect { - focusRequester.requestFocus() - } - - LaunchedEffect(dataState.isSuccess) { - if (dataState.isSuccess) { - keyboardController?.hide() - } - } - - LaunchedEffect(arguments) { - viewModel.reset(navigator) - val phoneNumber = arguments.phoneNumber.orEmpty() - - viewModel.setPhoneNumber(phoneNumber) - - arguments.signInEntropy - ?.let { viewModel.setSignInEntropy(it) } - viewModel.setIsPhoneLinking(arguments.isPhoneLinking) - viewModel.setIsNewAccount(arguments.isNewAccount) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/login/PhoneConfirmViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/login/PhoneConfirmViewModel.kt deleted file mode 100644 index bbe6c8f50..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/login/PhoneConfirmViewModel.kt +++ /dev/null @@ -1,395 +0,0 @@ -package com.getcode.view.login - -import android.annotation.SuppressLint -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.TextFieldValue -import com.getcode.R -import com.getcode.analytics.Action -import com.getcode.analytics.CodeAnalyticsService -import com.getcode.ed25519.Ed25519 -import com.getcode.manager.SessionManager -import com.getcode.manager.TopBarManager -import com.getcode.navigation.core.CodeNavigator -import com.getcode.navigation.screens.AccessKeyScreen -import com.getcode.navigation.screens.ScanScreen -import com.getcode.navigation.screens.PhoneNumberScreen -import com.getcode.network.repository.CheckVerificationResult -import com.getcode.network.repository.IdentityRepository -import com.getcode.network.repository.LinkAccountResult -import com.getcode.network.repository.OtpVerificationResult -import com.getcode.network.repository.PhoneRepository -import com.getcode.services.manager.MnemonicManager -import com.getcode.util.OtpSmsBroadcastReceiver -import com.getcode.util.PhoneUtils -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.ErrorUtils -import com.getcode.utils.encodeBase64 -import com.getcode.view.BaseViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.annotations.NonNull -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.disposables.Disposable -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import java.util.Timer -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import kotlin.concurrent.fixedRateTimer -import kotlin.math.max - -data class PhoneConfirmUiModel( - val isOtpValid: Boolean = false, - val otpInput: String? = null, - val otpInputTextFieldValue: TextFieldValue = TextFieldValue(), - val phoneNumber: String? = null, - val phoneNumberFormatted: String? = null, - val isLoading: Boolean = false, - val isResendingCode: Boolean = false, - val isSuccess: Boolean = false, - val isAutoSubmitted: Boolean = false, - val entropyB64: String? = null, - val isPhoneLinking: Boolean = false, - val isNewAccount: Boolean = false, - val isResendTimerRunning: Boolean = true, - val resetTimerTime: Int = PhoneConfirmViewModel.RESEND_TIMER_MAX, - val attempts: Int = 0, -) - -@HiltViewModel -class PhoneConfirmViewModel @Inject constructor( - private val analytics: CodeAnalyticsService, - private val identityRepository: IdentityRepository, - private val phoneRepository: PhoneRepository, - private val phoneUtils: PhoneUtils, - private val resources: ResourceHelper, - private val mnemonicManager: MnemonicManager, -) : BaseViewModel(resources) { - - companion object { - internal const val RESEND_TIMER_MAX = 60 - } - - val uiFlow = MutableStateFlow(PhoneConfirmUiModel()) - private var navigator: CodeNavigator? = null - private var timer: Timer? = null - private var otpSmsCodeDisposable: Disposable? = null - - fun reset(navigator: CodeNavigator) { - uiFlow.update { - PhoneConfirmUiModel() - } - this.navigator = navigator - - startTimer() - otpSmsCodeDisposable?.dispose() - otpSmsCodeDisposable = OtpSmsBroadcastReceiver.otpCode.subscribe { onOtpInputChange(it) } - } - - fun onSubmit() { - CoroutineScope(Dispatchers.IO).launch { - performConfirm(navigator) - } - } - - fun setPhoneNumber(phoneNumber: String) { - uiFlow.update { - it.copy( - phoneNumber = phoneNumber, - phoneNumberFormatted = phoneUtils.formatNumber(phoneNumber) - ) - } - } - - fun setIsPhoneLinking(isPhoneLinking: Boolean) { - uiFlow.update { - it.copy(isPhoneLinking = isPhoneLinking) - } - } - - fun setIsNewAccount(isNewAccount: Boolean) { - uiFlow.update { - it.copy(isNewAccount = isNewAccount) - } - } - - fun onOtpInputChange(otpInput: String) { - TopBarManager.setMessageShown() - - if (uiFlow.value.otpInput == otpInput) return - if (uiFlow.value.isLoading) return - val isFullLength = otpInput.length == OTP_LENGTH - - uiFlow.update { - it.copy( - otpInput = otpInput, - otpInputTextFieldValue = TextFieldValue( - text = otpInput, - selection = TextRange(max(0, otpInput.length)) - ), - isAutoSubmitted = !isFullLength && it.isAutoSubmitted - ) - } - uiFlow.update { - if (isFullLength && !it.isAutoSubmitted) { - onSubmit() - it.copy(isAutoSubmitted = true) - } else { - it - } - } - } - - fun setSignInEntropy(entropyB64: String) { - uiFlow.update { - it.copy(entropyB64 = entropyB64) - } - } - - private fun onOtpValidated() { - uiFlow.update { - it.copy(isOtpValid = true) - } - } - - private fun onOtpError() { - uiFlow.update { - it.copy( - otpInput = "", - otpInputTextFieldValue = TextFieldValue() - ) - } - } - - private fun startTimer() { - uiFlow.update { - it.copy( - isResendTimerRunning = true, - resetTimerTime = RESEND_TIMER_MAX - ) - } - timer?.cancel() - timer = fixedRateTimer("timer", false, 0L, 1000) { - uiFlow.update { - if (it.resetTimerTime <= 0) { - cancel() - it.copy(isResendTimerRunning = false) - } else { - it.copy(resetTimerTime = it.resetTimerTime - 1) - } - } - } - } - - fun resendCode() { - val phoneNumber = uiFlow.value.phoneNumber ?: return - - CoroutineScope(Dispatchers.IO).launch { - phoneRepository.sendVerificationCode(phoneNumber) - .firstElement() - .doOnSubscribe { setIsResending(true) } - .doOnTerminate { setIsResending(false) } - .doOnComplete { setIsResending(false) } - .observeOn(AndroidSchedulers.mainThread()) - .map { result -> - when (result) { - is OtpVerificationResult.Error -> { - TopBarManager.showMessage(getGenericError()) - false - } - OtpVerificationResult.Success -> true - } - } - .subscribe({ startTimer() }, ErrorUtils::handleError) - } - } - - private fun checkVerificationCode( - isOtpValid: Boolean, - phoneNumber: String, - otpInput: String - ): @NonNull Single { - return if (isOtpValid) { - Single.just(true) - } else { - analytics.action(Action.VerifyPhone) - phoneRepository.checkVerificationCode(phoneNumber, otpInput) - .map { res -> - when (res) { - CheckVerificationResult.Error.InvalidCode -> { - TopBarManager.showMessage(getInvalidCodeError()) - } - CheckVerificationResult.Error.NoVerification -> { - TopBarManager.showMessage(getTimeoutError()) - } - is CheckVerificationResult.Error -> { - TopBarManager.showMessage(getGenericError()) - } - CheckVerificationResult.Success -> Unit - } - - res == CheckVerificationResult.Success - }.firstOrError() - .doOnSuccess { isSuccess -> if (isSuccess) onOtpValidated() else onOtpError() } - } - } - - private fun linkAccount( - keyPair: Ed25519.KeyPair, - phoneValue: String, - code: String - ): Single { - return identityRepository.linkAccount(keyPair, phoneValue, code) - .map { res -> - when (res) { - LinkAccountResult.Error.InvalidCode -> TopBarManager.showMessage(getInvalidCodeError()) - is LinkAccountResult.Error -> getGenericError() - LinkAccountResult.Success -> Unit - } - res == LinkAccountResult.Success - } - } - - private fun getAssociatedPhoneNumber(owner: Ed25519.KeyPair): Flowable { - return phoneRepository.fetchAssociatedPhoneNumber(owner) - .flatMap { res -> - identityRepository.getUser(owner, res.phoneNumber).map { res.isSuccess } - } - } - - @SuppressLint("CheckResult") - private fun performConfirm(navigator: CodeNavigator?) { - val phoneNumber = uiFlow.value.phoneNumber ?: return - val otpInput = uiFlow.value.otpInput ?: return - val entropyB64 = uiFlow.value.entropyB64 - val isOtpValid = uiFlow.value.isOtpValid - val isPhoneLinking = uiFlow.value.isPhoneLinking - val isNewAccount = uiFlow.value.isNewAccount - val attempts = uiFlow.value.attempts - - if (entropyB64 == null && !isNewAccount) return - - val seedB64: String - val keyPair: Ed25519.KeyPair - - try { - keyPair = if (isNewAccount) { - seedB64 = Ed25519.createSeed16().encodeBase64() - mnemonicManager.getKeyPair(seedB64) - } else { - seedB64 = "" - SessionManager.getOrganizer()?.mnemonic?.let(mnemonicManager::getKeyPair)!! - } - } catch (e: Exception) { - e.printStackTrace() - TopBarManager.showMessage(getGenericError()) - return - } - - if (attempts + 1 >= 3) { - TopBarManager.showMessage(getMaximumAttemptsReachedError()) - CoroutineScope(Dispatchers.Main).launch { - navigator?.popAll() - } - return - } - - uiFlow.update { - it.copy(attempts = it.attempts + 1) - } - - checkVerificationCode(isOtpValid, phoneNumber, otpInput) - .toFlowable() - .observeOn(AndroidSchedulers.mainThread()) - .doOnSubscribe { setIsLoading(true) } - .flatMapSingle { isSuccess -> - if (isSuccess) linkAccount( - keyPair, - phoneNumber, - otpInput - ) else Single.just(isSuccess) - } - .flatMap { isSuccess -> - when { - isPhoneLinking && isSuccess -> getAssociatedPhoneNumber(keyPair) - else -> Flowable.just(isSuccess) - } - } - .concatMap { isSuccess -> - Flowable.just(isSuccess).delay(500L, TimeUnit.MILLISECONDS) - } - .concatMap { isSuccess -> - uiFlow.update { - it.copy(isLoading = false, isSuccess = isSuccess) - } - - if (!isSuccess) { - onOtpError() - } else { - timer?.cancel() - } - - Flowable.just(isSuccess).delay(if (isSuccess) 2L else 0L, TimeUnit.SECONDS) - } - .filter { it } - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { - when { - isPhoneLinking -> navigator?.popUntil { it is PhoneNumberScreen } - - isNewAccount -> { - navigator?.push( - AccessKeyScreen( - signInEntropy = seedB64, - isNewAccount = true, - phoneNumber = phoneNumber - ) - ) - } - - else -> navigator?.replaceAll(ScanScreen()) - } - }, { - setIsLoading(false) - TopBarManager.showMessage(getGenericError()) - } - ) - } - - override fun setIsLoading(isLoading: Boolean) { - uiFlow.update { - it.copy(isLoading = isLoading) - } - } - - private fun setIsResending(resending: Boolean) { - uiFlow.update { - it.copy(isResendingCode = resending) - } - } - - private fun getInvalidCodeError() = TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_invalidVerificationCode), - resources.getString(R.string.error_description_invalidVerificationCode) - ) - - private fun getTimeoutError() = TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_codeTimedOut), - resources.getString(R.string.error_description_codeTimedOut), - ) - - private fun getGenericError() = TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToVerifyPhone), - resources.getString(R.string.error_description_failedToVerifyPhone), - ) - - private fun getMaximumAttemptsReachedError() = TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_maxAttemptsReached), - resources.getString(R.string.error_description_maxAttemptsReached), - ) -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/login/PhoneVerify.kt b/apps/codeApp/src/main/java/com/getcode/view/login/PhoneVerify.kt deleted file mode 100644 index 02c7fda6a..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/login/PhoneVerify.kt +++ /dev/null @@ -1,290 +0,0 @@ -package com.getcode.view.login - -import android.app.Activity -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.IntentSenderRequest -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.shape.ZeroCornerSize -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Alignment.Companion.Center -import androidx.compose.ui.Alignment.Companion.CenterStart -import androidx.compose.ui.Alignment.Companion.CenterVertically -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusManager -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel -import com.getcode.R -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.LoginArgs -import com.getcode.theme.CodeTheme -import com.getcode.theme.White05 -import com.getcode.theme.White50 -import com.getcode.theme.extraSmall -import com.getcode.util.PhoneUtils -import com.getcode.ui.utils.getActivity -import com.getcode.ui.core.rememberedClickable -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.google.android.gms.auth.api.identity.GetPhoneNumberHintIntentRequest -import com.google.android.gms.auth.api.identity.Identity -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -@Preview -@Composable -internal fun PhoneVerify( - viewModel: PhoneVerifyViewModel = hiltViewModel(), - arguments: LoginArgs = LoginArgs(), - openCountrySelector: () -> Unit = { }, -) { - val navigator = LocalCodeNavigator.current - val dataState by viewModel.uiFlow.collectAsState() - val focusRequester = remember { FocusRequester() } - val context = LocalContext.current - val focusManager = LocalFocusManager.current - - Column( - modifier = Modifier - .fillMaxSize() - .windowInsetsPadding(WindowInsets.navigationBars) - .padding(bottom = CodeTheme.dimens.grid.x4) - .padding(horizontal = CodeTheme.dimens.inset) - .imePadding() - ) { - Box(modifier = Modifier.weight(1f), contentAlignment = Center) { - Column( - modifier = Modifier.align(Center), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.inset), - ) { - PhoneEntry( - modifier = Modifier - .height(CodeTheme.dimens.grid.x12), - focusManager = focusManager, - focusRequester = focusRequester, - locale = dataState.countryLocale, - isLoading = dataState.isLoading, - isSuccess = dataState.isSuccess, - value = dataState.phoneNumberFormattedTextFieldValue, - onValueChanged = { - viewModel.setPhoneInput(it.text, it.selection) - }, - openCountrySelector = openCountrySelector, - onSubmit = { viewModel.onSubmit(navigator, context.getActivity()) } - ) - - Text( - modifier = Modifier.padding(horizontal = CodeTheme.dimens.grid.x2), - style = CodeTheme.typography.textSmall, - textAlign = TextAlign.Center, - color = CodeTheme.colors.textSecondary, - text = stringResource(R.string.subtitle_phoneVerificationDescription) - ) - } - } - - - CodeButton( - modifier = Modifier.fillMaxWidth(), - onClick = { - viewModel.onSubmit(navigator, context.getActivity()) - }, - enabled = dataState.continueEnabled, - isLoading = dataState.isLoading, - isSuccess = dataState.isSuccess, - text = stringResource(R.string.action_next), - buttonState = ButtonState.Filled, - ) - } - - LaunchedEffect(arguments) { - arguments.signInEntropy - ?.let { viewModel.setSignInEntropy(it) } - viewModel.setIsPhoneLinking(arguments.isPhoneLinking) - viewModel.setIsNewAccount(arguments.isNewAccount) - } - - val phoneNumberHintLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartIntentSenderForResult() - ) { - if (it.resultCode != Activity.RESULT_OK) { - viewModel.dismissedHint() - return@rememberLauncherForActivityResult - } - - val phoneNum = Identity.getSignInClient(context) - .getPhoneNumberFromIntent(it.data) - - viewModel.setPhoneFromHint(phoneNum) - } - - LaunchedEffect(dataState.hasDismissedHint) { - val request = GetPhoneNumberHintIntentRequest - .builder() - .build() - - val isSettled = navigator.lastItem == navigator.lastModalItem || arguments.isNewAccount - if (isSettled && dataState.phoneNumberFormatted.isEmpty() && !dataState.hasDismissedHint) { - Identity.getSignInClient(context) - .getPhoneNumberHintIntent(request) - .addOnSuccessListener { - phoneNumberHintLauncher.launch( - IntentSenderRequest.Builder(it.intentSender).build() - ) - }.addOnFailureListener { - focusRequester.requestFocus() - } - } - } -} - -@Composable -private fun PhoneEntry( - modifier: Modifier = Modifier, - focusManager: FocusManager, - focusRequester: FocusRequester, - locale: PhoneUtils.CountryLocale, - isLoading: Boolean, - isSuccess: Boolean, - value: TextFieldValue, - onValueChanged: (TextFieldValue) -> Unit, - openCountrySelector: () -> Unit, - onSubmit: () -> Unit, -) { - val composeScope = rememberCoroutineScope() - Row( - modifier = modifier - .border( - width = CodeTheme.dimens.border, - color = CodeTheme.colors.brandLight, - shape = CodeTheme.shapes.extraSmall - ) - .background(White05) - ) { - Row( - modifier = Modifier - .height(CodeTheme.dimens.grid.x12) - .clip( - CodeTheme.shapes.extraSmall - .copy(bottomEnd = ZeroCornerSize, topEnd = ZeroCornerSize) - ) - .rememberedClickable { - composeScope.launch { - focusManager.clearFocus(true) - delay(500) - openCountrySelector() - } - }, - ) { - locale.resId?.let { resId -> - Image( - modifier = Modifier - .align(CenterVertically) - .padding(start = CodeTheme.dimens.grid.x3) - .size(CodeTheme.dimens.staticGrid.x5) - .clip(CodeTheme.shapes.large), - painter = painterResource(resId), - contentDescription = "", - ) - } - Box( - modifier = Modifier - .fillMaxHeight() - .padding(vertical = CodeTheme.dimens.border) - .padding(horizontal = CodeTheme.dimens.grid.x3,), - contentAlignment = Center - ) { - Text( - modifier = Modifier.padding(top = CodeTheme.dimens.border), - style = CodeTheme.typography.textLarge, - text = "+${locale.phoneCode}" - ) - } - } - Spacer( - modifier = Modifier - .background(CodeTheme.colors.brandLight) - .width(1.dp) - .fillMaxHeight() - ) - BasicTextField( - modifier = Modifier - .fillMaxHeight() - .weight(1f) - .focusRequester(focusRequester) - .padding(top = CodeTheme.dimens.thickBorder), - value = value, - textStyle = CodeTheme.typography.textLarge.copy(color = CodeTheme.colors.onBackground), - keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Phone), - cursorBrush = SolidColor(Color.White), - singleLine = true, - keyboardActions = KeyboardActions(onDone = { onSubmit() }), - onValueChange = { - if (!isLoading) { - onValueChanged(it) - } - }, - enabled = !isLoading && !isSuccess, - ) { textField -> - Box( - modifier = Modifier - .fillMaxSize() - .padding(start = CodeTheme.dimens.staticGrid.x2), - contentAlignment = CenterStart - ) { - if (value.text.isEmpty()) { - Text( - text = stringResource(id = R.string.title_phoneNumber), - color = White50, - fontSize = 20.sp - ) - } - textField() - } - } - - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/login/PhoneVerifyViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/login/PhoneVerifyViewModel.kt deleted file mode 100644 index 5cd9c8940..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/login/PhoneVerifyViewModel.kt +++ /dev/null @@ -1,273 +0,0 @@ -package com.getcode.view.login - -import android.annotation.SuppressLint -import android.app.Activity -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.TextFieldValue -import com.getcode.R -import com.getcode.analytics.Action -import com.getcode.analytics.CodeAnalyticsService -import com.getcode.manager.TopBarManager -import com.getcode.navigation.core.CodeNavigator -import com.getcode.navigation.screens.LoginPhoneConfirmationScreen -import com.getcode.navigation.screens.PhoneConfirmationScreen -import com.getcode.network.repository.OtpVerificationResult -import com.getcode.network.repository.PhoneRepository -import com.getcode.util.PhoneUtils -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.ErrorUtils -import com.getcode.services.utils.makeE164 -import com.getcode.view.BaseViewModel -import com.google.android.gms.auth.api.phone.SmsRetriever -import dagger.hilt.android.lifecycle.HiltViewModel -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Single -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import timber.log.Timber -import java.util.concurrent.TimeUnit -import javax.inject.Inject - - -data class PhoneVerifyUiModel( - val phoneInput: String = "", - val phoneNumberFormatted: String = "", - val phoneNumberFormattedTextFieldValue: TextFieldValue = TextFieldValue(), - val countryFlag: String = "", - val countryLocales: List = emptyList(), // PhoneUtils.countryLocales, - val countryLocalesFiltered: List = emptyList(), // PhoneUtils.countryLocales, - val countryLocale: PhoneUtils.CountryLocale = PhoneUtils.CountryLocale(name = "", phoneCode = 0, countryCode = ""), // PhoneUtils.defaultCountryLocale, - val countrySearchFilterString: String = "", - val continueEnabled: Boolean = false, - val isLoading: Boolean = false, - val isSuccess: Boolean = false, - val entropyB64: String? = null, - val isPhoneLinking: Boolean = false, - val isNewAccount: Boolean = false, - val hasDismissedHint: Boolean = false, -) - -@HiltViewModel -class PhoneVerifyViewModel @Inject constructor( - private val analytics: CodeAnalyticsService, - private val phoneRepository: PhoneRepository, - private val phoneUtils: PhoneUtils, - private val resources: ResourceHelper, -) : BaseViewModel(resources) { - val uiFlow = MutableStateFlow( - PhoneVerifyUiModel( - countryLocales = phoneUtils.countryLocales, - countryLocalesFiltered = phoneUtils.countryLocales, - countryLocale = phoneUtils.defaultCountryLocale, - ) - ) - - fun onSubmit(navigator: CodeNavigator, activity: Activity?) { - if (!uiFlow.value.continueEnabled) return - TopBarManager.setMessageShown() - CoroutineScope(Dispatchers.IO).launch { - performVerify(navigator, activity) - } - } - - fun setSignInEntropy(entropyB64: String) { - uiFlow.update { it.copy(entropyB64 = entropyB64) } - } - - fun setIsPhoneLinking(isPhoneLinking: Boolean) { - uiFlow.update { it.copy(isPhoneLinking = isPhoneLinking) } - } - - fun setIsNewAccount(isNewAccount: Boolean) { - uiFlow.update { it.copy(isNewAccount = isNewAccount) } - } - - fun dismissedHint() = uiFlow.update { it.copy(hasDismissedHint = true) } - - fun setCountryCode(countryLocale: PhoneUtils.CountryLocale) { - uiFlow.update { it.copy(countryLocale = countryLocale) } - } - - override fun setIsLoading(isLoading: Boolean) { - uiFlow.update { it.copy(isLoading = isLoading) } - } - - private fun setIsSuccess(isSuccess: Boolean) { - uiFlow.update { it.copy(isSuccess = isSuccess) } - } - - fun setPhoneFromHint(phoneNumber: String) { - val countryCode = phoneUtils.getCountryCode(phoneNumber) - val locale = phoneUtils.countryLocales - .firstOrNull { it.countryCode == countryCode } ?: phoneUtils.defaultCountryLocale - setCountryCode(locale) - - setPhoneInput( - phoneInput = phoneNumber - .replace("+${locale.phoneCode}", "") - - ) - } - fun setPhoneInput(phoneInput: String, selection_: TextRange? = null) { - val countryCode = uiFlow.value.countryLocale.phoneCode.toString() - val phoneInputFiltered = phoneInput.replace("+$countryCode", "") - val phoneNumber = "+$countryCode$phoneInputFiltered" - val phoneFormatted = phoneUtils.formatNumber( - number = phoneNumber, - countryCode = countryCode, - plus = false - ).replaceFirst(countryCode, "").replaceFirst("+", "").trimStart() - - uiFlow.update { - val selection = if (phoneFormatted.length > it.phoneNumberFormatted.length || selection_ == null) { - TextRange(phoneFormatted.length) - } else { - selection_ - } - - it.copy( - phoneInput = phoneInputFiltered, - phoneNumberFormatted = phoneFormatted, - phoneNumberFormattedTextFieldValue = TextFieldValue( - text = phoneFormatted, - selection = selection - ), - countryFlag = phoneUtils.toFlagEmoji(countryCode), - continueEnabled = phoneNumber.length > 7 && phoneUtils.isPhoneNumberValid( - phoneNumber, - countryCode - ) - ) - } - } - - @SuppressLint("CheckResult") - private fun performVerify(navigator: CodeNavigator, activity: Activity?) { - val areaCode = uiFlow.value.countryLocale.phoneCode - val countryCode = uiFlow.value.countryLocale.countryCode - val phoneInput = uiFlow.value.phoneInput - - val phoneNumberCombined = areaCode.toString() + phoneInput - - - val phoneNumber = phoneNumberCombined.makeE164( - java.util.Locale(java.util.Locale.getDefault().language, countryCode) - ) - - activity?.let { - // Starts SmsRetriever, which waits for ONE matching SMS message until timeout - // (5 minutes). The matching SMS message will be sent via a Broadcast Intent with - // action SmsRetriever#SMS_RETRIEVED_ACTION. - val client = SmsRetriever.getClient(activity) - client.startSmsRetriever() - } - - Timber.d("phoneNumber=$phoneNumber") - phoneRepository.sendVerificationCode(phoneNumber) - .firstElement() - .observeOn(AndroidSchedulers.mainThread()) - .doOnSubscribe { - analytics.action(Action.EnterPhone) - setIsLoading(true) - } - .doOnComplete { setIsLoading(false) } - .map { res -> - val message = when (res) { - OtpVerificationResult.Error.InvalidPhoneNumber, - OtpVerificationResult.Error.UnsupportedPhoneType -> getUnsupportedPhoneError() - - OtpVerificationResult.Error.UnsupportedCountry -> getUnsupportedCountryError() - OtpVerificationResult.Error.UnsupportedDevice -> getUnsupportedDeviceError() - - is OtpVerificationResult.Error -> getGenericError() - - OtpVerificationResult.Success -> null - } - - if (message != null) { - TopBarManager.showMessage(message) - } - - val success = res == OtpVerificationResult.Success - - if (!success) { - ErrorUtils.handleError(PhoneVerifyException(reason = res::class.java.simpleName)) - } - - success - } - .concatMapSingle { isSuccess -> - Single.just(isSuccess).delay(500L, TimeUnit.MILLISECONDS) - } - .concatMapSingle { isSuccess -> - setIsLoading(false) - setIsSuccess(isSuccess) - Single.just(isSuccess).delay(if (isSuccess) 2L else 0L, TimeUnit.SECONDS) - } - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { isSuccess -> - if (!isSuccess) { - setIsLoading(false) - return@subscribe - } - - if (uiFlow.value.isNewAccount) { - navigator.push( - LoginPhoneConfirmationScreen( - phoneNumber = phoneNumber, - signInEntropy = uiFlow.value.entropyB64, - isNewAccount = uiFlow.value.isNewAccount, - isPhoneLinking = uiFlow.value.isPhoneLinking, - ) - ) - } else { - navigator.push( - PhoneConfirmationScreen( - phoneNumber = phoneNumber, - signInEntropy = uiFlow.value.entropyB64, - isNewAccount = uiFlow.value.isNewAccount, - isPhoneLinking = uiFlow.value.isPhoneLinking, - ) - ) - } - }, { - it.printStackTrace() - setIsLoading(false) - TopBarManager.showMessage(getGenericError()) - } - ) - } - - private fun getGenericError() = TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToSendCode), - resources.getString(R.string.error_description_failedToSendCode) - ) - - private fun getUnsupportedPhoneError() = TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_eSimNotSupported), - resources.getString(R.string.error_description_eSimNotSupported) - ) - - private fun getUnsupportedDeviceError() = TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_deviceNotSupported), - resources.getString(R.string.error_description_deviceNotSupported) - ) - - private fun getUnsupportedCountryError() = TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_countryNotSupported), - resources.getString(R.string.error_description_countryNotSupported) - ) -} - -private class PhoneVerifyException( - cause: Throwable? = null, - val reason: String, -) : Exception(cause) { - override val message: String - get() = "Failed to verify phone number: $reason" -} - diff --git a/apps/codeApp/src/main/java/com/getcode/view/login/SeedDeepLink.kt b/apps/codeApp/src/main/java/com/getcode/view/login/SeedDeepLink.kt deleted file mode 100644 index 6391e180f..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/login/SeedDeepLink.kt +++ /dev/null @@ -1,125 +0,0 @@ -package com.getcode.view.login - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.getcode.AppHomeScreen -import com.getcode.R -import com.getcode.manager.SessionManager -import com.getcode.manager.TopBarManager -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.CodeLoginPermission -import com.getcode.navigation.screens.LoginScreen -import com.getcode.navigation.screens.PermissionRequestScreen -import com.getcode.ui.theme.CodeCircularProgressIndicator -import com.getcode.utils.decodeBase64 -import com.getcode.utils.encodeBase64 -import org.kin.sdk.base.tools.Base58 -import timber.log.Timber - -@Preview -@Composable -fun SeedDeepLink( - viewModel: SeedInputViewModel = hiltViewModel(), - seed: String? = null, -) { - val dataState by viewModel.uiFlow.collectAsState() - val navigator = LocalCodeNavigator.current - val context = LocalContext.current - val authState by SessionManager.authState.collectAsState() - - fun navigateMain() { - navigator.replaceAll(AppHomeScreen()) - } - fun navigateLogin() = navigator.replace(LoginScreen()) - - val onNotificationResult: (Boolean) -> Unit = { isGranted -> - if (isGranted) { - navigateMain() - } else { - navigator.push(PermissionRequestScreen(CodeLoginPermission.Notifications)) - } - } - val notificationPermissionCheck = - com.getcode.util.permissions.notificationPermissionCheck { onNotificationResult(it) } - - fun onError() { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - context.getString(R.string.error_title_failedToVerifyPhone), - context.getString(R.string.error_description_failedToVerifyPhone), - ) - ) - navigateLogin() - } - - var authHandled by rememberSaveable { - mutableStateOf(false) - } - - LaunchedEffect(authState.isAuthenticated) { - if (authHandled) return@LaunchedEffect - seed - ?.let { entropyB58 -> - val entropy: ByteArray - try { - entropy = Base58.decode(entropyB58) - } catch(e: Exception) { - onError() - return@let - } - - val entropyB64 = entropy.encodeBase64() - if (entropyB58.isBlank() || entropy.size != 16) { - onError() - return@let - } - val isSame = entropy.toList() == authState.entropyB64?.decodeBase64()?.toList() - Timber.d("seed isAuth=${authState.isAuthenticated}, same=$isSame") - if (isSame) { - notificationPermissionCheck(false) - } else { - authHandled = try { - viewModel.performLogin(navigator, entropyB64, deeplink = true) - true - } catch (e: Exception) { - onError() - false - } - } - } ?: run { - navigateLogin() - } - } - - LaunchedEffect(dataState.isSuccess) { - if (dataState.isSuccess) { - navigateLogin() - } - } - - if (dataState.isLoading) { - Box(modifier = Modifier.fillMaxSize()) { - CodeCircularProgressIndicator( - modifier = Modifier - .size(100.dp) - .align(Alignment.Center) - ) - } - } -} - - diff --git a/apps/codeApp/src/main/java/com/getcode/view/login/SeedInput.kt b/apps/codeApp/src/main/java/com/getcode/view/login/SeedInput.kt deleted file mode 100644 index 0202ea448..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/login/SeedInput.kt +++ /dev/null @@ -1,150 +0,0 @@ -package com.getcode.view.login - -import android.annotation.SuppressLint -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.constraintlayout.compose.ConstraintLayout -import com.getcode.R -import com.getcode.theme.* -import androidx.hilt.navigation.compose.hiltViewModel -import com.getcode.navigation.core.CodeNavigator -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.LoginArgs -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton - -@SuppressLint("InlinedApi") -@Preview -@Composable -fun SeedInput( - viewModel: SeedInputViewModel = hiltViewModel(), - arguments: LoginArgs = LoginArgs(), -) { - val navigator: CodeNavigator = LocalCodeNavigator.current - val dataState by viewModel.uiFlow.collectAsState() - val focusManager = LocalFocusManager.current - val focusRequester = FocusRequester() - - val notificationPermissionCheck = - com.getcode.util.permissions.notificationPermissionCheck(isShowError = false) { } - - Column( - modifier = Modifier - .fillMaxSize() - .windowInsetsPadding(WindowInsets.systemBars) - .padding(horizontal = CodeTheme.dimens.inset) - .padding(top = topBarHeight) - .padding(bottom = CodeTheme.dimens.grid.x4) - .verticalScroll(rememberScrollState()) - .imePadding(), - ) { - ConstraintLayout( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - ) { - val (input, wordCount, checkboxValid, captionText) = createRefs() - - Text( - modifier = Modifier - .constrainAs(captionText) { - top.linkTo(parent.top) - } - .padding(top = CodeTheme.dimens.grid.x4), - style = CodeTheme.typography.textSmall.copy(textAlign = TextAlign.Center), - color = CodeTheme.colors.textSecondary, - text = stringResource(R.string.subtitle_loginDescription) - ) - - OutlinedTextField( - modifier = Modifier - .constrainAs(input) { - top.linkTo(captionText.bottom) - } - .padding(top = CodeTheme.dimens.inset) - .fillMaxWidth() - .height(120.dp) - .focusRequester(focusRequester), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), - visualTransformation = VisualTransformation.None, - value = dataState.wordsString, - onValueChange = { viewModel.onTextChange(it) }, - textStyle = CodeTheme.typography.textLarge.copy( - fontSize = 16.sp, - ), - colors = inputColors(), - ) - - Text( - text = dataState.wordCount.toString(), - color = CodeTheme.colors.textSecondary, - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier - .constrainAs(wordCount) { - bottom.linkTo(input.bottom) - } - .padding(bottom = CodeTheme.dimens.grid.x2, start = CodeTheme.dimens.grid.x2) - ) - - if (dataState.isValid) { - Image( - painter = painterResource(id = R.drawable.ic_checked_blue), - modifier = Modifier - .constrainAs(checkboxValid) { - start.linkTo(wordCount.end) - top.linkTo(wordCount.top) - bottom.linkTo(wordCount.bottom) - } - .padding(bottom = CodeTheme.dimens.grid.x2, start = CodeTheme.dimens.grid.x1) - .height(CodeTheme.dimens.grid.x3), - contentDescription = "" - ) - } - } - - if (dataState.isSuccess) { - notificationPermissionCheck(true) - } - - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding( - top = CodeTheme.dimens.grid.x3, - bottom = CodeTheme.dimens.grid.x4 - ), - onClick = { - focusManager.clearFocus() - viewModel.onSubmit(navigator) - }, - isLoading = dataState.isLoading, - isSuccess = dataState.isSuccess, - enabled = dataState.continueEnabled, - text = stringResource(R.string.action_logIn), - buttonState = ButtonState.Filled, - ) - } - - LaunchedEffect(Unit) { - focusRequester.requestFocus() - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/login/SeedInputViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/login/SeedInputViewModel.kt deleted file mode 100644 index 6cd309c5a..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/login/SeedInputViewModel.kt +++ /dev/null @@ -1,171 +0,0 @@ -package com.getcode.view.login - -import android.annotation.SuppressLint -import android.app.Activity -import androidx.lifecycle.viewModelScope -import com.getcode.AppHomeScreen -import dagger.hilt.android.lifecycle.HiltViewModel -import com.getcode.R -import com.getcode.analytics.CodeAnalyticsService -import com.getcode.crypt.MnemonicPhrase -import com.getcode.manager.AccountManager -import com.getcode.manager.AuthManager -import com.getcode.manager.BottomBarManager -import com.getcode.services.manager.MnemonicManager -import com.getcode.manager.TopBarManager -import com.getcode.navigation.core.CodeNavigator -import com.getcode.navigation.screens.LoginPhoneVerificationScreen -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.ErrorUtils -import com.getcode.view.* -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update -import java.util.* -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -data class SeedInputUiModel( - val wordsString: String = "", - val wordCount: Int = 0, - val continueEnabled: Boolean = false, - val isValid: Boolean = false, - val isLoading: Boolean = false, - val isSuccess: Boolean = false, -) - -@HiltViewModel -class SeedInputViewModel @Inject constructor( - private val analyticsService: CodeAnalyticsService, - private val authManager: AuthManager, - private val resources: ResourceHelper, - private val mnemonicManager: MnemonicManager, - private val accountManager: AccountManager, -) : BaseViewModel(resources) { - val uiFlow = MutableStateFlow(SeedInputUiModel()) - private val mnemonicCode = mnemonicManager.mnemonicCode - - init { - viewModelScope.launch { - val token = accountManager.getToken() - if (token != null) { - analyticsService.unintentionalLogout() - ErrorUtils.handleError( - Throwable("We shouldn't be here. Login screen visible with associated account in AccountManager.") - ) - } - } - } - - fun onTextChange(wordsString: String) { - val isLoading = uiFlow.value.isLoading - val isSuccess = uiFlow.value.isSuccess - if (isLoading || isSuccess) return - - val userWordList = wordsString.lowercase(Locale.CANADA).split(" ") - val wordCount = getValidCount(userWordList, mnemonicCode.wordList) - uiFlow.update { - it.copy( - wordsString = wordsString, - wordCount = wordCount, - continueEnabled = wordCount == 12, - isValid = wordCount == 12 - ) - } - } - - fun onSubmit(navigator: CodeNavigator) { - val userWordList = - uiFlow.value.wordsString.trim().replace(Regex("(\\s)+"), " ").lowercase(Locale.getDefault()).split(" ") - val mnemonic = MnemonicPhrase.newInstance(userWordList) ?: return - - - CoroutineScope(Dispatchers.IO).launch { - val entropyB64: String - try { - entropyB64 = mnemonicManager.getEncodedBase64(mnemonic) - } catch (e: Exception) { - showError(navigator) - return@launch - } - - performLogin(navigator, entropyB64) - } - } - - fun logout(activity: Activity, onComplete: () -> Unit = {}) = - authManager.logout(activity, onComplete) - - @SuppressLint("CheckResult") - fun performLogin(navigator: CodeNavigator, entropyB64: String, deeplink: Boolean = false) { - authManager.login(entropyB64) - .subscribeOn(Schedulers.computation()) - .doOnSubscribe { - setState(isLoading = true, isSuccess = false, isContinueEnabled = false) - } - .concatWith( - Completable.complete() - .doOnSubscribe { - setState(isLoading = false, isSuccess = true, isContinueEnabled = false) - } - .delay(if (deeplink) 0L else 1L, TimeUnit.SECONDS) - ) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { - navigator.replaceAll(AppHomeScreen()) - }, { - if (it is AuthManager.AuthManagerException.TimelockUnlockedException) { - TopBarManager.showMessage( - getString(R.string.error_title_timelockUnlocked), - getString(R.string.error_description_timelockUnlocked) - ) - navigator.popAll() - } else { - showError(navigator) - } - setState(isLoading = false, isSuccess = false, isContinueEnabled = true) - } - ) - } - - private fun setState(isLoading: Boolean, isSuccess: Boolean, isContinueEnabled: Boolean) { - uiFlow.update { - it.copy( - isLoading = isLoading, - isSuccess = isSuccess, - continueEnabled = isContinueEnabled - ) - } - } - - override fun setIsLoading(isLoading: Boolean) { - uiFlow.update { - it.copy( - isLoading = isLoading, - continueEnabled = false - ) - } - } - - private fun getValidCount(userWordList: List, mnemonicWordList: List): Int { - return userWordList.filter { it in mnemonicWordList }.size - } - - private fun showError(navigator: CodeNavigator) { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = resources.getString(R.string.prompt_title_notCodeAccount), - subtitle = resources.getString(R.string.prompt_description_notCodeAccount), - positiveText = resources.getString(R.string.action_createNewCodeAccount), - negativeText = resources.getString(R.string.action_tryDifferentCodeAccount), - onPositive = { - navigator.push(LoginPhoneVerificationScreen()) - } - ) - ) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountAccessKeyViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountAccessKeyViewModel.kt deleted file mode 100644 index 738973bdd..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountAccessKeyViewModel.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.getcode.view.main.account - -import android.annotation.SuppressLint -import androidx.lifecycle.viewModelScope -import com.getcode.libs.qr.QRCodeGenerator -import com.getcode.media.MediaScanner -import com.getcode.services.manager.MnemonicManager -import com.getcode.util.resources.ResourceHelper -import com.getcode.view.login.BaseAccessKeyViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds - - -@HiltViewModel -class AccountAccessKeyViewModel @Inject constructor( - resources: ResourceHelper, - mnemonicManager: MnemonicManager, - mediaScanner: MediaScanner, - qrCodeGenerator: QRCodeGenerator, -) : BaseAccessKeyViewModel(resources, mnemonicManager, mediaScanner, qrCodeGenerator) { - @SuppressLint("CheckResult") - fun onSubmit() { - Completable.create { - val result = saveBitmapToFile() - if (result) it.onComplete() else it.onError(IllegalStateException()) - } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeOn(Schedulers.computation()) - .doOnSubscribe { - uiFlow.update { - it.copy(isLoading = true, isEnabled = false) - } - } - .doOnComplete { - viewModelScope.launch { - uiFlow.update { - it.copy(isLoading = false, isEnabled = false, isSuccess = true) - } - // wait 2s and reset button state - delay(2.seconds) - - uiFlow.update { - it.copy(isSuccess = false, isEnabled = true) - } - - } - } - .doOnError { - uiFlow.update { - it.copy(isLoading = false, isEnabled = true, isSuccess = false) - } - } - .subscribe({}, ::onSubmitError) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountDeposit.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountDeposit.kt deleted file mode 100644 index f58f88097..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountDeposit.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.getcode.view.main.account - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.style.TextAlign -import com.getcode.R -import com.getcode.manager.SessionManager -import com.getcode.theme.Brand -import com.getcode.theme.CodeTheme -import com.getcode.theme.White -import com.getcode.theme.White05 -import com.getcode.theme.extraSmall -import com.getcode.ui.core.rememberedClickable -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.components.MiddleEllipsisText -import org.kin.sdk.base.tools.Base58 - -@Composable -fun AccountDeposit() { - val address = SessionManager.getOrganizer()?.primaryVault - ?.let { Base58.encode(it.byteArray) } - ?: return - - val localClipboardManager = LocalClipboardManager.current - var isCopied by remember { mutableStateOf(false) } - - Column( - modifier = Modifier - .background(Brand) - .fillMaxSize() - .padding(horizontal = CodeTheme.dimens.inset) - .verticalScroll(rememberScrollState()) - ) { - Text( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - text = stringResource(R.string.subtitle_howToDeposit), - color = CodeTheme.colors.textSecondary, - style = CodeTheme.typography.textSmall.copy( - textAlign = TextAlign.Center, - ), - ) - - Row( - modifier = Modifier - .padding(vertical = CodeTheme.dimens.grid.x3) - .clip(CodeTheme.shapes.extraSmall) - .border(CodeTheme.dimens.border, CodeTheme.colors.brandLight, CodeTheme.shapes.extraSmall) - .fillMaxWidth() - .height(CodeTheme.dimens.grid.x10) - .background(White05) - .rememberedClickable { - localClipboardManager.setText(AnnotatedString(address)) - isCopied = true - } - .padding(CodeTheme.dimens.grid.x2), - ) { - MiddleEllipsisText( - modifier = Modifier - .align(Alignment.CenterVertically) - .weight(1f) - .padding(top = CodeTheme.dimens.grid.x1), - text = address, - color = White, - style = CodeTheme.typography.textMedium.copy( - textAlign = TextAlign.Center, - ), - ) - } - - Spacer(modifier = Modifier.weight(1f)) - - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = CodeTheme.dimens.grid.x2), - onClick = { - localClipboardManager.setText(AnnotatedString(address)) - isCopied = true - }, - text = stringResource(if (!isCopied) R.string.action_copyAddress else R.string.action_copied), - enabled = !isCopied, - isSuccess = isCopied, - buttonState = ButtonState.Filled, - ) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountDetails.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountDetails.kt deleted file mode 100644 index 63d482beb..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountDetails.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.getcode.view.main.account - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import com.getcode.R -import com.getcode.manager.BottomBarManager -import com.getcode.navigation.screens.BackupScreen -import com.getcode.navigation.screens.DeleteCodeScreen -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.PhoneNumberScreen - -@Composable -fun AccountDetails( - viewModel: AccountSheetViewModel, -) { - val context = LocalContext.current - val navigator = LocalCodeNavigator.current - val dataState by viewModel.stateFlow.collectAsState() - - fun handleItemClick(item: AccountPage) { - when (item) { - AccountPage.ACCESS_KEY -> { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = context.getString(R.string.prompt_title_viewAccessKey), - subtitle = context.getString(R.string.prompt_description_viewAccessKey), - positiveText = context.getString(R.string.action_viewAccessKey), - negativeText = context.getString(R.string.action_cancel), - onPositive = { navigator.push(BackupScreen) }, - onNegative = {} - ) - ) - } - - AccountPage.PHONE -> navigator.push(PhoneNumberScreen) - AccountPage.DELETE_ACCOUNT -> navigator.push(DeleteCodeScreen) - - else -> Unit - } - } - Box(modifier = Modifier.fillMaxHeight()) { - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - ) { - val actions: List = listOf( - AccountMainItem( - type = AccountPage.ACCESS_KEY, - name = R.string.title_accessKey, - icon = R.drawable.ic_menu_key - ), - AccountMainItem( - type = AccountPage.PHONE, - name = R.string.title_phoneNumber, - icon = R.drawable.ic_menu_phone, - isPhoneLinked = dataState.isPhoneLinked, - ), - AccountMainItem( - type = AccountPage.DELETE_ACCOUNT, - name = R.string.action_deleteAccount, - icon = R.drawable.ic_delete - ), - ) - - for (action in actions) { - ListItem(action) { - handleItemClick(action.type) - } - } - } - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountFaq.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountFaq.kt deleted file mode 100644 index 6983e9980..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountFaq.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.getcode.view.main.account - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.tooling.preview.Preview -import androidx.lifecycle.viewmodel.compose.viewModel -import com.getcode.theme.R as themeR -import com.getcode.theme.CodeTheme -import com.getcode.theme.White -import com.getcode.ui.components.MarkdownText - -@Preview -@Composable -fun AccountFaq( - viewModel: AccountFaqViewModel = viewModel(), -) { - val dataState by viewModel.stateFlow.collectAsState() - - LazyColumn( - contentPadding = PaddingValues(CodeTheme.dimens.inset), - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x11) - ) { - items(dataState.faqItems) { faqResponse -> - Column( - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2) - ) { - Text( - text = faqResponse.question, - color = White, - style = CodeTheme.typography.textLarge - ) - MarkdownText( - markdown = faqResponse.answer, - fontResource = themeR.font.avenir_next_demi, - style = CodeTheme.typography.textMedium, - color = CodeTheme.colors.textSecondary - ) - } - - } - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountFaqViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountFaqViewModel.kt deleted file mode 100644 index dc6e8f9de..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountFaqViewModel.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.getcode.view.main.account - -import androidx.lifecycle.viewModelScope -import com.getcode.R -import com.getcode.model.FaqItem -import com.getcode.util.resources.ResourceHelper -import com.getcode.view.BaseViewModel2 -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class AccountFaqViewModel @Inject constructor( - resources: ResourceHelper, -) : BaseViewModel2( - initialState = State(), - updateStateForEvent = updateStateForEvent -) { - - private val questions = listOf( - R.string.faq_q__1, - R.string.faq_q__2, - R.string.faq_q__3, - R.string.faq_q__4, - R.string.faq_q__5, - R.string.faq_q__6, - ).map { resources.getString(it) } - - private val answers = listOf( - R.string.faq_a__1, - R.string.faq_a__2, - R.string.faq_a__3, - R.string.faq_a__4, - R.string.faq_a__5, - R.string.faq_a__6, - ).map { resources.getString(it) } - - private val faqItems = questions.zip(answers).map { FaqItem(question = it.first, answer = it.second) } - - data class State( - val faqItems: List = emptyList() - ) - - sealed interface Event { - data class LoadWith(val items: List): Event - } - - init { - viewModelScope.launch { - dispatchEvent(Event.LoadWith(faqItems)) - } - } - - companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - is Event.LoadWith -> { state -> state.copy(faqItems = event.items) } - } - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountHome.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountHome.kt deleted file mode 100644 index 3b8294e92..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountHome.kt +++ /dev/null @@ -1,220 +0,0 @@ -package com.getcode.view.main.account - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Divider -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Alignment.Companion.CenterVertically -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.getcode.BuildConfig -import com.getcode.R -import com.getcode.manager.BottomBarManager -import com.getcode.manager.TopBarManager -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.AccountDebugOptionsScreen -import com.getcode.navigation.screens.AccountDetailsScreen -import com.getcode.navigation.screens.AppSettingsScreen -import com.getcode.navigation.screens.BuyMoreKinModal -import com.getcode.navigation.screens.BuySellScreen -import com.getcode.navigation.screens.DepositKinScreen -import com.getcode.navigation.screens.FaqScreen -import com.getcode.navigation.screens.WithdrawalAmountScreen -import com.getcode.theme.CodeTheme -import com.getcode.ui.theme.CodeScaffold -import com.getcode.ui.utils.getActivity -import com.getcode.ui.core.rememberedClickable -import com.getcode.ui.core.verticalScrollStateGradient -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -@Composable -fun AccountHome( - viewModel: AccountSheetViewModel, -) { - val navigator = LocalCodeNavigator.current - val dataState by viewModel.stateFlow.collectAsState() - val context = LocalContext.current - - val composeScope = rememberCoroutineScope() - - val handleItemClicked = remember { - { item: AccountPage -> - composeScope.launch { - when (item) { - AccountPage.BUY_KIN -> { - if (dataState.buyModule.enabled) { - if (dataState.buyModule.available) { - navigator.push(BuyMoreKinModal()) - } else { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = context.getString(R.string.error_title_buyModuleUnavailable), - message = context.getString(R.string.error_description_buyModuleUnavailable), - type = TopBarManager.TopBarMessageType.ERROR - ) - ) - } - } else { - navigator.push(BuySellScreen) - } - } - - AccountPage.DEPOSIT -> navigator.push(DepositKinScreen) - AccountPage.WITHDRAW -> navigator.push(WithdrawalAmountScreen) - AccountPage.FAQ -> navigator.push(FaqScreen) - AccountPage.ACCOUNT_DETAILS -> navigator.push(AccountDetailsScreen) - AccountPage.ACCOUNT_DEBUG_OPTIONS -> navigator.push(AccountDebugOptionsScreen) - AccountPage.APP_SETTINGS -> navigator.push(AppSettingsScreen) - AccountPage.LOGOUT -> { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = context.getString(R.string.prompt_title_logout), - subtitle = context - .getString(R.string.prompt_description_logout), - positiveText = context.getString(R.string.action_logout), - negativeText = context.getString(R.string.action_cancel), - onPositive = { - composeScope.launch { - delay(150) // wait for dismiss - context.getActivity()?.let { - viewModel.logout(it) - } - } - } - ) - ) - } - - AccountPage.PHONE -> Unit - AccountPage.DELETE_ACCOUNT -> Unit - AccountPage.ACCESS_KEY -> Unit - } - } - } - } - - CodeScaffold( - bottomBar = { - Box(modifier = Modifier.fillMaxWidth()) { - Text( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.Center), - text = "Version ${BuildConfig.VERSION_NAME} • Build ${BuildConfig.VERSION_CODE}", - color = CodeTheme.colors.textSecondary, - style = CodeTheme.typography.textSmall.copy( - textAlign = TextAlign.Center - ), - ) - } - } - ) { padding -> - val listState = rememberLazyListState() - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .verticalScrollStateGradient( - scrollState = listState, - isLongGradient = true, - ), - state = listState, - ) { - items(dataState.items, key = { it.type }, contentType = { it }) { item -> - ListItem(item = item) { - handleItemClicked(item.type) - } - } - } - } -} - -@Composable -fun ListItem(item: AccountMainItem, onClick: () -> Unit) { - Row( - modifier = Modifier - .rememberedClickable { onClick() } - .padding(CodeTheme.dimens.grid.x5) - .fillMaxWidth() - .wrapContentHeight(), - verticalAlignment = CenterVertically - ) { - Image( - modifier = Modifier - .padding(end = CodeTheme.dimens.inset) - .height(CodeTheme.dimens.staticGrid.x5) - .width(CodeTheme.dimens.staticGrid.x5), - painter = painterResource(id = item.icon), - contentDescription = "" - ) - Text( - modifier = Modifier.align(CenterVertically), - text = stringResource(item.name), - style = CodeTheme.typography.textLarge.copy( - fontWeight = FontWeight.Bold - ), - ) - item.isPhoneLinked?.let { isPhoneLinked -> - Row( - modifier = Modifier - .fillMaxWidth(), - verticalAlignment = CenterVertically, - horizontalArrangement = Arrangement.End - ) { - if (isPhoneLinked) { - Icon( - imageVector = Icons.Filled.Check, - tint = Color.Green, - contentDescription = "Linked", - modifier = Modifier.size(CodeTheme.dimens.staticGrid.x3) - ) - } - Text( - modifier = Modifier - .padding(start = CodeTheme.dimens.grid.x1), - text = if (isPhoneLinked) stringResource(id = R.string.title_linked) - else stringResource(id = R.string.title_notLinked), - color = CodeTheme.colors.textSecondary, - style = CodeTheme.typography.textMedium.copy( - fontSize = 12.sp - ), - ) - } - } - } - - Divider( - modifier = Modifier.padding(horizontal = CodeTheme.dimens.inset), - color = CodeTheme.colors.divider, - thickness = 0.5.dp - ) -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountPhone.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountPhone.kt deleted file mode 100644 index d04553819..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountPhone.kt +++ /dev/null @@ -1,117 +0,0 @@ -package com.getcode.view.main.account - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.constraintlayout.compose.ConstraintLayout -import com.getcode.R -import com.getcode.manager.BottomBarManager -import com.getcode.manager.SessionManager -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.PhoneVerificationScreen -import com.getcode.theme.Brand -import com.getcode.theme.CodeTheme -import com.getcode.theme.bolded -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.utils.urlEncode - -@Composable -fun AccountPhone( - viewModel: AccountPhoneViewModel, -) { - val context = LocalContext.current - val navigator = LocalCodeNavigator.current - val dataState: AccountPhoneUiModel by viewModel.uiFlow.collectAsState() - - ConstraintLayout( - modifier = Modifier - .background(Brand) - .fillMaxSize() - .padding(horizontal = CodeTheme.dimens.inset) - ) { - val (image, title, text, button) = createRefs() - Image( - modifier = Modifier - .padding(bottom = CodeTheme.dimens.staticGrid.x6) - .constrainAs(image) { - bottom.linkTo(title.top, 0.dp) - } - .size(CodeTheme.dimens.staticGrid.x17), - painter = painterResource(id = if (dataState.isLinked) R.drawable.ic_phone_filled else R.drawable.ic_phone_empty), - contentDescription = "" - ) - Text( - modifier = Modifier - .padding(bottom = CodeTheme.dimens.grid.x6) - .constrainAs(title) { - centerVerticallyTo(parent, 0.4f) - }, - text = if (!dataState.isLinked) stringResource(R.string.subtitle_noLinkedPhoneNumber) else dataState.phoneNumberFormatted.orEmpty(), - style = CodeTheme.typography.displayMedium.bolded() - ) - Text( - modifier = Modifier - .constrainAs(text) { - top.linkTo(title.bottom) - }, - text = if (!dataState.isLinked) stringResource(R.string.subtitle_noLinkedPhoneNumberDescription) - else stringResource(R.string.subtitle_linkedPhoneNumberDescription), - style = CodeTheme.typography.textMedium - ) - - CodeButton( - onClick = { - if (!dataState.isLinked) { - val entropyB64 = SessionManager.authState.value?.entropyB64 - if (!entropyB64.isNullOrBlank()) { - navigator.push( - PhoneVerificationScreen( - signInEntropy = entropyB64.urlEncode(), - isPhoneLinking = true - ) - ) - } - } else { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = context.getString(R.string.prompt_title_unlinkPhoneNumber), - subtitle = context.getString(R.string.prompt_description_unlinkPhoneNumber), - positiveText = "Yes", - negativeText = context.getString(R.string.action_cancel), - onPositive = viewModel::unlinkPhone, - onNegative = {} - ) - ) - } - }, - text = if (!dataState.isLinked) stringResource(R.string.action_linkPhoneNumber) else stringResource( - R.string.action_removeYourPhoneNumber - ), - buttonState = if (!dataState.isLinked) ButtonState.Filled else ButtonState.Filled50, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = CodeTheme.dimens.inset) - .constrainAs(button) { - bottom.linkTo(parent.bottom) - } - ) - } - - SideEffect { - viewModel.init() - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountPhoneViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountPhoneViewModel.kt deleted file mode 100644 index bd2a939c0..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountPhoneViewModel.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.getcode.view.main.account - -import com.getcode.manager.SessionManager -import com.getcode.network.repository.IdentityRepository -import com.getcode.network.repository.PhoneRepository -import com.getcode.network.repository.UnlinkAccountResult -import com.getcode.util.PhoneUtils -import com.getcode.util.resources.ResourceHelper -import com.getcode.services.utils.makeE164 -import com.getcode.view.BaseViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import javax.inject.Inject - - -data class AccountPhoneUiModel( - val isLinked: Boolean = false, - val phoneNumber: String? = null, - val phoneNumberFormatted: String? = null -) - -@HiltViewModel -class AccountPhoneViewModel @Inject constructor( - private val identityRepository: IdentityRepository, - private val phoneRepository: PhoneRepository, - private val phoneUtils: PhoneUtils, - resources: ResourceHelper, -) : BaseViewModel(resources) { - val uiFlow = MutableStateFlow(AccountPhoneUiModel()) - - fun init() { - phoneRepository.getAssociatedPhoneNumberLocal() - .subscribe { - uiFlow.value = AccountPhoneUiModel( - isLinked = it.isLinked, - phoneNumber = it.phoneNumber, - phoneNumberFormatted = phoneUtils.formatNumber(it.phoneNumber) - ) - } - } - - fun unlinkPhone() { - val keyPair = SessionManager.authState.value?.keyPair ?: return - val phoneNumber = uiFlow.value.phoneNumber - ?.filter { it.isDigit() }?.makeE164() ?: return - - identityRepository.unlinkAccount(keyPair, phoneNumber).subscribe { result -> - if (result is UnlinkAccountResult.Success) { - phoneRepository.phoneLinked.value = false - uiFlow.value = AccountPhoneUiModel(false, null) - } - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountSheetViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountSheetViewModel.kt deleted file mode 100644 index b59817c1b..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/AccountSheetViewModel.kt +++ /dev/null @@ -1,228 +0,0 @@ -package com.getcode.view.main.account - -import android.app.Activity -import androidx.compose.runtime.Stable -import androidx.lifecycle.viewModelScope -import com.getcode.R -import com.getcode.manager.AuthManager -import com.getcode.model.BuyModuleFeature -import com.getcode.model.Feature -import com.getcode.services.model.PrefsBool -import com.getcode.network.repository.BetaFlagsRepository -import com.getcode.network.repository.BetaOptions -import com.getcode.network.repository.FeatureRepository -import com.getcode.network.repository.PhoneRepository -import com.getcode.network.repository.PrefRepository -import com.getcode.view.BaseViewModel2 -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import javax.inject.Inject - -data class AccountMainItem( - val type: AccountPage, - val name: Int, - val description: Int? = null, - val icon: Int, - val isPhoneLinked: Boolean? = null, -) - -enum class AccountPage { - BUY_KIN, - DEPOSIT, - WITHDRAW, - PHONE, - DELETE_ACCOUNT, - ACCESS_KEY, - FAQ, - ACCOUNT_DETAILS, - APP_SETTINGS, - ACCOUNT_DEBUG_OPTIONS, - LOGOUT -} - -@HiltViewModel -class AccountSheetViewModel @Inject constructor( - private val authManager: AuthManager, - private val prefRepository: PrefRepository, - betaFlags: BetaFlagsRepository, - features: FeatureRepository, - phoneRepository: PhoneRepository, -) : BaseViewModel2( - initialState = State(), - updateStateForEvent = updateStateForEvent -) { - @Stable - data class State( - val logoClickCount: Int = 0, - val items: List = emptyList(), - val isHome: Boolean = true, - val page: AccountPage? = null, - val isPhoneLinked: Boolean = false, - val betaFlagsVisible: Boolean = false, - val buyModule: Feature = BuyModuleFeature(), - ) - - sealed interface Event { - data class OnPhoneLinked(val linked: Boolean) : Event - data class BetaFlagsChanged(val options: BetaOptions) : Event - data class OnBetaVisibilityChanged(val visible: Boolean) : Event - data class OnBuyModuleStateChanged(val module: Feature) : Event - data object LogoClicked : Event - data class Navigate(val page: AccountPage) : Event - data class OnItemsChanged(val items: List) : Event - } - - // TODO: handle this differently - fun logout(activity: Activity) { - authManager.logout(activity, onComplete = {}) - } - - init { - betaFlags.observe() - .distinctUntilChanged() - .onEach { dispatchEvent(Dispatchers.Main, Event.BetaFlagsChanged(it)) } - .launchIn(viewModelScope) - - prefRepository - .observeOrDefault(PrefsBool.IS_DEBUG_ACTIVE, false) - .distinctUntilChanged() - .onEach { dispatchEvent(Dispatchers.Main, Event.OnBetaVisibilityChanged(it)) } - .launchIn(viewModelScope) - - features.buyModule - .onEach { dispatchEvent(Event.OnBuyModuleStateChanged(it)) } - .launchIn(viewModelScope) - - phoneRepository - .phoneLinked - .onEach { dispatchEvent(Event.OnPhoneLinked(it)) } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .filter { prefRepository.get(PrefsBool.IS_DEBUG_ALLOWED, false) } - .map { stateFlow.value.logoClickCount } - .filter { it >= 10 } - .map { stateFlow.value.betaFlagsVisible } - .onEach { - prefRepository.set(PrefsBool.IS_DEBUG_ACTIVE, !it) - }.launchIn(viewModelScope) - } - - companion object { - private val fullItemSet = listOf( - AccountMainItem( - type = AccountPage.BUY_KIN, - name = R.string.title_buySellKin, - icon = R.drawable.ic_currency_dollar_active - ), - AccountMainItem( - type = AccountPage.DEPOSIT, - name = R.string.title_depositKin, - icon = R.drawable.ic_menu_deposit - ), - AccountMainItem( - type = AccountPage.WITHDRAW, - name = R.string.title_withdrawKin, - icon = R.drawable.ic_menu_withdraw - ), - AccountMainItem( - type = AccountPage.ACCOUNT_DETAILS, - name = R.string.title_myAccount, - icon = R.drawable.ic_menu_account - ), - AccountMainItem( - type = AccountPage.APP_SETTINGS, - name = R.string.title_appSettings, - icon = R.drawable.ic_settings_outline, - ), - AccountMainItem( - type = AccountPage.ACCOUNT_DEBUG_OPTIONS, - name = R.string.title_betaFlags, - icon = R.drawable.ic_bug, - ), - AccountMainItem( - type = AccountPage.FAQ, - name = R.string.title_faq, - icon = R.drawable.ic_faq, - ), - AccountMainItem( - type = AccountPage.LOGOUT, - name = R.string.action_logout, - icon = R.drawable.ic_menu_logout - ) - ) - - private fun buildItemSet( - betaFlagsVisible: Boolean, - buyModuleEnabled: Boolean, - ): List { - val fullItems = fullItemSet - - val items = fullItems - .map { - when (it.type) { - AccountPage.BUY_KIN -> { - if (buyModuleEnabled) { - it.copy(name = R.string.action_addCash) - } else { - it.copy(name = R.string.title_buySellKin) - } - } - else -> it - } - } - .filter { - if (it.type == AccountPage.ACCOUNT_DEBUG_OPTIONS) { - betaFlagsVisible - } else { - true - } - } - - return items - } - - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - is Event.OnPhoneLinked -> { state -> state.copy(isPhoneLinked = event.linked) } - Event.LogoClicked -> { state -> - val count = state.logoClickCount + 1 - state.copy(logoClickCount = count) - } - - is Event.BetaFlagsChanged -> { state -> - val items = buildItemSet( - betaFlagsVisible = state.betaFlagsVisible, - buyModuleEnabled = event.options.buyModuleEnabled, - ) - - state.copy(items = items,) - } - - is Event.OnBetaVisibilityChanged -> { state -> - val items = buildItemSet( - betaFlagsVisible = event.visible, - buyModuleEnabled = state.buyModule.enabled - ) - - state.copy( - betaFlagsVisible = event.visible, - items = items, - logoClickCount = 0 - ) - } - - is Event.OnItemsChanged -> { state -> state.copy(items = event.items) } - is Event.Navigate -> { state -> state } - is Event.OnBuyModuleStateChanged -> { state -> state.copy(buyModule = event.module) } - } - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/AppSettingsScreen.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/AppSettingsScreen.kt deleted file mode 100644 index 0435260dd..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/AppSettingsScreen.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.getcode.view.main.account - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import com.getcode.libs.biometrics.Biometrics -import com.getcode.services.model.PrefsBool -import com.getcode.ui.components.SettingsSwitchRow -import kotlinx.coroutines.launch - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun AppSettingsScreen( - viewModel: AppSettingsViewModel -) { - val context = LocalContext.current - val state by viewModel.stateFlow.collectAsState() - val scope = rememberCoroutineScope() - - LazyColumn { - items(state.settings) { option -> - if (option.visible) { - SettingsSwitchRow( - modifier = Modifier.animateItemPlacement(), - enabled = option.available, - title = stringResource(id = option.name), - icon = option.icon, - subtitle = option.description?.let { stringResource(id = it) }, - checked = option.enabled - ) { - val toggle = { - viewModel.dispatchEvent( - AppSettingsViewModel.Event.SettingChanged(option.type, !option.enabled) - ) - } - - when (option.type) { - PrefsBool.CAMERA_START_BY_DEFAULT -> toggle() - PrefsBool.REQUIRE_BIOMETRICS -> { - scope.launch { - Biometrics.prompt(context, delay = 300) - .onSuccess { toggle() } - } - } - } - - } - } - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/AppSettingsViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/AppSettingsViewModel.kt deleted file mode 100644 index e1f2bd8c9..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/AppSettingsViewModel.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.getcode.view.main.account - -import androidx.lifecycle.viewModelScope -import com.getcode.mapper.AppSettingsMapper -import com.getcode.services.model.AppSetting -import com.getcode.models.SettingItem -import com.getcode.network.repository.AppSettingsRepository -import com.getcode.view.BaseViewModel2 -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import javax.inject.Inject - -@HiltViewModel -class AppSettingsViewModel @Inject constructor( - appSettings: AppSettingsRepository, - appSettingsMapper: AppSettingsMapper, -) : BaseViewModel2( - initialState = State(emptyList()), - updateStateForEvent = updateStateForEvent -) { - data class State( - val settings: List, - ) - - sealed interface Event { - data class UpdateSettings(val settings: List) : Event - data class SettingChanged(val setting: AppSetting, val value: Boolean): Event - } - - init { - appSettings.observe() - .distinctUntilChanged() - .map { settings -> appSettingsMapper.map(settings) } - .onEach { settings -> dispatchEvent(Event.UpdateSettings(settings)) } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.setting to it.value } - .onEach { (setting, value) -> - appSettings.update(setting, value) - }.launchIn(viewModelScope) - } - - companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - is Event.SettingChanged -> { state -> state } - is Event.UpdateSettings -> { state -> state.copy(settings = event.settings) } - } - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/BackupKey.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/BackupKey.kt deleted file mode 100644 index 19f027ab7..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/BackupKey.kt +++ /dev/null @@ -1,205 +0,0 @@ -package com.getcode.view.main.account - -import android.Manifest -import android.os.Build -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.MutableTransitionState -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.isSpecified -import com.getcode.R -import com.getcode.manager.TopBarManager -import com.getcode.theme.CodeTheme -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.components.Cloudy -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.components.SelectionContainer -import com.getcode.ui.components.rememberSelectionState -import com.getcode.ui.core.addIf -import com.getcode.ui.core.measured -import com.getcode.util.launchAppSettings -import com.getcode.util.permissions.PermissionResult -import com.getcode.util.permissions.getPermissionLauncher -import com.getcode.util.permissions.rememberPermissionHandler - -@Composable -fun BackupKey( - viewModel: AccountAccessKeyViewModel, -) { - val dataState by viewModel.uiFlow.collectAsState() - - val context = LocalContext.current - var isExportSeedRequested by remember { mutableStateOf(false) } - var isStoragePermissionGranted by remember { mutableStateOf(false) } - val isAccessKeyVisible = remember { MutableTransitionState(false) } - - val onPermissionResult = { result: PermissionResult -> - isStoragePermissionGranted = result == PermissionResult.Granted - - if (!isStoragePermissionGranted) { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = context.getString(R.string.error_title_failedToSave), - message = context.getString(R.string.error_description_failedToSave), - type = TopBarManager.TopBarMessageType.ERROR, - secondaryText = context.getString(R.string.action_openSettings), - secondaryAction = { context.launchAppSettings() } - ) - ) - } - } - - val launcher = getPermissionLauncher(Manifest.permission.WRITE_EXTERNAL_STORAGE, onPermissionResult) - val permissionChecker = rememberPermissionHandler() - if (isExportSeedRequested && isStoragePermissionGranted) { - viewModel.onSubmit() - isExportSeedRequested = false - } - - val onExportClick = { - isExportSeedRequested = true - - if (Build.VERSION.SDK_INT > 29) { - isStoragePermissionGranted = true - } else { - permissionChecker.request( - permission = Manifest.permission.WRITE_EXTERNAL_STORAGE, - onPermissionResult = onPermissionResult, - launcher = launcher - ) - } - } - - - var buttonHeight by remember { - mutableStateOf(Dp.Unspecified) - } - - var textHeight by remember { - mutableStateOf(Dp.Unspecified) - } - - val selectionState = rememberSelectionState( - content = dataState.words.joinToString(" ") - ) - - SelectionContainer( - modifier = Modifier - .fillMaxSize() - .windowInsetsPadding(WindowInsets.navigationBars), - state = selectionState, - ) { - Cloudy( - modifier = Modifier - .fillMaxSize(), - enabled = selectionState.shown - ) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = CodeTheme.dimens.inset,) - .padding(vertical = CodeTheme.dimens.grid.x4), - ) { - Text( - modifier = Modifier - .align(Alignment.TopCenter) - .padding(bottom = CodeTheme.dimens.grid.x2) - .measured { textHeight = it.height }, - style = CodeTheme.typography.textSmall.copy(textAlign = TextAlign.Center), - color = CodeTheme.colors.textSecondary, - text = stringResource(R.string.subtitle_accessKeyDescription) - .replace(". ", ".\n") - ) - - Column( - modifier = Modifier - .align(Alignment.BottomCenter) - .measured { buttonHeight = it.height }, - ) { - CodeButton( - modifier = Modifier.fillMaxWidth(), - onClick = { onExportClick() }, - text = stringResource(R.string.action_saveAccessKey), - buttonState = ButtonState.Filled, - isLoading = dataState.isLoading, - enabled = dataState.isEnabled, - isSuccess = dataState.isSuccess, - ) - } - } - } - - Column( - modifier = Modifier - .align(Alignment.TopCenter) - .fillMaxHeight() - .addIf(textHeight.isSpecified) { Modifier.padding(top = textHeight) } - .addIf(buttonHeight.isSpecified) { Modifier.padding(bottom = buttonHeight + CodeTheme.dimens.grid.x4) }, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Column( - modifier = Modifier - // highly specific aspect ratio from iOS :) - .aspectRatio(0.607f, matchHeightConstraintsFirst = true) - .fillMaxWidth() - .weight(1f), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - AnimatedVisibility( - visibleState = isAccessKeyVisible, - enter = fadeIn(animationSpec = tween(300, 0)), - exit = fadeOut(animationSpec = tween(300, 0)) - ) { - dataState.accessKeyCroppedBitmap?.let { bitmap -> - Image( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .scale(selectionState.scale.value), - bitmap = bitmap.asImageBitmap(), - contentDescription = dataState.wordsFormatted, - ) - } - } - } - } - } - - LaunchedEffect(viewModel) { - viewModel.init() - } - - LaunchedEffect(dataState.accessKeyCroppedBitmap) { - isAccessKeyVisible.targetState = dataState.accessKeyCroppedBitmap != null - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/BetaFlagsScreen.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/BetaFlagsScreen.kt deleted file mode 100644 index bf7df81e6..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/BetaFlagsScreen.kt +++ /dev/null @@ -1,183 +0,0 @@ -package com.getcode.view.main.account - -import android.widget.Toast -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import com.getcode.R -import com.getcode.services.model.Immutable -import com.getcode.services.model.PrefsBool -import com.getcode.network.repository.BetaOptions -import com.getcode.theme.CodeTheme -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.components.SettingsSwitchRow -import dev.bmcreations.tipkit.engines.LocalTipsEngine - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun BetaFlagsScreen( - viewModel: BetaFlagsViewModel, -) { - data class BetaFeature( - val flag: PrefsBool, - val titleResId: Int, - val subtitleText: String, - val dataState: Boolean, - val onChange: (Boolean) -> Unit = { value -> - viewModel.dispatchEvent(BetaFlagsViewModel.Event.Toggle(flag, value)) - } - ) - - val state by viewModel.stateFlow.collectAsState() - - val tipsEngine = LocalTipsEngine.current - - val options = listOf( - BetaFeature( - PrefsBool.KADO_WEBVIEW_ENABLED, - R.string.beta_kado_webview, - stringResource(id = R.string.beta_kado_webview_description), - state.kadoWebViewEnabled, - ), - BetaFeature( - PrefsBool.SHARE_TWEET_TO_TIP, - R.string.beta_share_tweet_tip, - stringResource(id = R.string.beta_share_tweet_tip_description), - state.shareTweetToTip, - ), - BetaFeature( - PrefsBool.CAMERA_GESTURES_ENABLED, - R.string.beta_camera_gestures, - stringResource(id = R.string.beta_camera_gestures_description), - state.cameraGesturesEnabled, - ), - BetaFeature( - PrefsBool.CAMERA_DRAG_INVERTED, - R.string.beta_camera_invert_drag, - stringResource(id = R.string.beta_camera_invert_drag_description), - state.invertedDragZoom, - ), - BetaFeature( - PrefsBool.TIP_CARD_FLIPPABLE, - R.string.beta_tipcard_can_flip, - stringResource(id = R.string.beta_tipcard_can_flip_description), - state.canFlipTipCard, - ), - BetaFeature( - PrefsBool.GALLERY_ENABLED, - R.string.beta_photo_gallery, - stringResource(id = R.string.beta_photo_gallery_description), - state.galleryEnabled, - ), - BetaFeature( - PrefsBool.VIBRATE_ON_SCAN, - R.string.beta_vibrate_on_scan, - stringResource(R.string.beta_vibrate_on_scan_description), - state.tickOnScan - ), - BetaFeature( - PrefsBool.SHOW_CONNECTIVITY_STATUS, - R.string.beta_network_dropoff, - stringResource(R.string.beta_network_connectivity_description), - state.showNetworkDropOff - ), - BetaFeature( - PrefsBool.BUCKET_DEBUGGER_ENABLED, - R.string.beta_bucket_debugger, - stringResource(R.string.beta_bucket_debugger_description), - state.canViewBuckets - ), - BetaFeature( - PrefsBool.BALANCE_CURRENCY_SELECTION_ENABLED, - R.string.beta_balance_currency, - stringResource(R.string.beta_balance_currency_description), - state.balanceCurrencySelectionEnabled - ), - BetaFeature( - PrefsBool.GIVE_REQUESTS_ENABLED, - R.string.beta_give_requests_mode, - stringResource(id = R.string.beta_give_requests_description), - state.giveRequestsEnabled - ), - BetaFeature( - PrefsBool.BUY_MODULE_ENABLED, - R.string.beta_buy_kin, - stringResource(id = R.string.beta_buy_kin_description), - state.buyModuleEnabled - ), - BetaFeature( - PrefsBool.CHAT_UNSUB_ENABLED, - R.string.beta_chat_unsub, - stringResource(id = R.string.beta_chat_unsub_description), - state.chatUnsubEnabled, - ), - BetaFeature( - PrefsBool.TIPS_ENABLED, - R.string.beta_tipcard, - stringResource(id = R.string.beta_tipcard_description), - state.tipsEnabled, - ), - BetaFeature( - PrefsBool.TIP_CARD_ON_HOMESCREEN, - R.string.beta_tipcard_on_homescreen, - stringResource(id = R.string.beta_tipcard_on_homescreen_description), - state.tipCardOnHomeScreen, - ), - BetaFeature( - PrefsBool.CONVERSATION_CASH_ENABLED, - R.string.beta_conversations_cash, - stringResource(id = R.string.beta_conversations_cash_description), - state.conversationCashEnabled, - ), - BetaFeature( - PrefsBool.DISPLAY_ERRORS, - R.string.beta_display_errors, - "", - state.displayErrors, - ) - ).filter { state.canMutate(it.flag) } - - LazyColumn { - items(options) { option -> - SettingsSwitchRow( - modifier = Modifier.animateItemPlacement(), - title = stringResource(id = option.titleResId), - subtitle = option.subtitleText, - checked = option.dataState - ) { - option.onChange(!option.dataState) - } - } - - item { - val context = LocalContext.current - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.grid.x3), - buttonState = ButtonState.Filled, - text = stringResource(id = R.string.beta_resetTooltips), - onClick = { - tipsEngine?.invalidateAllTips() - Toast.makeText(context, "Tooltips reset", Toast.LENGTH_LONG).show() - } - ) - } - } -} - -private fun BetaOptions.canMutate(flag: PrefsBool): Boolean { - return when (flag) { - is Immutable -> false - else -> true - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/BetaFlagsViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/BetaFlagsViewModel.kt deleted file mode 100644 index 2fc8b8533..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/BetaFlagsViewModel.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.getcode.view.main.account - -import androidx.lifecycle.viewModelScope -import com.getcode.services.model.PrefsBool -import com.getcode.network.repository.BetaFlagsRepository -import com.getcode.network.repository.BetaOptions -import com.getcode.network.repository.PrefRepository -import com.getcode.util.Pacman -import com.getcode.utils.ErrorUtils -import com.getcode.view.BaseViewModel2 -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import javax.inject.Inject - -@HiltViewModel -class BetaFlagsViewModel @Inject constructor( - betaFlags: BetaFlagsRepository, - prefRepository: PrefRepository, - pacman: Pacman, -) : BaseViewModel2( - initialState = BetaOptions.Defaults, - updateStateForEvent = updateStateForEvent -) { - sealed interface Event { - data class UpdateSettings(val settings: BetaOptions) : Event - data class Toggle(val setting: PrefsBool, val state: Boolean): Event - } - - init { - betaFlags.observe() - .distinctUntilChanged() - .onEach { settings -> - dispatchEvent(Event.UpdateSettings(settings)) - }.launchIn(viewModelScope) - - - eventFlow - .filterIsInstance() - .onEach { - prefRepository.set( - it.setting, - it.state - ) - - when (it.setting) { - PrefsBool.SHARE_TWEET_TO_TIP -> { - pacman.enableTweetShare(it.state) - } - PrefsBool.DISPLAY_ERRORS -> { - ErrorUtils.setDisplayErrors(it.state) - } - else -> Unit - } - }.launchIn(viewModelScope) - } - - companion object { - val updateStateForEvent: (Event) -> ((BetaOptions) -> BetaOptions) = { event -> - when (event) { - is Event.UpdateSettings -> { _ -> - event.settings - } - - is Event.Toggle -> { state -> state } - } - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/BucketDebugger.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/BucketDebugger.kt deleted file mode 100644 index 326e122c0..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/BucketDebugger.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.getcode.view.main.account - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.unit.dp -import com.getcode.manager.SessionManager -import com.getcode.model.displayName -import com.getcode.solana.keys.base58 -import com.getcode.solana.organizer.AccountType -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.MiddleEllipsisText -import com.getcode.ui.core.rememberedClickable - - -@Composable -fun BucketDebugger() { - val accountList = SessionManager.getOrganizer()?.buckets ?: return - - val buckets = accountList.sortedWith { lhs, rhs -> - val la = lhs.accountType - val ra = rhs.accountType - if (la is AccountType.Relationship && ra is AccountType.Relationship) { - la.domain.urlString.compareTo(ra.domain.urlString) - } else { - la.sortOrder().compareTo(ra.sortOrder()) - } - } - - val clipboard = LocalClipboardManager.current - - LazyColumn { - items(buckets) { info -> - Column( - modifier = Modifier - .rememberedClickable { - clipboard.setText(AnnotatedString(info.address.base58())) - } - .padding(horizontal = CodeTheme.dimens.grid.x3) - ) { - Row( - modifier = Modifier.padding(vertical = CodeTheme.dimens.grid.x1) - ) { - Text( - modifier = Modifier - .weight(1f), - text = info.displayName, - style = CodeTheme.typography.textMedium, - ) - } - - Row( - modifier = Modifier.padding(vertical = CodeTheme.dimens.grid.x1) - ) { - MiddleEllipsisText( - modifier = Modifier - .align(Alignment.CenterVertically) - .weight(1f) - .padding(end = 100.dp), - text = info.address.base58(), - color = CodeTheme.colors.textSecondary, - style = CodeTheme.typography.textSmall - ) - - val kinValue = info.balance.toKinValueDouble() - val format = if (kinValue % 1 == 0.0) { "%,.0f" } else { "%,.5f" } - - Text( - text = "K ${String.format(format, info.balance.toKinValueDouble())}", - color = CodeTheme.colors.textSecondary, - style = CodeTheme.typography.textSmall - ) - } - - Row( - modifier = Modifier.padding(vertical = CodeTheme.dimens.grid.x1) - ) { - Text( - modifier = Modifier.weight(1f), - text = "${info.managementState.name} • ${info.blockchainState.name}", - color = CodeTheme.colors.textSecondary, - style = CodeTheme.typography.textSmall - ) - Text( - text = "", - color = CodeTheme.colors.textSecondary, - style = CodeTheme.typography.textSmall - ) - } - - Spacer(modifier = Modifier - .padding(vertical = CodeTheme.dimens.grid.x2) - .background(CodeTheme.colors.brandLight) - .fillMaxWidth() - .height(1.dp)) - } - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/ConfirmDeleteAccount.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/ConfirmDeleteAccount.kt deleted file mode 100644 index 7e8b1bb4d..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/ConfirmDeleteAccount.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.getcode.view.main.account - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material.Text -import androidx.compose.material.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource -import com.getcode.R -import com.getcode.manager.BottomBarManager -import com.getcode.theme.CodeTheme -import com.getcode.theme.extraSmall -import com.getcode.theme.inputColors -import com.getcode.ui.utils.getActivity -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -fun ConfirmDeleteAccount( - viewModel: DeleteAccountViewModel -) { - val context = LocalContext.current - val keyboardController = LocalSoftwareKeyboardController.current - Column( - Modifier - .padding(CodeTheme.dimens.grid.x4) - .imePadding(), - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2) - ) { - Text( - text = stringResource(id = R.string.subtitle_deleteAccountDescription), - style = CodeTheme.typography.textSmall - ) - TextField( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - placeholder = { - Text( - stringResource(id = R.string.subtitle_typeDelete).format(viewModel.requiredPhrase), - style = CodeTheme.typography.textMedium - ) - }, - value = viewModel.typedText.collectAsState().value, - onValueChange = { - viewModel.onTextUpdated(it) - }, - textStyle = CodeTheme.typography.textMedium, - singleLine = true, - colors = inputColors(), - shape = CodeTheme.shapes.extraSmall - ) - Spacer(modifier = Modifier.weight(1f)) - CodeButton( - modifier = Modifier.fillMaxWidth(), - onClick = { - keyboardController?.hide() - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = context.getString(R.string.prompt_title_deleteAccount), - positiveText = context.getString(R.string.action_deleteAccount), - negativeText = context.getString(R.string.action_cancel), - onPositive = { - context.getActivity()?.let { viewModel.onConfirmDelete(it) } - }, - onNegative = { } - )) - }, - text = stringResource(R.string.action_deleteAccount), - buttonState = ButtonState.Filled, - enabled = viewModel.isDeletionAllowed() - ) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/DeleteAccount.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/DeleteAccount.kt deleted file mode 100644 index 6ba905e39..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/DeleteAccount.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.getcode.view.main.account - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import com.getcode.R -import com.getcode.navigation.screens.DeleteConfirmationScreen -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.theme.CodeTheme -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.components.TextSection - -@Composable -fun DeleteCodeAccount() { - val navigator = LocalCodeNavigator.current - Column(Modifier.padding(CodeTheme.dimens.grid.x4)) { - LazyColumn( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x6) - ) { - item { - Image( - painter = painterResource(id = R.drawable.ic_delete_bubble), - contentDescription = "Delete" - ) - } - item { - TextSection( - title = stringResource(id = R.string.deleteAccount_title_willDo), - description = stringResource(id = R.string.deleteAccount_description_willDo) - ) - } - item { - TextSection( - title = stringResource(id = R.string.deleteAccount_title_wontDo), - description = stringResource(id = R.string.deleteAccount_description_wontDo) - ) - } - item { - TextSection( - title = stringResource(id = R.string.deleteAccount_title_willHappen), - description = stringResource(id = R.string.deleteAccount_description_willHappen) - ) - } - } - CodeButton( - modifier = Modifier.fillMaxWidth(), - onClick = { navigator.push(DeleteConfirmationScreen) }, - text = stringResource(R.string.action_continue), - buttonState = ButtonState.Filled, - ) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/DeleteAccountViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/DeleteAccountViewModel.kt deleted file mode 100644 index 03f2ad475..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/DeleteAccountViewModel.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.getcode.view.main.account - -import android.app.Activity -import com.getcode.manager.AuthManager -import com.getcode.util.resources.ResourceHelper -import com.getcode.view.BaseViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import javax.inject.Inject - - -@HiltViewModel -class DeleteAccountViewModel @Inject constructor( - private val authManager: AuthManager, - resources: ResourceHelper, -) : BaseViewModel(resources) { - val requiredPhrase = "Delete" - // TODO: don't do this - blocks UI - val typedText = MutableStateFlow("") - - fun onTextUpdated(text: String) { - typedText.value = text - } - - fun isDeletionAllowed() = typedText.value.equals(requiredPhrase, ignoreCase = true) - - fun onConfirmDelete(activity: Activity) { - authManager.deleteAndLogout(activity) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAddress.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAddress.kt deleted file mode 100644 index 3883bd9cf..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAddress.kt +++ /dev/null @@ -1,148 +0,0 @@ -package com.getcode.view.main.account.withdraw - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextAlign -import com.getcode.R -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.WithdrawalArgs -import com.getcode.theme.CodeTheme -import com.getcode.theme.Success -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.components.TextInput - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun AccountWithdrawAddress( - viewModel: AccountWithdrawAddressViewModel, - arguments: WithdrawalArgs, -) { - val navigator = LocalCodeNavigator.current - val dataState by viewModel.uiFlow.collectAsState() - - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .padding(horizontal = CodeTheme.dimens.inset) - .imePadding() - ) { - Text( - modifier = Modifier - .fillMaxWidth(), - text = stringResource(R.string.subtitle_whereToWithdrawKin), - style = CodeTheme.typography.textMedium.copy(textAlign = TextAlign.Center), - color = CodeTheme.colors.textSecondary - ) - - TextInput( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = CodeTheme.dimens.grid.x4), - state = dataState.addressText, - maxLines = 1, - placeholder = stringResource(R.string.subtitle_enterDestinationAddress), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), - ) - - Column { - dataState.isValid?.let { isValid -> - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Image( - modifier = Modifier.size(CodeTheme.dimens.staticGrid.x4), - painter = painterResource( - if (isValid) R.drawable.ic_checked_green else R.drawable.ic_xmark_red - ), - colorFilter = ColorFilter.tint( - if (isValid) Success else CodeTheme.colors.errorText - ), - contentDescription = "" - ) - - val text = - if (isValid) { - if (dataState.hasResolvedDestination) { - stringResource(id = R.string.subtitle_validOwnerAccount) - } else stringResource(id = R.string.subtitle_validTokenAccount) - } else { - stringResource(id = R.string.subtitle_invalidTokenAccount) - } - - Text( - modifier = Modifier - .padding(start = CodeTheme.dimens.grid.x2), - text = text, - color = if (isValid) Success else CodeTheme.colors.errorText, - style = CodeTheme.typography.caption - ) - } - - if (!isValid) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.size(CodeTheme.dimens.staticGrid.x4)) - Text( - modifier = Modifier - .padding(start = CodeTheme.dimens.grid.x2), - text = stringResource(R.string.subtitle_invalidTokenAccountDescription), - color = CodeTheme.colors.errorText, - style = CodeTheme.typography.caption - ) - } - } - } - } - - CodeButton( - modifier = Modifier.fillMaxWidth() - .padding(top = CodeTheme.dimens.grid.x2), - onClick = { viewModel.pasteAddress() }, - enabled = dataState.isPasteEnabled, - text = stringResource(R.string.action_pasteFromClipboard), - buttonState = ButtonState.Filled, - ) - - Spacer(modifier = Modifier.weight(1f)) - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = CodeTheme.dimens.inset), - onClick = { - viewModel.onSubmit(navigator, arguments) - }, - enabled = dataState.isNextEnabled, - text = stringResource(R.string.action_next), - buttonState = ButtonState.Filled, - ) - } - - SideEffect { - viewModel.refreshPasteButtonState() - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAddressViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAddressViewModel.kt deleted file mode 100644 index 31d02b6b6..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAddressViewModel.kt +++ /dev/null @@ -1,141 +0,0 @@ -package com.getcode.view.main.account.withdraw - -import android.annotation.SuppressLint -import android.content.ClipboardManager -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd -import androidx.compose.runtime.snapshotFlow -import androidx.lifecycle.viewModelScope -import com.getcode.navigation.core.CodeNavigator -import com.getcode.navigation.screens.WithdrawalArgs -import com.getcode.navigation.screens.WithdrawalSummaryScreen -import com.getcode.network.client.Client -import com.getcode.network.client.fetchDestinationMetadata -import com.getcode.network.repository.TransactionRepository -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.base58 -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.ErrorUtils -import com.getcode.view.* -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import org.kin.sdk.base.tools.Base58 -import javax.inject.Inject -import kotlin.time.Duration.Companion.milliseconds - -@OptIn(ExperimentalFoundationApi::class) -data class AccountWithdrawAddressUiModel( - val addressText: TextFieldState = TextFieldState(""), - val isPasteEnabled: Boolean = false, - val isNextEnabled: Boolean = false, - val isValid: Boolean? = null, - val hasResolvedDestination: Boolean = false, - val resolvedAddress: String? = null -) - -@HiltViewModel -class AccountWithdrawAddressViewModel @Inject constructor( - private val client: Client, - private val clipboard: ClipboardManager, - resources: ResourceHelper, -) : BaseViewModel(resources) { - val uiFlow = MutableStateFlow(AccountWithdrawAddressUiModel()) - - - init { - uiFlow.map { it.addressText } - .flatMapLatest { snapshotFlow { it.text } } - .map { it.toString() } - .debounce(300.milliseconds) - .onEach { updated -> setAddress(updated) } - .launchIn(viewModelScope) - } - private fun getClipboardValue(): String { - return clipboard.primaryClip?.getItemAt(0)?.text?.toString().orEmpty() - } - - fun refreshPasteButtonState() { - CoroutineScope(Dispatchers.IO).launch { - delay(400) - val addressText = getClipboardValue() - val isValid = isAddressValid(addressText) - uiFlow.value = uiFlow.value.copy(isPasteEnabled = isValid) - } - } - - private fun isAddressValid(addressText: String): Boolean { - return try { - val decoded = Base58.decode(addressText) - decoded.size == 32 - } catch (e: Exception) { - false - } - } - - fun pasteAddress() { - val addressText = getClipboardValue() - if (isAddressValid(addressText)) { - setAddress(addressText) - } - } - - private fun setAddress(text: String) { - val publicKey: PublicKey? = try { - val decoded = Base58.decode(text) - val isValid = decoded.size == 32 - if (isValid) PublicKey(decoded.toList()) else null - } catch (e: Exception) { - null - } - - uiFlow.value.addressText.setTextAndPlaceCursorAtEnd(text) - - if (publicKey != null) { - getDestinationMetaData(publicKey) - } - } - - fun onSubmit( - navigator: CodeNavigator, - arguments: WithdrawalArgs, - ) { - val resolvedDestination = uiFlow.value.resolvedAddress ?: return - navigator.push(WithdrawalSummaryScreen(arguments.copy(resolvedDestination = resolvedDestination))) - } - - @SuppressLint("CheckResult") - private fun getDestinationMetaData(publicKey: PublicKey) { - client.fetchDestinationMetadata(publicKey) - .subscribe({ result -> - when (result.kind) { - TransactionRepository.DestinationMetadata.Kind.OwnerAccount, - TransactionRepository.DestinationMetadata.Kind.TokenAccount -> { - uiFlow.value = uiFlow.value.copy( - resolvedAddress = result.resolvedDestination.base58(), - isValid = true, - isNextEnabled = true, - hasResolvedDestination = result.hasResolvedDestination - ) - } - else -> { - uiFlow.value = uiFlow.value.copy( - resolvedAddress = "", - isValid = false, - isNextEnabled = false, - hasResolvedDestination = false - ) - } - } - }, ErrorUtils::handleError) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAmount.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAmount.kt deleted file mode 100644 index 23d7eeda6..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAmount.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.getcode.view.main.account.withdraw - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import com.getcode.R -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.CurrencySelectionModal -import com.getcode.theme.Alert -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.text.AmountArea -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.theme.CodeKeyPad - -@Composable -fun AccountWithdrawAmount( - viewModel: AccountWithdrawAmountViewModel, -) { - val navigator = LocalCodeNavigator.current - val dataState by viewModel.uiFlow.collectAsState() - - Column( - modifier = Modifier - .fillMaxSize() - .padding(bottom = CodeTheme.dimens.grid.x4), - horizontalAlignment = Alignment.CenterHorizontally - ) { - val color = - if (dataState.amountModel.balanceKin < dataState.amountModel.amountKin.toKinValueDouble()) CodeTheme.colors.errorText else CodeTheme.colors.brandLight - - Box( - modifier = Modifier.weight(0.5f) - ) { - AmountArea( - modifier = Modifier.align(Alignment.Center), - amountPrefix = dataState.amountModel.amountPrefix, - amountSuffix = dataState.amountModel.amountSuffix, - amountText = dataState.amountModel.amountText, - captionText = dataState.amountModel.captionText, - isAltCaption = dataState.amountModel.isCaptionConversion, - altCaptionColor = color, - currencyResId = dataState.currencyModel.selectedCurrency?.resId, - uiModel = dataState.amountAnimatedModel, - isAnimated = true, - textStyle = CodeTheme.typography.displayLarge, - ) { - navigator.push(CurrencySelectionModal()) - } - } - - CodeKeyPad( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = CodeTheme.dimens.inset) - .weight(1f), - onNumber = viewModel::onNumber, - onClear = viewModel::onBackspace, - onDecimal = viewModel::onDot, - isDecimal = dataState.amountModel.isDecimalAllowed - ) - - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset), - onClick = { - viewModel.onSubmit(navigator) - }, - enabled = dataState.continueEnabled, - text = stringResource(R.string.action_next), - buttonState = ButtonState.Filled, - ) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAmountViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAmountViewModel.kt deleted file mode 100644 index 3d0dc2ab3..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawAmountViewModel.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.getcode.view.main.account.withdraw - -import androidx.lifecycle.viewModelScope -import com.getcode.R -import com.getcode.manager.TopBarManager -import com.getcode.navigation.core.CodeNavigator -import com.getcode.navigation.screens.WithdrawalAddressScreen -import com.getcode.network.client.Client -import com.getcode.network.client.receiveIfNeeded -import com.getcode.network.exchange.Exchange -import com.getcode.network.repository.BalanceRepository -import com.getcode.network.repository.PrefRepository -import com.getcode.network.repository.TransactionRepository -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.ErrorUtils -import com.getcode.ui.components.text.AmountAnimatedInputUiModel -import com.getcode.view.main.giveKin.AmountUiModel -import com.getcode.view.main.giveKin.BaseAmountCurrencyViewModel -import com.getcode.view.main.giveKin.CurrencyUiModel -import com.getcode.view.main.giveKin.FlowType -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import javax.inject.Inject - -data class AccountWithdrawAmountUiModel( - val currencyModel: CurrencyUiModel = CurrencyUiModel(), - val amountAnimatedModel: AmountAnimatedInputUiModel = AmountAnimatedInputUiModel(), - val amountModel: AmountUiModel = AmountUiModel(), - val continueEnabled: Boolean = false, -) - -@HiltViewModel -class AccountWithdrawAmountViewModel @Inject constructor( - client: Client, - exchange: Exchange, - prefsRepository: PrefRepository, - balanceRepository: BalanceRepository, - transactionRepository: TransactionRepository, - localeHelper: com.getcode.util.locale.LocaleHelper, - currencyUtils: com.getcode.utils.CurrencyUtils, - networkObserver: com.getcode.utils.network.NetworkConnectivityListener, - resources: ResourceHelper, -) : BaseAmountCurrencyViewModel( - client, - prefsRepository, - exchange, - balanceRepository, - transactionRepository, - localeHelper, - currencyUtils, - resources, - networkObserver -) { - val uiFlow = MutableStateFlow(AccountWithdrawAmountUiModel()) - - override val flowType: FlowType = FlowType.Withdrawal - - init { - init() - viewModelScope.launch(Dispatchers.IO) { - client.receiveIfNeeded().subscribe({}, ErrorUtils::handleError) - } - } - - override fun reset() { - numberInputHelper.reset() - onAmountChanged(true) - viewModelScope.launch { - uiFlow.update { - it.copy(continueEnabled = false) - } - } - } - - fun onSubmit(navigator: CodeNavigator) { - val uiModel = uiFlow.value - if (uiModel.amountModel.amountKin.toKinValueDouble() > uiModel.amountModel.balanceKin) { - TopBarManager.showMessage( - resources.getString(R.string.error_title_insuffiecientKin), - resources.getString(R.string.error_description_insuffiecientKin) - ) - return - } - if (uiModel.amountModel.amountKin.toKinTruncatingLong() == 0L) { - return - } - - val currency = uiModel.currencyModel.selectedCurrency ?: return - - navigator.push( - WithdrawalAddressScreen( - uiModel.amountModel.amountDouble, - uiModel.amountModel.amountKin.quarks, - uiModel.amountModel.amountText, - currency.code, - currency.resId, - currency.rate - ) - ) - } - - override fun onAmountChanged(lastPressedBackspace: Boolean) { - super.onAmountChanged(lastPressedBackspace) - uiFlow.update { - // only enable if sufficient balance and non-zero - it.copy(continueEnabled = !it.amountModel.isInsufficient && numberInputHelper.amount != 0.0) - } - } - - override fun setCurrencyUiModel(currencyUiModel: CurrencyUiModel) { - uiFlow.update { it.copy(currencyModel = currencyUiModel) } - } - - override fun setAmountUiModel(amountUiModel: AmountUiModel) { - uiFlow.update { it.copy(amountModel = amountUiModel) } - } - - override fun setAmountAnimatedInputUiModel(amountAnimatedInputUiModel: AmountAnimatedInputUiModel) { - uiFlow.update { it.copy(amountAnimatedModel = amountAnimatedInputUiModel) } - } - - override fun getCurrencyUiModel(): CurrencyUiModel { - return uiFlow.value.currencyModel - } - - override fun getAmountUiModel(): AmountUiModel { - return uiFlow.value.amountModel - } - - override fun getAmountAnimatedInputUiModel(): AmountAnimatedInputUiModel { - return uiFlow.value.amountAnimatedModel - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawSummary.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawSummary.kt deleted file mode 100644 index d122ecb2f..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawSummary.kt +++ /dev/null @@ -1,120 +0,0 @@ -package com.getcode.view.main.account.withdraw - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Alignment.Companion.CenterHorizontally -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.constraintlayout.compose.ConstraintLayout -import com.getcode.R -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.WithdrawalArgs -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.text.AmountArea -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton - -@Composable -fun AccountWithdrawSummary( - viewModel: AccountWithdrawSummaryViewModel, - arguments: WithdrawalArgs, -) { - val navigator = LocalCodeNavigator.current - val dataState by viewModel.uiFlow.collectAsState() - - ConstraintLayout( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .padding(horizontal = CodeTheme.dimens.inset) - .imePadding() - ) { - val (centerColumn, nextButton) = createRefs() - - Column( - modifier = Modifier - .wrapContentHeight() - .constrainAs(centerColumn) { - top.linkTo(parent.top) - bottom.linkTo(nextButton.top) - } - ) { - Box( - modifier = Modifier - .border(width = CodeTheme.dimens.border, color = CodeTheme.colors.brandLight, shape = CodeTheme.shapes.medium) - .background(CodeTheme.colors.brandDark) - .padding(CodeTheme.dimens.grid.x4) - ) { - AmountArea( - currencyResId = dataState.currencyResId, - amountText = dataState.amountText, - captionText = String.format("%,.0f", dataState.amountKin?.toKin()?.toDouble()), - isAltCaption = true, - isAltCaptionKinIcon = true, - altCaptionColor = CodeTheme.colors.textSecondary - ) - } - - Image( - modifier = Modifier - .padding(vertical = CodeTheme.dimens.inset) - .align(CenterHorizontally), - painter = painterResource(id = R.drawable.ic_arrow_down), - contentDescription = "" - ) - - Box( - modifier = Modifier - .border(width = CodeTheme.dimens.border, color = CodeTheme.colors.brandLight, shape = CodeTheme.shapes.medium) - .background(CodeTheme.colors.brandDark) - .padding(CodeTheme.dimens.grid.x4) - ) { - Text( - text = dataState.resolvedDestination, - style = CodeTheme.typography.textLarge.copy(textAlign = TextAlign.Center) - ) - } - } - - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = CodeTheme.dimens.inset) - .constrainAs(nextButton) { - bottom.linkTo(parent.bottom) - }, - onClick = { - viewModel.onSubmit(navigator, arguments) - }, - enabled = true, - text = stringResource(R.string.action_withdrawKin), - buttonState = ButtonState.Filled, - ) - } - - LaunchedEffect(rememberUpdatedState(Unit)) { - viewModel.setArguments(navigator, arguments) - } - - LaunchedEffect(dataState.isSuccess) { - if (dataState.isSuccess == true) { - navigator.hide() - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawSummaryViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawSummaryViewModel.kt deleted file mode 100644 index 1490cbe57..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/account/withdraw/AccountWithdrawSummaryViewModel.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.getcode.view.main.account.withdraw - -import android.annotation.SuppressLint -import androidx.lifecycle.viewModelScope -import com.getcode.R -import com.getcode.analytics.CodeAnalyticsService -import com.getcode.manager.BottomBarManager -import com.getcode.solana.keys.PublicKey -import com.getcode.manager.SessionManager -import com.getcode.manager.TopBarManager -import com.getcode.model.CurrencyCode -import com.getcode.model.Kin -import com.getcode.model.KinAmount -import com.getcode.model.Rate -import com.getcode.navigation.core.CodeNavigator -import com.getcode.navigation.screens.ScanScreen -import com.getcode.navigation.screens.WithdrawalArgs -import com.getcode.network.NotificationCollectionHistoryController -import com.getcode.network.client.* -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.ErrorUtils -import com.getcode.view.* -import dagger.hilt.android.lifecycle.HiltViewModel -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch -import org.kin.sdk.base.tools.Base58 -import javax.inject.Inject - -private const val TAG = "AccountWithdrawSummaryViewModel" - -data class AccountWithdrawSummaryUiModel( - val amountFiat: Double = 0.0, - val amountKin: Kin? = null, - val amountText: String = "", - val currencyCode: String = "", - val currencyResId: Int? = null, - val currencyRate: Float? = null, - val resolvedDestination: String = "", - val isSuccess: Boolean? = null -) - -@HiltViewModel -class AccountWithdrawSummaryViewModel @Inject constructor( - private val analytics: CodeAnalyticsService, - private val client: Client, - private val historyController: NotificationCollectionHistoryController, - private val resources: ResourceHelper, -) : BaseViewModel(resources) { - val uiFlow = MutableStateFlow(AccountWithdrawSummaryUiModel()) - - fun setArguments(n: CodeNavigator, arguments: WithdrawalArgs) { - val amountFiat = arguments.amountFiat ?: return goBack(n) - val amountKin = arguments.amountKinQuarks ?: return goBack(n) - val amountText = arguments.amountText ?: return goBack(n) - val currencyCode = arguments.currencyCode ?: return goBack(n) - val currencyResId = arguments.currencyResId ?: return goBack(n) - val currencyRate = arguments.currencyRate?.toFloat() ?: return goBack(n) - val resolvedDestination = arguments.resolvedDestination ?: return goBack(n) - - uiFlow.value = - AccountWithdrawSummaryUiModel( - amountFiat, - Kin(amountKin), - amountText, - currencyCode, - currencyResId, - currencyRate, - resolvedDestination - ) - } - - fun onSubmit( - navigator: CodeNavigator, - arguments: WithdrawalArgs, - ) { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = resources.getString(R.string.prompt_title_confirmWithdrawal), - subtitle = resources.getString(R.string.prompt_description_confirmWithdrawal), - positiveText = resources.getString(R.string.action_withdrawKin), - negativeText = resources.getString(R.string.action_cancel), - onPositive = { startWithdraw(navigator) } - )) - } - - private fun goBack(navigator: CodeNavigator) { - navigator.popAll() - } - - @SuppressLint("CheckResult") - private fun startWithdraw(navigator: CodeNavigator) { - val uiModel = uiFlow.value - val currencyRate = uiModel.currencyRate?.toDouble() ?: return - val kin = uiModel.amountKin ?: return - val currencyCode = CurrencyCode.tryValueOf(uiModel.currencyCode) ?: return - - val amount = KinAmount( - kin = kin.toKinTruncating(), - fiat = uiModel.amountFiat, - rate = Rate( - fx = currencyRate, - currency = currencyCode - ) - ) - - val organizer = SessionManager.getOrganizer() ?: return - val destination = - PublicKey(Base58.decode(uiModel.resolvedDestination).toList()) - - client.withdrawExternally(amount, organizer, destination) - .observeOn(AndroidSchedulers.mainThread()) - .doOnComplete { viewModelScope.launch { historyController.fetch() } } - .subscribe({ - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - getString(R.string.success_title_withdrawalComplete), - getString(R.string.success_description_withdrawalComplete), - TopBarManager.TopBarMessageType.NOTIFICATION - ) - ) - - analytics.withdrawal(amount = amount, successful = true) - - navigator.replaceAll(ScanScreen()) - - uiFlow.value = uiFlow.value.copy(isSuccess = true) - }, { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - getString(R.string.error_title_failedWithdrawal), - getString(R.string.error_description_failedWithdrawal) - ) - ) - analytics.withdrawal(amount = amount, successful = false) - ErrorUtils.handleError(it) - }) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/balance/BalanceSheet.kt b/apps/codeApp/src/main/java/com/getcode/view/main/balance/BalanceSheet.kt deleted file mode 100644 index b38474f4a..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/balance/BalanceSheet.kt +++ /dev/null @@ -1,379 +0,0 @@ -package com.getcode.view.main.balance - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.text.ClickableText -import androidx.compose.material.Divider -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment.Companion.CenterHorizontally -import androidx.compose.ui.Alignment.Companion.CenterVertically -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.getcode.R -import com.getcode.manager.TopBarManager -import com.getcode.model.Currency -import com.getcode.model.CurrencyCode -import com.getcode.model.Rate -import com.getcode.model.chat.Chat -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.BuyMoreKinModal -import com.getcode.navigation.screens.NotificationCollectionScreen -import com.getcode.navigation.screens.CurrencySelectionModal -import com.getcode.navigation.screens.FaqScreen -import com.getcode.theme.CodeTheme -import com.getcode.theme.DesignSystem -import com.getcode.theme.White10 -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.theme.CodeCircularProgressIndicator -import com.getcode.utils.Kin -import com.getcode.view.main.account.BucketDebugger -import com.getcode.view.main.currency.CurrencySelectKind -import com.getcode.ui.components.text.AmountArea - - -@Composable -fun BalanceScreen( - state: BalanceSheetViewModel.State, - dispatch: (BalanceSheetViewModel.Event) -> Unit, -) { - val navigator = LocalCodeNavigator.current - - AnimatedContent( - targetState = state.isBucketDebuggerVisible, - label = "show/hide buckets", - transitionSpec = { - slideIntoContainer( - AnimatedContentTransitionScope.SlideDirection.End - ) togetherWith slideOutOfContainer( - AnimatedContentTransitionScope.SlideDirection.Start - ) - } - ) { buckets -> - if (buckets) { - BucketDebugger() - } else { - BalanceContent( - state = state, - dispatch = dispatch, - faqOpen = { navigator.push(FaqScreen) }, - openChat = { navigator.push(NotificationCollectionScreen(it.id)) }, - buyMoreKin = { navigator.push(BuyMoreKinModal()) } - ) - } - } -} - -@Composable -fun BalanceContent( - state: BalanceSheetViewModel.State, - dispatch: (BalanceSheetViewModel.Event) -> Unit, - faqOpen: () -> Unit, - openChat: (Chat) -> Unit, - buyMoreKin: () -> Unit, -) { - val lazyListState = rememberLazyListState() - val navigator = LocalCodeNavigator.current - - val chatsEmpty by remember(state.chats) { - derivedStateOf { state.chats.isEmpty() } - } - - val canClickBalance by remember(state.currencySelection.enabled) { - derivedStateOf { state.currencySelection.enabled } - } - - val context = LocalContext.current - - LazyColumn( - modifier = Modifier - .fillMaxWidth(), - state = lazyListState - ) { - item { - Column( - modifier = Modifier - .fillParentMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset,) - .padding(top = CodeTheme.dimens.grid.x7) - ) { - BalanceTop( - state, - canClickBalance, - ) { - navigator.push(CurrencySelectionModal(CurrencySelectKind.Local)) - } - } - } - - item { - Column( - modifier = Modifier - .fillParentMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset) - ) { - if (!chatsEmpty && !state.chatsLoading) { - KinValueHint(faqOpen) - } - } - } - - item { - Column( - modifier = Modifier - .fillParentMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset) - .padding(bottom = CodeTheme.dimens.grid.x11), - ) { - if (!chatsEmpty && !state.chatsLoading && state.buyModule.enabled) { - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(top = CodeTheme.dimens.grid.x5), - buttonState = ButtonState.Filled, - onClick = { - if (state.buyModule.available) { - buyMoreKin() - } else { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = context.getString(R.string.error_title_buyModuleUnavailable), - message = context.getString(R.string.error_description_buyModuleUnavailable), - type = TopBarManager.TopBarMessageType.ERROR - ) - ) - } - }, - text = stringResource(id = R.string.action_addCash) - ) - } - } - } - itemsIndexed( - state.chats, - key = { _, item -> item.id }, - contentType = { _, item -> item } - ) { index, chat -> - ChatNode(chat = chat, onClick = { openChat(chat) }) - Divider( - modifier = Modifier.padding(start = CodeTheme.dimens.inset), - color = CodeTheme.colors.divider, - ) - } - - when { - state.chatsLoading -> { - item { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = CenterHorizontally, - verticalArrangement = Arrangement.spacedBy( - CodeTheme.dimens.grid.x2, - CenterVertically - ), - ) { - CodeCircularProgressIndicator() - Text( - modifier = Modifier.fillMaxWidth(0.6f), - text = stringResource(R.string.subtitle_loadingBalanceAndTransactions), - textAlign = TextAlign.Center - ) - } - } - } - - chatsEmpty -> { - item { - EmptyTransactionsHint(faqOpen) - } - } - } - } -} - -@Composable -fun BalanceTop( - state: BalanceSheetViewModel.State, - isClickable: Boolean, - onClick: () -> Unit = {} -) { - AmountArea( - amountText = state.amountText, - isAltCaption = false, - isAltCaptionKinIcon = false, - isLoading = state.chatsLoading, - currencyResId = state.currencyFlag, - isClickable = isClickable, - onClick = onClick, - textStyle = CodeTheme.typography.displayLarge, - ) -} - -@Composable -private fun ColumnScope.KinValueHint(onClick: () -> Unit) { - val context = LocalContext.current - Row( - modifier = Modifier - .align(CenterHorizontally) - ) { - val annotatedBalanceString = buildAnnotatedString { - val infoString = stringResource(R.string.subtitle_valueKinChanges) - val actionString = stringResource(R.string.subtitle_learnMore) - val textString = "$infoString $actionString" - - val startIndex = textString.indexOf(actionString) - val endIndex = textString.length - append(textString) - - addStyle( - style = SpanStyle( - textDecoration = TextDecoration.Underline - ), start = startIndex, end = endIndex - ) - addStyle( - style = SpanStyle(color = CodeTheme.colors.textSecondary), - start = 0, - end = textString.length - ) - addStringAnnotation( - tag = stringResource(R.string.subtitle_learnMore), - annotation = "", - start = startIndex, - end = endIndex - ) - } - - ClickableText( - modifier = Modifier - .padding(horizontal = CodeTheme.dimens.grid.x1), - text = annotatedBalanceString, - style = CodeTheme.typography.textMedium, - onClick = { - annotatedBalanceString - .getStringAnnotations( - context.getString(R.string.subtitle_learnMore), - it, - it - ) - .firstOrNull()?.let { onClick() } - } - ) - } -} - -@Composable -private fun EmptyTransactionsHint(faqOpen: () -> Unit) { - val context = LocalContext.current - Column( - modifier = Modifier - .height(200.dp) - .padding(horizontal = CodeTheme.dimens.grid.x6), - verticalArrangement = Arrangement.Bottom, - ) { - Row( - modifier = Modifier - .align(CenterHorizontally) - ) { - Text( - modifier = Modifier.padding(vertical = CodeTheme.dimens.grid.x1), - text = stringResource(R.string.subtitle_dontHaveKin), - color = CodeTheme.colors.textSecondary, - style = CodeTheme.typography.textMedium - ) - } - - val annotatedLinkString: AnnotatedString = buildAnnotatedString { - val linkString = "Check out the FAQ" - val remainderString = " to find out how to get some." - val textString = linkString + remainderString - - val startIndex = textString.indexOf(linkString) - val endIndex = linkString.length - - append(textString) - addStyle( - style = SpanStyle( - color = CodeTheme.colors.textSecondary, - ), start = 0, end = textString.length - ) - addStyle( - style = SpanStyle( - textDecoration = TextDecoration.Underline - ), start = startIndex, end = endIndex - ) - - addStringAnnotation( - tag = context.getString(R.string.title_faq), - annotation = "", - start = startIndex, - end = endIndex - ) - } - - Row( - modifier = Modifier - .align(CenterHorizontally) - ) { - ClickableText( - text = annotatedLinkString, - style = CodeTheme.typography.textMedium.copy(textAlign = TextAlign.Center), - onClick = { - annotatedLinkString - .getStringAnnotations( - context.getString(R.string.title_faq), - it, - it - ) - .firstOrNull()?.let { _ -> faqOpen() } - } - ) - } - } -} - - -@Preview -@Composable -private fun TopPreview() { - DesignSystem { - val model = BalanceSheetViewModel.State( - amountText = "$12.34 of Kin", - marketValue = 2_225_100.0, - selectedRate = Rate(Currency.Kin.rate, CurrencyCode.KIN), - chatsLoading = false, - currencyFlag = R.drawable.ic_currency_kin, - chats = emptyList(), - isBucketDebuggerEnabled = false, - isBucketDebuggerVisible = false, - ) - - BalanceTop( - state = model, - isClickable = true - ) - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/balance/BalanceSheetViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/balance/BalanceSheetViewModel.kt deleted file mode 100644 index 3ab43ddb1..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/balance/BalanceSheetViewModel.kt +++ /dev/null @@ -1,170 +0,0 @@ -package com.getcode.view.main.balance - -import androidx.lifecycle.viewModelScope -import com.getcode.model.BalanceCurrencyFeature -import com.getcode.model.BuyModuleFeature -import com.getcode.model.chat.Chat -import com.getcode.model.Currency -import com.getcode.model.Feature -import com.getcode.services.model.PrefsBool -import com.getcode.model.Rate -import com.getcode.network.BalanceController -import com.getcode.network.NotificationCollectionHistoryController -import com.getcode.network.repository.FeatureRepository -import com.getcode.network.repository.PrefRepository -import com.getcode.utils.Kin -import com.getcode.view.BaseViewModel2 -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import javax.inject.Inject - - -@HiltViewModel -class BalanceSheetViewModel @Inject constructor( - balanceController: BalanceController, - history: NotificationCollectionHistoryController, - prefsRepository: PrefRepository, - features: FeatureRepository, - networkObserver: com.getcode.utils.network.NetworkConnectivityListener, -) : BaseViewModel2( - initialState = State(), - updateStateForEvent = updateStateForEvent -) { - data class State( - val amountText: String = "", - val marketValue: Double = 0.0, - val selectedRate: Rate? = null, - val isKinSelected: Boolean = false, - val currencyFlag: Int? = null, - val chatsLoading: Boolean = false, - val chats: List = emptyList(), - val isBucketDebuggerEnabled: Boolean = false, - val isBucketDebuggerVisible: Boolean = false, - val buyModule: Feature = BuyModuleFeature(), - val currencySelection: Feature = BalanceCurrencyFeature() - ) - - sealed interface Event { - data class OnDebugBucketsEnabled(val enabled: Boolean) : Event - data class OnDebugBucketsVisible(val show: Boolean) : Event - data class OnBuyModuleStateChanged(val module: Feature) : Event - data class OnCurrencySelectionStateChanged(val module: Feature): Event - data class OnLatestRateChanged(val rate: Rate) : Event - - data class OnBalanceChanged( - val flagResId: Int?, - val marketValue: Double, - val display: String, - val isKin: Boolean, - ) : Event - - data class OnChatsLoading(val loading: Boolean) : Event - data class OnChatsUpdated(val chats: List) : Event - data object OnOpened: Event - } - - init { - features.buyModule - .onEach { dispatchEvent(Event.OnBuyModuleStateChanged(it)) } - .launchIn(viewModelScope) - - features.balanceCurrencySelection - .onEach { dispatchEvent(Event.OnCurrencySelectionStateChanged(it)) } - .launchIn(viewModelScope) - - prefsRepository.observeOrDefault(PrefsBool.BUCKET_DEBUGGER_ENABLED, false) - .distinctUntilChanged() - .onEach { enabled -> - dispatchEvent(Dispatchers.Main, Event.OnDebugBucketsEnabled(enabled)) - }.launchIn(viewModelScope) - - balanceController.formattedBalance - .filterNotNull() - .distinctUntilChanged() - .onEach { - dispatchEvent( - Dispatchers.Main, - Event.OnBalanceChanged( - flagResId = it.currency?.resId, - marketValue = it.marketValue, - display = it.formattedValue, - isKin = it.currency == Currency.Kin - ) - ) - } - .launchIn(viewModelScope) - - history.notifications - .onEach { - if (it == null || (it.isEmpty() && !networkObserver.isConnected)) { - dispatchEvent(Dispatchers.Main, Event.OnChatsLoading(true)) - } - } - .map { chats -> - when { - chats == null -> null // await for confirmation it's empty - chats.isEmpty() && !networkObserver.isConnected -> null // remain loading while disconnected - history.loadingCollections -> null // remain loading while fetching messages - else -> chats - } - } - .filterNotNull() - .onEach { update -> - dispatchEvent(Dispatchers.Main, Event.OnChatsUpdated(update)) - }.onEach { - dispatchEvent(Dispatchers.Main, Event.OnChatsLoading(false)) - }.launchIn(viewModelScope) - } - - companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - is Event.OnDebugBucketsEnabled -> { state -> - state.copy(isBucketDebuggerEnabled = event.enabled) - } - - is Event.OnDebugBucketsVisible -> { state -> - state.copy(isBucketDebuggerVisible = event.show) - } - - is Event.OnBuyModuleStateChanged -> { state -> - state.copy( - buyModule = event.module - ) - } - - is Event.OnCurrencySelectionStateChanged -> { state -> - state.copy( - currencySelection = event.module - ) - } - - is Event.OnLatestRateChanged -> { state -> - state.copy(selectedRate = event.rate) - } - - is Event.OnBalanceChanged -> { state -> - state.copy( - currencyFlag = event.flagResId, - marketValue = event.marketValue, - amountText = event.display, - isKinSelected = event.isKin - ) - } - is Event.OnChatsLoading -> { state -> - state.copy(chatsLoading = event.loading) - } - is Event.OnChatsUpdated -> { state -> - state.copy(chats = event.chats) - } - - Event.OnOpened -> { state -> state } - } - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/balance/ChatNode.kt b/apps/codeApp/src/main/java/com/getcode/view/main/balance/ChatNode.kt deleted file mode 100644 index aa0b08bcc..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/balance/ChatNode.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.getcode.view.main.balance - -import androidx.compose.foundation.text.InlineTextContent -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.AnnotatedString -import com.getcode.model.chat.Chat -import com.getcode.model.chat.MessageContent -import com.getcode.ui.components.chat.ChatNode -import com.getcode.ui.components.chat.utils.localized -import com.getcode.ui.components.chat.utils.localizedText - -@Composable -fun ChatNode( - modifier: Modifier = Modifier, - chat: Chat, - showAvatar: Boolean = false, - onClick: () -> Unit, -) { - ChatNode( - modifier = modifier, - title = chat.title.localized, - messagePreview = chat.messagePreview, - avatar = if (showAvatar) chat.imageData else null, - timestamp = chat.lastMessageMillis, - isMuted = chat.isMuted, - unreadCount = chat.unreadCount, - onClick = onClick - ) -} - -private val Chat.messagePreview: Pair> - @Composable get() { - val contents = newestMessage?.contents ?: return AnnotatedString("No content") to emptyMap() - - var filtered: List = contents.filterIsInstance() - if (filtered.isEmpty()) { - filtered = contents - } - - // joinToString does expose a Composable scoped lambda - @Suppress("SimplifiableCallChain") - val messageBody = filtered.map { it.localizedText }.joinToString(" ") - - return AnnotatedString(messageBody) to emptyMap() - } \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/bill/AnimatedBill.kt b/apps/codeApp/src/main/java/com/getcode/view/main/bill/AnimatedBill.kt deleted file mode 100644 index 102528977..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/bill/AnimatedBill.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.getcode.view.main.bill - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.ContentTransform -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.DismissDirection -import androidx.compose.material.DismissState -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import com.getcode.models.Bill -import com.getcode.ui.theme.CustomSwipeToDismiss - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun HomeBill( - modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(), - dismissState: DismissState, - dismissed: Boolean, - transitionSpec: AnimatedContentTransitionScope.() -> ContentTransform, - bill: Bill?, -) { - AnimatedContent( - modifier = modifier, - targetState = bill, - label = "animate bill", - transitionSpec = transitionSpec - ) { b -> - Box( - modifier = Modifier.fillMaxSize(), - ) { - CustomSwipeToDismiss( - modifier = Modifier.align(Alignment.Center), - state = dismissState, - dismissContent = { - if (b != null && !dismissed) { - Bill( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .padding(contentPadding), - bill = b - ) - } - }, - directions = setOf(DismissDirection.EndToStart, DismissDirection.StartToEnd), - ) - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/bill/Bill.kt b/apps/codeApp/src/main/java/com/getcode/view/main/bill/Bill.kt deleted file mode 100644 index 4c0b6cfb5..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/bill/Bill.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.getcode.view.main.bill - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.getcode.services.model.CodePayload -import com.getcode.model.CurrencyCode -import com.getcode.model.Fiat -import com.getcode.model.KinAmount -import com.getcode.services.model.Kind -import com.getcode.model.fromFiatAmount -import com.getcode.models.Bill -import com.getcode.theme.DesignSystem - -@Composable -fun Bill( - modifier: Modifier = Modifier, - bill: Bill, -) { - when (bill) { - is Bill.Cash -> CashBill( - modifier = modifier, - payloadData = bill.data, - amount = bill.amount - ) - - is Bill.Payment -> Receipt( - modifier = modifier, - data = bill.data, - currencyCode = bill.payload.fiat?.currency, - amount = bill.amount - ) - - is Bill.Login -> LoginBill( - modifier = modifier, - data = bill.data, - ) - - is Bill.Tip -> TipCard( - modifier = modifier, - username = bill.payload.username.orEmpty(), - data = bill.data, - interactive = bill.canFlip - ) - } -} - -@Preview -@Composable -fun Preview_CashBill() { - DesignSystem { - val payload = CodePayload( - Kind.Cash, - value = Fiat(CurrencyCode.USD, 3.00), - nonce = listOf( - -85, -37, -27, -38, 37, -1, -4, -128, 102, 123, -35 - ).map { it.toByte() } - ) - - CashBill( - amount = KinAmount.fromFiatAmount( - fiat = 3.00, - fx = 0.00001585, - CurrencyCode.USD - ), - payloadData = payload.codeData.toList(), - ) - } -} - -@Preview -@Composable -fun Preview_PaymentBill() { - DesignSystem { - val payload = CodePayload( - Kind.RequestPayment, - value = Fiat(CurrencyCode.USD, 0.25), - nonce = listOf( - -85, -37, -27, -38, 37, -1, -4, -128, 102, 123, -35 - ).map { it.toByte() } - ) - - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Receipt( - amount = KinAmount.fromFiatAmount( - fiat = 0.25, - fx = 0.00001585, - CurrencyCode.USD - ), - data = payload.codeData.toList(), - currencyCode = payload.fiat?.currency - ) - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/bill/BillAmount.kt b/apps/codeApp/src/main/java/com/getcode/view/main/bill/BillAmount.kt deleted file mode 100644 index 111be9e1f..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/bill/BillAmount.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.getcode.view.main.bill - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.layout.layout -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import com.getcode.R -import com.getcode.theme.CodeTheme -import com.getcode.ui.utils.nonScaledSp - -@Composable -@Preview -fun BillAmount(modifier: Modifier = Modifier, text: String = "") { - Box(modifier = modifier) { - Row( - modifier = Modifier - .vertical() - .rotate(-90f) - ) { - Image( - modifier = Modifier - .padding(end = CodeTheme.dimens.grid.x2) - .height(CodeTheme.dimens.grid.x3) - .width(CodeTheme.dimens.grid.x3) - .align(Alignment.CenterVertically), - painter = painterResource(id = R.drawable.ic_kin_white), - contentDescription = "" - ) - Text( - text = text, - style = CodeTheme.typography.displayLarge.copy( - fontSize = 40.nonScaledSp - ), - color = CodeTheme.colors.onBackground - ) - } - } -} - -fun Modifier.vertical() = - layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - layout(placeable.height, placeable.width) { - placeable.place( - x = -(placeable.width / 2 - placeable.height / 2), - y = -(placeable.height / 2 - placeable.width / 2) - ) - } - } \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/bill/BillManagementOptions.kt b/apps/codeApp/src/main/java/com/getcode/view/main/bill/BillManagementOptions.kt deleted file mode 100644 index 1cd82acb8..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/bill/BillManagementOptions.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.getcode.view.main.bill - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.unit.dp -import com.getcode.models.BillState -import com.getcode.theme.CodeTheme -import com.getcode.theme.White -import com.getcode.ui.theme.CodeCircularProgressIndicator -import com.getcode.ui.components.Pill -import com.getcode.ui.core.rememberedClickable - -@Composable -internal fun BillManagementOptions( - modifier: Modifier = Modifier, - isSending: Boolean = false, - isInteractable: Boolean = true, - primaryAction: BillState.Action? = null, - secondaryAction: BillState.Action? = null, -) { - Box( - modifier = Modifier - .fillMaxWidth() - .then(modifier), - ) { - Row( - modifier = Modifier - .padding(bottom = 30.dp) - .align(Alignment.BottomCenter), - horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x8) - ) { - if (primaryAction != null) { - Pill( - modifier = Modifier - .rememberedClickable(enabled = !isSending) { primaryAction.action() }, - contentPadding = PaddingValues(15.dp), - backgroundColor = CodeTheme.colors.action, - ) { - Box { - Row( - modifier = Modifier.alpha(if (!isSending) 1f else 0f), - verticalAlignment = Alignment.CenterVertically - ) { - Image( - painter = primaryAction.asset, - contentDescription = "", - modifier = Modifier.width(22.dp) - ) - primaryAction.label?.let { label -> - Text( - modifier = Modifier.padding(start = 10.dp), - text = label - ) - } - } - - if (isSending) { - CodeCircularProgressIndicator( - strokeWidth = 2.dp, - color = White, - modifier = Modifier - .size(20.dp) - .align(Alignment.Center) - ) - } - } - } - } - if (secondaryAction != null) { - Pill( - modifier = Modifier - .rememberedClickable(enabled = isInteractable) { secondaryAction.action() }, - contentPadding = PaddingValues(15.dp), - backgroundColor = CodeTheme.colors.action, - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Image( - painter = secondaryAction.asset, - contentDescription = "", - modifier = Modifier.size(18.dp) - ) - secondaryAction.label?.let { label -> - Text( - modifier = Modifier.padding(start = 10.dp), - text = label - ) - } - } - } - } - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/bill/CashBill.kt b/apps/codeApp/src/main/java/com/getcode/view/main/bill/CashBill.kt deleted file mode 100644 index 8ea067ba2..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/bill/CashBill.kt +++ /dev/null @@ -1,401 +0,0 @@ -package com.getcode.view.main.bill - -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredWidth -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBarsIgnoringVisibility -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.isSpecified -import com.getcode.R -import com.getcode.model.KinAmount -import com.getcode.solana.keys.Mint -import com.getcode.solana.keys.base58 -import com.getcode.theme.CodeTheme -import com.getcode.ui.utils.Geometry -import com.getcode.ui.core.drawWithGradient -import com.getcode.ui.utils.nonScaledSp -import com.getcode.ui.core.punchCircle -import com.getcode.ui.core.punchRectangle -import com.getcode.extensions.formattedRaw -import kotlin.math.ceil -import kotlin.math.roundToInt - -@Suppress("ConstPropertyName") -private object CashBillDefaults { - const val AspectRatio = 0.555f - - val BillColor: Color - @Composable get() = CodeTheme.colors.cashBillColor - val DecorColor: Color - @Composable get() = CodeTheme.colors.cashBillDecorColor - - const val CodeBackgroundOpacity = 0.65f - - const val SecurityStripCount = 3 -} - -private class CashBillGeometry(width: Dp, height: Dp) : Geometry(width, height) { - - val brandWidth: Dp - get() = ceil(size.width.value * 0.18f).dp - - override val codeSize: Dp - get() = size.width * 0.6f - - val globeWidth: Dp - get() = size.width * 1.5f - val globePosition: Offset - get() = Offset( - x = -(size.width.value * 0.75f), - y = size.height.value * 0.65f - ) - - val gridWidth: Dp - get() = size.width * 1.75f - - val gridHeight: Dp - get() = size.height * 0.7f - - val gridPosition: Offset - get() = Offset( - x = 0f, - y = securityStripPosition.y.value + securityStripSize.height.value - ) - - val linesHeight: Dp - get() = (topStripHeight.value - 2).dp - val lineSpacing: Dp - get() = ceil(size.width.value * 0.032f).dp - - val mintPadding: Dp - get() = ceil(size.height.value * 0.01f).dp - val securityStripSize: DpSize - get() = DpSize(size.width, ceil(size.height.value * 0.063f).dp) - - val securityStripPosition: DpOffset - get() = DpOffset( - x = 0.dp, - y = (topStripHeight) - ) - - val topStripHeight: Dp - get() = ceil(size.height.value * 0.05f).dp - - val valuePadding: Dp - get() = ceil(size.width.value * 0.025f).dp - - val wavesPosition: Offset - get() = Offset( - x = (size.width.value * 0.5f), - y = (size.height.value * 0.9f) - ) -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -internal fun CashBill( - modifier: Modifier = Modifier, - payloadData: List, - amount: KinAmount, -) { - BoxWithConstraints( - modifier = modifier - .windowInsetsPadding(WindowInsets.statusBarsIgnoringVisibility) - .padding( - horizontal = CodeTheme.dimens.inset, - vertical = CodeTheme.dimens.grid.x2 - ), - contentAlignment = Alignment.Center - ) { - BoxWithConstraints( - modifier = Modifier - .aspectRatio(CashBillDefaults.AspectRatio, matchHeightConstraintsFirst = true) - .fillMaxHeight() - .fillMaxWidth(0.95f) - .background(CashBillDefaults.BillColor) - .clipToBounds() - ) { - val geometry = remember(maxWidth, maxHeight) { - CashBillGeometry(maxWidth, maxHeight) - } - - // Hexagons - BillDecorImage( - modifier = Modifier - .fillMaxSize(), - image = loadBillAsset(R.drawable.ic_bill_hexagons), - blendMode = BlendMode.Multiply, - alpha = 0.6f, - ) - - // Grid pattern - BillDecorImage( - modifier = Modifier - .fillMaxSize(), - image = loadBillAsset(R.drawable.ic_bill_grid), - size = DpSize(width = geometry.gridWidth, height = geometry.gridHeight), - topLeft = Offset( - x = geometry.gridPosition.x, - y = geometry.gridPosition.y, - ), - alpha = 0.5f, - ) - - // Globe - Image( - modifier = Modifier - .fillMaxHeight() - .requiredWidth(geometry.globeWidth) - .offset { - IntOffset( - x = geometry.globePosition.x.toInt(), - y = geometry.globePosition.y.toInt() - ) - }, - painter = painterResource(R.drawable.ic_bill_globe), - contentDescription = null - ) - - // Waves - Image( - modifier = Modifier - .requiredWidth(geometry.globeWidth) - .fillMaxHeight() - .offset { IntOffset(x = geometry.wavesPosition.x.toInt(), y = 0) } - .drawWithGradient( - color = CashBillDefaults.BillColor.copy(CashBillDefaults.CodeBackgroundOpacity), - startY = { it / 2f }, - blendMode = BlendMode.DstIn - ), - contentDescription = null, - contentScale = ContentScale.FillBounds, - painter = painterResource(R.drawable.ic_bill_waves), - ) - - // Security strip - SecurityStrip(geometry = geometry) - - - // Bill Value Top Left - BillAmount( - modifier = Modifier - .align(Alignment.TopStart) - .padding(top = geometry.topStripHeight + geometry.securityStripSize.height * 0.5f) - .padding(start = geometry.valuePadding), - text = amount.formattedRaw() - ) - - // Bill Value Bottom Right - BillAmount( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(bottom = geometry.topStripHeight + geometry.securityStripSize.height * 0.5f) - .padding(end = geometry.valuePadding), - text = amount.formattedRaw() - ) - - // Lines - Column( - modifier = Modifier - .fillMaxSize() - .padding(start = geometry.valuePadding, end = geometry.valuePadding * 2), - ) { - Row( - modifier = Modifier - .height(geometry.linesHeight) - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - // September - Lines(count = 9, spacing = geometry.lineSpacing) - // Sept 12 - Lines(count = 12, spacing = geometry.lineSpacing) - } - - Spacer(modifier = Modifier.weight(1f)) - - Row( - modifier = Modifier.padding(bottom = geometry.mintPadding) - ) { - // Mint - Text( - text = Mint.kin.base58(), - fontSize = 8.nonScaledSp, - color = CashBillDefaults.DecorColor, - ) - } - - Row( - modifier = Modifier - .height(geometry.linesHeight) - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Image( - modifier = Modifier - .width(geometry.brandWidth), - contentScale = ContentScale.FillWidth, - painter = painterResource( - R.drawable.ic_code_logo_offwhite_small - ), - colorFilter = ColorFilter.tint(CashBillDefaults.DecorColor), - contentDescription = "", - ) - Spacer(modifier = Modifier.weight(1f)) - // 2017 - Lines(count = 17, spacing = geometry.lineSpacing) - } - } - - // Scan code - BillCode( - modifier = Modifier.align(Alignment.Center), - geometry = geometry, - data = payloadData - ) - } - } -} - -@Composable -private fun SecurityStrip( - modifier: Modifier = Modifier, - geometry: CashBillGeometry, -) { - Row( - modifier = modifier - .size(geometry.securityStripSize) - .offset(geometry.securityStripPosition.x, geometry.securityStripPosition.y) - .punchRectangle(CashBillDefaults.BillColor.copy(CashBillDefaults.CodeBackgroundOpacity)), - ) { - for (i in 0 until CashBillDefaults.SecurityStripCount) { - Image( - modifier = Modifier - .weight(1f) - .alpha(0.5f), - contentScale = ContentScale.FillBounds, - painter = painterResource(id = R.drawable.ic_bill_security_strip), - contentDescription = null - ) - } - } -} - -@Composable -private fun Lines( - modifier: Modifier = Modifier, - count: Int, - spacing: Dp, -) { - Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(spacing)) { - for (i in 0 until count) { - Box( - modifier = Modifier - .rotate(-18f) - .background(CashBillDefaults.DecorColor) - .fillMaxHeight() - .width(1.dp) - ) - } - } -} - -@Composable -private fun BillDecorImage( - modifier: Modifier = Modifier, - image: ImageBitmap?, - alpha: Float = 1f, - size: DpSize = DpSize.Unspecified, - topLeft: Offset = Offset.Zero, - blendMode: BlendMode = DrawScope.DefaultBlendMode, -) { - Canvas( - modifier = modifier, - ) { - // Hexagons - image?.let { - drawImage( - image = it, - dstSize = IntSize( - width = if (size.isSpecified && size.width.isSpecified) size.width.roundToPx() else this.size.width.toInt(), - height = if (size.isSpecified && size.height.isSpecified) size.height.roundToPx() else this.size.height.toInt(), - ), - alpha = alpha, - dstOffset = IntOffset(topLeft.x.roundToInt(), topLeft.y.roundToInt()), - blendMode = blendMode, - ) - } - } -} - -@Composable -private fun BillCode(modifier: Modifier = Modifier, geometry: CashBillGeometry, data: List) { - Box( - modifier = modifier - .punchCircle(CashBillDefaults.BillColor.copy(0.9f)), - contentAlignment = Alignment.Center - ) { - if (data.isNotEmpty()) { - ScannableCode( - modifier = Modifier - .size(geometry.codeSize), - data = data - ) - } - } -} - -@Composable -private fun loadBillAsset(drawableRes: Int): ImageBitmap { - val option = BitmapFactory.Options() - option.inPreferredConfig = Bitmap.Config.ARGB_8888 - return BitmapFactory.decodeResource( - LocalContext.current.resources, - drawableRes, - option - ).asImageBitmap() -} diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/bill/LoginBill.kt b/apps/codeApp/src/main/java/com/getcode/view/main/bill/LoginBill.kt deleted file mode 100644 index e38cdefd8..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/bill/LoginBill.kt +++ /dev/null @@ -1,114 +0,0 @@ -package com.getcode.view.main.bill - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBarsIgnoringVisibility -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.getcode.R -import com.getcode.theme.CodeTheme -import com.getcode.ui.utils.Geometry -import com.getcode.view.main.bill.LoginBillDefaults.DecorColor - - -private object LoginBillDefaults { - val DecorColor: Color = Color(0xFFA9A9B1) -} - -private class LoginBillGeometry(width: Dp, height: Dp): Geometry(width, height) { - val brandWidth: Dp - get() = size.width * 0.18f -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun LoginBill( - modifier: Modifier = Modifier, - data: List, -) { - val screenHeight = LocalConfiguration.current.screenHeightDp.dp - BoxWithConstraints( - modifier = modifier - .windowInsetsPadding(WindowInsets.statusBarsIgnoringVisibility) - .padding(horizontal = CodeTheme.dimens.inset), - contentAlignment = Alignment.Center - ) { - BoxWithConstraints( - modifier = Modifier - .padding(bottom = screenHeight * 0.10f) - .clip(CodeTheme.shapes.small) - .padding(top = CodeTheme.dimens.grid.x12) - .heightIn(0.dp, 800.dp) - .aspectRatio(0.68f), - ) { - val geometry = remember(maxWidth, maxHeight) { - LoginBillGeometry(maxWidth, maxHeight) - } - - Column( - modifier = Modifier - .fillMaxSize() - .background(loginBillGradient(geometry = geometry), CodeTheme.shapes.small) - ) { - Spacer(modifier = Modifier.weight(1f)) - if (data.isNotEmpty()) { - ScannableCode( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .size(geometry.codeSize), - data = data - ) - } - Spacer(modifier = Modifier.weight(1f)) - Image( - modifier = Modifier - .align(Alignment.Start) - .width(geometry.brandWidth) - .padding(CodeTheme.dimens.grid.x2), - contentScale = ContentScale.FillWidth, - painter = painterResource( - R.drawable.ic_code_logo_offwhite_small - ), - colorFilter = ColorFilter.tint(DecorColor), - contentDescription = "", - ) - } - } - } -} - -@Composable -private fun loginBillGradient(geometry: Geometry): Brush { - return Brush.linearGradient( - colorStops = arrayOf( - 0f to Color(31, 35, 35), - 1f to Color(18, 21, 20) - ), - start = Offset(x = geometry.size.width.value * 0.5f, y = 0f), - end = Offset(x = geometry.size.width.value * 0.5f, y = geometry.size.height.value) - ) -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/bill/PaymentReceiptBill.kt b/apps/codeApp/src/main/java/com/getcode/view/main/bill/PaymentReceiptBill.kt deleted file mode 100644 index 7936e1666..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/bill/PaymentReceiptBill.kt +++ /dev/null @@ -1,128 +0,0 @@ -package com.getcode.view.main.bill - -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBarsIgnoringVisibility -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.ProvideTextStyle -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.PathEffect -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.getcode.model.CurrencyCode -import com.getcode.model.KinAmount -import com.getcode.theme.CodeTheme -import com.getcode.theme.DashEffect -import com.getcode.theme.receipt -import com.getcode.theme.monospace -import com.getcode.ui.components.PriceWithFlag - -@OptIn(ExperimentalLayoutApi::class) -@Composable -internal fun Receipt( - modifier: Modifier = Modifier, - data: List, - amount: KinAmount, - currencyCode: CurrencyCode?, -) { - BoxWithConstraints( - modifier = modifier - .windowInsetsPadding(WindowInsets.statusBarsIgnoringVisibility) - .padding(horizontal = CodeTheme.dimens.inset), - contentAlignment = Alignment.Center - ) { - val mW = maxWidth - val codeSize = remember { mW * 0.65f } - - Column( - modifier = Modifier - .background(CodeTheme.colors.onBackground, shape = CodeTheme.shapes.receipt()) - .padding(top = CodeTheme.dimens.grid.x12) - .heightIn(0.dp, 800.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x5) - ) { - if (data.isNotEmpty()) { - ScannableCode( - modifier = Modifier - .size(codeSize) - .background(CodeTheme.colors.brandMuted, CircleShape), - data = data - ) - } - - if (currencyCode != null) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding( - top = CodeTheme.dimens.grid.x8, - bottom = CodeTheme.dimens.grid.x17, - ) - .padding(horizontal = CodeTheme.dimens.inset), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x9) - ) { - DoubleDashedLine() - ProvideTextStyle( - value = CodeTheme.typography.textMedium - .monospace(weight = FontWeight.W500) - ) { - PriceWithFlag(currencyCode = currencyCode, amount = amount) - } - } - } - } - } -} - -@Composable -private fun DoubleDashedLine( - modifier: Modifier = Modifier, - dashColor: Color = DashEffect, - dashLength: Dp = CodeTheme.dimens.staticGrid.x1, -) { - @Composable - fun DashedLine() { - val density = LocalDensity.current - val dashLengthPx = with(density) { dashLength.toPx() } - val pathEffect = PathEffect.dashPathEffect(floatArrayOf(dashLengthPx, dashLengthPx), 0f) - Canvas( - Modifier - .fillMaxWidth(), - ) { - drawLine( - color = dashColor, - start = Offset(0f, 0f), - strokeWidth = 1.dp.toPx(), - end = Offset(this.size.width - dashLengthPx, 0f), - pathEffect = pathEffect - ) - } - } - - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.staticGrid.x1) - ) { - DashedLine() - DashedLine() - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/bill/ScannableCode.kt b/apps/codeApp/src/main/java/com/getcode/view/main/bill/ScannableCode.kt deleted file mode 100644 index 55907693e..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/bill/ScannableCode.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.getcode.view.main.bill - -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.content.ContextCompat -import com.getcode.R -import com.kik.kikx.kincodes.KikCodeContentView - -@Composable -internal fun ScannableCode( - modifier: Modifier = Modifier, - data: List, -) { - BoxWithConstraints( - modifier = modifier, - contentAlignment = Alignment.Center - ) { - AndroidView( - modifier = Modifier - .fillMaxWidth(), - factory = { context -> - KikCodeContentView(context).apply { - this.logo = - ContextCompat.getDrawable( - context, - R.drawable.ic_logo_round_white - ) - this.encodedKikCode = data.toByteArray() - } - }, - update = { } - ) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/bill/TipCard.kt b/apps/codeApp/src/main/java/com/getcode/view/main/bill/TipCard.kt deleted file mode 100644 index bfe1ce175..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/bill/TipCard.kt +++ /dev/null @@ -1,275 +0,0 @@ -package com.getcode.view.main.bill - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBarsIgnoringVisibility -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.inset -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.getcode.LocalDownloadQrCode -import com.getcode.R -import com.getcode.theme.Brand -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.CardFace -import com.getcode.ui.theme.CodeCircularProgressIndicator -import com.getcode.ui.components.FlippableCard -import com.getcode.ui.components.Row -import com.getcode.ui.components.TwitterUsernameDisplay -import com.getcode.ui.utils.Geometry -import dev.chrisbanes.haze.HazeState -import dev.chrisbanes.haze.HazeStyle -import dev.chrisbanes.haze.haze -import dev.chrisbanes.haze.hazeChild - -private class TipCardGeometry(width: Dp, height: Dp) : Geometry(width, height) { - - val backQrSize: Dp - get() = size.width * 0.40f - - val platformLogoSize: Dp - @Composable get () = CodeTheme.dimens.staticGrid.x5 -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun TipCard( - modifier: Modifier = Modifier, - username: String, - data: List, - interactive: Boolean = false, -) { - val screenHeight = LocalConfiguration.current.screenHeightDp.dp - BoxWithConstraints( - modifier = modifier - .windowInsetsPadding(WindowInsets.statusBarsIgnoringVisibility) - .padding(horizontal = CodeTheme.dimens.inset), - contentAlignment = Alignment.Center - ) { - Box( - modifier = Modifier - .aspectRatio(0.57f, matchHeightConstraintsFirst = true) - .padding(bottom = screenHeight * 0.05f) - .padding(top = CodeTheme.dimens.grid.x9) - ) { - - var cardFace by rememberSaveable { mutableStateOf(CardFace.Front) } - FlippableCard( - modifier = Modifier - .padding(horizontal = CodeTheme.dimens.grid.x3) - .clickable( - enabled = interactive, - indication = null, - interactionSource = remember { MutableInteractionSource() }) { - cardFace = cardFace.next - }, - cardFace = cardFace, - back = { - BoxWithConstraints { - val geometry = remember(maxWidth, maxHeight) { - TipCardGeometry(maxWidth, maxHeight) - } - Box( - modifier = Modifier - .matchParentSize() - .background(CodeTheme.colors.background) - ) - Back(geometry) - } - }, - front = { - BoxWithConstraints { - val geometry = remember(maxWidth, maxHeight) { - TipCardGeometry(maxWidth, maxHeight) - } - HazedBackground(geometry = geometry) - Front(geometry = geometry, data = data, username = username) - } - } - ) - } - } -} - -@Composable -private fun HazedBackground( - geometry: Geometry, -) { - val hazeState = remember { HazeState() } - Box( - modifier = Modifier - .background(color = Brand) - .cardBorder() - .haze(state = hazeState) - ) { - val background = CodeTheme.colors.background - Box( - modifier = Modifier - .fillMaxSize() - // blur - .hazeChild( - state = hazeState, - style = HazeStyle(blurRadius = geometry.size.width * 0.07f) - ) - // mimic an inner shadow from Figma - .drawBehind { - inset( - horizontal = 2.dp.toPx(), - vertical = 2.dp.toPx() - ) { - drawRect( - brush = Brush.radialGradient( - colors = listOf( - Color.White.copy(0.45f), - background.copy(alpha = 0.24f) - ), - center = Offset.Zero, - radius = size.maxDimension - ), - ) - } - } - ) - } -} - -@Composable -private fun Front( - geometry: TipCardGeometry, - data: List, - username: String, -) { - Column( - modifier = Modifier - .fillMaxSize(), - ) { - Spacer(modifier = Modifier.weight(1f)) - if (data.isNotEmpty()) { - ScannableCode( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .size(geometry.codeSize), - data = data - ) - } - Spacer(modifier = Modifier.weight(1f)) - TwitterUsernameDisplay( - modifier = Modifier.fillMaxWidth(), - username = username - ) - Spacer(modifier = Modifier.weight(1f)) - } -} - -@Composable -private fun Back( - geometry: TipCardGeometry, -) { - Column( - modifier = Modifier - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Spacer(modifier = Modifier.weight(1f)) - Text( - text = stringResource(R.string.title_tipCardBack), - style = CodeTheme.typography.textLarge, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.weight(1f)) - - val qrCode = LocalDownloadQrCode.current - - if (qrCode != null) { - Image( - modifier = Modifier - .size(geometry.backQrSize), - painter = qrCode, - contentDescription = "qr" - ) - } else { - CodeCircularProgressIndicator() - } - - Row( - modifier = Modifier.padding(top = CodeTheme.dimens.grid.x3), - horizontalArrangement = Arrangement.spacedBy( - space = CodeTheme.dimens.inset, - alignment = Alignment.CenterHorizontally - ), - verticalAlignment = Alignment.CenterVertically - ) { - Image( - modifier = Modifier.size(geometry.platformLogoSize), - painter = painterResource(id = R.drawable.ic_apple_icon), - contentDescription = null - ) - Image( - modifier = Modifier.size(geometry.platformLogoSize), - painter = painterResource(id = R.drawable.ic_android_icon), - contentDescription = null - ) - } - - - Spacer(modifier = Modifier.weight(1f)) - Text( - modifier = Modifier - .fillMaxWidth(0.8f) - .padding(top = CodeTheme.dimens.grid.x2), - text = stringResource(R.string.subtitle_tipCardBack), - style = CodeTheme.typography.textSmall, - color = CodeTheme.colors.onBackground, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.weight(1f)) - } -} - -@Composable -private fun Modifier.cardBorder() = border( - BorderStroke( - 1.dp, - Brush.linearGradient( - colorStops = arrayOf( - 0f to Color(0xFF15141B), - 1f to Color(0xFF161325) - ) - ) - ) -) \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/chat/ChatScreen.kt b/apps/codeApp/src/main/java/com/getcode/view/main/chat/ChatScreen.kt deleted file mode 100644 index e947afdc0..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/chat/ChatScreen.kt +++ /dev/null @@ -1,114 +0,0 @@ -package com.getcode.view.main.chat - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.Dp -import androidx.paging.compose.LazyPagingItems -import com.getcode.R -import com.getcode.manager.BottomBarManager -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.components.Row -import com.getcode.ui.components.VerticalDivider -import com.getcode.ui.components.chat.MessageList -import com.getcode.ui.components.chat.utils.ChatItem -import com.getcode.ui.components.chat.utils.localized -import com.getcode.ui.core.withTopBorder - -@Composable -fun ChatScreen( - state: NotificationCollectionViewModel.State, - messages: LazyPagingItems, - dispatch: (NotificationCollectionViewModel.Event) -> Unit, -) { - val listState = rememberLazyListState() - - val context = LocalContext.current - val title = state.title.localized - - Column(modifier = Modifier.fillMaxSize()) { - MessageList( - modifier = Modifier.weight(1f), - listState = listState, - messages = messages, - ) - - Row( - modifier = Modifier - .fillMaxWidth() - .height(IntrinsicSize.Min) - .withTopBorder() - ) { - if (state.canMute) { - CodeButton( - modifier = Modifier.weight(1f), - onClick = { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = context.getString( - if (state.isMuted) R.string.prompt_title_unmute else R.string.prompt_title_mute, - title - ), - subtitle = context.getString( - if (state.isMuted) R.string.prompt_description_unmute else R.string.prompt_description_mute, - title - ), - positiveText = context.getString(if (state.isMuted) R.string.action_unmute else R.string.action_mute), - negativeText = context.getString(R.string.action_nevermind), - onPositive = { dispatch(NotificationCollectionViewModel.Event.OnMuteToggled) }, - ) - ) - }, - shape = RectangleShape, - buttonState = ButtonState.Subtle, - text = stringResource(if (state.isMuted) R.string.action_unmute else R.string.action_mute) - ) - } - - if (state.canMute && state.canUnsubscribe) { - VerticalDivider( - thickness = Dp.Hairline, - ) - } - - if (state.canUnsubscribe) { - CodeButton( - modifier = Modifier.weight(1f), - onClick = { - if (!state.isSubscribed) { - dispatch(NotificationCollectionViewModel.Event.OnSubscribeToggled) - return@CodeButton - } - - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = context.getString(R.string.prompt_title_unsubscribe, title), - subtitle = context.getString( - R.string.prompt_description_unsubscribe, - title - ), - positiveText = context.getString(R.string.action_unsubscribe), - negativeText = context.getString(R.string.action_nevermind), - onPositive = { dispatch(NotificationCollectionViewModel.Event.OnSubscribeToggled) }, - ) - ) - }, - shape = RectangleShape, - buttonState = ButtonState.Subtle, - text = if (state.isSubscribed) stringResource(R.string.action_unsubscribe) else stringResource( - id = R.string.action_subscribe - ) - ) - } - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/chat/NotificationCollectionViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/chat/NotificationCollectionViewModel.kt deleted file mode 100644 index 5e992634e..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/chat/NotificationCollectionViewModel.kt +++ /dev/null @@ -1,224 +0,0 @@ -package com.getcode.view.main.chat - -import androidx.lifecycle.viewModelScope -import androidx.paging.flatMap -import androidx.paging.insertSeparators -import androidx.paging.map -import com.getcode.model.ID -import com.getcode.model.chat.MessageContent -import com.getcode.model.chat.MessageStatus -import com.getcode.model.chat.NotificationCollectionEntity -import com.getcode.model.chat.Reference -import com.getcode.model.chat.Sender -import com.getcode.model.chat.Title -import com.getcode.model.chat.Verb -import com.getcode.network.NotificationCollectionHistoryController -import com.getcode.network.repository.BetaFlagsRepository -import com.getcode.ui.components.chat.utils.ChatItem -import com.getcode.ui.components.chat.utils.ChatMessageIndice -import com.getcode.util.formatDateRelatively -import com.getcode.util.toInstantFromMillis -import com.getcode.utils.base58 -import com.getcode.view.BaseViewModel2 -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import timber.log.Timber -import javax.inject.Inject - -@OptIn(ExperimentalCoroutinesApi::class) -@HiltViewModel -class NotificationCollectionViewModel @Inject constructor( - historyController: NotificationCollectionHistoryController, - betaFlags: BetaFlagsRepository, -) : BaseViewModel2( - initialState = State( - chatId = null, - chat = null, - title = null, - canMute = false, - isMuted = false, - _canUnsubscribe = false, - unsubscribeEnabled = false, - isSubscribed = false, - ), - updateStateForEvent = updateStateForEvent -) { - data class State( - val chatId: ID?, - val chat: NotificationCollectionEntity?, - val title: Title?, - val canMute: Boolean, - val isMuted: Boolean, - private val _canUnsubscribe: Boolean, - private val unsubscribeEnabled: Boolean, - val isSubscribed: Boolean, - ) { - val canUnsubscribe: Boolean - get() = _canUnsubscribe && unsubscribeEnabled - } - - sealed interface Event { - data class OnChatIdChanged(val id: ID?) : Event - data class OnChatChanged(val chat: NotificationCollectionEntity) : Event - data object OnMuteToggled : Event - data object OnSubscribeToggled : Event - data class SetMuted(val muted: Boolean) : Event - data class SetSubscribed(val subscribed: Boolean) : Event - data class EnableUnsubscribe(val enabled: Boolean) : Event - data class OpenMessageChat(val reference: Reference) : Event - } - - init { - stateFlow - .map { it.chatId } - .onEach { Timber.d("chatid=${it?.base58}") } - .filterNotNull() - .onEach { historyController.advanceReadPointer(it) } - .flatMapLatest { historyController.notifications } - .flowOn(Dispatchers.IO) - .filterNotNull() - .mapNotNull { chats -> chats.firstOrNull { it.id == stateFlow.value.chatId } } - .onEach { dispatchEvent(Dispatchers.Main, Event.OnChatChanged(it)) } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { stateFlow.value.chat to stateFlow.value.isMuted } - .filter { it.first != null } - .map { it.first!! to it.second } - .map { (chat, muted) -> - dispatchEvent(Event.SetMuted(!muted)) - historyController.setMuted(chat, !muted) - } - .onEach { result -> - if (result.isSuccess) { - val muted = result.getOrNull() ?: false - Timber.d(if (muted) "Muted chat" else "Unmuted chat") - } else { - result.exceptionOrNull()?.printStackTrace() - dispatchEvent(Event.SetMuted(!stateFlow.value.isMuted)) - } - } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { stateFlow.value.chat to stateFlow.value.isSubscribed } - .filter { it.first != null } - .map { it.first!! to it.second } - .map { (chat, subscribed) -> - dispatchEvent(Event.SetSubscribed(!subscribed)) - historyController.setSubscribed(chat, !subscribed) - } - .onEach { result -> - if (result.isSuccess) { - val subbed = result.getOrNull() ?: false - Timber.d(if (subbed) "Subscribed" else "Unsubscribe") - } else { - result.exceptionOrNull()?.printStackTrace() - dispatchEvent(Event.SetSubscribed(!stateFlow.value.isSubscribed)) - } - } - .launchIn(viewModelScope) - - betaFlags.observe() - .map { it.chatUnsubEnabled } - .distinctUntilChanged() - .onEach { - dispatchEvent(Event.EnableUnsubscribe(it)) - }.launchIn(viewModelScope) - } - - val chatMessages = stateFlow - .map { it.chatId } - .filterNotNull() - .flatMapLatest { historyController.collectionFlow(it) } - .mapLatest { page -> - page.flatMap { message -> - message.contents - .sortedWith(compareBy { it is MessageContent.Localized }) - .map { - ChatMessageIndice( - message, - it, - ) - } - } - } - .mapLatest { page -> - page.map { (message, contents) -> - ChatItem.Message( - chatMessageId = message.id, - message = contents, - date = message.dateMillis.toInstantFromMillis(), - status = if (contents.isFromSelf) MessageStatus.Sent else MessageStatus.Unknown, - sender = Sender( - id = null, - name = null, - profileImageUrl = null, - isHost = false, - isSelf = contents.isFromSelf, - - ) - ) - } - } - .mapLatest { page -> - page.insertSeparators { before: ChatItem.Message?, after: ChatItem.Message? -> - if (before?.date != after?.date) { - before?.date?.let { ChatItem.Date(it) } - } else { - null - } - } - } - - companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - is Event.OnChatIdChanged -> { state -> - state.copy(chatId = event.id) - } - - is Event.OnChatChanged -> { state -> - state.copy( - chat = event.chat, - title = event.chat.title, - canMute = event.chat.canMute, - isMuted = event.chat.isMuted, - isSubscribed = event.chat.isSubscribed, - _canUnsubscribe = event.chat.canUnsubscribe, - ) - } - - is Event.OpenMessageChat, - Event.OnMuteToggled, - Event.OnSubscribeToggled -> { state -> state } - - is Event.SetMuted -> { state -> - state.copy(isMuted = event.muted) - } - - is Event.SetSubscribed -> { state -> - state.copy(isSubscribed = event.subscribed) - } - - is Event.EnableUnsubscribe -> { state -> - state.copy(unsubscribeEnabled = event.enabled) - } - } - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/currency/CurrencySelectionSheet.kt b/apps/codeApp/src/main/java/com/getcode/view/main/currency/CurrencySelectionSheet.kt deleted file mode 100644 index 51cac011f..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/currency/CurrencySelectionSheet.kt +++ /dev/null @@ -1,478 +0,0 @@ -package com.getcode.view.main.currency - -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredSize -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.DismissDirection -import androidx.compose.material.DismissState -import androidx.compose.material.DismissValue -import androidx.compose.material.Divider -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.FixedThreshold -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.SwipeToDismiss -import androidx.compose.material.Text -import androidx.compose.material.TextField -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.outlined.Close -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.getcode.R -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.theme.Brand -import com.getcode.theme.CodeTheme -import com.getcode.theme.White50 -import com.getcode.theme.inputColors -import com.getcode.ui.theme.CodeCircularProgressIndicator -import com.getcode.ui.utils.keyboardAsState -import com.getcode.ui.core.rememberedClickable -import com.getcode.view.main.giveKin.CurrencyListItem -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - - -@Composable -fun CurrencySelectionSheet( - viewModel: CurrencyViewModel, -) { - val navigator = LocalCodeNavigator.current - val state by viewModel.stateFlow.collectAsState() - val keyboardController = LocalSoftwareKeyboardController.current - val composeScope = rememberCoroutineScope() - var searchQuery by remember { - mutableStateOf(TextFieldValue()) - } - - LaunchedEffect(searchQuery, state.currencySearchText) { - if (searchQuery.text != state.currencySearchText) { - viewModel.dispatchEvent(CurrencyViewModel.Event.OnSearchQueryChanged(searchQuery.text)) - } - } - - val keyboard by keyboardAsState() - - Column( - modifier = Modifier.imePadding() - ) { - TextField( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(bottom = CodeTheme.dimens.grid.x2) - .padding(horizontal = CodeTheme.dimens.grid.x3), - leadingIcon = { - Icon( - imageVector = Icons.Filled.Search, - contentDescription = null, - tint = White50, - ) - }, - placeholder = { - Text( - stringResource(id = R.string.subtitle_searchCurrencies), - style = CodeTheme.typography.textLarge.copy( - fontSize = 16.sp, - ) - ) - }, - trailingIcon = { - if (searchQuery.text.isNotEmpty()) { - IconButton( - onClick = { - searchQuery = TextFieldValue() - }, - ) { - Icon( - imageVector = Icons.Outlined.Close, - contentDescription = null, - tint = White50, - ) - } - } - }, - value = searchQuery, - onValueChange = { searchQuery = it }, - textStyle = CodeTheme.typography.textLarge.copy( - fontSize = 16.sp, - ), - singleLine = true, - colors = inputColors(), - shape = RoundedCornerShape(size = 5.dp) - ) - - val groups by remember(state.listItems) { - derivedStateOf { - if (state.listItems.isEmpty()) return@derivedStateOf emptyList() to emptyList() - val index = state.listItems.indexOfLast { it is CurrencyListItem.TitleItem } - if (index == 0) { - // no recents - emptyList() to state.listItems - } else { - // recents - state.listItems.subList(0, index) to state.listItems.subList( - index, - state.listItems.lastIndex - ) - } - } - } - - val (recents, other) = groups - - var recentItems by remember(recents) { - mutableStateOf(recents) - } - - var otherItems by remember(other) { - mutableStateOf(other) - } - - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .background(Brand) - .weight(1f) - ) { - if (state.loading) { - item { - Box(Modifier.fillParentMaxSize()) { - CodeCircularProgressIndicator( - Modifier.align( - Alignment.TopCenter - ) - ) - } - } - } - - items(recentItems) { listItem -> - val currencyCode = when (listItem) { - is CurrencyListItem.RegionCurrencyItem -> listItem.currency.code - else -> "" - } - - Box( - modifier = Modifier - .fillMaxWidth() - .height(if (listItem !is CurrencyListItem.TitleItem) 70.dp else 60.dp) - ) { - - when (listItem) { - is CurrencyListItem.TitleItem -> { - GroupHeader( - modifier = Modifier.align(Alignment.BottomStart), - text = listItem.text - ) - } - - is CurrencyListItem.RegionCurrencyItem -> { - ListRowItem( - item = listItem, - isSelected = state.selectedCurrencyCode.orEmpty() == currencyCode, - onRemoved = { - if (recentItems.count() == 2) { - recentItems = emptyList() - } else { - recentItems = recentItems.minus(listItem) - } - val title = otherItems[0] - val items = - otherItems.filterIsInstance() + listItem - - otherItems = listOf(title) + items.sortedBy { it.currency.name } - viewModel.dispatchEvent( - CurrencyViewModel.Event.OnRecentCurrencyRemoved( - listItem.currency - ) - ) - }, - ) { - composeScope.launch { - if (keyboard) { - keyboardController?.hide() - delay(500) - } - navigator.popWithResult(listItem.currency) - } - viewModel.dispatchEvent( - CurrencyViewModel.Event.OnSelectedCurrencyChanged( - listItem.currency - ) - ) - } - } - } - } - } - - items(otherItems) { listItem -> - val currencyCode = when (listItem) { - is CurrencyListItem.RegionCurrencyItem -> listItem.currency.code - else -> "" - } - - Box( - modifier = Modifier - .fillMaxWidth() - .height(if (listItem !is CurrencyListItem.TitleItem) 70.dp else 60.dp) - ) { - - when (listItem) { - is CurrencyListItem.TitleItem -> { - GroupHeader( - modifier = Modifier.align(Alignment.BottomStart), - text = listItem.text - ) - } - - is CurrencyListItem.RegionCurrencyItem -> { - var isSwipedAway by remember(listItem) { - mutableStateOf(false) - } - - val animatedHeight by animateDpAsState( - targetValue = if (!isSwipedAway) 70.dp else 0.dp, - label = "height animation", - animationSpec = tween(300), - finishedListener = { - if (it == 0.dp) { - viewModel.dispatchEvent( - CurrencyViewModel.Event.OnRecentCurrencyRemoved( - listItem.currency - ) - ) - } - } - ) - ListRowItem( - modifier = Modifier.height(animatedHeight), - item = listItem, - isSelected = state.selectedCurrencyCode.orEmpty() == currencyCode, - onRemoved = { - isSwipedAway = true - viewModel.dispatchEvent( - CurrencyViewModel.Event.OnRecentCurrencyRemoved( - listItem.currency - ) - ) - }, - ) { - composeScope.launch { - if (keyboard) { - keyboardController?.hide() - delay(500) - } - navigator.popWithResult(listItem.currency) - } - viewModel.dispatchEvent( - CurrencyViewModel.Event.OnSelectedCurrencyChanged( - listItem.currency - ) - ) - } - } - } - } - } - } - } -} - -@Composable -private fun GroupHeader(modifier: Modifier = Modifier, text: String) { - Column( - modifier = modifier, - ) { - Row( - modifier = Modifier.padding(horizontal = CodeTheme.dimens.inset) - ) { - Text( - modifier = Modifier.padding(bottom = CodeTheme.dimens.grid.x2), - style = CodeTheme.typography.textSmall, - color = CodeTheme.colors.textSecondary, - text = text - ) - } - Divider( - color = CodeTheme.colors.dividerVariant, - modifier = Modifier - .fillMaxWidth() - .height(1.dp) - ) - } -} - - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun ListRowItem( - modifier: Modifier = Modifier, - item: CurrencyListItem.RegionCurrencyItem, - isSelected: Boolean, - onRemoved: () -> Unit, - onClick: () -> Unit -) { - - var removed by remember(item) { - mutableStateOf(false) - } - - val dismissState = remember(item) { - DismissState( - initialValue = DismissValue.Default, - confirmStateChange = { - if (it == DismissValue.DismissedToStart) { - removed = true - true - } else false - } - ) - } - - SwipeToDismiss( - state = dismissState, - dismissThresholds = { FixedThreshold(150.dp) }, - directions = if (item.isRecent) setOf(DismissDirection.EndToStart) else emptySet(), - background = { - if (item.isRecent) { - DismissBackground(dismissState) - } - } - ) { - Box( - modifier = Modifier - .fillMaxSize() - .background(Brand) - .let { - if (item.currency.rate > 0) { - it.rememberedClickable { onClick() } - } else it - } - ) { - Box( - modifier = Modifier.fillMaxSize() - .padding(horizontal = CodeTheme.dimens.inset) - ) { - Row( - modifier = Modifier - .fillMaxSize() - .align(Alignment.CenterStart) - .alpha(if (item.currency.rate <= 0) 0.25f else 1.0f) - ) { - item.currency.resId?.let { resId -> - Image( - modifier = Modifier - .padding(end = CodeTheme.dimens.grid.x3) - .requiredSize(CodeTheme.dimens.staticGrid.x6) - .clip(CodeTheme.shapes.large) - .align(Alignment.CenterVertically), - painter = painterResource(resId), - contentDescription = "" - ) - } - Column( - modifier = Modifier - .wrapContentWidth() - .align(Alignment.CenterVertically), - ) { - Text( - text = item.currency.name, - style = CodeTheme.typography.textMedium - ) - } - } - - Image( - modifier = Modifier - .wrapContentWidth() - .align(Alignment.CenterEnd) - .alpha(if (item.currency.rate <= 0) 0.25f else 1.0f), - painter = painterResource( - if (isSelected) - R.drawable.ic_checked_blue else R.drawable.ic_unchecked - ), - contentDescription = "" - ) - } - - Divider( - color = CodeTheme.colors.dividerVariant, - modifier = Modifier - .fillMaxWidth() - .height(1.dp) - .align(Alignment.BottomCenter) - .padding(start = CodeTheme.dimens.inset) - ) - } - } - - LaunchedEffect(removed) { - if (removed) { - delay(200) - onRemoved() - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun DismissBackground(dismissState: DismissState) { - val color = when (dismissState.dismissDirection) { - DismissDirection.EndToStart -> CodeTheme.colors.error - else -> Brand - } - val direction = dismissState.dismissDirection - - Row( - modifier = Modifier - .fillMaxSize() - .background(color) - .padding(end = CodeTheme.dimens.inset), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End - ) { - if (direction == DismissDirection.EndToStart) { - Icon( - modifier = Modifier.size(CodeTheme.dimens.staticGrid.x6), - painter = painterResource(id = R.drawable.ic_delete), - contentDescription = "delete" - ) - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/currency/CurrencyViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/currency/CurrencyViewModel.kt deleted file mode 100644 index 418ee9319..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/currency/CurrencyViewModel.kt +++ /dev/null @@ -1,309 +0,0 @@ -package com.getcode.view.main.currency - -import androidx.compose.runtime.Stable -import androidx.lifecycle.viewModelScope -import com.getcode.R -import com.getcode.model.Currency -import com.getcode.services.model.PrefsBool -import com.getcode.services.model.PrefsString -import com.getcode.network.exchange.Exchange -import com.getcode.network.repository.PrefRepository -import com.getcode.util.locale.LocaleHelper -import com.getcode.util.resources.ResourceHelper -import com.getcode.view.BaseViewModel2 -import com.getcode.view.main.giveKin.CurrencyListItem -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart -import javax.inject.Inject - -enum class CurrencySelectKind { - Entry, - Local; - - val key: PrefsString - get() = when (this) { - Entry -> PrefsString.KEY_ENTRY_CURRENCY - Local -> PrefsString.KEY_LOCAL_CURRENCY - } -} - -@HiltViewModel -class CurrencyViewModel @Inject constructor( - localeHelper: LocaleHelper, - currencyUtils: com.getcode.utils.CurrencyUtils, - exchange: Exchange, - private val prefsRepository: PrefRepository, - private val resources: ResourceHelper, -) : BaseViewModel2( - initialState = State(), - updateStateForEvent = updateStateForEvent -) { - @Stable - data class State( - val kind: CurrencySelectKind? = null, - val loading: Boolean = false, - val currenciesFiltered: List = listOf(), - val currenciesRecent: List? = null, - val listItems: List = listOf(), - val wasLocalRemovedFromRecents: Boolean = false, - val currencySearchText: String = "", - val selectedCurrencyCode: String? = null, - val selectedCurrencyResId: Int? = null, - ) - - sealed interface Event { - data class OnKindChanged(val kind: CurrencySelectKind): Event - data class OnLoadingChanged(val loading: Boolean) : Event - data class OnCurrenciesLoaded(val currencies: List) : Event - data class OnRecentCurrenciesUpdated(val currencies: List) : Event - data class OnSearchQueryChanged(val query: String) : Event - data class OnFilteredCurrenciesUpdated(val currencies: List) : Event - data class OnSelectedCurrencyChanged(val currency: Currency, val fromUser: Boolean = true) : - Event - - data object RemovedLocalFromRecents : Event - - data class OnRecentCurrencyRemoved(val currency: Currency) : Event - } - - - init { - exchange.observeRates() - .onStart { dispatchEvent(Dispatchers.Main, Event.OnLoadingChanged(true)) } - .distinctUntilChanged() - .map { rates -> currencyUtils.getCurrenciesWithRates(rates) } - .onEach { dispatchEvent(Dispatchers.Main, Event.OnFilteredCurrenciesUpdated(it)) } - .launchIn(viewModelScope) - - stateFlow - .map { it.kind?.key } - .filterNotNull() - .flatMapLatest { - prefsRepository.observeOrDefault(it, localeHelper.getDefaultCurrencyName()) - } - .flowOn(Dispatchers.IO) - .distinctUntilChanged() - .mapNotNull { currencyUtils.getCurrency(it) } - .mapNotNull { currencyWithoutRate -> - val currencies = currencyUtils.getCurrenciesWithRates(exchange.rates()) - currencies.find { it.code == currencyWithoutRate.code } - } - .onEach { dispatchEvent(Event.OnSelectedCurrencyChanged(it, false)) } - .launchIn(viewModelScope) - - prefsRepository - .observeOrDefault(PrefsBool.HAS_REMOVED_LOCAL_CURRENCY, false) - .flowOn(Dispatchers.IO) - .distinctUntilChanged() - .filter { it } - .onEach { - dispatchEvent(Dispatchers.Main, Event.RemovedLocalFromRecents) - }.launchIn(viewModelScope) - - - prefsRepository - .observeOrDefault( - PrefsString.KEY_CURRENCIES_RECENT, "" - ) - .flowOn(Dispatchers.IO) - .map { it.split(",") } - .map { recents -> - val currencies = currencyUtils.getCurrenciesWithRates(exchange.rates()) - recents - .mapNotNull { currencies.find { c -> c.code == it } } - .sortedBy { c -> c.code } - .toMutableList() - .let { sorted -> - val currency = - currencies.find { it.code == localeHelper.getDefaultCurrencyName() } - if (currency != null) { - // only add local currency if not removed by user this session - if (!sorted.contains(currency) && !stateFlow.value.wasLocalRemovedFromRecents) { - sorted.add(currency) - addToRecents(currency) - } - } - sorted - } - .sortedBy { it.code } - }.distinctUntilChanged() - .onEach { dispatchEvent(Event.OnRecentCurrenciesUpdated(it)) } - .launchIn(viewModelScope) - - - stateFlow - .filter { it.currenciesFiltered.isNotEmpty() && it.currenciesRecent != null } - .map { - with(it) { - getCurrenciesLocalesListItems( - currenciesFiltered, - currenciesRecent.orEmpty(), - currencySearchText - ) - } - }.distinctUntilChanged() - .onEach { - dispatchEvent(Event.OnCurrenciesLoaded(it.toImmutableList())) - }.onEach { - dispatchEvent(Event.OnLoadingChanged(false)) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .filter { it.fromUser } - .map { it.currency } - .distinctUntilChanged() - .onEach { selected -> - val key = stateFlow.value.kind?.key - if (key != null) { - prefsRepository.set(key, selected.code) - } - addToRecents(selected) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.currency } - .map { selected -> - val currencies = stateFlow.value - .currenciesRecent.orEmpty() - .filter { it.code != selected.code } - - if (selected.code == localeHelper.getDefaultCurrencyName()) { - dispatchEvent(Event.RemovedLocalFromRecents) - } - - prefsRepository.set( - PrefsString.KEY_CURRENCIES_RECENT, - currencies.joinToString(",") { it.code } - ) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .onEach { - prefsRepository.set(PrefsBool.HAS_REMOVED_LOCAL_CURRENCY, true) - }.launchIn(viewModelScope) - } - - private fun addToRecents(currency: Currency) { - val recents = - (stateFlow.value.currenciesRecent.orEmpty() + currency).distinctBy { it.code } - prefsRepository.set( - PrefsString.KEY_CURRENCIES_RECENT, - recents.joinToString(",") { it.code } - ) - } - - private fun getCurrenciesLocalesListItems( - currencies: List, - currenciesRecent: List, - searchString: String - ): MutableList { - val currenciesLocalesList = mutableListOf() - - if (searchString.isBlank()) { - if (currenciesRecent.isNotEmpty()) { - currenciesLocalesList.add( - CurrencyListItem.TitleItem( - resources.getString(R.string.title_recentCurrencies) - ) - ) - currenciesRecent.forEach { currency -> - currenciesLocalesList.add( - CurrencyListItem.RegionCurrencyItem( - currency, - isRecent = true - ) - ) - } - } - - currenciesLocalesList.add( - CurrencyListItem.TitleItem( - resources.getString(R.string.title_otherCurrencies) - ) - ) - } else { - currenciesLocalesList.add( - CurrencyListItem.TitleItem( - resources.getString(R.string.title_results), - ) - ) - } - - currencies - .filter { - (searchString.isEmpty() || - it.name.lowercase().contains(searchString.lowercase()) || - it.code.lowercase().contains(searchString.lowercase()) - ) - } - .forEach { currency -> - if (searchString.isNotEmpty() || !currenciesRecent.contains(currency)) { - currenciesLocalesList.add( - CurrencyListItem.RegionCurrencyItem( - currency, - isRecent = false - ) - ) - } - } - - return currenciesLocalesList - } - - companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - is Event.OnKindChanged -> { state -> - state.copy(kind = event.kind) - } - is Event.OnLoadingChanged -> { state -> state.copy(loading = event.loading) } - is Event.OnCurrenciesLoaded -> { state -> - state.copy(listItems = event.currencies) - } - - is Event.OnRecentCurrenciesUpdated -> { state -> - state.copy(currenciesRecent = event.currencies) - } - - is Event.OnFilteredCurrenciesUpdated -> { state -> - state.copy(currenciesFiltered = event.currencies) - } - - is Event.OnSearchQueryChanged -> { state -> - state.copy( - currencySearchText = event.query - ) - } - - is Event.OnRecentCurrencyRemoved -> { state -> state } - is Event.OnSelectedCurrencyChanged -> { state -> - if (event.fromUser) { - state - } else { - state.copy( - selectedCurrencyCode = event.currency.code, - selectedCurrencyResId = event.currency.resId - ) - } - } - - is Event.RemovedLocalFromRecents -> { state -> state.copy(wasLocalRemovedFromRecents = true) } - } - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/getKin/BuyAndSellKin.kt b/apps/codeApp/src/main/java/com/getcode/view/main/getKin/BuyAndSellKin.kt deleted file mode 100644 index ecffcd58d..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/getKin/BuyAndSellKin.kt +++ /dev/null @@ -1,180 +0,0 @@ -package com.getcode.view.main.getKin - -import android.content.Context -import android.content.Intent -import android.net.Uri -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.lifecycle.viewmodel.compose.viewModel -import com.getcode.R -import com.getcode.theme.CodeTheme -import com.getcode.theme.bolded -import com.getcode.ui.core.rememberedClickable -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach - -@Composable -fun BuyAndSellKin( - viewModel: BuyAndSellKinViewModel = viewModel() -) { - val context = LocalContext.current - - val state by viewModel.stateFlow.collectAsState() - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .map { it.link } - .onEach { openVideo(context, it) } - .launchIn(this) - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .map { it.link } - .onEach { shareVideo(context, it) } - .launchIn(this) - } - - ConstraintLayout( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset), - ) { - val (topSection) = createRefs() - - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .constrainAs(topSection) { - top.linkTo(parent.top) - start.linkTo(parent.start) - end.linkTo(parent.end) - }, - ) { - item { - Text( - text = stringResource(R.string.title_buySellKin), - style = CodeTheme.typography.displayMedium.bolded(), - modifier = Modifier.padding(vertical = CodeTheme.dimens.grid.x3), - ) - } - item { - Text( - text = stringResource(R.string.subtitle_buySellDescription), - style = CodeTheme.typography.textMedium, - modifier = Modifier.padding(vertical = CodeTheme.dimens.grid.x6), - ) - } - - items(state.items.toImmutableList(), key = { it.link }) { item -> - - VideoThumbnail( - context = context, - imageResId = item.imageResId, - link = item.link, - onVideoClick = { _, link -> - viewModel.dispatchEvent(BuyAndSellKinViewModel.Event.OpenVideo(link)) - }, - ) - - CodeButton( - modifier = Modifier.fillMaxWidth(), - onClick = { - viewModel.dispatchEvent(BuyAndSellKinViewModel.Event.OpenVideo(item.link)) - }, - text = stringResource(id = item.buttonTextResId), - buttonState = ButtonState.Filled, - ) - - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = CodeTheme.dimens.grid.x6), - onClick = { - viewModel.dispatchEvent(BuyAndSellKinViewModel.Event.ShareVideo(item.link)) - }, - text = stringResource(id = R.string.action_shareVideo), - buttonState = ButtonState.Subtle, - ) - } - } - } -} - -@Composable -private fun VideoThumbnail( - context: Context, - imageResId: Int, - link: Uri, - onVideoClick: (Context, Uri) -> Unit, -) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = CodeTheme.dimens.grid.x2) - .clip(CodeTheme.shapes.small) - .rememberedClickable { onVideoClick(context, link) }, - - ) { - Image( - modifier = Modifier - .aspectRatio(16f / 9f) - .fillMaxWidth() - .align(Alignment.CenterStart), - contentScale = ContentScale.Fit, - painter = painterResource(id = imageResId), - contentDescription = "Video Thumbnail", - ) - - Image( - modifier = Modifier - .size(CodeTheme.dimens.staticGrid.x14) - .align(Alignment.Center), - painter = painterResource(id = R.drawable.youtube), - contentDescription = "Youtube Logo", - ) - } -} - -private fun shareVideo(context: Context, link: Uri) { - val sendIntent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, link.toString()) - type = "text/plain" - } - val shareIntent = Intent.createChooser(sendIntent, null) - context.startActivity(shareIntent) -} - -private fun openVideo(context: Context, link: Uri) { - val intent = Intent(Intent.ACTION_VIEW, link) - if (intent.resolveActivity(context.packageManager) != null) { - context.startActivity(intent) - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/getKin/BuyAndSellKinViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/getKin/BuyAndSellKinViewModel.kt deleted file mode 100644 index 663317917..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/getKin/BuyAndSellKinViewModel.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.getcode.view.main.getKin - -import android.net.Uri -import androidx.compose.runtime.Immutable -import com.getcode.R -import com.getcode.view.BaseViewModel2 -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList -import javax.inject.Inject - - -data class BuyAndSellKinItem( - val imageResId: Int, - val buttonTextResId: Int, - val link: Uri -) - -@HiltViewModel -class BuyAndSellKinViewModel @Inject constructor() : BaseViewModel2( - initialState = State.Initial, - updateStateForEvent = updateStateForEvent, -) { - - @Immutable - data class State( - val items: ImmutableList - ) { - companion object { - val Initial = State( - items = listOf( - BuyAndSellKinItem( - imageResId = R.drawable.video_buy_kin_2x, - buttonTextResId = R.string.action_learnBuyKin, - link = Uri.parse("https://www.youtube.com/watch?v=s2aqkF3dJcI") - ), - BuyAndSellKinItem( - imageResId = R.drawable.video_sell_kin_2x, - buttonTextResId = R.string.action_learnSellKin, - link = Uri.parse("https://www.youtube.com/watch?v=cyb9Da_mV9I") - ) - ).toImmutableList() - ) - } - } - - sealed interface Event { - data class ShareVideo(val link: Uri): Event - data class OpenVideo(val link: Uri): Event - } - - companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - is Event.OpenVideo, - is Event.ShareVideo -> { state -> state } - } - } - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/getKin/BuyKinScreen.kt b/apps/codeApp/src/main/java/com/getcode/view/main/getKin/BuyKinScreen.kt deleted file mode 100644 index a1c6aca26..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/getKin/BuyKinScreen.kt +++ /dev/null @@ -1,142 +0,0 @@ -package com.getcode.view.main.getKin - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel -import com.getcode.LocalBetaFlags -import com.getcode.R -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.KadoWebScreen -import com.getcode.theme.CodeTheme -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.theme.CodeKeyPad -import com.getcode.ui.components.Row -import com.getcode.util.showNetworkError -import com.getcode.utils.ErrorUtils -import com.getcode.utils.network.LocalNetworkObserver -import com.getcode.ui.components.text.AmountArea -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlin.time.Duration.Companion.seconds - -@Composable -fun BuyKinScreen( - viewModel: BuyKinViewModel = hiltViewModel(), - onRedirected: () -> Unit, -) { - val composeScope = rememberCoroutineScope() - val context = LocalContext.current - val navigator = LocalCodeNavigator.current - val dataState by viewModel.state.collectAsState() - val networkObserver = LocalNetworkObserver.current - val networkState by networkObserver.state.collectAsState() - val betaFlags = LocalBetaFlags.current - val uriHandler = LocalUriHandler.current - - Column( - modifier = Modifier - .fillMaxSize() - .padding(bottom = CodeTheme.dimens.grid.x4), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Box( - modifier = Modifier.weight(0.65f) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2) - ) { - AmountArea( - modifier = Modifier - .padding(horizontal = CodeTheme.dimens.inset), - amountPrefix = dataState.amountModel.amountPrefix, - amountSuffix = dataState.amountModel.amountSuffix, - amountText = dataState.amountModel.amountText, - currencyResId = dataState.currencyModel.selectedCurrency?.resId, - isAltCaptionKinIcon = false, - uiModel = dataState.amountAnimatedModel, - isAnimated = true, - isClickable = false, - networkState = networkState, - textStyle = CodeTheme.typography.displayLarge, - ) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy( - CodeTheme.dimens.grid.x1, - Alignment.CenterHorizontally - ), - ) { - Text( - text = stringResource(R.string.subtitle_poweredBy), - style = CodeTheme.typography.textMedium, - color = CodeTheme.colors.textSecondary, - ) - Image( - painter = painterResource(id = R.drawable.ic_kado), - contentDescription = "Kado" - ) - } - } - } - - CodeKeyPad( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = CodeTheme.dimens.inset) - .weight(1f), - onNumber = viewModel::onNumber, - onClear = viewModel::onBackspace, - onDecimal = viewModel::onDot, - isDecimal = false, // no decimal allowed for buys - ) - - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset), - onClick = { - if (!networkObserver.isConnected) { - ErrorUtils.showNetworkError(context) - return@CodeButton - } - - composeScope.launch { - viewModel.initiatePurchase()?.let { - if (betaFlags.kadoWebViewEnabled) { - navigator.push(KadoWebScreen(it)) - } else { - uriHandler.openUri(it) - delay(1.seconds) - onRedirected() - } - } - } - }, - enabled = dataState.continueEnabled, - text = stringResource(R.string.action_next), - buttonState = ButtonState.Filled, - ) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/getKin/BuyKinViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/getKin/BuyKinViewModel.kt deleted file mode 100644 index c14cf5cf7..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/getKin/BuyKinViewModel.kt +++ /dev/null @@ -1,300 +0,0 @@ -package com.getcode.view.main.getKin - -import android.net.Uri -import androidx.lifecycle.viewModelScope -import com.getcode.BuildConfig -import com.getcode.R -import com.getcode.manager.SessionManager -import com.getcode.manager.TopBarManager -import com.getcode.model.BuyLimit -import com.getcode.model.Currency -import com.getcode.model.CurrencyCode -import com.getcode.model.Fiat -import com.getcode.model.KinAmount -import com.getcode.model.Rate -import com.getcode.model.fromFiatAmount -import com.getcode.network.client.Client -import com.getcode.network.client.declareFiatPurchase -import com.getcode.network.client.linkAdditionalAccount -import com.getcode.network.exchange.Exchange -import com.getcode.network.repository.BalanceRepository -import com.getcode.network.repository.PhoneRepository -import com.getcode.network.repository.PrefRepository -import com.getcode.network.repository.TransactionRepository -import com.getcode.services.utils.makeE164 -import com.getcode.solana.organizer.AccountType -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.FormatUtils -import com.getcode.utils.blockchainMemo -import com.getcode.ui.components.text.AmountAnimatedInputUiModel -import com.getcode.view.main.giveKin.AmountUiModel -import com.getcode.view.main.giveKin.BaseAmountCurrencyViewModel -import com.getcode.view.main.giveKin.CurrencyUiModel -import com.getcode.view.main.giveKin.FlowType -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import timber.log.Timber -import java.util.UUID -import javax.inject.Inject - -@HiltViewModel -class BuyKinViewModel @Inject constructor( - client: Client, - exchange: Exchange, - prefsRepository: PrefRepository, - balanceRepository: BalanceRepository, - transactionRepository: TransactionRepository, - localeHelper: com.getcode.util.locale.LocaleHelper, - private val currencyUtils: com.getcode.utils.CurrencyUtils, - private val networkObserver: com.getcode.utils.network.NetworkConnectivityListener, - resources: ResourceHelper, - private val phoneRepository: PhoneRepository, -) : BaseAmountCurrencyViewModel( - client, - prefsRepository, - exchange, - balanceRepository, - transactionRepository, - localeHelper, - currencyUtils, - resources, - networkObserver -) { - data class State( - val currencyModel: CurrencyUiModel = CurrencyUiModel(), - val amountAnimatedModel: AmountAnimatedInputUiModel = AmountAnimatedInputUiModel(), - val amountModel: AmountUiModel = AmountUiModel(), - val continueEnabled: Boolean = false, - val relationshipEstablished: Boolean = false, - ) - - val state = MutableStateFlow(State()) - - override fun canChangeCurrency(): Boolean { - return false - } - - override val flowType: FlowType = FlowType.Buy - - val supportedCurrencies = listOf( - CurrencyCode.USD, CurrencyCode.EUR, CurrencyCode.CAD, CurrencyCode.GBP, CurrencyCode.MXN, - CurrencyCode.COP, CurrencyCode.INR, CurrencyCode.CHF, CurrencyCode.AUD, CurrencyCode.ARS, - CurrencyCode.BRL, CurrencyCode.CLP, CurrencyCode.JPY, CurrencyCode.KRW, CurrencyCode.PEN, - CurrencyCode.PHP, CurrencyCode.SGD, CurrencyCode.TRY, CurrencyCode.UYU, CurrencyCode.TWD, - CurrencyCode.VND, CurrencyCode.CRC, CurrencyCode.SEK, CurrencyCode.PLN, CurrencyCode.DKK, - CurrencyCode.NOK, CurrencyCode.NZD - ) - - override fun observeRate(): Flow { - return super.observeRate() - .map { - // if device is US-based, force currency to USD - val deviceCurrency = localeHelper.getDefaultCurrencyName() - if (deviceCurrency == CurrencyCode.USD.name) { - return@map exchange.rateForUsd()!! - } - - // if selected currency is supported, allow it - if (supportedCurrencies.contains(it.currency)) { - return@map it - } - - // otherwise use USD - return@map exchange.rateForUsd()!! - } - } - - init { - init() - - viewModelScope.launch { - establishSwapRelationship() - } - } - - private fun checkLocalRate(rate: Rate): Currency { - val deviceCurrencyName = localeHelper.getDefaultCurrencyName() - val deviceCurrency = currencyUtils.getCurrency(deviceCurrencyName) - if (deviceCurrency?.code == CurrencyCode.USD.name) { - return deviceCurrency - } - - if (!supportedCurrencies.contains(rate.currency)) { - // default to USD - val currency = currencyUtils.getCurrency(CurrencyCode.USD.name)!! - return currency - } - - val currency = currencyUtils.getCurrency(rate.currency.name)!! - return currency - } - - - override fun setCurrencyUiModel(currencyUiModel: CurrencyUiModel) { - state.update { - it.copy(currencyModel = currencyUiModel) - } - } - - override fun setAmountUiModel(amountUiModel: AmountUiModel) { - state.update { - it.copy(amountModel = amountUiModel) - } - } - - override fun setAmountAnimatedInputUiModel(amountAnimatedInputUiModel: AmountAnimatedInputUiModel) { - state.update { - it.copy(amountAnimatedModel = amountAnimatedInputUiModel) - } - } - - override fun getCurrencyUiModel(): CurrencyUiModel { - // force currency to be local to device - return state.value.currencyModel.copy() - } - - override fun getAmountUiModel(): AmountUiModel { - return state.value.amountModel.copy() - } - - override fun getAmountAnimatedInputUiModel(): AmountAnimatedInputUiModel { - return state.value.amountAnimatedModel.copy() - } - - override fun reset() { - numberInputHelper.reset() - onAmountChanged(true) - state.update { - it.copy(continueEnabled = false) - } - } - - override fun onAmountChanged(lastPressedBackspace: Boolean) { - super.onAmountChanged(lastPressedBackspace) - state.update { - it.copy( - continueEnabled = numberInputHelper.amount > 0.0 && - BuildConfig.KADO_API_KEY.isNotEmpty() && - networkObserver.isConnected - ) - } - } - - private suspend fun establishSwapRelationship() { - val organizer = SessionManager.getOrganizer() ?: return - if (organizer.info(AccountType.Swap) != null) { - Timber.d("USDC deposit account established already.") - state.update { - it.copy(relationshipEstablished = true) - } - return - } - - client.linkAdditionalAccount( - owner = organizer.ownerKeyPair, - linkedAccount = organizer.swapKeyPair - ).onFailure { - if (networkObserver.isConnected) { - TopBarManager.showMessage( - resources.getString(R.string.error_title_account_error), - resources.getString(R.string.error_description_usdc_deposit_failure) - ) - } - }.onSuccess { - state.update { it.copy(relationshipEstablished = true) } - } - } - - private suspend fun buildKadoUrl(amount: KinAmount, rate: Rate, nonce: UUID): Uri? { - val apiKey = BuildConfig.KADO_API_KEY - if (apiKey.isEmpty()) { - return null - } - - return Uri.Builder() - .scheme("https") - .authority("app.kado.money") - .appendQueryParameter("apiKey", apiKey) - .appendQueryParameter("onPayAmount", amount.fiat.toString()) - .appendQueryParameter("onPayCurrency", rate.currency.name.uppercase()) - .appendQueryParameter("onRevCurrency", "USDC") - .appendQueryParameter("mode", "minimal") - .appendQueryParameter("network", "SOLANA") - .appendQueryParameter("fiatMethodList", "debit_only") - .appendQueryParameter("phone", phoneRepository.phoneNumber.makeE164()) - .appendQueryParameter("onToAddress", SessionManager.getOrganizer()?.swapDepositAddress) - .appendQueryParameter("memo", nonce.blockchainMemo) - .appendQueryParameter("isMobileWebview", true.toString()) - .build() - } - - private val checkMinimumMet: (amount: KinAmount, rate: Rate) -> Boolean = { amount, rate -> - val threshold = transactionRepository.buyLimitFor(rate.currency) ?: BuyLimit.Zero - val isUnderMinimum = amount.fiat < threshold.min - if (isUnderMinimum) { - val formatted = FormatUtils.formatCurrency(threshold.min, rate.currency) - - TopBarManager.showMessage( - resources.getString(R.string.error_title_purchaseTooSmall), - resources.getString(R.string.error_description_purchaseTooSmall, formatted) - ) - } - !isUnderMinimum - } - - private val checkUnderMax: (amount: KinAmount, rate: Rate) -> Boolean = { amount, rate -> - val threshold = transactionRepository.buyLimitFor(rate.currency) ?: BuyLimit.Zero - val isOverLimit = amount.fiat > threshold.max - if (isOverLimit) { - val formatted = FormatUtils.formatCurrency(threshold.max, rate.currency) - TopBarManager.showMessage( - resources.getString(R.string.error_title_purchaseTooLarge), - resources.getString(R.string.error_description_purchaseTooLarge, formatted) - ) - } - - !isOverLimit - } - - suspend fun initiatePurchase(): String? { - val currencyModel = getCurrencyUiModel() - val amountAnimatedModel = getAmountAnimatedInputUiModel() - val currencySymbol = CurrencyCode - .tryValueOf(currencyModel.selectedCurrency?.code) ?: CurrencyCode.USD - - val rate = exchange.rateFor(currencySymbol) ?: exchange.rateForUsd()!! - - val kadoAmount = Fiat.fromString( - currencySymbol, - amountAnimatedModel.amountData.amount - )?.let { KinAmount.fromFiatAmount(fiat = it, rate = rate) } ?: return null - - if (!checkMinimumMet(kadoAmount, rate)) { - return null - } - - if (!checkUnderMax(kadoAmount, rate)) { - return null - } - - val nonce = UUID.randomUUID() - val kadoUrl = buildKadoUrl(kadoAmount, rate, nonce) - val organizer = SessionManager.getOrganizer() ?: return null - - client.declareFiatPurchase( - owner = organizer.ownerKeyPair, - amount = kadoAmount, - nonce = nonce - ) - - return withContext(Dispatchers.Main) { - kadoUrl?.toString() - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/getKin/GetKinSheet.kt b/apps/codeApp/src/main/java/com/getcode/view/main/getKin/GetKinSheet.kt deleted file mode 100644 index d3df59bfe..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/getKin/GetKinSheet.kt +++ /dev/null @@ -1,290 +0,0 @@ -package com.getcode.view.main.getKin - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredHeight -import androidx.compose.foundation.layout.size -import androidx.compose.material.Snackbar -import androidx.compose.material.SnackbarHost -import androidx.compose.material.SnackbarHostState -import androidx.compose.material.SnackbarResult -import androidx.compose.material.Text -import androidx.compose.material.rememberScaffoldState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.navigator.currentOrThrow -import com.getcode.LocalSession -import com.getcode.R -import com.getcode.manager.TopBarManager -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.BuyMoreKinModal -import com.getcode.navigation.screens.BuySellScreen -import com.getcode.navigation.screens.ConnectAccount -import com.getcode.navigation.screens.RequestKinModal -import com.getcode.theme.BrandMuted -import com.getcode.theme.CodeTheme -import com.getcode.theme.Success -import com.getcode.theme.White -import com.getcode.theme.White05 -import com.getcode.theme.bolded -import com.getcode.ui.theme.CodeCircularProgressIndicator -import com.getcode.ui.theme.CodeScaffold -import com.getcode.ui.components.snack.showSnackbar -import com.getcode.ui.core.addIf -import com.getcode.ui.core.rememberedClickable -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach - -data class GetKinItem( - val imageResId: Int, - val inactiveImageResId: Int = imageResId, - val titleText: String, - val subtitleText: String? = null, - val isVisible: Boolean = true, - val isActive: Boolean = true, - val isLoading: Boolean = false, - val isStrikeThrough: Boolean = false, - val onClick: () -> Unit, -) - -@Composable -fun GetKinSheet( - viewModel: GetKinSheetViewModel, -) { - val navigator = LocalCodeNavigator.current - val session = LocalSession.currentOrThrow - - val dataState by viewModel.stateFlow.collectAsState() - - var sheetAnimatedIn by rememberSaveable(viewModel) { - mutableStateOf(false) - } - - val context = LocalContext.current - val items = listOf( - GetKinItem( - imageResId = R.drawable.ic_currency_dollar_active, - titleText = stringResource(R.string.subtitle_buyKin), - onClick = { - if (dataState.buyModule.enabled) { - if (dataState.buyModule.available) { - navigator.push(BuyMoreKinModal()) - } else { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = context.getString(R.string.error_title_buyModuleUnavailable), - message = context.getString(R.string.error_description_buyModuleUnavailable), - type = TopBarManager.TopBarMessageType.ERROR - ) - ) - } - } else { - navigator.push(BuySellScreen) - } - }, - ), - GetKinItem( - imageResId = R.drawable.ic_menu_tip_card, - titleText = stringResource(R.string.title_requestTip), - isVisible = dataState.tips.enabled, - onClick = { - if (dataState.isTipCardConnected) { - session.presentShareableTipCard() - navigator.hide() - } else { - navigator.push(ConnectAccount()) - } - }, - ), - GetKinItem( - imageResId = R.drawable.ic_menu_buy_kin, - titleText = stringResource(R.string.title_requestKin), - isVisible = dataState.requestKin.enabled, - onClick = { - navigator.push(RequestKinModal()) - }, - ), - ) - - LaunchedEffect(viewModel) { - snapshotFlow { navigator.sheetFullyVisible } - .onEach { - sheetAnimatedIn = true - }.launchIn(this) - } - - val snackbarHostState = remember { SnackbarHostState() } - val scaffoldState = rememberScaffoldState(snackbarHostState = snackbarHostState) - - CodeScaffold( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(), - scaffoldState = scaffoldState, - snackbarHost = { - SnackbarHost(it) { data -> - Snackbar( - snackbarData = data, - shape = CodeTheme.shapes.small, - backgroundColor = BrandMuted, - contentColor = CodeTheme.colors.onBackground, - actionColor = Success - ) - } - } - ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = CodeTheme.dimens.inset) - .then(Modifier.padding(padding)), - verticalArrangement = Arrangement.SpaceBetween - ) { - Header() - Items(items) - Spacer(modifier = Modifier.requiredHeight(CodeTheme.dimens.grid.x12)) - } - } - - - LaunchedEffect(sheetAnimatedIn, dataState.snackbarData) { - dataState.snackbarData?.let { - delay(400) - val result = snackbarHostState.showSnackbar(it) - if (result == SnackbarResult.ActionPerformed) { - session.presentShareableTipCard() - navigator.hide() - } - viewModel.dispatchEvent(GetKinSheetViewModel.Event.ClearSnackbar) - } - } -} - -@Composable -private fun Header() { - Column(modifier = Modifier.fillMaxWidth()) { - Image( - painter = painterResource(R.drawable.ic_graphic_wallet), - contentDescription = "", - modifier = Modifier.padding(vertical = CodeTheme.dimens.grid.x2), - ) - Text( - text = stringResource(R.string.title_getCash), - style = CodeTheme.typography.displayMedium.bolded(), - modifier = Modifier.padding(vertical = CodeTheme.dimens.grid.x3), - ) - Text( - text = stringResource(R.string.subtitle_getKin), - style = CodeTheme.typography.textMedium, - modifier = Modifier.padding(vertical = CodeTheme.dimens.grid.x2), - ) - } -} - -@Composable -private fun Items(items: List) { - Column(modifier = Modifier.fillMaxWidth()) { - Column { - items - .filter { it.isVisible } - .onEach { item -> - Spacer( - modifier = Modifier - .fillMaxWidth() - .height(1.dp) - .background(White05), - ) - GetKinItemRow(item = item) - } - } - - Spacer( - modifier = Modifier - .fillMaxWidth() - .height(1.dp) - .background(White05), - ) - } -} - -@Composable -private fun GetKinItemRow(modifier: Modifier = Modifier, item: GetKinItem) { - Row( - modifier = modifier - .fillMaxWidth() - .addIf( - item.isStrikeThrough.not(), - ) { - Modifier.rememberedClickable { item.onClick() } - } - .padding( - vertical = CodeTheme.dimens.grid.x4, - horizontal = CodeTheme.dimens.grid.x2 - ), - verticalAlignment = Alignment.CenterVertically, - ) { - Image( - modifier = Modifier.size(CodeTheme.dimens.staticGrid.x5), - painter = if (item.isActive) painterResource(id = item.imageResId) else painterResource( - id = item.inactiveImageResId - ), - contentDescription = "", - ) - Column( - modifier = Modifier - .padding(start = CodeTheme.dimens.grid.x3) - .weight(1f), - ) { - Text( - text = item.titleText, - color = if (item.isActive) Color.White else CodeTheme.colors.brandLight, - style = CodeTheme.typography.textSmall.copy( - textDecoration = if (item.isStrikeThrough) TextDecoration.LineThrough else null, - ), - ) - item.subtitleText?.let { - Text( - modifier = Modifier.padding(top = CodeTheme.dimens.grid.x1), - text = it, - style = CodeTheme.typography.caption, - color = CodeTheme.colors.textSecondary - ) - } - } - - if (item.isLoading) { - CodeCircularProgressIndicator( - strokeWidth = CodeTheme.dimens.thickBorder, - color = White, - modifier = Modifier - .size(CodeTheme.dimens.grid.x3) - .align(Alignment.CenterVertically), - ) - } - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/getKin/GetKinSheetViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/getKin/GetKinSheetViewModel.kt deleted file mode 100644 index 6f590905c..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/getKin/GetKinSheetViewModel.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.getcode.view.main.getKin - -import androidx.lifecycle.viewModelScope -import com.getcode.model.BuyModuleFeature -import com.getcode.model.Feature -import com.getcode.model.RequestKinFeature -import com.getcode.model.TipCardFeature -import com.getcode.network.TipController -import com.getcode.network.repository.FeatureRepository -import com.getcode.ui.components.snack.SnackData -import com.getcode.view.BaseViewModel2 -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import javax.inject.Inject - -@HiltViewModel -class GetKinSheetViewModel @Inject constructor( - features: FeatureRepository, - tipController: TipController, -) : BaseViewModel2( - initialState = State(), - updateStateForEvent = updateStateForEvent -) { - - data class State( - val buyModule: Feature = BuyModuleFeature(), - val tips: Feature = TipCardFeature(), - val requestKin: Feature = RequestKinFeature(), - val isTipCardConnected: Boolean = false, - val snackbarData: SnackData? = null, - ) - - sealed interface Event { - data class OnBuyModuleStateChanged(val module: Feature) : Event - data class OnTipsStateChanged(val module: Feature) : Event - data class OnRequestKinStateChanged(val module: Feature) : Event - data class OnConnectionStateChanged( - val connected: Boolean, - ) : Event - - data class ShowSnackbar(val data: SnackData?) : Event - data object ClearSnackbar : Event - } - - init { - features.buyModule - .onEach { dispatchEvent(Event.OnBuyModuleStateChanged(it)) } - .launchIn(viewModelScope) - - features.tipCards - .onEach { dispatchEvent(Event.OnTipsStateChanged(it)) } - .launchIn(viewModelScope) - - features.requestKin - .onEach { dispatchEvent(Event.OnRequestKinStateChanged(it)) } - .launchIn(viewModelScope) - - tipController.connectedAccount - .onEach { connectedAccount -> - dispatchEvent( - Event.OnConnectionStateChanged( - connected = connectedAccount != null, - ) - ) - }.launchIn(viewModelScope) - } - - companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - is Event.OnBuyModuleStateChanged -> { state -> - state.copy( - buyModule = event.module - ) - } - is Event.OnTipsStateChanged -> { state -> - state.copy( - tips = event.module - ) - } - is Event.OnRequestKinStateChanged -> { state -> - state.copy( - requestKin = event.module - ) - } - - is Event.OnConnectionStateChanged -> { state -> - state.copy( - isTipCardConnected = event.connected, - ) - } - - is Event.ShowSnackbar -> { state -> - state.copy(snackbarData = event.data) - } - - Event.ClearSnackbar -> { state -> state.copy(snackbarData = null) } - } - } - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/getKin/KadoWebScreen.kt b/apps/codeApp/src/main/java/com/getcode/view/main/getKin/KadoWebScreen.kt deleted file mode 100644 index 7bbfbe684..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/getKin/KadoWebScreen.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.getcode.view.main.getKin - -import android.graphics.Bitmap -import android.net.Uri -import android.webkit.WebView -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.imePadding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import com.getcode.manager.TopBarManager -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.KadoWebScreen.BuyKinWebInterface -import com.getcode.ui.theme.CodeCircularProgressIndicator -import com.getcode.ui.utils.toAGColor -import com.kevinnzou.web.AccompanistWebViewClient -import com.kevinnzou.web.LoadingState -import com.kevinnzou.web.WebView -import com.kevinnzou.web.WebViewNavigator -import com.kevinnzou.web.WebViewState - -@Composable -fun BoxScope.KadoWebScreen( - viewModel: KadoWebViewModel, - state: WebViewState, - webNavigator: WebViewNavigator, -) { - val navigator = LocalCodeNavigator.current - - val loadingState = state.loadingState - if (loadingState is LoadingState.Loading) { - CodeCircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) - } - - var orderId by remember { mutableStateOf("") } - - val client = remember { - object : AccompanistWebViewClient() { - override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) { - super.onPageStarted(view, url, favicon) - if (url?.startsWith("https://app.kado.money/ramp/order/") == true) { - // order created, extract order id - orderId = Uri.parse(url).lastPathSegment.orEmpty() - } - } - } - } - - LaunchedEffect(orderId) { - if (orderId.isNotEmpty()) { - // order created, start to check status - viewModel.checkOrderStatus(orderId) - .onSuccess { - TopBarManager.showMessage( - "Success! Funds Available Soon", - "Your funds should be available in your Code Wallet in 5 to 10 minutes.", - type = TopBarManager.TopBarMessageType.SUCCESS - ) - navigator.hide() - }.onFailure { - TopBarManager.showMessage( - "Something went wrong", - "Your payment method was not charged. Please try again later." - ) - navigator.hide() - } - } - } - WebView( - modifier = Modifier - .fillMaxSize() - .imePadding(), - captureBackPresses = false, - navigator = webNavigator, - state = state, - client = client, - onCreated = { nativeWebView -> - nativeWebView.addJavascriptInterface(BuyKinWebInterface(), "Android") - nativeWebView.clipToOutline = true - nativeWebView.setBackgroundColor(Color.Transparent.toAGColor()) - nativeWebView.settings.apply { - javaScriptEnabled = true - domStorageEnabled = true - } - } - ) -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/getKin/KadoWebViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/getKin/KadoWebViewModel.kt deleted file mode 100644 index d879e2e15..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/getKin/KadoWebViewModel.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.getcode.view.main.getKin - -import com.getcode.api.KadoApi -import com.getcode.util.resources.ResourceHelper -import com.getcode.view.BaseViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import retrofit2.HttpException -import retrofit2.Response -import java.io.IOException -import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds - -@HiltViewModel -class KadoWebViewModel @Inject constructor( - resources: ResourceHelper, - private val kadoApi: KadoApi, -): BaseViewModel(resources) { - - suspend fun checkOrderStatus(orderId: String): Result { - while (true) { - println("checking order status for $orderId") - val result = kadoApi.getOrderStatus(orderId).toResult() - - result.map { - val ret = parsePaymentStatus(it.string()) - if (ret != null) { - return ret - } - delay(2.seconds) - } - } - } - - private fun parsePaymentStatus(jsonString: String): Result? { - val json = Json.parseToJsonElement(jsonString).jsonObject["data"]?.jsonObject ?: return null - val paymentStatus = json["paymentStatus"]?.jsonPrimitive?.content - - return when (paymentStatus) { - "success" -> Result.success(Unit) - "failed" -> Result.failure(Throwable("Payment failed")) - else -> null - } - } -} - -fun Response.toResult(): Result { - return try { - if (isSuccessful) { - val body = body() - if (body != null) { - Result.success(body) - } else { - Result.failure(NullPointerException("Response body is null")) - } - } else { - Result.failure(HttpException(this)) - } - } catch (e: IOException) { - Result.failure(e) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/giveKin/BaseAmountCurrencyViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/giveKin/BaseAmountCurrencyViewModel.kt deleted file mode 100644 index c9ad01ccf..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/giveKin/BaseAmountCurrencyViewModel.kt +++ /dev/null @@ -1,379 +0,0 @@ -package com.getcode.view.main.giveKin - -import androidx.lifecycle.viewModelScope -import com.getcode.R -import com.getcode.model.Currency -import com.getcode.model.CurrencyCode -import com.getcode.model.Kin -import com.getcode.model.Rate -import com.getcode.network.client.Client -import com.getcode.network.exchange.Exchange -import com.getcode.network.repository.BalanceRepository -import com.getcode.network.repository.PrefRepository -import com.getcode.network.repository.TransactionRepository -import com.getcode.ui.components.text.AmountAnimatedInputUiModel -import com.getcode.ui.components.text.AmountInputViewModel -import com.getcode.utils.Kin -import com.getcode.ui.components.text.NumberInputHelper -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.FormatUtils -import com.getcode.utils.replaceParam -import com.getcode.view.BaseViewModel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlin.math.min - -sealed class CurrencyListItem { - data class TitleItem(val text: String) : CurrencyListItem() - data class RegionCurrencyItem(val currency: Currency, val isRecent: Boolean) : - CurrencyListItem() -} - -enum class FundsDirection { - Incoming, - Outgoing, -} - -sealed interface FlowType { - val direction: FundsDirection - data object Give: FlowType { - override val direction: FundsDirection = FundsDirection.Outgoing - } - - data object Tip: FlowType { - override val direction: FundsDirection = FundsDirection.Outgoing - } - - data object Withdrawal: FlowType { - override val direction: FundsDirection = FundsDirection.Outgoing - } - - data object Request: FlowType { - override val direction: FundsDirection = FundsDirection.Incoming - } - - data object Buy: FlowType { - override val direction: FundsDirection = FundsDirection.Incoming - } -} - -data class CurrencyUiModel( - val selectedCurrency: Currency? = null, -) - -data class AmountUiModel( - val balanceKin: Double = 0.0, - val amountText: String = "", - val amountDouble: Double = 0.0, - val amountKin: Kin = Kin(0), - val amountPrefix: String = "", - val amountSuffix: String = "", - val captionText: String = "", - val isCaptionConversion: Boolean = false, - val isInsufficient: Boolean = false, - val sendLimit: Double = 0.0, - val sendLimitKin: Kin = Kin(0), - val buyLimit: Double = 0.0, - val buyLimitKin: Kin = Kin(0), - val isDecimalAllowed: Boolean = false, -) - -abstract class BaseAmountCurrencyViewModel( - val client: Client, - private val prefsRepository: PrefRepository, - protected val exchange: Exchange, - private val balanceRepository: BalanceRepository, - private val transactionRepository: TransactionRepository, - protected val localeHelper: com.getcode.util.locale.LocaleHelper, - private val currencyUtils: com.getcode.utils.CurrencyUtils, - protected val resources: ResourceHelper, - private val networkObserver: com.getcode.utils.network.NetworkConnectivityListener, -) : BaseViewModel(resources), AmountInputViewModel { - protected val numberInputHelper = NumberInputHelper() - abstract fun setCurrencyUiModel(currencyUiModel: CurrencyUiModel) - abstract fun setAmountUiModel(amountUiModel: AmountUiModel) - abstract fun setAmountAnimatedInputUiModel(amountAnimatedInputUiModel: AmountAnimatedInputUiModel) - abstract fun getCurrencyUiModel(): CurrencyUiModel - abstract fun getAmountUiModel(): AmountUiModel - abstract fun getAmountAnimatedInputUiModel(): AmountAnimatedInputUiModel - - open fun canChangeCurrency(): Boolean = true - - open fun observeRate(): Flow { - return exchange.observeEntryRate() - } - - open fun init() { - numberInputHelper.reset() - - combine( - observeRate(), - networkObserver.state, - balanceRepository.balanceFlow - ) { rate, _, balance -> - val currency = currencyUtils.getCurrencyWithRate(rate.currency.name, rate.fx) - if (canChangeCurrency()) { - if (currency?.code != getCurrencyUiModel().selectedCurrency?.code) { - reset() - } - } - getModelsWithSelectedCurrency( - getCurrencyUiModel(), - getAmountUiModel().copy(balanceKin = balance.coerceAtLeast(0.0)), - currency, - numberInputHelper.amount, - numberInputHelper.getFormattedString() - ) - }.filterNotNull().onEach { (currencyModel, amountModel) -> - setCurrencyUiModel(currencyModel) - setAmountUiModel(amountModel) - }.launchIn(viewModelScope) - } - - internal abstract fun reset() - - internal abstract val flowType: FlowType - - protected open fun onAmountChanged( - lastPressedBackspace: Boolean = false - ) { - val currencyUiModel = getCurrencyUiModel() - val amountAnimatedInputUiModel = getAmountAnimatedInputUiModel() - - val selectedCurrency = currencyUiModel.selectedCurrency ?: return - val amount = numberInputHelper.amount - val amountText = numberInputHelper.getFormattedString() - - val amountUiModel = - getAmountUiFormattedModel(getAmountUiModel(), selectedCurrency, amount, amountText) - - setAmountUiModel(amountUiModel) - setAmountAnimatedInputUiModel( - amountAnimatedInputUiModel.copy( - amountDataLast = amountAnimatedInputUiModel.amountData, - amountData = numberInputHelper.getFormattedStringForAnimation(), - lastPressedBackspace = lastPressedBackspace, - ) - ) - } - - override fun onNumber(number: Int) { - numberInputHelper.maxLength = 9 - numberInputHelper.onNumber(number) - onAmountChanged() - } - - override fun onDot() { - numberInputHelper.onDot() - onAmountChanged() - } - - override fun onBackspace() { - numberInputHelper.onBackspace() - onAmountChanged(true) - } - - private fun getModelsWithSelectedCurrency( - currencyModel: CurrencyUiModel, - amountUiModel: AmountUiModel, - selectedCurrency: Currency?, - amount: Double, - formattedString: String - ): Pair? { - if (selectedCurrency == null) return null - val currencyModelN = currencyModel.copy( - selectedCurrency = selectedCurrency, - ) - - numberInputHelper.fractionUnits = if (selectedCurrency != Currency.Kin) 2 else 0 - - val amountModelN = - getAmountUiFormattedModel(amountUiModel, selectedCurrency, amount, formattedString) - - return Pair(currencyModelN, amountModelN) - } - - private fun getAmountUiFormattedModel( - amountUiModel: AmountUiModel, - selectedCurrency: Currency, - amount: Double, - amountText: String - ): AmountUiModel { - val currentBalance = balanceRepository.balanceFlow.value.coerceAtLeast(0.0) - val amountKin = FormatUtils.getKinValue(amount, selectedCurrency.rate) - .inflating() - - val fiatValue = - FormatUtils.getFiatValue(currentBalance, selectedCurrency.rate) - - val currency = CurrencyCode.tryValueOf(selectedCurrency.code) - val sendLimit = currency?.let { - transactionRepository.sendLimitFor(it)?.nextTransaction - } ?: fiatValue - - val sendLimitKin = FormatUtils.getKinValue(sendLimit, selectedCurrency.rate) - .inflating() - - val buyLimit = currency?.let { - transactionRepository.buyLimitFor(it)?.max - } ?: 0.0 - val buyLimitKin = FormatUtils.getKinValue(buyLimit, selectedCurrency.rate) - .inflating() - - // allow full balance withdrawal - val amountAvailable = if (flowType is FlowType.Withdrawal) { - fiatValue - } else { - min(sendLimit, fiatValue) - } - - val isInsufficient = when (flowType.direction) { - FundsDirection.Incoming -> { - FormatUtils.getFiatValue(amount, selectedCurrency.rate) > buyLimit - } - FundsDirection.Outgoing -> { - amount > amountAvailable || - amountKin.toKinTruncatingLong() > currentBalance - } - } - - return amountUiModel.copy( - amountText = formatAmount(amountText, selectedCurrency), - amountDouble = amount, - amountKin = amountKin, - isDecimalAllowed = selectedCurrency != Currency.Kin, - amountPrefix = formatPrefix(selectedCurrency).takeIf { it != selectedCurrency.code }.orEmpty(), - amountSuffix = formatSuffix(selectedCurrency), - captionText = formatCaption( - selectedCurrency, - amount, - amountKin.toKinTruncatingLong().toDouble(), - amountAvailable, - buyLimit, - ), - isCaptionConversion = isCaptionConversion(selectedCurrency, amount), - isInsufficient = isInsufficient, - sendLimit = sendLimit, - buyLimit = buyLimit, - buyLimitKin = buyLimitKin, - sendLimitKin = sendLimitKin - ) - } - - protected fun formatPrefix(selectedCurrency: Currency?): String { - if (selectedCurrency == null) return "" - return if (!isKin(selectedCurrency)) selectedCurrency.symbol else "" - } - - private fun formatSuffix(selectedCurrency: Currency): String { - return if (!isKin(selectedCurrency)) " ${ - resources.getString(R.string.core_ofKin) - }" else "" - } - - private fun formatAmount(amountText: String, selectedCurrency: Currency): String { - val symbol = selectedCurrency.symbol - - return StringBuilder().apply { - val isKin = isKin(selectedCurrency) - if (!isKin) append(symbol) - append(amountText) - if (!isKin) { - append(" ") - append(resources.getString(R.string.core_ofKin)) - } - }.toString() - } - - private fun formatCaption( - currency: Currency, - amountInput: Double, //currency - amountInputKin: Double, //kin conversion - amountAvailable: Double, //currency - buyLimit: Double, // buy limit - ): String { - val isKin = isKin(currency) - - return when (flowType.direction) { - FundsDirection.Incoming -> { - val buyLimitFormatted = FormatUtils.formatCurrency(buyLimit, currency.code) - if (isKin) { - "${getString(R.string.subtitle_enterUpTo).replaceParam(buyLimitFormatted)} " + - resources.getString(R.string.core_kin) - } else if (amountInput == 0.0) { - val currencyValue = FormatUtils.format(buyLimit) - val kinValue = if (currency.code != currency.symbol) { - "${currency.symbol}$currencyValue" - } else { - currencyValue - } - - "${getString(R.string.subtitle_enterUpTo).replaceParam(kinValue)} " + - getString(R.string.core_ofKin) - } else { - if (amountInput > buyLimit) { - getString(R.string.subtitle_canOnlyRequestUpTo) - .replaceParam(buyLimitFormatted) - .plus(" ") - .plus(getString(R.string.core_ofKin)) - } else { - String.format("%,.0f", amountInputKin) - } - } - } - FundsDirection.Outgoing -> { - val kinAmountFormatted = - FormatUtils.formatWholeRoundDown(amountAvailable) - - if (isKin) { - "${getString(R.string.subtitle_enterUpTo).replaceParam(kinAmountFormatted)} " + - resources.getString(R.string.core_kin) - } else if (amountInput == 0.0) { - val currencyValue = FormatUtils.format(amountAvailable) - val kinValue = if (currency.code != currency.symbol) { - "${currency.symbol}$currencyValue" - } else { - currencyValue - } - - "${getString(R.string.subtitle_enterUpTo).replaceParam(kinValue)} " + - getString(R.string.core_ofKin) - } else { - if (amountInput > amountAvailable) { - if (flowType is FlowType.Tip) { - getString(R.string.subtitle_canOnlyTipUpTo) - .replaceParam( - FormatUtils.formatCurrency( - amountAvailable, - currency.code - ) - ) - .plus(" ") - .plus(getString(R.string.core_ofKin)) - } else { - getString(R.string.subtitle_canOnlyGiveUpTo) - .replaceParam( - FormatUtils.formatCurrency( - amountAvailable, - currency.code - ) - ) - .plus(" ") - .plus(getString(R.string.core_ofKin)) - } - } else { - String.format("%,.0f", amountInputKin) - } - } - } - } - } - - private fun isKin(selectedCurrency: Currency): Boolean = selectedCurrency.code == Currency.Kin.code - - private fun isCaptionConversion(selectedCurrency: Currency, amount: Double?): Boolean = - !isKin(selectedCurrency) && amount != 0.0 -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/giveKin/GiveKinScreen.kt b/apps/codeApp/src/main/java/com/getcode/view/main/giveKin/GiveKinScreen.kt deleted file mode 100644 index 291c4cf38..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/giveKin/GiveKinScreen.kt +++ /dev/null @@ -1,126 +0,0 @@ -package com.getcode.view.main.giveKin - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel -import cafe.adriel.voyager.navigator.currentOrThrow -import com.getcode.LocalSession -import com.getcode.R -import com.getcode.models.Bill -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.CurrencySelectionModal -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.text.AmountArea -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.theme.CodeKeyPad -import com.getcode.util.showNetworkError -import com.getcode.utils.ErrorUtils -import com.getcode.utils.network.LocalNetworkObserver -import kotlinx.coroutines.launch - -@Preview -@Composable -fun GiveKinScreen( - viewModel: GiveKinSheetViewModel = hiltViewModel(), -) { - val context = LocalContext.current - val navigator = LocalCodeNavigator.current - val dataState by viewModel.uiFlow.collectAsState() - val composeScope = rememberCoroutineScope() - - val networkObserver = LocalNetworkObserver.current - val networkState by networkObserver.state.collectAsState() - - val session = LocalSession.currentOrThrow - - Column( - modifier = Modifier.fillMaxSize() - .padding(bottom = CodeTheme.dimens.grid.x4), - horizontalAlignment = Alignment.CenterHorizontally - ) { - val isInError by remember(dataState.amountModel) { - derivedStateOf { - dataState.amountModel.amountKin > dataState.amountModel.sendLimitKin || - dataState.amountModel.balanceKin < dataState.amountModel.amountKin.toKinValueDouble() - } - } - - val color = - if (isInError) CodeTheme.colors.errorText else CodeTheme.colors.brandLight - Box( - modifier = Modifier.weight(0.65f) - ) { - AmountArea( - amountPrefix = dataState.amountModel.amountPrefix, - amountSuffix = dataState.amountModel.amountSuffix, - amountText = dataState.amountModel.amountText, - captionText = dataState.amountModel.captionText, - isAltCaption = dataState.amountModel.isCaptionConversion, - isAltCaptionKinIcon = !isInError, - altCaptionColor = color, - currencyResId = dataState.currencyModel.selectedCurrency?.resId, - uiModel = dataState.amountAnimatedModel, - isAnimated = true, - networkState = networkState, - textStyle = CodeTheme.typography.displayLarge, - modifier = Modifier - .padding(horizontal = CodeTheme.dimens.inset) - .align(Alignment.Center) - ) { - navigator.push(CurrencySelectionModal()) - } - } - - CodeKeyPad( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = CodeTheme.dimens.inset) - .weight(1f), - onNumber = viewModel::onNumber, - onClear = viewModel::onBackspace, - onDecimal = viewModel::onDot, - isDecimal = dataState.amountModel.isDecimalAllowed - ) - - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset), - onClick = { - if (!networkObserver.isConnected) { - ErrorUtils.showNetworkError(context) - return@CodeButton - } - - composeScope.launch { - val amount = viewModel.onSubmit() ?: return@launch - if (dataState.giveRequestsEnabled) { - session.presentRequest(amount = amount, payload = null, request = null) - } else { - session.showBill(Bill.Cash(amount)) - } - - navigator.hide() - } - }, - enabled = dataState.continueEnabled, - text = stringResource(R.string.action_next), - buttonState = ButtonState.Filled, - ) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/giveKin/GiveKinSheetViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/giveKin/GiveKinSheetViewModel.kt deleted file mode 100644 index 2f35b8e2c..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/giveKin/GiveKinSheetViewModel.kt +++ /dev/null @@ -1,155 +0,0 @@ -package com.getcode.view.main.giveKin - -import androidx.lifecycle.viewModelScope -import com.getcode.R -import com.getcode.manager.TopBarManager -import com.getcode.model.CurrencyCode -import com.getcode.model.KinAmount -import com.getcode.model.fromFiatAmount -import com.getcode.network.client.Client -import com.getcode.network.client.receiveIfNeeded -import com.getcode.network.exchange.Exchange -import com.getcode.network.repository.BalanceRepository -import com.getcode.network.repository.PrefRepository -import com.getcode.network.repository.TransactionRepository -import com.getcode.ui.components.text.AmountAnimatedInputUiModel -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.ErrorUtils -import com.getcode.utils.replaceParam -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import javax.inject.Inject - -data class GiveKinSheetUiModel( - val giveRequestsEnabled: Boolean = false, - val currencyModel: CurrencyUiModel = CurrencyUiModel(), - val amountAnimatedModel: AmountAnimatedInputUiModel = AmountAnimatedInputUiModel(), - val amountModel: AmountUiModel = AmountUiModel(), - val continueEnabled: Boolean = false, -) - -@HiltViewModel -class GiveKinSheetViewModel @Inject constructor( - client: Client, - exchange: Exchange, - prefsRepository: PrefRepository, - balanceRepository: BalanceRepository, - transactionRepository: TransactionRepository, - localeHelper: com.getcode.util.locale.LocaleHelper, - currencyUtils: com.getcode.utils.CurrencyUtils, - networkObserver: com.getcode.utils.network.NetworkConnectivityListener, - resources: ResourceHelper, -) : BaseAmountCurrencyViewModel( - client, - prefsRepository, - exchange, - balanceRepository, - transactionRepository, - localeHelper, - currencyUtils, - resources, - networkObserver -) { - - val uiFlow = MutableStateFlow(GiveKinSheetUiModel()) - - override val flowType: FlowType = FlowType.Give - - init { - init() - viewModelScope.launch(Dispatchers.IO) { - client.receiveIfNeeded().subscribe({}, ErrorUtils::handleError) - } - } - - override fun reset() { - numberInputHelper.reset() - onAmountChanged(true) - viewModelScope.launch { - uiFlow.update { - it.copy( - continueEnabled = false - ) - } - } - } - - suspend fun onSubmit(): KinAmount? { - val uiModel = uiFlow.value - val checkBalanceLimit: () -> Boolean = { - val isOverBalance = - uiModel.amountModel.amountKin.toKinValueDouble() > uiModel.amountModel.balanceKin - if (isOverBalance) { - TopBarManager.showMessage( - resources.getString(R.string.error_title_insuffiecientKin), - resources.getString(R.string.error_description_insuffiecientKin) - ) - } - isOverBalance - } - val checkSendLimit: () -> Boolean = { - val isOverLimit = uiModel.amountModel.amountDouble > uiModel.amountModel.sendLimit - if (isOverLimit) { - val currencySymbol = CurrencyCode - .tryValueOf(uiModel.currencyModel.selectedCurrency?.code) ?: CurrencyCode.USD - TopBarManager.showMessage( - resources.getString(R.string.error_title_giveLimitReached), - resources.getString(R.string.error_description_giveLimitReached) - .replaceParam( - "$currencySymbol${uiModel.amountModel.sendLimit.toInt()}" - ) - ) - } - isOverLimit - } - - if (checkBalanceLimit() || checkSendLimit()) return null - - val amountFiat = uiModel.amountModel.amountDouble - - exchange.fetchRatesIfNeeded() - val rate = exchange.entryRate - - return KinAmount.fromFiatAmount(amountFiat, rate) - } - - override fun onAmountChanged(lastPressedBackspace: Boolean) { - super.onAmountChanged(lastPressedBackspace) - uiFlow.update { - val minValue = - if (it.currencyModel.selectedCurrency?.code == CurrencyCode.KIN.name) 1.0 else 0.01 - it.copy( - continueEnabled = numberInputHelper.amount >= minValue && !it.amountModel.isInsufficient - ) - } - } - - override fun setCurrencyUiModel(currencyUiModel: CurrencyUiModel) { - uiFlow.update { it.copy(currencyModel = currencyUiModel) } - } - - override fun setAmountUiModel(amountUiModel: AmountUiModel) { - uiFlow.update { - it.copy(amountModel = amountUiModel) - } - } - - override fun setAmountAnimatedInputUiModel(amountAnimatedInputUiModel: AmountAnimatedInputUiModel) { - uiFlow.update { it.copy(amountAnimatedModel = amountAnimatedInputUiModel) } - } - - override fun getCurrencyUiModel(): CurrencyUiModel { - return uiFlow.value.currencyModel.copy() - } - - override fun getAmountUiModel(): AmountUiModel { - return uiFlow.value.amountModel.copy() - } - - override fun getAmountAnimatedInputUiModel(): AmountAnimatedInputUiModel { - return uiFlow.value.amountAnimatedModel.copy() - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/requestKin/RequestKinScreen.kt b/apps/codeApp/src/main/java/com/getcode/view/main/requestKin/RequestKinScreen.kt deleted file mode 100644 index a8a68d22b..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/requestKin/RequestKinScreen.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.getcode.view.main.requestKin - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel -import cafe.adriel.voyager.navigator.currentOrThrow -import com.getcode.LocalSession -import com.getcode.R -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.CurrencySelectionModal -import com.getcode.theme.CodeTheme -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.theme.CodeKeyPad -import com.getcode.util.showNetworkError -import com.getcode.utils.ErrorUtils -import com.getcode.utils.network.LocalNetworkObserver -import com.getcode.ui.components.text.AmountArea -import kotlinx.coroutines.launch - -@Preview -@Composable -fun RequestKinScreen( - viewModel: RequestKinViewModel = hiltViewModel(), -) { - val context = LocalContext.current - val navigator = LocalCodeNavigator.current - val dataState by viewModel.state.collectAsState() - val composeScope = rememberCoroutineScope() - - val networkObserver = LocalNetworkObserver.current - val networkState by networkObserver.state.collectAsState() - - val session = LocalSession.currentOrThrow - - Column( - modifier = Modifier.fillMaxSize() - .padding(bottom = CodeTheme.dimens.grid.x4), - horizontalAlignment = Alignment.CenterHorizontally - ) { - val color = - if (dataState.amountModel.amountKin > dataState.amountModel.buyLimitKin) CodeTheme.colors.errorText else CodeTheme.colors.brandLight - Box( - modifier = Modifier.weight(0.65f) - ) { - AmountArea( - amountPrefix = dataState.amountModel.amountPrefix, - amountSuffix = dataState.amountModel.amountSuffix, - amountText = dataState.amountModel.amountText, - captionText = dataState.amountModel.captionText, - isAltCaption = dataState.amountModel.isCaptionConversion, - altCaptionColor = color, - currencyResId = dataState.currencyModel.selectedCurrency?.resId, - uiModel = dataState.amountAnimatedModel, - isAnimated = true, - networkState = networkState, - textStyle = CodeTheme.typography.displayLarge, - modifier = Modifier - .padding(horizontal = CodeTheme.dimens.inset) - .align(Alignment.Center) - ) { - navigator.push(CurrencySelectionModal()) - } - } - - CodeKeyPad( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = CodeTheme.dimens.inset) - .weight(1f), - onNumber = viewModel::onNumber, - onClear = viewModel::onBackspace, - onDecimal = viewModel::onDot, - isDecimal = dataState.amountModel.isDecimalAllowed - ) - - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset), - onClick = { - if (!networkObserver.isConnected) { - ErrorUtils.showNetworkError(context) - return@CodeButton - } - - composeScope.launch { - val amount = viewModel.onSubmit() ?: return@launch - session.presentRequest(amount = amount, payload = null, request = null) - navigator.hide() - } - }, - enabled = dataState.continueEnabled, - text = stringResource(R.string.action_next), - buttonState = ButtonState.Filled, - ) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/requestKin/RequestKinViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/requestKin/RequestKinViewModel.kt deleted file mode 100644 index 9ddd984e6..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/requestKin/RequestKinViewModel.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.getcode.view.main.requestKin - -import androidx.lifecycle.viewModelScope -import com.getcode.model.CurrencyCode -import com.getcode.model.KinAmount -import com.getcode.model.fromFiatAmount -import com.getcode.network.client.Client -import com.getcode.network.client.receiveIfNeeded -import com.getcode.network.exchange.Exchange -import com.getcode.network.repository.BalanceRepository -import com.getcode.network.repository.PrefRepository -import com.getcode.network.repository.TransactionRepository -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.ErrorUtils -import com.getcode.ui.components.text.AmountAnimatedInputUiModel -import com.getcode.view.main.giveKin.AmountUiModel -import com.getcode.view.main.giveKin.BaseAmountCurrencyViewModel -import com.getcode.view.main.giveKin.CurrencyUiModel -import com.getcode.view.main.giveKin.FlowType -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class RequestKinViewModel @Inject constructor( - client: Client, - exchange: Exchange, - prefsRepository: PrefRepository, - balanceRepository: BalanceRepository, - transactionRepository: TransactionRepository, - localeHelper: com.getcode.util.locale.LocaleHelper, - currencyUtils: com.getcode.utils.CurrencyUtils, - networkObserver: com.getcode.utils.network.NetworkConnectivityListener, - resources: ResourceHelper, -) : BaseAmountCurrencyViewModel( - client, - prefsRepository, - exchange, - balanceRepository, - transactionRepository, - localeHelper, - currencyUtils, - resources, - networkObserver -) { - - data class State( - val currencyModel: CurrencyUiModel = CurrencyUiModel(), - val amountAnimatedModel: AmountAnimatedInputUiModel = AmountAnimatedInputUiModel(), - val amountModel: AmountUiModel = AmountUiModel(), - val continueEnabled: Boolean = false, - ) - - val state = MutableStateFlow(State()) - - override val flowType: FlowType = FlowType.Request - - init { - init() - viewModelScope.launch(Dispatchers.IO) { - client.receiveIfNeeded().subscribe({}, ErrorUtils::handleError) - } - } - - override fun reset() { - numberInputHelper.reset() - onAmountChanged(true) - viewModelScope.launch { - state.update { - it.copy( - continueEnabled = false - ) - } - } - } - - suspend fun onSubmit(): KinAmount? { - val uiModel = state.value - - val amountFiat = uiModel.amountModel.amountDouble - val amountKin = uiModel.amountModel.amountKin - - val currencyCode = - CurrencyCode.tryValueOf(uiModel.currencyModel.selectedCurrency?.code.orEmpty()) - ?: return null - - exchange.fetchRatesIfNeeded() - val rate = exchange.rateFor(currencyCode) ?: return null - - return KinAmount.fromFiatAmount(amountKin, amountFiat, rate.fx, currencyCode) - } - - override fun onAmountChanged(lastPressedBackspace: Boolean) { - super.onAmountChanged(lastPressedBackspace) - state.update { - val minValue = - if (it.currencyModel.selectedCurrency?.code == CurrencyCode.KIN.name) 1.0 else 0.01 - it.copy( - continueEnabled = numberInputHelper.amount >= minValue && - !it.amountModel.isInsufficient && - numberInputHelper.amount <= it.amountModel.buyLimit - ) - } - } - - override fun setCurrencyUiModel(currencyUiModel: CurrencyUiModel) { - state.update { it.copy(currencyModel = currencyUiModel) } - } - - override fun setAmountUiModel(amountUiModel: AmountUiModel) { - state.update { - it.copy(amountModel = amountUiModel) - } - } - - override fun setAmountAnimatedInputUiModel(amountAnimatedInputUiModel: AmountAnimatedInputUiModel) { - state.update { it.copy(amountAnimatedModel = amountAnimatedInputUiModel) } - } - - override fun getCurrencyUiModel(): CurrencyUiModel { - return state.value.currencyModel.copy() - } - - override fun getAmountUiModel(): AmountUiModel { - return state.value.amountModel.copy() - } - - override fun getAmountAnimatedInputUiModel(): AmountAnimatedInputUiModel { - return state.value.amountAnimatedModel.copy() - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/scanner/DecorView.kt b/apps/codeApp/src/main/java/com/getcode/view/main/scanner/DecorView.kt deleted file mode 100644 index bf944864a..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/scanner/DecorView.kt +++ /dev/null @@ -1,212 +0,0 @@ -package com.getcode.view.main.scanner - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.getcode.SessionState -import com.getcode.R -import com.getcode.theme.CodeTheme -import com.getcode.theme.xxl -import com.getcode.ui.components.Pill -import com.getcode.ui.tips.DefinedTips -import com.getcode.ui.core.unboundedClickable -import com.getcode.utils.network.LocalNetworkObserver -import com.getcode.view.main.scanner.components.HomeBottom -import dev.bmcreations.tipkit.LocalTipProvider -import dev.bmcreations.tipkit.engines.LocalTipsEngine -import dev.bmcreations.tipkit.popoverTip -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -@Composable -internal fun DecorView( - dataState: SessionState, - isCameraReady: Boolean, - isPaused: Boolean, - modifier: Modifier = Modifier, - onAction: (UiElement) -> Unit, -) { - val tips = LocalTipsEngine.current!!.tips as DefinedTips - val tipProvider = LocalTipProvider.current - - LaunchedEffect(isCameraReady) { - tips.downloadCodeTip.homeOpen.reset() - // record app open - if (isCameraReady) { - tips.downloadCodeTip.homeOpen.record() - } - } - - val scope = rememberCoroutineScope() - val openDownloadModal = { - onAction(UiElement.SHARE_DOWNLOAD) - scope.launch { - delay(300) - tipProvider.dismiss() - } - } - - Box( - modifier = Modifier - .fillMaxSize() - .then(modifier) - ) { - Image( - modifier = Modifier - .statusBarsPadding() - .padding(vertical = CodeTheme.dimens.grid.x3) - .padding(horizontal = CodeTheme.dimens.grid.x3) - .align(Alignment.TopStart) - .popoverTip( - visible = dataState.billState.bill == null, - tip = tips.downloadCodeTip, - alignment = Alignment.BottomStart - ) - .clickable { - openDownloadModal() - }, - painter = painterResource( - R.drawable.ic_code_logo_white - ), - contentDescription = "", - ) - - Column( - modifier = Modifier - .statusBarsPadding() - .padding(vertical = CodeTheme.dimens.grid.x2) - .padding(horizontal = CodeTheme.dimens.grid.x3) - .align(Alignment.TopEnd), - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.inset) - ) { - Image( - modifier = Modifier - .clip(CircleShape) - .unboundedClickable { - onAction(UiElement.ACCOUNT) - }, - painter = painterResource(R.drawable.ic_home_options), - contentDescription = "", - ) - - if (dataState.gallery.enabled) { - Image( - modifier = Modifier - .clip(CircleShape) - .unboundedClickable { - onAction(UiElement.GALLERY) - }, - painter = painterResource(R.drawable.ic_gallery), - contentDescription = "", - ) - } - } - - Column(modifier = Modifier.align(Alignment.BottomCenter)) { - AnimatedVisibility( - modifier = Modifier - .align(Alignment.End) - .padding(end = CodeTheme.dimens.grid.x5), - visible = dataState.billState.showToast && dataState.billState.toast != null, - enter = slideInVertically(animationSpec = tween(600), initialOffsetY = { it }) + - fadeIn(animationSpec = tween(500, 100)), - exit = if (!isPaused) - slideOutVertically(animationSpec = tween(600), targetOffsetY = { it }) + - fadeOut(animationSpec = tween(500, 100)) - else fadeOut(animationSpec = tween(0)), - ) { - val toast by remember(dataState.billState.toast) { - derivedStateOf { dataState.billState.toast } - } - Pill( - text = toast?.formattedAmount.orEmpty(), - textStyle = CodeTheme.typography.textSmall.copy( - fontWeight = FontWeight.Bold - ), - shape = CodeTheme.shapes.xxl, - ) - } - - val networkState by LocalNetworkObserver.current.state.collectAsState() - - AnimatedVisibility( - modifier = Modifier - .align(Alignment.CenterHorizontally), - visible = dataState.showNetworkOffline && !networkState.connected, - enter = fadeIn(animationSpec = tween(500, 100)), - exit = fadeOut(animationSpec = tween(500, 100)), - ) { - Row( - modifier = Modifier - .wrapContentSize() - .clip(CodeTheme.shapes.xxl) - .background(CodeTheme.colors.error) - .padding( - horizontal = CodeTheme.dimens.grid.x2, - vertical = CodeTheme.dimens.grid.x1 - ), - horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.staticGrid.x1), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - modifier = Modifier.size(16.dp), - painter = painterResource(id = R.drawable.ic_wifi_slash), - contentDescription = null - ) - Text( - text = stringResource(id = R.string.title_badge_no_connection), - color = Color.White, - style = CodeTheme.typography.caption - ) - } - } - - HomeBottom( - modifier = Modifier - .windowInsetsPadding(WindowInsets.navigationBars) - .padding(bottom = CodeTheme.dimens.grid.x3), - state = dataState, - onPress = { - onAction(it) - }, - ) - } - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/scanner/ScannerScreen.kt b/apps/codeApp/src/main/java/com/getcode/view/main/scanner/ScannerScreen.kt deleted file mode 100644 index a02f38007..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/scanner/ScannerScreen.kt +++ /dev/null @@ -1,516 +0,0 @@ -package com.getcode.view.main.scanner - -import android.Manifest -import android.app.Activity -import androidx.activity.compose.BackHandler -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.PickVisualMediaRequest -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.EnterExitState -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material.DismissState -import androidx.compose.material.DismissValue -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment.Companion.BottomCenter -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.unit.dp -import androidx.lifecycle.Lifecycle -import com.getcode.PresentationStyle -import com.getcode.R -import com.getcode.SessionController -import com.getcode.SessionEvent -import com.getcode.SessionState -import com.getcode.manager.TopBarManager -import com.getcode.models.Bill -import com.getcode.models.DeepLinkRequest -import com.getcode.navigation.core.CodeNavigator -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.AccountModal -import com.getcode.navigation.screens.BalanceModal -import com.getcode.navigation.screens.ConnectAccount -import com.getcode.navigation.screens.EnterTipModal -import com.getcode.navigation.screens.GetKinModal -import com.getcode.navigation.screens.GiveKinModal -import com.getcode.navigation.screens.ShareDownloadLinkModal -import com.getcode.ui.biometrics.LocalBiometricsState -import com.getcode.ui.components.OnLifecycleEvent -import com.getcode.ui.components.restrictions.ContentRestrictedView -import com.getcode.ui.core.RestrictionType -import com.getcode.ui.core.measured -import com.getcode.ui.modals.ReceivedKinConfirmation -import com.getcode.ui.scanner.CodeScanner -import com.getcode.ui.scanner.views.CameraDisabledView -import com.getcode.ui.scanner.views.CameraPermissionsMissingView -import com.getcode.ui.utils.AnimationUtils -import com.getcode.util.launchAppSettings -import com.getcode.util.permissions.PermissionResult -import com.getcode.util.permissions.getPermissionLauncher -import com.getcode.util.permissions.rememberPermissionHandler -import com.getcode.utils.ErrorUtils -import com.getcode.view.main.bill.BillManagementOptions -import com.getcode.view.main.bill.HomeBill -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import timber.log.Timber -import kotlin.time.Duration.Companion.milliseconds - - -enum class UiElement { - NONE, - ACCOUNT, - GIVE_KIN, - GET_KIN, - BALANCE, - SHARE_DOWNLOAD, - TIP_CARD, - GALLERY -} - -@Composable -fun ScanScreen( - session: SessionController, - cashLink: String? = null, - request: DeepLinkRequest? = null, -) { - val navigator = LocalCodeNavigator.current - val dataState by session.state.collectAsState() - - when (val restrictionType = dataState.restrictionType) { - RestrictionType.ACCESS_EXPIRED, - RestrictionType.FORCE_UPGRADE, - RestrictionType.TIMELOCK_UNLOCKED -> { - ContentRestrictedView(restrictionType) { - session.logout(it) - } - } - - null -> { - ScannerContent( - session = session, - dataState = dataState, - cashLink = cashLink, - request = request, - ) - - val notificationPermissionChecker = - com.getcode.util.permissions.notificationPermissionCheck { } - val context = LocalContext.current - LaunchedEffect(session) { - session.eventFlow - .onEach { - when (it) { - SessionEvent.PresentTipEntry -> { - navigator.show(EnterTipModal()) - } - - is SessionEvent.SendIntent -> { - context.startActivity(it.intent) - } - - SessionEvent.RequestNotificationPermissions -> { - notificationPermissionChecker(true) - } - - is SessionEvent.OnChatPaidForSuccessfully -> Unit - } - } - .launchIn(this) - } - } - } -} - -@Composable -private fun ScannerContent( - session: SessionController, - dataState: SessionState, - cashLink: String?, - request: DeepLinkRequest?, -) { - val navigator = LocalCodeNavigator.current - val scope = rememberCoroutineScope() - - var isPaused by remember { mutableStateOf(false) } - - val context = LocalContext.current - var previewing by remember { - mutableStateOf(false) - } - - var cameraStarted by remember { - mutableStateOf(dataState.autoStartCamera == true) - } - - LaunchedEffect(previewing) { - session.onCameraScanning(previewing) - } - - val focusManager = LocalFocusManager.current - - var cashLinkSaved by remember(cashLink) { - mutableStateOf(cashLink) - } - - var requestPayloadSaved by remember(request) { - mutableStateOf(request) - } - - val biometricsState = LocalBiometricsState.current - LaunchedEffect( - biometricsState, - previewing, - dataState.balance, - cashLinkSaved, - requestPayloadSaved - ) { - if (previewing) { - focusManager.clearFocus() - } - - if (biometricsState.passed && !cashLinkSaved.isNullOrBlank()) { - session.openCashLink(cashLink) - cashLinkSaved = null - } - - if (biometricsState.passed && requestPayloadSaved != null && dataState.balance != null) { - delay(500.milliseconds) - session.handleRequest(request) - requestPayloadSaved = null - } - } - - val pickPhoto = - rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> - if (uri != null) { - session.onImageSelected(uri) - } - } - - fun handleAction(action: UiElement) { - scope.launch { - when (action) { - UiElement.NONE -> Unit - UiElement.GIVE_KIN -> navigator.show(GiveKinModal) - UiElement.ACCOUNT -> navigator.show(AccountModal) - UiElement.GET_KIN -> navigator.show(GetKinModal) - UiElement.BALANCE -> navigator.show(BalanceModal) - UiElement.SHARE_DOWNLOAD -> navigator.show(ShareDownloadLinkModal) - UiElement.TIP_CARD -> { - if (dataState.tipCardConnected) { - session.presentShareableTipCard() - } else { - navigator.show(ConnectAccount()) - } - } - - UiElement.GALLERY -> { - pickPhoto.launch( - PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) - ) - } - } - } - } - - BillContainer( - isPaused = isPaused, - isCameraReady = previewing, - isCameraStarted = cameraStarted, - dataState = dataState, - session = session, - onStartCamera = { cameraStarted = true }, - scannerView = { - CodeScanner( - scanningEnabled = previewing, - cameraGesturesEnabled = dataState.cameraGestures.enabled, - invertedDragZoomEnabled = dataState.invertedDragZoom.enabled, - onPreviewStateChanged = { previewing = it }, - onCodeScanned = { - if (previewing) { - session.onCodeScan(it) - } - }, - onError = { ErrorUtils.handleError(it) } - ) - }, - onAction = { handleAction(it) }, - ) - - OnLifecycleEvent { _, event -> - when (event) { - Lifecycle.Event.ON_START -> { - Timber.d("onStart") - isPaused = false - } - - Lifecycle.Event.ON_STOP -> { - Timber.d("onStop") - if (dataState.autoStartCamera == false) { - cameraStarted = false - } - } - - Lifecycle.Event.ON_PAUSE -> { - Timber.d("onPause") - isPaused = true - session.startSheetDismissTimer { - Timber.d("hiding from timeout") - navigator.hide() - } - } - - Lifecycle.Event.ON_RESUME -> { - Timber.d("onResume") - isPaused = false - session.stopSheetDismissTimer() - } - - else -> Unit - } - } - - DisposableEffect(LocalCodeNavigator.current) { - onDispose { - previewing = false - } - } - - LaunchedEffect(navigator.isVisible) { - previewing = !navigator.isVisible - } - - LaunchedEffect(dataState.billState.bill) { - if (dataState.billState.bill != null) { - navigator.hide() - } - session.resetScreenTimeout(context as Activity) - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun BillContainer( - modifier: Modifier = Modifier, - isCameraReady: Boolean, - isCameraStarted: Boolean, - isPaused: Boolean, - dataState: SessionState, - session: SessionController, - scannerView: @Composable () -> Unit, - onStartCamera: () -> Unit, - onAction: (UiElement) -> Unit, -) { - val context = LocalContext.current as Activity - val onPermissionResult = { result: PermissionResult -> - session.onCameraPermissionResult(result) - if (result == PermissionResult.ShouldShowRationale) { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = context.getString(R.string.action_allowCameraAccess), - message = context.getString(R.string.error_description_cameraAccessRequired), - type = TopBarManager.TopBarMessageType.ERROR, - secondaryText = context.getString(R.string.action_openSettings), - secondaryAction = { context.launchAppSettings() } - ) - ) - } - } - - val cameraPermissionLauncher = getPermissionLauncher(Manifest.permission.CAMERA, onPermissionResult) - - val permissionChecker = rememberPermissionHandler() - - val checkPermission = { shouldRequest: Boolean -> - permissionChecker.request( - permission = Manifest.permission.CAMERA, - shouldRequest = shouldRequest, - onPermissionResult = onPermissionResult, - launcher = cameraPermissionLauncher - ) - } - - SideEffect { - checkPermission(false) - } - - Box( - modifier = Modifier - .fillMaxSize() - .then(modifier) - ) { - when { - LocalBiometricsState.current.isAwaitingAuthentication -> { - // waiting for result - } - - dataState.isCameraPermissionGranted == true || dataState.isCameraPermissionGranted == null -> { - if (dataState.autoStartCamera == null) { - // waiting for result - } else if (!dataState.autoStartCamera && !isCameraStarted) { - CameraDisabledView(modifier = Modifier.fillMaxSize()) { - onStartCamera() - } - } else { - scannerView() - } - } - - else -> { - CameraPermissionsMissingView( - modifier = Modifier.fillMaxSize(), - onClick = { checkPermission(true) } - ) - } - } - - val updatedState by rememberUpdatedState(newValue = dataState) - - var dismissed by remember { - mutableStateOf(false) - } - - // bill dismiss state, restarted for every bill - val billDismissState = remember(updatedState.billState.bill) { - DismissState( - initialValue = DismissValue.Default, - confirmStateChange = { - val canDismiss = - it == DismissValue.DismissedToEnd && updatedState.billState.canSwipeToDismiss - if (canDismiss) { - session.cancelSend() - dismissed = true - } - canDismiss - } - ) - } - - LaunchedEffect(dismissed) { - if (dismissed) { - delay(500) - dismissed = false - } - } - - // Composable animation for the decor - AnimatedVisibility( - visible = updatedState.billState.bill == null || billDismissState.targetValue != DismissValue.Default, - enter = fadeIn(), - exit = fadeOut(), - modifier = Modifier.fillMaxSize() - ) { - DecorView(updatedState, isCameraReady, isPaused) { onAction(it) } - } - - var managementHeight by remember { - mutableStateOf(0.dp) - } - - val showManagementOptions by remember(updatedState.billState) { - derivedStateOf { - billDismissState.targetValue == DismissValue.Default && - (updatedState.billState.valuation != null || updatedState.billState.bill is Bill.Tip) && - !updatedState.billState.hideBillButtons - } - } - - HomeBill( - modifier = Modifier.fillMaxSize(), - dismissState = billDismissState, - dismissed = dismissed, - contentPadding = PaddingValues(bottom = managementHeight), - bill = updatedState.billState.bill, - transitionSpec = { - if (updatedState.presentationStyle is PresentationStyle.Slide) { - AnimationUtils.animationBillEnterGive - } else { - AnimationUtils.animationBillEnterGrabbed - } togetherWith if (updatedState.presentationStyle is PresentationStyle.Slide) { - AnimationUtils.animationBillExitReturned - } else { - AnimationUtils.animationBillExitGrabbed - } - } - ) - - //Bill management options - AnimatedVisibility( - modifier = Modifier - .align(BottomCenter) - .measured { managementHeight = it.height }, - visible = showManagementOptions, - enter = fadeIn(), - exit = fadeOut(tween(100)), - ) { - var canCancel by remember { - mutableStateOf(false) - } - BillManagementOptions( - modifier = Modifier - .windowInsetsPadding(WindowInsets.navigationBars), - primaryAction = updatedState.billState.primaryAction, - secondaryAction = updatedState.billState.secondaryAction, - isSending = updatedState.isRemoteSendLoading, - isInteractable = canCancel, - ) - - LaunchedEffect(transition.isRunning, transition.targetState) { - // wait for spring settle to enable cancel to not prematurely cancel - // the enter. doing so causing the exit of the bill to not run, or run its own dismiss animation - if (transition.targetState == EnterExitState.Visible && transition.currentState == transition.targetState) { - delay(300) - canCancel = true - } - } - - BackHandler(updatedState.billState.bill is Bill.Tip && canCancel) { - session.cancelSend() - } - } - - //Bill Received Bottom Dialog - AnimatedVisibility( - modifier = Modifier.align(BottomCenter), - visible = (updatedState.billState.bill as? Bill.Cash)?.didReceive ?: false, - enter = AnimationUtils.modalEnter, - exit = AnimationUtils.modalExit, - ) { - if (updatedState.billState.bill != null) { - Box( - contentAlignment = BottomCenter - ) { - ReceivedKinConfirmation( - bill = updatedState.billState.bill as Bill.Cash, - onClaim = { session.cancelSend() } - ) - } - } - } - } -} diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/scanner/components/BottomBar.kt b/apps/codeApp/src/main/java/com/getcode/view/main/scanner/components/BottomBar.kt deleted file mode 100644 index ff7517eca..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/scanner/components/BottomBar.kt +++ /dev/null @@ -1,207 +0,0 @@ -package com.getcode.view.main.scanner.components - -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.scaleIn -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.layout.Layout -import androidx.compose.ui.layout.layoutId -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEach -import com.getcode.R -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.Badge -import com.getcode.ui.components.Row -import com.getcode.ui.utils.heightOrZero -import com.getcode.ui.core.unboundedClickable -import com.getcode.ui.utils.widthOrZero -import com.getcode.view.main.scanner.UiElement -import com.getcode.SessionState - -@Preview -@Composable -internal fun HomeBottom( - modifier: Modifier = Modifier, - state: SessionState = SessionState(), - onPress: (homeBottomSheet: UiElement) -> Unit = {}, -) { - Row( - modifier = Modifier - .fillMaxWidth() - .then(modifier), - verticalAlignment = Alignment.Bottom, - horizontalArrangement = Arrangement.SpaceAround, - ) { - state.scannerElements.fastForEach { action -> - when (action) { - UiElement.GIVE_KIN -> { - BottomBarAction( - modifier = Modifier.weight(1f), - label = stringResource(R.string.action_give), - painter = painterResource(R.drawable.ic_kin_white_small), - badgeCount = 0, - onClick = { onPress(action) } - ) - } - - UiElement.GET_KIN -> { - BottomBarAction( - modifier = Modifier.weight(1f), - label = stringResource(R.string.action_receive), - painter = painterResource(R.drawable.ic_wallet), - badgeCount = 0, - onClick = { onPress(action) }, - ) - } - - UiElement.BALANCE -> { - BottomBarAction( - modifier = Modifier.weight(1f), - label = stringResource(R.string.action_balance), - painter = painterResource(R.drawable.ic_balance), - badgeCount = state.notificationUnreadCount, - onClick = { onPress(UiElement.BALANCE) }, - ) - } - - UiElement.TIP_CARD -> { - BottomBarAction( - modifier = Modifier.weight(1f), - label = stringResource(R.string.action_receive), - painter = painterResource(R.drawable.ic_tip_card), - onClick = { onPress(action) }, - badgeCount = if (state.splatTipCard) 1 else 0, - ) - } - - else -> { - BottomBarAction( - modifier = Modifier - .weight(1f) - .alpha(0f), - label = "", - painter = painterResource(R.drawable.ic_empty_bottom_action), - badgeCount = 0, - onClick = null, - ) - } - } - } - } -} - -@Composable -private fun BottomBarAction( - modifier: Modifier = Modifier, - label: String, - contentPadding: PaddingValues = PaddingValues( - vertical = CodeTheme.dimens.grid.x2 - ), - painter: Painter, - imageSize: Dp = CodeTheme.dimens.staticGrid.x10, - badgeCount: Int = 0, - onClick: (() -> Unit)?, -) { - BottomBarAction( - modifier = modifier, - label = label, - contentPadding = contentPadding, - painter = painter, - imageSize = imageSize, - badge = { - Badge( - modifier = Modifier.padding(top = 6.dp, end = 1.dp), - count = badgeCount, - color = CodeTheme.colors.indicator, - enterTransition = scaleIn( - animationSpec = tween( - durationMillis = 300, - delayMillis = 1000 - ) - ) + fadeIn() - ) - }, - onClick = onClick - ) -} - -@Composable -private fun BottomBarAction( - modifier: Modifier = Modifier, - label: String, - contentPadding: PaddingValues = PaddingValues( - vertical = CodeTheme.dimens.grid.x2 - ), - painter: Painter, - imageSize: Dp = CodeTheme.dimens.staticGrid.x10, - badge: @Composable () -> Unit = { }, - onClick: (() -> Unit)?, -) { - Layout( - modifier = modifier, - content = { - Column( - modifier = Modifier - .unboundedClickable( - enabled = onClick != null, - rippleRadius = imageSize - ) { onClick?.invoke() } - .layoutId("action"), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Image( - modifier = Modifier - .padding(contentPadding) - .size(imageSize), - painter = painter, - contentDescription = null, - ) - Text( - text = label, - style = CodeTheme.typography.textSmall - ) - } - - Box(modifier = Modifier.layoutId("badge")) { - badge() - } - } - ) { measurables, incomingConstraints -> - val constraints = incomingConstraints.copy(minWidth = 0, minHeight = 0) - val actionPlaceable = - measurables.find { it.layoutId == "action" }?.measure(constraints) - val badgePlaceable = - measurables.find { it.layoutId == "badge" }?.measure(constraints) - - val maxWidth = widthOrZero(actionPlaceable) - val maxHeight = heightOrZero(actionPlaceable) - layout( - width = maxWidth, - height = maxHeight, - ) { - actionPlaceable?.placeRelative(0, 0) - badgePlaceable?.placeRelative( - x = maxWidth - widthOrZero(badgePlaceable), - y = -(heightOrZero(badgePlaceable) / 3) - ) - } - } -} - diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/tip/ConnectAccountScreen.kt b/apps/codeApp/src/main/java/com/getcode/view/main/tip/ConnectAccountScreen.kt deleted file mode 100644 index 72951ee0e..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/tip/ConnectAccountScreen.kt +++ /dev/null @@ -1,185 +0,0 @@ -package com.getcode.view.main.tip - -import android.content.Context -import android.content.Intent -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.hilt.navigation.compose.hiltViewModel -import com.getcode.R -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.theme.Brand -import com.getcode.theme.BrandSubtle -import com.getcode.theme.CodeTheme -import com.getcode.theme.DesignSystem -import com.getcode.theme.bolded -import com.getcode.theme.extraSmall -import com.getcode.ui.components.Row -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.theme.CodeScaffold -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach - -enum class IdentityConnectionReason { - TipCard, - IdentityReveal, - Login, -} - -@Composable -fun ConnectAccountScreen( - viewModel: ConnectAccountViewModel = hiltViewModel(), - titleAlignment: TextAlign = TextAlign.Start, -) { - val state by viewModel.stateFlow.collectAsState() - val navigator = LocalCodeNavigator.current - val context = LocalContext.current - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .onEach { - composeTweet(context, it.intent) - delay(1_000) - when (state.reason) { - IdentityConnectionReason.Login, - IdentityConnectionReason.IdentityReveal -> navigator.pop() - else -> navigator.hide() - } - } - .launchIn(this) - } - - CodeScaffold( - bottomBar = { - CodeButton( - modifier = Modifier.fillMaxWidth() - .padding(CodeTheme.dimens.inset), - onClick = { - viewModel.dispatchEvent(ConnectAccountViewModel.Event.PostToX) - }, - buttonState = ButtonState.Filled, - content = { - Image( - painter = rememberVectorPainter(image = ImageVector.vectorResource(id = R.drawable.ic_twitter_x)), - colorFilter = ColorFilter.tint(Brand), - contentDescription = null - ) - Spacer(Modifier.width(CodeTheme.dimens.grid.x2)) - Text( - text = stringResource(R.string.action_messageGetCode), - ) - } - ) - }, - ) { padding -> - Column( - Modifier - .padding(CodeTheme.dimens.inset) - .padding(padding), - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.inset) - ) { - RequestContent(state = state, titleAlignment = titleAlignment) - } - } - -} - -@Composable -private fun ColumnScope.RequestContent(state: ConnectAccountViewModel.State, titleAlignment: TextAlign = TextAlign.Start) { - Text( - modifier = Modifier.fillMaxWidth(), - text = when(state.reason) { - IdentityConnectionReason.TipCard -> stringResource(id = R.string.title_receiveTips) - IdentityConnectionReason.IdentityReveal -> stringResource(id = R.string.title_connectAccount) - IdentityConnectionReason.Login -> stringResource(id = R.string.title_connectYourX) - null -> "" - }, - style = CodeTheme.typography.displayMedium.bolded(), - textAlign = titleAlignment, - ) - Text( - modifier = Modifier.fillMaxWidth(), - text = when(state.reason) { - IdentityConnectionReason.TipCard -> stringResource(id = R.string.subtitle_tipCardXDescription) - IdentityConnectionReason.IdentityReveal -> stringResource(id = R.string.subtitle_connectXAccount) - IdentityConnectionReason.Login -> stringResource(id = R.string.subtitle_identityInApp, stringResource(R.string.app_name_without_variant)) - null -> "" - }, - style = CodeTheme.typography.textSmall, - textAlign = titleAlignment, - ) - Spacer(modifier = Modifier.weight(0.3f)) - TweetPreview(modifier = Modifier.fillMaxWidth(), xMessage = state.xMessage) - Spacer(modifier = Modifier.weight(0.7f)) -} - -@Composable -private fun TweetPreview( - modifier: Modifier = Modifier, - xMessage: String -) { - Row( - modifier = Modifier - .background(BrandSubtle, shape = CodeTheme.shapes.extraSmall) - .padding(CodeTheme.dimens.inset) - .then(modifier), - ) { - Image( - modifier = Modifier - .size(CodeTheme.dimens.grid.x5) - .clip(CircleShape), - painter = painterResource(id = R.drawable.ic_placeholder_user), - contentDescription = null - ) - Spacer(modifier = Modifier.width(CodeTheme.dimens.grid.x3)) - Text( - modifier = Modifier.weight(1f), - text = xMessage, - color = Color.White, - style = CodeTheme.typography.textSmall, - ) - } -} - -private fun composeTweet(context: Context, intent: Intent) { - context.startActivity(intent) -} - -@Preview -@Composable -private fun Preview_TweetPreview() { - DesignSystem { - TweetPreview(xMessage = "${stringResource(R.string.subtitle_connectXTweetText)}\n" + - "\n" + - "CodeAccount:349pQtzGmiBxU9vADVf6AUdMLLXyCCU3Zu4smrQPXved:zGmiBxU9vADVf6AUdMLLXyCCU3Zu4smrQP") - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/tip/ConnectAccountViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/tip/ConnectAccountViewModel.kt deleted file mode 100644 index d20c2be2c..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/tip/ConnectAccountViewModel.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.getcode.view.main.tip - -import android.content.Intent -import androidx.lifecycle.viewModelScope -import com.getcode.R -import com.getcode.analytics.Action -import com.getcode.analytics.CodeAnalyticsService -import com.getcode.network.IdentityManager -import com.getcode.network.TipController -import com.getcode.util.IntentUtils -import com.getcode.util.resources.ResourceHelper -import com.getcode.view.BaseViewModel2 -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import javax.inject.Inject - -@HiltViewModel -class ConnectAccountViewModel @Inject constructor( - resources: ResourceHelper, - tipController: TipController, - analytics: CodeAnalyticsService, - identityManager: IdentityManager, -) : BaseViewModel2( - initialState = State(null, ""), - updateStateForEvent = updateStateForEvent -) { - - data class State( - val reason: IdentityConnectionReason?, - val xMessage: String, - ) - - sealed interface Event { - data class OnReasonChanged(val reason: IdentityConnectionReason) : Event - data class UpdateMessage(val message: String) : Event - data object PostToX : Event - data class OpenX(val intent: Intent) : Event - } - - init { - eventFlow - .filterIsInstance() - .map { it.reason } - .mapNotNull { - when (it) { - IdentityConnectionReason.TipCard -> { - val verificationMessage = identityManager.generateVerificationTweet(resources.getString(R.string.account_name_link)) ?: return@mapNotNull null - - """ - ${resources.getString(R.string.subtitle_connectXTweetText)} - - $verificationMessage - """.trimIndent() - } - - IdentityConnectionReason.IdentityReveal -> { - val verificationMessage = identityManager.generateVerificationTweet(resources.getString(R.string.account_name_link)) ?: return@mapNotNull null - """ - ${resources.getString(R.string.subtitle_linkingTwitterToRevealIdentity)} - - $verificationMessage - """.trimIndent() - } - - IdentityConnectionReason.Login -> { - val verificationMessage = identityManager.generateVerificationTweet(resources.getString(R.string.account_name_link)) ?: return@mapNotNull null - - """ - ${resources.getString(R.string.subtitle_linkingTwitterToLogin, resources.getString(R.string.handle))} - - $verificationMessage - """.trimIndent() - } - } - }.onEach { - dispatchEvent(Event.UpdateMessage(it)) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { stateFlow.value.xMessage } - .map { IntentUtils.tweet(it) } - .onEach { - analytics.action(Action.MessageCodeOnX) - dispatchEvent(Event.OpenX(it)) - tipController.startVerification() - }.launchIn(viewModelScope) - } - - companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - is Event.OnReasonChanged -> { state -> state.copy(reason = event.reason) } - is Event.UpdateMessage -> { state -> state.copy(xMessage = event.message) } - is Event.OpenX -> { state -> state } - Event.PostToX -> { state -> state } - } - } - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/tip/EnterTipScreen.kt b/apps/codeApp/src/main/java/com/getcode/view/main/tip/EnterTipScreen.kt deleted file mode 100644 index a9b695216..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/tip/EnterTipScreen.kt +++ /dev/null @@ -1,120 +0,0 @@ -package com.getcode.view.main.tip - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel -import cafe.adriel.voyager.navigator.currentOrThrow -import com.getcode.LocalSession -import com.getcode.R -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.CurrencySelectionModal -import com.getcode.theme.Alert -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.text.AmountArea -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.theme.CodeKeyPad -import com.getcode.util.showNetworkError -import com.getcode.utils.ErrorUtils -import com.getcode.utils.network.LocalNetworkObserver -import kotlinx.coroutines.launch - -@Composable -fun EnterTipScreen( - viewModel: TipPaymentViewModel = hiltViewModel(), - onSendTip: () -> Unit, -) { - val context = LocalContext.current - val navigator = LocalCodeNavigator.current - val dataState by viewModel.state.collectAsState() - val composeScope = rememberCoroutineScope() - - val networkObserver = LocalNetworkObserver.current - val networkState by networkObserver.state.collectAsState() - - val session = LocalSession.currentOrThrow - - Column( - modifier = Modifier - .fillMaxSize() - .padding(bottom = CodeTheme.dimens.grid.x4), - horizontalAlignment = Alignment.CenterHorizontally - ) { - val isInError by remember(dataState.amountModel) { - derivedStateOf { - dataState.amountModel.amountKin > dataState.amountModel.sendLimitKin || - dataState.amountModel.balanceKin < dataState.amountModel.amountKin.toKinValueDouble() - } - } - - val color = if (isInError) Alert else CodeTheme.colors.textSecondary - Box( - modifier = Modifier.weight(0.65f) - ) { - AmountArea( - amountPrefix = dataState.amountModel.amountPrefix, - amountSuffix = dataState.amountModel.amountSuffix, - amountText = dataState.amountModel.amountText, - captionText = dataState.amountModel.captionText, - isAltCaption = dataState.amountModel.isCaptionConversion, - isAltCaptionKinIcon = !isInError, - altCaptionColor = color, - currencyResId = dataState.currencyModel.selectedCurrency?.resId, - uiModel = dataState.amountAnimatedModel, - isAnimated = true, - networkState = networkState, - textStyle = CodeTheme.typography.displayLarge, - modifier = Modifier - .padding(horizontal = CodeTheme.dimens.inset) - .align(Alignment.Center) - ) { - navigator.push(CurrencySelectionModal()) - } - } - - CodeKeyPad( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = CodeTheme.dimens.inset) - .weight(1f), - onNumber = viewModel::onNumber, - onClear = viewModel::onBackspace, - onDecimal = viewModel::onDot, - isDecimal = dataState.amountModel.isDecimalAllowed - ) - - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset), - onClick = { - if (!networkObserver.isConnected) { - ErrorUtils.showNetworkError(context) - return@CodeButton - } - - composeScope.launch { - val amount = viewModel.onSubmit() ?: return@launch - session.presentTipConfirmation(amount) - onSendTip() - } - }, - enabled = dataState.continueEnabled, - text = stringResource(R.string.action_next), - buttonState = ButtonState.Filled, - ) - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/java/com/getcode/view/main/tip/TipPaymentViewModel.kt b/apps/codeApp/src/main/java/com/getcode/view/main/tip/TipPaymentViewModel.kt deleted file mode 100644 index f45bc1e32..000000000 --- a/apps/codeApp/src/main/java/com/getcode/view/main/tip/TipPaymentViewModel.kt +++ /dev/null @@ -1,217 +0,0 @@ -package com.getcode.view.main.tip - -import androidx.lifecycle.viewModelScope -import com.getcode.R -import com.getcode.model.CurrencyCode -import com.getcode.model.Kin -import com.getcode.model.KinAmount -import com.getcode.model.Rate -import com.getcode.model.SendLimit -import com.getcode.model.fromFiatAmount -import com.getcode.network.client.Client -import com.getcode.network.client.receiveIfNeeded -import com.getcode.network.exchange.Exchange -import com.getcode.network.repository.BalanceRepository -import com.getcode.network.repository.PrefRepository -import com.getcode.network.repository.TransactionRepository -import com.getcode.extensions.formattedRaw -import com.getcode.manager.TopBarManager -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.ErrorUtils -import com.getcode.utils.FormatUtils -import com.getcode.ui.components.text.AmountAnimatedInputUiModel -import com.getcode.view.main.giveKin.AmountUiModel -import com.getcode.view.main.giveKin.BaseAmountCurrencyViewModel -import com.getcode.view.main.giveKin.CurrencyUiModel -import com.getcode.view.main.giveKin.FlowType -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class TipPaymentViewModel @Inject constructor( - client: Client, - exchange: Exchange, - prefsRepository: PrefRepository, - balanceRepository: BalanceRepository, - private val transactionRepository: TransactionRepository, - localeHelper: com.getcode.util.locale.LocaleHelper, - currencyUtils: com.getcode.utils.CurrencyUtils, - networkObserver: com.getcode.utils.network.NetworkConnectivityListener, - resources: ResourceHelper, -) : BaseAmountCurrencyViewModel( - client, - prefsRepository, - exchange, - balanceRepository, - transactionRepository, - localeHelper, - currencyUtils, - resources, - networkObserver -) { - - data class State( - val currencyModel: CurrencyUiModel = CurrencyUiModel(), - val amountAnimatedModel: AmountAnimatedInputUiModel = AmountAnimatedInputUiModel(), - val amountModel: AmountUiModel = AmountUiModel(), - val continueEnabled: Boolean = false, - ) - - val state = MutableStateFlow(State()) - - override val flowType: FlowType = FlowType.Tip - - init { - init() - viewModelScope.launch(Dispatchers.IO) { - client.receiveIfNeeded().subscribe({}, ErrorUtils::handleError) - } - } - - override fun reset() { - numberInputHelper.reset() - onAmountChanged(true) - viewModelScope.launch { - state.update { - it.copy( - continueEnabled = false - ) - } - } - } - - private val minLimit: (rate: Rate) -> Kin = { rate -> - KinAmount.fromFiatAmount(minFiatLimit(rate), rate).kin - } - - private val maxFiatLimit: (rate: Rate) -> Double = { rate -> - (transactionRepository.sendLimitFor(rate.currency) ?: SendLimit.Zero).nextTransaction - } - - private val minFiatLimit: (rate: Rate) -> Double = { rate -> - (transactionRepository.sendLimitFor(rate.currency) - ?: SendLimit.Zero).maxPerTransaction / 250.0 - } - - - private val hasAvailableTransactionLimit: (amount: KinAmount, rate: Rate) -> Boolean = { amount, _ -> - transactionRepository.hasAvailableTransactionLimit(amount) - } - - private val hasSufficientFundsToSend: (amount: KinAmount, rate: Rate) -> Boolean = { amount, _ -> - getAmountUiModel().amountKin >= amount.kin - } - - private val hasAvailableDailyLimit: Boolean - get() = transactionRepository.hasAvailableDailyLimit() - - private val isTipLargeEnough: (amount: KinAmount) -> Boolean = { amount -> - amount.kin >= minLimit(amount.rate) - } - - suspend fun onSubmit(): KinAmount? { - val uiModel = state.value - - val amountFiat = uiModel.amountModel.amountDouble - val amountKin = uiModel.amountModel.amountKin - - val currencyCode = CurrencyCode - .tryValueOf(uiModel.currencyModel.selectedCurrency?.code) ?: return null - - exchange.fetchRatesIfNeeded() - val rate = exchange.rateFor(currencyCode) ?: return null - - val amount = KinAmount.fromFiatAmount(amountKin, amountFiat, rate.fx, currencyCode) - - if (!hasSufficientFundsToSend(amount, rate)) { - TopBarManager.showMessage( - resources.getString(R.string.error_title_insuffiecientKin), - resources.getString(R.string.error_description_insuffiecientKin) - ) - return null - } - - if (!hasAvailableDailyLimit) { - TopBarManager.showMessage( - resources.getString(R.string.error_title_giveLimitReached), - resources.getString(R.string.error_description_giveLimitReached) - ) - return null - } - - if (!hasAvailableTransactionLimit(amount, rate)) { - val formatted = if (rate.currency == CurrencyCode.KIN) { - "${FormatUtils.formatWholeRoundDown(maxFiatLimit(rate))} ${resources.getString(R.string.core_kin)}" - } else { - FormatUtils.formatCurrency(maxFiatLimit(rate), rate.currency) - } - TopBarManager.showMessage( - resources.getString(R.string.error_title_tipTooLarge), - resources.getString(R.string.error_description_tipTooLarge, formatted) - ) - return null - } - - if (!isTipLargeEnough(amount)) { - // min amount is based on send limit for USD - val kin = KinAmount.fromFiatAmount(minFiatLimit(amount.rate), amount.rate).kin - // convert min amount in USD to selected currency - val normalizedAmount = KinAmount.newInstance(kin, rate) - // format for display - val formatted = if (rate.currency == CurrencyCode.KIN) { - "${normalizedAmount.formattedRaw()} ${resources.getString(R.string.core_kin)}" - } else { - FormatUtils.formatCurrency(normalizedAmount.fiat, rate.currency) - } - TopBarManager.showMessage( - resources.getString(R.string.error_title_tipTooSmall), - resources.getString(R.string.error_description_tipTooSmall, formatted) - ) - return null - } - - return amount - } - - override fun onAmountChanged(lastPressedBackspace: Boolean) { - super.onAmountChanged(lastPressedBackspace) - state.update { - val minValue = - if (it.currencyModel.selectedCurrency?.code == CurrencyCode.KIN.name) 1.0 else 0.01 - it.copy( - continueEnabled = numberInputHelper.amount >= minValue && - !it.amountModel.isInsufficient - ) - } - } - - override fun setCurrencyUiModel(currencyUiModel: CurrencyUiModel) { - state.update { it.copy(currencyModel = currencyUiModel) } - } - - override fun setAmountUiModel(amountUiModel: AmountUiModel) { - state.update { - it.copy(amountModel = amountUiModel) - } - } - - override fun setAmountAnimatedInputUiModel(amountAnimatedInputUiModel: AmountAnimatedInputUiModel) { - state.update { it.copy(amountAnimatedModel = amountAnimatedInputUiModel) } - } - - override fun getCurrencyUiModel(): CurrencyUiModel { - return state.value.currencyModel.copy() - } - - override fun getAmountUiModel(): AmountUiModel { - return state.value.amountModel.copy() - } - - override fun getAmountAnimatedInputUiModel(): AmountAnimatedInputUiModel { - return state.value.amountAnimatedModel.copy() - } -} \ No newline at end of file diff --git a/apps/codeApp/src/main/res/drawable-nodpi/ic_access_key_bg.webp b/apps/codeApp/src/main/res/drawable-nodpi/ic_access_key_bg.webp deleted file mode 100644 index 98b1ab254..000000000 Binary files a/apps/codeApp/src/main/res/drawable-nodpi/ic_access_key_bg.webp and /dev/null differ diff --git a/apps/codeApp/src/main/res/drawable/ic_account_menu_white.xml b/apps/codeApp/src/main/res/drawable/ic_account_menu_white.xml deleted file mode 100644 index bbafb13e4..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_account_menu_white.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_arrow_down.xml b/apps/codeApp/src/main/res/drawable/ic_arrow_down.xml deleted file mode 100644 index bb2d46231..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_arrow_down.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_arrow_turn_left_down.xml b/apps/codeApp/src/main/res/drawable/ic_arrow_turn_left_down.xml deleted file mode 100644 index 47e608fd3..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_arrow_turn_left_down.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_background_1.xml b/apps/codeApp/src/main/res/drawable/ic_background_1.xml deleted file mode 100644 index e4e4b4115..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_background_1.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_balance.xml b/apps/codeApp/src/main/res/drawable/ic_balance.xml deleted file mode 100644 index 134e269a1..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_balance.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_bell.xml b/apps/codeApp/src/main/res/drawable/ic_bell.xml deleted file mode 100644 index 21127d820..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_bell.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/apps/codeApp/src/main/res/drawable/ic_bill2.webp b/apps/codeApp/src/main/res/drawable/ic_bill2.webp deleted file mode 100644 index d16f19873..000000000 Binary files a/apps/codeApp/src/main/res/drawable/ic_bill2.webp and /dev/null differ diff --git a/apps/codeApp/src/main/res/drawable/ic_biometrics.xml b/apps/codeApp/src/main/res/drawable/ic_biometrics.xml deleted file mode 100644 index 0195f8ce6..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_biometrics.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_bug.xml b/apps/codeApp/src/main/res/drawable/ic_bug.xml deleted file mode 100644 index bbf623a1d..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_bug.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_camera_outline.xml b/apps/codeApp/src/main/res/drawable/ic_camera_outline.xml deleted file mode 100644 index 60b4f3dd9..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_camera_outline.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_circle_outline.xml b/apps/codeApp/src/main/res/drawable/ic_circle_outline.xml deleted file mode 100644 index 5ac72b811..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_circle_outline.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_code_invite.xml b/apps/codeApp/src/main/res/drawable/ic_code_invite.xml deleted file mode 100644 index 3e0689013..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_code_invite.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_code_logo_near_white.xml b/apps/codeApp/src/main/res/drawable/ic_code_logo_near_white.xml deleted file mode 100644 index e551252a5..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_code_logo_near_white.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_code_logo_offwhite_small.xml b/apps/codeApp/src/main/res/drawable/ic_code_logo_offwhite_small.xml deleted file mode 100644 index b85c04c61..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_code_logo_offwhite_small.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_code_logo_outline.xml b/apps/codeApp/src/main/res/drawable/ic_code_logo_outline.xml deleted file mode 100644 index 5c071de48..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_code_logo_outline.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_code_logo_white.xml b/apps/codeApp/src/main/res/drawable/ic_code_logo_white.xml deleted file mode 100644 index 5bd942ede..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_code_logo_white.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_code_menu_white.xml b/apps/codeApp/src/main/res/drawable/ic_code_menu_white.xml deleted file mode 100644 index 96b76fce8..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_code_menu_white.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_code_splash_bg.xml b/apps/codeApp/src/main/res/drawable/ic_code_splash_bg.xml deleted file mode 100644 index bfb779d2d..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_code_splash_bg.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_copy.xml b/apps/codeApp/src/main/res/drawable/ic_copy.xml deleted file mode 100644 index 8a54f4869..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_copy.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_delete_bubble.xml b/apps/codeApp/src/main/res/drawable/ic_delete_bubble.xml deleted file mode 100644 index 3af2bb3b5..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_delete_bubble.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_empty_bottom_action.xml b/apps/codeApp/src/main/res/drawable/ic_empty_bottom_action.xml deleted file mode 100644 index 061c182d8..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_empty_bottom_action.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_faq.xml b/apps/codeApp/src/main/res/drawable/ic_faq.xml deleted file mode 100644 index 24fc71623..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_faq.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_gallery.xml b/apps/codeApp/src/main/res/drawable/ic_gallery.xml deleted file mode 100644 index 91a961f24..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_gallery.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_gift.xml b/apps/codeApp/src/main/res/drawable/ic_gift.xml deleted file mode 100644 index 78935f050..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_gift.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_gift_inactive.xml b/apps/codeApp/src/main/res/drawable/ic_gift_inactive.xml deleted file mode 100644 index c5b5908c2..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_gift_inactive.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_give_kin.xml b/apps/codeApp/src/main/res/drawable/ic_give_kin.xml deleted file mode 100644 index ab5abc1bf..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_give_kin.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_graphic_wallet.xml b/apps/codeApp/src/main/res/drawable/ic_graphic_wallet.xml deleted file mode 100644 index 14ddc6148..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_graphic_wallet.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_history.xml b/apps/codeApp/src/main/res/drawable/ic_history.xml deleted file mode 100644 index 3a3d64a2e..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_history.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_home_bill_image.webp b/apps/codeApp/src/main/res/drawable/ic_home_bill_image.webp deleted file mode 100644 index 2e09d7344..000000000 Binary files a/apps/codeApp/src/main/res/drawable/ic_home_bill_image.webp and /dev/null differ diff --git a/apps/codeApp/src/main/res/drawable/ic_home_options.xml b/apps/codeApp/src/main/res/drawable/ic_home_options.xml deleted file mode 100644 index 7e4dc7a4b..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_home_options.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_invites.xml b/apps/codeApp/src/main/res/drawable/ic_invites.xml deleted file mode 100644 index 5171a11ca..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_invites.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_kado.xml b/apps/codeApp/src/main/res/drawable/ic_kado.xml deleted file mode 100644 index 50d1f40ee..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_kado.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_labs.xml b/apps/codeApp/src/main/res/drawable/ic_labs.xml deleted file mode 100644 index 8cbbda012..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_labs.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_launcher_background.xml b/apps/codeApp/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index cd2810fe5..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_launcher_foreground.xml b/apps/codeApp/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index 83a682aa7..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_logo_round_white.xml b/apps/codeApp/src/main/res/drawable/ic_logo_round_white.xml deleted file mode 100644 index 77ca6a6af..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_logo_round_white.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_menu_account.xml b/apps/codeApp/src/main/res/drawable/ic_menu_account.xml deleted file mode 100644 index 675bc7598..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_menu_account.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_menu_buy_kin.xml b/apps/codeApp/src/main/res/drawable/ic_menu_buy_kin.xml deleted file mode 100644 index fed506eef..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_menu_buy_kin.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_menu_debug.xml b/apps/codeApp/src/main/res/drawable/ic_menu_debug.xml deleted file mode 100644 index 9ff7b3f83..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_menu_debug.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_menu_deposit.xml b/apps/codeApp/src/main/res/drawable/ic_menu_deposit.xml deleted file mode 100644 index 88d3e17bc..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_menu_deposit.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_menu_key.xml b/apps/codeApp/src/main/res/drawable/ic_menu_key.xml deleted file mode 100644 index 5b9a32d81..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_menu_key.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_menu_logout.xml b/apps/codeApp/src/main/res/drawable/ic_menu_logout.xml deleted file mode 100644 index 37f717ddb..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_menu_logout.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_menu_phone.xml b/apps/codeApp/src/main/res/drawable/ic_menu_phone.xml deleted file mode 100644 index 83d1c428b..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_menu_phone.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_menu_snowflake.xml b/apps/codeApp/src/main/res/drawable/ic_menu_snowflake.xml deleted file mode 100644 index 3b6d7fc74..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_menu_snowflake.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_menu_switchaccounts.xml b/apps/codeApp/src/main/res/drawable/ic_menu_switchaccounts.xml deleted file mode 100644 index ea4f1544d..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_menu_switchaccounts.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_menu_tip_card.xml b/apps/codeApp/src/main/res/drawable/ic_menu_tip_card.xml deleted file mode 100644 index 4f85302b4..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_menu_tip_card.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_menu_withdraw.xml b/apps/codeApp/src/main/res/drawable/ic_menu_withdraw.xml deleted file mode 100644 index 7f33dff00..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_menu_withdraw.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_notification_request.png b/apps/codeApp/src/main/res/drawable/ic_notification_request.png deleted file mode 100644 index cbb74188d..000000000 Binary files a/apps/codeApp/src/main/res/drawable/ic_notification_request.png and /dev/null differ diff --git a/apps/codeApp/src/main/res/drawable/ic_phone_empty.xml b/apps/codeApp/src/main/res/drawable/ic_phone_empty.xml deleted file mode 100644 index 586a54780..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_phone_empty.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_phone_filled.xml b/apps/codeApp/src/main/res/drawable/ic_phone_filled.xml deleted file mode 100644 index 3afed47ca..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_phone_filled.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_send2.xml b/apps/codeApp/src/main/res/drawable/ic_send2.xml deleted file mode 100644 index 0dcbc8052..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_send2.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_send2_inactive.xml b/apps/codeApp/src/main/res/drawable/ic_send2_inactive.xml deleted file mode 100644 index cb2ffbbc3..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_send2_inactive.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_tip_card.xml b/apps/codeApp/src/main/res/drawable/ic_tip_card.xml deleted file mode 100644 index 5299028d7..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_tip_card.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/apps/codeApp/src/main/res/drawable/ic_wallet.xml b/apps/codeApp/src/main/res/drawable/ic_wallet.xml deleted file mode 100644 index 8dbf7a7ce..000000000 --- a/apps/codeApp/src/main/res/drawable/ic_wallet.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - diff --git a/apps/codeApp/src/main/res/drawable/payment_bill_pattern.webp b/apps/codeApp/src/main/res/drawable/payment_bill_pattern.webp deleted file mode 100644 index 1402df2c9..000000000 Binary files a/apps/codeApp/src/main/res/drawable/payment_bill_pattern.webp and /dev/null differ diff --git a/apps/codeApp/src/main/res/drawable/splash.xml b/apps/codeApp/src/main/res/drawable/splash.xml deleted file mode 100644 index b2cecb2fb..000000000 --- a/apps/codeApp/src/main/res/drawable/splash.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - \ No newline at end of file diff --git a/apps/codeApp/src/main/res/drawable/video_buy_kin_2x.webp b/apps/codeApp/src/main/res/drawable/video_buy_kin_2x.webp deleted file mode 100644 index 0ebf8c2d8..000000000 Binary files a/apps/codeApp/src/main/res/drawable/video_buy_kin_2x.webp and /dev/null differ diff --git a/apps/codeApp/src/main/res/drawable/youtube.webp b/apps/codeApp/src/main/res/drawable/youtube.webp deleted file mode 100644 index 3ff0d3004..000000000 Binary files a/apps/codeApp/src/main/res/drawable/youtube.webp and /dev/null differ diff --git a/apps/codeApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/apps/codeApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 7353dbd1f..000000000 --- a/apps/codeApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/apps/codeApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/apps/codeApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 7353dbd1f..000000000 --- a/apps/codeApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/apps/codeApp/src/main/res/values-ar/strings-localized.xml b/apps/codeApp/src/main/res/values-ar/strings-localized.xml deleted file mode 100644 index d08dbcd61..000000000 --- a/apps/codeApp/src/main/res/values-ar/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - أضف نقودًا عن طريق استخدام بطاقة الخصم - السماح بإتاحة الوصول إلى الكاميرا - السماح بإتاحة الوصول إلى جهات الاتصال - السماح بالإشعارات اللحظية - الرصيد - شراء Kin - اشترِ مزيدًا من عملات Kin - إلغاء - إلغاء الإرسال - الدردشة - اجمع هذه النقود - تأكيد - الربط بحساب X - الاستمرار - تمّ النسخ - نسخ - نسخ العنوان - إنشاء حساب - إنشاء حساب Code جديد - حذف الحساب - تم - قم بالتنزيل الآن - تفعيل خاصية التعرّف على الوجه - تفعيل خاصية التعرّف على بصمة الإصبع - الخروج - أعط - منح الأموال - الدعوة - الدعوات - الانضمام إلى قائمة الانتظار - لاحقًا - تعلّم كيف تشتري Kin - تعلّم كيف تبيع Kin - إرفاق رقم الهاتف - تسجيل الدخول - تسجيل الخروج - أرسل رسالة إلي @getcode للربط - كتم الصوت - التالي - لا, حاول مرة أخرى - ليس الآن - أوافق - فتح الإعدادات - لصق - اللصق من الحافظة - انشر لربط حساب - الوضع في المحفظة - استقبل - استعادة الحساب الموجود بالفعل - التذكير - حذف رقم الهاتف - حذف رقم هاتفك - اطلب الإكرامية - حفظ مفتاح الوصول في \"صوري\" - الحفظ في \"الصور\" - تم الإرسال - إرسال رمز التحقق - مشاركة - مشاركة كعنوان URL - شارك رابط التنزيل - شارك مقطع الفيديو هذا - عرض بطاقة الإكرامية الخاصة بي - بدء تشغيل الكاميرا - اشترك - قم بالتمرير لتسجيل الدخول - مرر للدفع - تخطي إلى الإكرامية - تجربة حساب Code مختلف - غرّد لهم - فتح Code - إلغاء كتم الصوت - إلغاء الاشتراك - تحديث - عرض مفتاح الوصول - سحب الأموال - هل كتبت الاثنا عشرة كلمة بدلاً من ذلك؟ - نعم - نعم, سحب الأموال - نعم, كتبتها - و - الأموال - من العملة - قم بإزالة جميع المعلومات المرتبطة بحسابك من خوادم Code (رقم الهاتف وجهات الاتصال وسجل المعاملات) - يمكنك الوصول إلى حسابك في تطبيقات محفظة العملات المشفرة الأخرى باستخدام عبارة الاسترداد السرية. لن تتمكن من استخدام حسابك في Code - قم بإزالة حسابك من سلسلة الكتل - ما سيفعله الحذف - ما سيحدث - ما لن يفعله الحذف - جمع شخص آخر عملة Kin هذه. - تم جمع وحدات Kin هذه بالفعل من قبل شخص ما. - يُرجى السماح بالوصول إلى الكاميرا في الإعدادات لاستخدام Code. - لقد انتهت صلاحية هذه النقود. - يُرجى إعادة إدخال رقم هاتفك والمحاولة مرة أخرى. - يُرجى السماح بالوصول إلى جهات الاتصال في الإعدادات لإرسال الدعوات. - Code غير متوفر حالياً في بلدك. - لا نستطيع دعم جهازك في الوقت الحالي\n - من المحتمل أن يأتي دعم eSims في إصدار مستقبلي من Code. - لقد حدث خطأ ما. لا يمكن جمع هذه الـ Kin. - لم نتوقع حدوث ذلك. حدث خطأ ما. يُرجى محاولة إنشاء هذا الحساب مرة أخرى. - يُرجى السماح لـ Code بالوصول إلى الصور في الإعدادات من أجل حفظ مفتاح الوصول الخاص بك. - يوجد خطأ ما. يُرجى التأكد من إدخال رقم هاتفك بشكل صحيح.\n - حدث خطأ ما. يُرجى المحاولة مرة أخرى. - فشلت عملية سحب أموالك. حدث خطأ ما, يُرجى محاولة السحب مرة أخرى. - تم تصميم Code للمعاملات الصغيرة اليومية في حدود %1$s أو أقل. - تم تصميم Code للمعاملات الصغيرة اليومية. سيزيد حد العطاء اليومي غدًا. - لمعرفة كيفية الحصول على المزيد من الأموال, انتقل إلى الأسئلة الشائعة في الإعدادات. - يُرجى إدخال رمز دعوة مختلف والمحاولة مرة أخرى. - يُرجى إدخال رقم هاتف صالح والمحاولة مرة أخرى. - بطاقة الإكرامية هذه غير صالحة. - يُرجى إدخال رمز صالح والمحاولة مرة أخرى. - عذرًا, لقد واجهنا مشكلة في الشبكة. يُرجى محاولة دعوة صديقك مرة أخرى. - تمت إعادة عملة Kin هذه تلقائيًا إلى المرسل لأنه لم يتم جمعها في غضون 24 ساعة. من فضلك اطلب منهم إرسال Kin مرة أخرى. - يُرجى إعادة إدخال رقم هاتفك والمحاولة مرة أخرى. - يُرجى محاولة التقاط صورة توضح الرمز بشكل أكثر وضوحاً. - يُرجى التحقق من اتصالك بالإنترنت أو المحاولة مرة أخرى لاحقًا. - Code حاليًا بالدعوة فقط. سنخطرك عند توفر المزيد من الدعوات. - للانضمام إلى قائمة الانتظار, اذهب إلى %1$s - أكبر مبلغ يمكنك الشراء به هو %1$s. يرجى إدخال مبلغ أصغر. - أصغر مبلغ يمكنك الشراء به هو %1$s. يرجى إدخال مبلغ أكبر. - بدأ مفتاح الوصول الخاص بك في إلغاء القفل. نتيجة لذلك, لن تتمكن بعد الآن من استخدام مفتاح الوصول هذا في Code. - أرسل تغريدة إلى هذا الشخص لتفعيل بطاقته للنصائح. - الحد الأقصى الذي يمكنك منح إكرامية به هو %1$s. يُرجى إدخال مبلغ أصغر. - الحد الأدنى الذي يمكنك منح إكرامية به هو %1$s. يُرجى إدخال مبلغ أكبر. - لا يُمكنك إلا إنشاء عدد محدد من الحسابات الجديدة في اليوم. - يقتصر Code حالياً على حساب واحد لكل جهاز. من المحتمل أن تتوفر خدمة دعم حسابات متعددة في إصدار مستقبلي من Code. - يقتصر Code حالياً على حساب واحد لكل رقم هاتف. من المحتمل أن تتوفر خدمة دعم حسابات متعددة في إصدار مستقبلي من Code. - تم جمع Kin بالفعل - تم جمع وحدات Kin بالفعل - مطلوب إتاحة الوصول إلى الكاميرا - انتهت صلاحية النقود - انتهت مهلة رمز التحقق - مطلوب الوصول إلى جهات الاتصال - البلد غير مدعوم\n - الجهاز غير مدعوم\n - eSims غير مدعومة حالياً\n - لقد فشل الجمع - فشلت عملية إنشاء الحساب - فشلت عملية الحفظ - فشل الإرسال - فشل التأكيد - فشلت المعاملة - تم الوصول إلى حد العطاء - تم الوصول إلى الحد اليومي - أموال غير كافية - إن رمز الدعوة غير صالح أو منتهي الصلاحية - رقم الهاتف غير صالح - بطاقة الإكرامية غير صالحة - رمز غير صالح - فشلت الدعوة - انتهت صلاحية الرابط - تم بلوغ الحد الأقصى من المحاولات - لم نعثر على الرمز - لا توجد اتصال بشبكة الإنترنت - لا توجد لديك دعوات - لم تتم دعوتك بعد - مبلغ الشراء كبير جدًا - مبلغ الشراء صغير جدًا - إن مفتاح الوصول لم يعد صالحًا للاستخدام في Code - بطاقة النصائح لم تُفعّل بعد - الإكرامية أكثر من الحد المسموح به - الإكرامية أقل من الحد المسموح به - تم إنشاء حسابات أكثر من المسموح به - تم إنشاء الحساب بالفعل - تم إنشاء الحساب بالفعل - نحن نؤمن بأن المدفوعات ينبغي أن تكون بسيطة وقوية وعالمية. عن طريق البناء باستخدام تقنية blockchain المتقدمة, يوفر Code ميزات لا يمكن لتطبيقات المدفوعات التقليدية القيام بتقديمها, مثل التحويلات العالمية بين الأقران, والمدفوعات الصغيرة التي تفتح مواد فردية عبر الإنترنت, ونصائح بدون رسوم لمنشئي المحتوى المفضلين لديك. - Kin هي عملة مشفرة مثل Bitcoin, ولكنها مصممة كذلك لعمليات الدفع السريعة وغير المكلفة. - مثل Bitcoin, لا يوجد غير كمية محدودة من عملة Kin المتاحة. في حال قام المزيد من الأشخاص بشراء عملة Kin, فإن القيمة ترتفع, وفي حال قام المزيد من الأشخاص ببيع عملة Kin, فإن القيمة تنخفض. تسمح هذه الديناميكية لكل من يمتلك عملة Kin بالمشاركة في خلق القيمة إذا زاد اعتماد وتبني عملة Kin. - يمكنك شراء عملة Kin عن طريق استخدام بطاقة الخصم الخاصة بك. من الممكن الوصول إلى هذا في علامة التبويب Get Kin. - نعم يمكنك ذلك. يتم دعم بيع عملة Kin في عدد من منصات تداول العملات المشفرة. - توجد هناك 3 طرق أساسية يمكنك من خلالها القيام بالمساعدة: التحدث عن تجربتك على Code على وسائل التواصل الاجتماعي, وتشجيع أصدقائك على تجربة Code بأنفسهم, وتشجيع مواقع الويب المفضلة لديك على دمج مدفوعات Code عن طريق مطالبتهم بالاطلاع على [getcode.com](https://getcode.com) - ما هو Code؟ - لماذا تكون مدفوعات Code مقومة بـ Kin؟ - لماذا تتغير قيمة عملة Kin؟ - كيف يمكنني شراء المزيد من عملة Kin؟ - هل يمكنني بيع عملة Kin؟ - كيف يمكنني المساعدة؟ - توافق على - بالنقر على \"إنشاء حساب\" أو \"تسجيل الدخول\", فأنت - تُستخدم الكاميرا الخاصة بك لاستقبال الأموال. يُرجى السماح بالوصول إلى الكاميرا للمتابعة. - نحتاج إلى الإشعارات اللحظية لإرسال معلومات حول حسابك في الوقت المناسب. - عمليات السحب لا رجعة فيها ولا يُمكن التراجع عنها بمجرد بدئها. - سيتم فقد جميع الأموال في هذا الحساب. إن حذف حسابك أمر نهائي ولا يُمكن التراجع عنه. هل أنت متأكد أنك تودّ حذف هذا الحساب؟ - سيتم إرجاع أي Kin لم يتم تحصيله خلال 24 ساعة إلى رصيدك تلقائيًا. - ستحتاج إلى إعادة إنشاء الحساب والتحقق من رقم هاتفك مرة أخرى. - يُمكنك العودة إلى هذا الحساب باستخدام مفتاح الوصول الخاص بك - لن يتم إعلامك بأي رسائل جديدة من %1$s. يمكنك إلغاء كتم الصوت في أي وقت. - يتم دعم الحسابات التي تم إنشاؤها من خلال Code فقط حاليًا. - لن يتمكن أصدقاؤك بعد الآن من العثور عليك باستخدام رقم الهاتف هذا. - سيتم إعلامك بجميع الرسائل الجديدة من %1$s. يمكنك كتم الصوت في أي وقت. - لن تتلقى أي رسائل من %1$s حتى تدفع مرة أخرى. - يمنحك مفتاح الوصول حق الوصول إلى حساب Code الخاص بك. حافظ على خصوصيته وأمنه. - هذه الكلمات الاثنا عشر هي الطريقة الوحيدة لاستعادة حساب Code الخاص بك. تأكد من كتابتها, وحافظ على خصوصيتها وأمنها. - هل أنت متأكد؟ - هل أنت متأكد أنك تودّ حذف هذا الحساب؟ - هل أرسلت الرابط؟ - هل أنت متأكد أنك تودّ الخروج؟ - هل أنت متأكد أنك تريد تسجيل الخروج؟ - هل تريد كتم الصوت في %1$s؟ - هذا ليس حساب في Code - هل أنت متأكد؟ - هل تريد إلغاء كتم الصوت في %1$s؟ - هل تريد إلغاء الاشتراك في %1$s؟ - هل تودّ عرض مفتاح الوصول الخاص بك؟ - هل أنت متأكد؟ - تم إيداع %1$s وحدات Kin في حسابك. - لم يتم تجميع %1$s التي أرسلتها أمس. تم إرجاعه تلقائيًا إلى رصيدك. - احصل على صديق بدأ على Code واحصل على 5 دولارات - أرسل النقود من خلال أي تطبيق messenger - لقد تلقيت %1$s وحدات Kin لإرسال وحدات Kin الأولى لشخص ما. - يمكنك الآن طلب النصائح.\n - تم استلام الإيداع - وحدات Kin المرتجعة - جديد في Code - تم استلام مكافأة الإحالة - حساب X متصل\n - انتهت صلاحية دعوتك للوصول إلى الرمز. يُمكنك تسجيل الخروج واستخدام حساب مختلف بدعوة حالتها صالحة. - مفتاح الوصول الخاص بك هو الطريقة الوحيدة للوصول إلى أموالك. يُرجى أن تحافظ على خصوصيته وأمنه. - التحقق من هويتك لعرض مفتاح الوصول الخاص بك. - اضغط على أيقونة Google Lens لفتح رمز الاستجابة السريعة لتسجيل الدخول إلى Code. ويُمكنك بدلاً من ذلك تسجيل الدخول يدويًا عن طريق إدخال 12 كلمة في شاشة تسجيل الدخول في Code. - تنبيه! تتيح لك هذه الصورة الوصول إلى جميع الأموال التي لديك في Code. لا تشارك هذه الصورة مع أي شخص آخر. احتفظ بها في مكان آمن. - يُتيح لك Code تلقي الأموال من خلال توجيه الكاميرا إلى الفاتورة الرقمية على هاتف مستخدم آخر - يجب عليك السماح بالوصول إلى الكاميرا لتتمكن من استقبال الأموال - قم بالمصادقة للوصول إلى Code. - شراء Kin - قم بشراء Kin (مُتاحة قريبًا) - يُعد شراء وبيع Kin حاليًا عملية معقدة. ستصبح هذه العمليات أسهل بمرور الوقت. إذا كنت تريد معرفة كيفية شراء وبيع Kin اليوم, فيمكنك مشاهدة مقاطع الفيديو أدناه.\n - يمكنك إعطاء ما يصل إلى %1$s فقط - يمكنك منح إكرامية تصل إلى %1$s فقط - عملة Kin الخاصة بك متوفرة الآن للاستخدام في تطبيق Code لديك - لقد أرسلت إلى شخص ما أول Kin له! إليك مكافأة الإحالة الخاصة بك: - جارٍ تحويل عملة الدولار الأمريكي الخاصة بك إلى عملة Kin. سوف يستغرق هذا الأمر دقيقة واحدة تقريبًا حتى يكتمل - لقد أودعت بنجاح عملة الدولار الأمريكي. افتح تطبيق Code لاستكمال عملية الشراء - مكافأة ترحيبية - اختر بلدًا - قريباً - \@getcode أرغب في ربط حسابي على X حتى أتمكن من تلقي النصائح من أشخاص في جميع أنحاء العالم - حذف - تأكد من حفظ عبارة الاسترداد السرية الخاصة بك, ثم أدخل \"حذف\" لحذف حساب Code الخاص بك. لا يمكن الرجوع في هذا الإجراء. - ألم تتلقى رمزًا على %1$s؟ - ألم تتلقى الرمز؟ إعادة الإرسال - يتطلب تعطيل خاصية بصمة الوجه التحقق من هويتك. - ألا يوجد لديك تطبيق المحفظة Code؟ - لا يوجد لديك أموال بعد. - قم بتفعيل خاصية بصمة الوجه لزيادة تعزيز أمان المعاملة في Code. - أدخل عنوان الوجهة - أدخل ما يصل إلى %1$s - احصل على 5 دولارات من عملة الـ Kin مجانًا عندما يقوم صديق بالتسجيل في Code وترسل له أول Kin. - يستخدم Code العملة المشفرة Kin للمدفوعات. إليك بعض الطرق للحصول على المزيد من Kin في محفظة Code الخاصة بك. - احصل على أول 1 دولار لك من وحدات Kin مجانًا - التحقق من هويتك لمنح الأموال. - قم بإيداع الأموال في محفظة Code الخاصة بك عن طريق إرسالها إلى عنوان الإيداع الخاص بك أدناه. انقر للنسخ. - يُرجى الحصول على المزيد من Kin ثم محاولة الدفع مرة أخرى - حساب المستلم غير صحيح - يرجى التأكد من أن العنوان الذي تقوم بالسحب إليه قد تمت تهيئته بواسطة مزود المحفظة الخاص بك. هناك طريقة مختصرة لتحقيق ذلك وهي استبدال مبلغ صغير من SOL بعملة Kin في المحفظة التي تحاول الإرسال إليها. - رمز الدعوة - الوصول إلى Code حاليًا بالدعوة فقط. ستحتاج إلى رمز دعوة للوصول إلى التطبيق. - %1$d دعوات - Code هو تطبيق محفظة عملات مشفرة جديد للدعوات فقط حاليًا. لتنزيل Code, انتقل إلى %1$s - معرفة المزيد - رقم هاتفك مُرفق بحساب Code هذا. يُمكن لأصدقائك العثور عليك باستخدام رقم الهاتف هذا. - أربط حساب X الخاص بي بـ @getcode حتى أتمكن من تلقي الإكراميات من الأشخاص في جميع أنحاء العالم. - تحميل رصيدك وسجل معاملاتك - تحقق من صورك بحثًا عن مفتاح الوصول الذي حفظته عند إنشاء حسابك لأول مرة. - أنت حاليًا مسجل الدخول في حساب. يُرجى التأكد من حفظ مفتاح الوصول الخاص بك قبل المتابعة. هل ترغب في تسجيل الخروج والدخول بحساب جديد؟ - رقم الهاتف\nغير مُرفق - ليس لديك رقم هاتف مُرفق بحساب Code هذا. أرفق واحدًا حتى يتمكن أصدقاؤك من العثور عليك. - لا يوجد اتصال بالشبكة - في Code - افتح تطبيق Code ووجه الكاميرا للحصول على هذه النقود - تنظيم جهات الاتصال الخاصة بك - لا يوجد رقم الهاتف هذا في جهات الاتصال الخاصة بك. لا يزال بإمكانك دعوتهم إلى Code. - أدخل رقم هاتفك متضمنًا رمز البلد. تأكّد من استخدام نفس رقم الهاتف الذي تلقّى الدعوة. - مشغّل بواسطة - يمكنك الآن طلب الإكراميات - تم الحصول على مكافأة الإحالة - إرسال %1$s - إليك %1$s - طلب واحد جديد في خلال %1$s - امسح رمز الاستجابة السريعة هذا ضوئيًا باستخدام كاميرا هاتفك لتنزيل تطبيق المحفظة Code - قم بالمسح الضوئي لتنزيل\nتطبيق المحفظة Code - البحث عن العملات - البحث عن جهات الاتصال - تم إرسال رسالة نصية قصيرة بها رمز التحقق إلى رقم هاتفك. يُرجى إدخال رمز التحقق أعلاه. - أرسل إليك شخص ما النقود - منحك شخص ما إكرامية - أنت بحاجة إلى تشغيل الكاميرا لمسح الرموز ضوئيًا - تتيح لك بطاقة الإكرامية الخاصة بك تلقي الإكراميات من مستخدمي Code في جميع أنحاء العالم. للوصول إلى بطاقة الإكرامية الخاصة بك, قم بتوصيل هوية X الخاصة بك. - تتيح لك بطاقة النصائح تلقي نصائح من مستخدمي Code في جميع أنحاء العالم. للوصول إلى بطاقة النصائح انشر على X. - تتيح لك بطاقة الإكرامية الخاصة بك تلقي الإكراميات من مستخدمي Code في جميع أنحاء العالم. للوصول إلى بطاقة الإكرامية الخاصة بك أرسل رسالة إلي @getcode على منصة X. - اسمح لـ Code بإرسال إشعارات إليك عندما تتلقى نصائح من مستخدمي Code الآخرين. - اكتب \"%1$s\" - لقد أجرينا بعض التغييرات لتحسين التجربة. ستحتاج إلى تحديث التطبيق للاستمرار في استخدام Code. - حساب المالك صحيح - حساب الرمز صحيح - قيمة تغيير الأموال. - تم إرجاعه إليك - إلى أين تريد سحب أموالك؟ - التحقق من هويتك لسحب الأموال. - لقد أودعت - لقد منحت - لديك %1$d دعوات - إن Code بالدعوة فقط في الوقت الحالي. متبقي لديك %1$d. - لقد دفعت - لقد منحت - لقد تلقيت - لقد أرسلت - لقد أنفقت - لقد منحك أحد ما إكرامية - لقد سحبت - تم سحب أموالك بنجاح. - لقد تم ربط حساب X الخاص بك بحساب Code الخاص بك بنجاح. يمكنك الآن طلب الإكراميات. - نجحت عملية السحب - تم ربط حساب X بنجاح - انتهت صلاحية إتاحة الوصول - مفتاح الوصول - إعدادات التطبيق - التشغيل التلقائي للكاميرا - الرصيد - علامات ميزات البرنامج - المكافأة - اشترِ وبع Kin - مدفوعات نقدية - فريق Code - عمليات شراء Kin - المدفوعات عن طريق الويب - الإكراميات - خيارات إصلاح الأخطاء - إيداع الأموال - المودعة - إدخال كلمات مفتاح الوصول - أدخل رقم الهاتف - فشلت العملية - الأسئلة الشائعة - الممنوحة - احصل غلى نقود - احصل على صديق بدأ في Code - احصل على Kin - احصل على المزيد من Kin - أعط النقود - منح الأموال - الرصيد غير كافٍ - دعوة صديق - عرض لفترة محدودة - المُرفق - العملة المحلية - حسابي - غير مُرفق - عملات أخرى - تم الدفع - قيد الانتظار - رقم الهاتف - سياسة الخصوصية - تم الشراء - تم ربط حساب X بنجاح - تلقي النصائح - المستلمة - العملات الأخيرة - ارسل طلب إحالة لصديق, واحصل على 5 دولارات - مكافأة الإحالة - طلب نقود - اطلب Kin - اطلب إكرامية - مطلوب Face ID\n\n - مطلوب رمز المرور - مطلوب مستشعر Touch ID - النتائج - تم الإرجاع - حدد حسابًا - حدد بلدًا - اختر عملة - تم الإرسال - النقود المصروفة - تبديل الحسابات - شروط الخدمة - بطاقة Tip - إكرامية Kin - تشغيل الإشعارات لـ Code - غير معروف - التحديث مطلوب - التحقق من رقم الهاتف - مكافأة ترحيبية - سحب الأموال - المسحوبة - مفتاح الوصول الخاص بك - انقر فوق الشعار لمشاركة رابط تنزيل التطبيق - diff --git a/apps/codeApp/src/main/res/values-bg/strings-localized.xml b/apps/codeApp/src/main/res/values-bg/strings-localized.xml deleted file mode 100644 index 5eac2ee21..000000000 --- a/apps/codeApp/src/main/res/values-bg/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Добавете парична сума с дебитна карта - Разреши достъп до камерата - Разреши достъп до контактите - Разреши насочени известия - Баланс - Купи Kin - Закупете повече Kin - Откажи - Отмени изпращането - Чат - Вземете парите - Потвърди - Свържи се с Х - Продължи - Копирано - Копирай - Копирай адрес - Създаване на профил - Създай нов кодов акаунт - Изтрий профил - Готово - Изтеглете ги сега - Разрешете лицево разпознаване - Разрешете разпознаване чрез докосване - Изход - Дайте - Задай член на семейството - Покани - Покани - Включване в изчакващи - По-късно - Научете как да купувате Kin - Научете как да продавате Kin - Свързване на телефонен номер - Вход - Изход - Изпратете съобщение @getcode за свързване - Изключване на звука - Следващ - Не, опитайте отново - Не сега - ОК - Отваряне на настройки - Постави - Постави от клипборда - Публикувайте за да свържете акаунт - Постави в портфейл - Получете - Възстанови съществуващ профил - Напомняне - Премахване на телефонен номер - Премахна твоя телефонен номер - Поискай бакшиш - Съхрани ключа за достъп до моите снимки - Съхрани в снимки - Изпращане - Изпрати код за потвърждение - Сподели - Споделете като URL - Споделете линк за сваляне - Споделете този видео клип - Покажи моята карта за бакшиши - Стартирайте камерата - Абонирай се - Плъзнете за влизане - Плъзнете, за да платите - Плъзни за даване на бакшиш - Опитай различен кодов акаунт - Изпратете им туит - Код за отключване - Включване на звука - Отписване - Обнови - Преглед на ключа за достъп - Оттегли член на семейството - Записахте ли 12 думи вместо това? - Да - Да, оттегли член на семейството - Да, записах ги - и - Член на семейството - в Kin - Премахване на цялата информация свързана с вашия профил от сървърите на Код (телефонен номер, контакти, история на транзакциите) - Можете да получите достъп до своя профил чрез други платформи за крипто портфейли като използвате своята Тайна фраза за възстановяване. Няма да можете да използвате профила си в Код - Премахване на вашия профил от блокчейна - Какво ще направи изтриването - Какво ще се случи - Какво няма да направи изтриването - Тези средства в Kin вече са събрани от някой друг - Този Kin вече е бил събран от някого. - Моля, разрешете достъп до камерата в настройките, за да използвате код. - Тези парични средства са с изтекъл срок. - Моля, въведете телефонния номер отново и опитайте отново. - Моля, разрешете достъпа до контактите чрез Настройки, за да изпращате покани. - В момента Code не е наличен във вашата страна. - В момента не можем да поддържаме вашето устройство - Поддръжката за eSim вероятно ще дойде в бъдеща версия на Code. - Нещо се обърка. Този Kin не може да бъде събран. - Неочаквано развитие. Нещо се обърка. Моля, опитайте да създадете профила отново. - Моля, разрешете достъп на кода до снимки в настройките, за да може да съхраните вашия ключ за достъп. - Нещо се обърка. Моля, уверете се, че вашият телефонен номер е въведен правилно. - Нещо се обърка. Моля, опитайте отново по - късно. - Неуспешно изтегляне на вашите средства. Нещо се обърка, моля, опитайте отново. - Код е създаден за малки и ежедневни транзакции, които са за %1$s или по-малко. - Код е създаден за малки и ежедневни транзакции. Вашият дневен лимит за даване ще се увеличи утре. - За повече информация как да получите повече роднини отидете на често задавани въпроси в настройките. - Моля, въведете различна покана Code и опитайте отново. - Моля, въведете валиден телефонен номер и опитайте отново. - Това е невалидна карта за бакшиши. - Моля, въведете валиден код за достъп и опитайте отново. - Съжаляваме, но имаме проблем с връзката. Моля, опитайте да поканите вашия приятел отново. - Тези средства в Kin бяха автоматично върнати на подателя, защото не бяха взети в рамките на 24 часа. Моля, помолете ги да ги изпратят отново. - Моля, въведете отново вашия телефонен номер и опитайте отново. - Моля, опитайте да вземете изображение, което показва кода по-ясно. - Моля, проверете вашата връзка с Интернет или опитайте отново по - късно. - Кодът временно е само с покана. Ще бъдете известени, когато има налични покани. - Отидете на %1$s, за да се включите в изчакващи. - Максималното количество, което можете да купите, е %1$s. Моля, въведете по-ниска сума. - Минималното количество, което можете да купите, е %1$s. Моля, въведете по-голяма сума. - Вашият ключ за достъп вече е активирал отключване. В резултат на това вече няма да можете да използвате този ключ за достъп в Code. - Изпратете туит до този човек, за да активирате неговата карта за бакшиши. - Максималната сума, която можете да изпращате като бакшиш, е %1$s. Моля, въведете по-малка стойност. - Минималната сума, която можете да изпратите като бакшиш, е %1$s. Моля, въведете по-голяма стойност. - Може да създавате само определен брой нови профили на ден. - В момента Code има ограничение до един акаунт на устройство. Вероятно бъдеща версия на Code ще включва поддръжката на няколко акаунта. - В момента Code има ограничение до един акаунт на телефонен номер. Вероятно бъдеща версия на Code ще включва поддръжката на няколко акаунта. - Средствата вече са събрани - Kin вече е събран - Необходим е достъп до камерата - Паричните средства са с изтекъл срок - Изтекъл код код потвърждение - Изисква се достъп до Контакти - Държавата не се поддържа - Устройството не се поддържа - eSims не се поддържат в момента - Неуспешно събиране - Неуспешно създаване на профил - Неуспешно запазване - Неуспешно изпращане - Неуспешно потвърждение - Неуспешна транзакция - Достигнат лимит за Даване - Достигнат дневен лимит - Недостатъчни роднини - Поканата Code е невалидна или изтекла - Невалиден телефонен номер - Невалидна карта за бакшиши - Невалиден код - Неуспешна покана - Връзката е изтекла - Максималният брой опити е достигнат - Не е намерен код - Нямате връзка с Интернет - Нямате покани - Все още нямате покана - Покупката е твърде голяма - Покупката е твърде малка - Ключът за достъп вече не е използваем в Code - Картата за бакшиши все още не е активирана - Баккшишът е прекалено голям - Бакшишът е прекалено малък - Твърде много създадени профили - Този акаунт вече е създаден - Този акаунт вече е създаден - Вярваме, че плащанията трябва да бъдат опростени, мощни и глобални. Чрез изграждане с усъвършенствана блокчейн технология, Code предлага функции, които традиционните приложения за плащания не могат, като глобални равноправни трансфери, микроплащания, които отключват отделни статии онлайн, и бакшиши с нулеви такси за вашите любими творци. - Kin е криптовалута като биткойн, но също така е предназначена за бързи и евтини плащания. - Подобно на биткойн, има само ограничено количество налични Kin. Ако повече хора купуват Kin, стойността се повишава, а ако повече хора продават Kin, стойността намалява. Тази динамика позволява на всеки, който притежава Kin, да участва в създаването на стойност, ако приемането на Kin расте. - Можете да закупите Kin с вашата дебитна карта. Това е достъпно в раздела Get Kin. - Да, можете. Продажбата на Kin се поддържа от редица борси за криптовалута. - Има три основни начина, по които можете да помогнете: говорете за опита си с Code в социалните медии, насърчете приятелите си да изпробват Code сами и насърчете любимите си уебсайтове да интегрират плащания с Code, като ги помолите да разгледат [getcode.com](https://getcode.com). - Какво е код? - Защо плащанията на Code са деноминирани в Kin? - Защо стойността на Kin се променя? - Как мога да купя повече Kin? - Мога ли да продам Kin? - Как мога да помогна? - се съгласявате с нашите - С натискане на създаване на профил\" или \"вход\" вие - Вашата камера се използва за получаване на роднини. Моля, разрешите достъп до камерата, за да продължите\n - За да получавате информация за вашия профил навреме са необходими насочени известия. - Тегленето на средства е необратимо и не може да бъде отменено след като е направено.. - Всички средства в този профил ще бъдат изгубени. Изтриването на вашия профил е постоянно и да може да бъде върнато. Сигурни ли сте, че искате да изтриете този профил? - Всеки Kin, който не бъде събран в рамките на 24 часа, ще бъде автоматично върнат във вашата сметка. - Необходимо е да започнете отново създаването на профил и да потвърдите вашия телефонен номер отново. - Можете да влезете отново в този профил с вашия ключ за достъп - Няма да бъдете известявани за нови съобщения от %1$s. Можете да премахнете заглушаването по всяко време. - В момента се поддържат само профили, създадени с код. - Вашите приятели няма да могат да ви откриват повече с този телефонен номер. - Ще бъдете известявани за нови съобщения от %1$s. Можете да активирате заглушаване по всяко време. - Няма да полчавате съобщения от %1$s докато не им платите отново. - Вашият ключ за достъп ще ви предостави достъп до кода на вашия профил. Съхранявайте го в безопасност. - Тези 12 думи са единствения начин да възстановите вашия код на профил. Уверете се, че сте ги записали и ги съхранявате добре. - Сигурни ли сте? - Сигурни ли сте, че искате да изтриете този профил? - Изпратихте ли връзката? - Сигурни ли сте, че искате да излезете? - Сигурни ли сте, че искате да излезете? - Заглушаване на %1$s? - Не е код на профил - Сигурни ли сте? - Премахни заглушаването на %1$s? - Отписване от абонамент към %1$s? - Прегледайте вашия ключ за достъп? - Сигурни ли сте? - %1$s Kin бяха депозирани във вашата сметка. - %1$s, който изпратихте вчера не е събран. Той автоматично беше върнат във вашата сметка. - Поканете приятел да се присъедини към Code и вземете $5 - Изпратете пари в брой през произволно приложение за съобщения - Получихте %1$s от Kin за изпращане на първи Kin към някого. - Вече може да искате бакшиши. - Депозитът е получен - Върнат Kin - Ново в Code - Получен бонус за препоръка - Акаунтът в социалната мрежа X е свързан - Вашата покана за код за достъп е изтекла. Можете да излезете и да използвате друг профил с валидна покана. - Вашият ключ за достъп е единственият начин за достъп до вашите сметки. Моля, съхранявате ги поверително и в безопасност. - Потвърдете вашата идентичност, за да видите вашия ключ за достъп. - Докоснете иконата на Google Lens, за да отворите QR кода, за да влезете в Code. Като алтернатива можете да влезете ръчно, като въведете 12-те думи на екрана за влизане с код. - Внимание! Това изображение дава достъп до всички средства, с които разполагате в Code. Не споделяйте това изображение с никой друг. Съхранявайте го на сигурно и безопасно място. - Кодът ви позволява да получавате член на семейството като насочите камерата към дигиталната касова бележка на телефона на друг потребител - Трябва да позволите достъп до камерата, за да получавате член на семейството - Удостоверете се за достъп до кода. - Купете Kin - Купете Kin (Очаквайте скоро) - Купуването и продажбата на Kin в момента е сложен процес. Тези процеси ще се опростят с времето. Ако искате да научите как да купувате и продавате Kin днес, можете да гледате видео клиповете по - долу. - Можеш да даваш само до %1$s - Можете да давате бакшиши само до %1$s - Вашият Kin вече е наличен за употреба в приложението Code - Вие изпратихте на някого първия Кин! Ето вашия бонус за препоръка: - Вашия USDC се конвертира в Kin. Това би трябвало да отнеме около една минута - Успешно депозирахте USDC. Отворете приложението Code, за да извършите покупката си - Бонус за донре дошли - Изберете държава - Предстоящо - \@getcode Искам да свържа моя акаунт в X, за да мога да получавам съвети от хора по цял свят - Изтриване - Уверете се, че сте запазили своята Тайна фраза за възстановяване и след това въведете \"Изтриване\", за за изтриете своя профил в Код. Това действие е необратимо. - Не получихте код на %1$s? - Не сте получили кодът? Повторно изпращане - За да деактивирате лицевата идентификация трябва да потвърдите вашата идентичност. - Нямате приложението Code Wallet? - Все още нямате зададен член на семейството - Активирайте лицева идентификация, за да засилите сигурността на транзакцията с код. - Въведи адрес на дестинация - Въведи до %1$s - Вземете $5 Kin безплатно, когато ваш приятел се присъедини към Code и му изпратите първият му Kin. - Code използва крипто валутата Kin за плащания. Ето няколко начина да получите повече Kin във вашия портфейл с код. - Вземете първия си $1 в Kin безплатно - Потвърдете вашата идентичност, за да зададете член на семейството. - Депозирайте член на семейството към вашия кодиран портфейл като изпратите член на семейството към вашия адрес на депозиране по - долу. Натиснете, за да копирате. - Моля, вземете повече Kin и след това опитайте да платите отново - Невалидна сметка на получателя - Моля, уверете се, че адресът, на който теглите, е инициализиран от Вашия доставчик на портфейл. Пряк път за постигане на това е първо да размените малко количество SOL за Kin в портфейла, до който се опитвате да изпратите. - Покана Code - Code към момента е само с покани. Ще ви е необходима покана Code, за да получите достъп до приложението. - %1$d покани - Code е ново приложение крипто портфейл, което в момента е само с покана. За да изтеглите Code, отидете на %1$s. - Научи повече - Вашият телефонен номер е свързан с този код на профил. Приятелите могат да те откриват чрез телефонния номер. - Свързвам своя профил в X с @getcode, така че да получавам бакшиши от хора по целия свят. - Зареждане на баланса и история на транзакциите ви - Проверка на вашите снимки за ключ за достъп, който сте съхранили при първоначалното създаване на профил. - В момента сте влезли в акаунт. Моля, уверете се, че сте запазили своя ключ за достъп, преди да продължите. Искате ли да излезете и да влезете с нов акаунт? - Няма свързани телефонни номера - Нямате телефонни номера, свързани с този код на профил. Свържи телефонен номер, за да може вашите приятели да те открият. \n - Няма мрежова връзка - С код - Отворете приложението Code и насочете камерата си, за да вземете тези пари - Организиране на вашите контакти - Този телефонен номер не е във вашите Контакти. Все пак можете да го поканите в Код. - Въведете вашия телефонен номер заедно с кода на държавата. Моля, използвайте същия телефонен номер, който е получил поканата. - Предоставено от - Сега може да изисквате бакшиши - Бонусът за препоръка е получен - Изпращане на %1$s - Вижте %1$s - Заявете нов код в %1$s - Сканирайте QR кода с камерата на телефона си, за да изтеглите приложението Code Wallet - Сканирайте, за да изтеглите\nПриложението Code Wallet - Търсене на валути - Търсене в контакти - До вашия телефонен номер беше изпратен SMS с код за потвърждение. Моля, въведете кода за потвърждение по-горе. - Някой ви е изпратил пари - Някой ви изпрати бакшиш - Трябва да стартирате камерата си, за да сканирате кодове - Вашата карта за бакшиши ви позволява да получавате бакшиши от потребители на Code от целия свят. За достъп до вашата карта за бакшиши, свържете своята идентичност в Х. - С вашата карта за бакшиши може да получавате подаръци от потребители на Code по цял свят. За достъп до картата за бакшиши публикувайте в социалната мрежа X. - Твоята карта за средства ти позволява да получаваш средства от потребители на Code по целия свят. За достъп до съобщението за картата за средства въведи @getcode в X. - Позволете на Code да Ви изпраща известия, когато получавате съвети от други потребители на Code. - Напишете \"%1$s\" - Направихме някои промени, за да подобрим качеството. Трябва да подновите приложението, за да използвате кода. - Валидна сметка на собственика - Валидна жетон сметка - Стойност на промените на член на семейството - Ви бяха върнати - До къде искате да изтеглите вашия роднина? - Потвърдете вашата идентичност, за да оттеглите член на семейството. - Депозирахте - Дадохте - Имате %1$d покани - Кодът е само за текуща покана. Имате %1$d оставащи. - Платихте - Дадохте - Вие получихте - Изпратихте - Изхарчихте - Изпратихте бакшиш на някого - Изтеглихте - Вашите средства бяха изтеглени успешно. - Вашият профил в Х беше свързан успешно с вашия профил в Code. Сега можете да изисквате бакшиши. - Успешно изтегляне на средства. - Профилът в Х е свързан успешно - Изтекъл достъп - Ключ за достъп - Настройки на приложението - Автоматично стартиране на камерата - Баланс - Бета флагчета - Бонус - Купувайте и продавайте Kin - Парични плащания - Екипът на Code - Kin покупки - Уеб плащания - Бакшиши - Опции за отстраняване на грешки - Задаване на член на семейството - Депозирано - Въведи ключ за достъп - Въведи телефонен номер - Неуспешно - Често задавани въпроси - Даде - Вземете парична сума - Вземете \"Приятел се присъедини\" към Code - Получаване на средства в Kin - Получавайте повече Kin - Дайте кеш - Посочете член на семейството - Недостатъчна наличност - Покани приятел - Оферта с ограничение във времето - Свързано - Местна валута - Моят профил - Не е свързан - Други валути - Платено - Изчакване - Телефонен номер - Политика за поверителност - Закупено - Профилът в Хе свързан успешно - Получаване на съвети - Получено - Последно използвани валути - Препоръчай на приятел, вземи $5 - Бонус за препоръка - Направете заявка за парична сума - Поискай Kin - Поискай бакшиш - Изискване на Face ID - Изискване на парола - Изискване на Touch ID - Резултати - Върнато - Избери профил - Избери държава - Избор на валута - Изпратено - Похарчено - Смени акаунти - Условия на ползване - Карта за бакшиши - Бакшиш Kin - Включете известия за Code - Неизвестен - Необходимо е обновяване - Потвърди телефонен номер - Бонус за \"Добре дошли\" - Премахни член на семейството - Оттегли - Вашия ключ за достъп - Докоснете логото, за да споделите връзката за изтегляне на приложението - diff --git a/apps/codeApp/src/main/res/values-cs/strings-localized.xml b/apps/codeApp/src/main/res/values-cs/strings-localized.xml deleted file mode 100644 index a0695102f..000000000 --- a/apps/codeApp/src/main/res/values-cs/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Přidání hotovosti pomocí debetní karty - Umožnit přístup fotoaparátu - Povolit přístup ke kontaktům - Povolit zprávy push - Zůstatek - Koupit Kin - Kupte další Kiny - Zrušit - Zrušit odeslání - Chat - Vybrat tuto hotovost - Potvrdit - Připojení k X - Pokračovat - Zkopírováno - Kopírovat - Zkopírovat adresu - Vytvořit účet - Vytvořit účet s jiným kódem - Vymazat účet - Hotovo - Stáhněte si ji nyní - Povolit Face ID - Povolit Touch ID - Odejít - Dát - Darovat kin - Přizvat - Pozvánky - Přidat se na čekací listinu - Později - Zjistěte, jak kupovat Kin - Zjistěte, jak prodávat Kin - Odkázat na telefonní číslo - Přihlásit se - Odhlásit se - Zpráva @getcode za účelem propojení - Ztlumit - Dále - Ne, zkusit znovu - Teď ne - OK - Otevřít nastavení - Vložit - Vložit z vývěsky - Zveřejnit příspěvek pro připojení účtu - Vložit do peněženky - Přijmout - Obnovit stávající účet - Připomenout - Odstranit telefonní číslo - Odstranit vaše telefonní číslo - Požádat o spropitné - Uložit přístupový klíč k mým snímkům - Uložit do snímků - Odeslat - Zaslat ověřovací kód - Sdílet - Sdílet jako adresu URL - Sdílet odkaz stažení - Sdílet toto video - Ukázat kartu spropitného - Spustit fotoaparát - Přihlaste se k odběru - Přihlaste se přejetím prstu - Zaplaťte potažením - Přejeďte prstem pro poslání spropitného - Vyzkoušet účet s jiným kódem - Tomuto člověku tweetněte - Odemknout Code - Zrušit ztlumení - Zrušit odběr - Aktualizovat - Zobrazit přístupový klíč - Vybrat kin - Zapsal/a jste si místo toho 12 slov? - Ano - Ano, vybrat kin - Ano, zapsal/a jsem si je - a - kin - kinu - Odstranit všechny informace spojené s vaším účtem ze serverů společnosti Code (telefonní číslo, kontakty, historie transakcí). - Pomocí tajné fráze pro obnovení můžete získat přístup ke svému účtu v jiných aplikacích kryptopeněženky. Svůj účet nebudete moci používat v aplikaci Code - Odebrání účtu z blockchainu - Co způsobí odstranění - Co se stane - Co odstranění nezpůsobí - Tento Kin už získal někdo jiný. - Tento Kin již byl někým vybrán. - Abyste mohli využívat kód, povolte v nastavení přístup k fotoaparátu. - Těmto prostředkům vypršela platnost. - Zadejte znovu telefonní číslo a zkuste to znovu. - Povolte přístup ke kontaktům v Nastavení pro odesílání pozvánek. - Aplikace Code momentálně není dostupná ve vaší zemi. - V tuto chvíli nemůžeme podporovat vaše zařízení - Podpora pro eSim se pravděpodobně objeví v některé z budoucích verzí Code. - Něco se pokazilo. Tento Kin se nepodařilo sesbírat. - Toto jsme nečekali. Nastala chyba. Zkuste tento účet vytvořit znovu. - Povolte kódu přístup ke snímkům v nastavení a tím si uložte přístupový klíč. - Došlo k chybě. Zkontrolujte, zda je telefonní číslo zadáno správně. - Nastala chyba. Zkuste to znovu. - Vaše peníze se nepodařilo vybrat. Nastala chyba, zkuste provést výběr znovu. - Code je určen pro malé každodenní transakce, které jsou za %1$s nebo méně. - Code je určen pro každodenní drobné transakce. Váš denní limit se zítra navýší. - V častých dotazech v nastavení zjistíte, jak získat více kinu. - Zadejte prosím jiný kód pozvánky a zkuste to znovu. - Zadejte prosím platné telefonní číslo a zkuste to znovu. - Toto je neplatná karta spropitného - Zadejte platný kód a zkuste to znovu. - Litujeme, ale nastal problém v síti. Zkuste přítele přizvat znovu. - Tento druh byl automaticky vrácen odesílateli, protože nebyl vyzvednut do 24 hodin. Požádejte je, aby Kin poslali znovu. - Zadejte znovu telefonní číslo a zkuste to znovu. - Zkuste získat snímek, který zobrazuje kód jasněji. - Zkontrolujte připojení k internetu nebo to zkuste později. - Kód momentálně funguje jen na přizvání. Jakmile bude k dispozici více pozvánek, dáme vám vědět. - Pokud se chcete zapsat na čekací listinu, jděte na %1$s - Nakoupit můžete maximálně %1$s. Zadejte prosím menší částku. - Nakoupit můžete minimálně %1$s. Zadejte prosím větší částku. - Váš přístupový klíč inicioval odemknutí. V důsledku toho již nebudete moci tento přístupový klíč v Code používat. - Pošlete této osobě tweet a její Tip Card se aktivuje. - Maximální výše spropitného je %1$s. Zadejte prosím menší částku. - Minimální výše spropitného je %1$s. Zadejte prosím vyšší částku. - Každý den můžete vytvořit pouze tolik účtů. - Aplikace Code je momentálně omezena na jeden účet na zařízení. Podpora více účtů se pravděpodobně objeví v některé z budoucích verzí aplikace Code. - Aplikace Code je momentálně omezena na jeden účet na telefonní číslo. Podpora více účtů se pravděpodobně objeví v některé z budoucích verzí aplikace Code. - Kin byl již získán - Kin byl již vybrán - Je nutný přístup k fotoaparátu - Vypršelé prostředky - Ověřovací kód vypršel - Je vyžadován přístup ke kontaktům - Země není podporována - Zařízení není podporováno - eSim nejsou momentálně podporovány - Nepodařilo se sesbírat - Nepodařilo se vytvořit účet - Ukládání se nezdařilo - Odeslání se nezdařilo - Potvrzení se nezdařilo - Transakce zhavarovala - Dosažení limitu příspěvků - Dosažení denního limitu - Nedostatek kinu - Neplatný nebo vypršený kód pozvánky - Neplatné telefonní číslo - Neplatná karta spropitného - Neplatný kód - Přizvání se nezdařilo - Platnost odkazu vypršela - Byl dosažen maximální počet pokusů - Kód nebyl nalezen - Chybí internetové připojení - Nemáte žádná přizvání - Dosud jste nebyli přizváni. - Příliš velký nákup - Příliš malý nákup - Přístupový klíč již není v Code použitelný - Tip Card ještě nebyla aktivována - Příliš velké spropitné - Příliš malé spropitné - Vytvořeno příliš mnoho účtů - Účet již byl vytvořen - Účet již byl vytvořen - Věříme, že platby by měly být jednoduché, výkonné a globální. Stavbou s pokročilou blockchainovou technologií nabízí Code funkce, které tradiční platební aplikace nemohou, jako globální peer-to-peer převody, mikroplatby, které odemykají jednotlivé on-line články a spropitné s nulovými poplatky pro vaše oblíbené tvůrce. - Kin je kryptoměna jako Bitcoin, ale je také navržena pro rychlé, dostupné platby. - Stejně jako Bitcoin je Kin dostupný pouze v omezeném množství. Pokud si Kin kupuje více lidí, hodnota stoupá, a pokud více lidí Kin prodává, hodnota klesá. Tato dynamika umožňuje každému, kdo Kin drží, sdílet hodnotu tvorby, pokud se používání Kin rozroste. - Kin si můžete koupit svojí platební kartou. Toto je k dispozici v záložce Získat Kin. - Ano, můžete. Prodání Kin je podporováno na řadě kryptoměnových burz. - Existují tři hlavní způsoby, jak můžete pomoci: mluvte o své zkušenosti s Code na sociálních sítích, povzbuďte své přátele, aby sami Code vyzkoušeli, a povzbuďte své oblíbené webové stránky, aby integrovali platby Code tím, že se požádáte, aby se podívali na [getcode.com](https://getcode.com). - Co je to Code? - Proč jsou platby Code denominované v Kin? - Proč se hodnota Kin mění? - Jak si koupím více Kin? - Můžu Kin prodat? - Jak mohu pomoci? - souhlasíte s našimi - Klepnutím na „Vytvořit účet\" nebo na „Přihlásit\" - K získávání kinu se využívá fotoaparát. Pokud chcete pokračovat, povolte přístup k fotoaparátu. - Ke včasnému odeslání údajů o vašem účtu potřebujeme zprávy push. - Výběry jsou nevratné a po zahájení je nelze vrátit zpět. - Veškeré peníze na tomto účtu se ztratí. Vymazání účtu je trvalé a nelze je vrátit zpět. Opravdu chcete tento účet vymazat? - Jakýkoli Kin, který nebude vyzvednut do 24 hodin, bude automaticky vrácen na váš účet. - Budete muset vytváření účtu restartovat a znovu si ověřit telefonní číslo. - Do účtu se vrátíte s využitím přístupového kódu - Na nové zprávy od %1$s nebudete upozorňováni. Ztlumení můžete kdykoli zrušit. - Momentálně fungují pouze účty vytvořené pomocí kódu. - Přátelé vás pomocí tohoto telefonního čísla už nebudou moci najít. - Budete upozorněni na všechny nové zprávy od %1$s. Zprávy můžete kdykoli ztlumit. - Dokud znovu nezaplatíte, nebudete od %1$s dostávat žádné zprávy. - Přístupový kód vám umožní přístup k vlastnímu kódovému účtu. Chraňte si ho bezpečně v soukromí. - Obnovení kódového účtu lze provést pouze pomocí těchto 12 slov. Určitě si je někam poznamenejte a chraňte si je bezpečně v soukromí. - Opravdu? - Opravdu chcete tento účet vymazat? - Odeslali jste odkaz? - Opravdu chcete odejít? - Opravdu se chcete odhlásit? - Ztlumit %1$s? - Toto není kódový účet - Opravdu? - Zrušit ztlumení %1$s? - Odhlásit se z odběru %1$s? - Chcete vidět svůj přístupový kód? - Opravdu? - %1$s Kin(ů) bylo vloženo na váš účet. - %1$s, který jste poslali včera nebyl vybrán. Byl navrácen automaticky do vašeho zůstatku. - Přiveďte kamaráda na Code a získejte 5 USD - Poslat peníze skrze aplikaci messengeru - Získali jste %1$s Kin(ů) za to, že jste někomu poslali jeho první Kin. - Nyní můžete přijímat spropitné. - Vklad přijat - Kin byl navrácen - Nové na Code - Bonus za doporučení obdržen - Účet X připojen - Vaše přizvání k přístupovému kódu vypršela. Můžete se odhlásit a využít jiný účet s platným stavem pozvánky. - Jako jediný způsob přístupu ke svým financím vám slouží přístupový klíč. Chraňte si ho bezpečně v soukromí. - Abyste si mohli zobrazit přístupový kód, nechejte si ověřit totožnost. - Pokud se chcete přihlásit do aplikace Code, klepněte na ikonu Google Lens a otevřetete QR kód. Případně se můžete přihlásit ručně zadáním 12 slov na přihlašovací obrazovce aplikace Code. - Pozor! Tento obrázek umožňuje přístup ke všem prostředkům, které máte v Code. Nesdílejte tento obrázek s nikým dalším. Uchovávejte jej v bezpečí. - Kód vám umožňuje získávat kin zaměřením fotoaparátu na digitální účet nebo na telefon jiného uživatele. - Abyste mohli získávat kin, budete muset povolit přístup k fotoaparátu - Ověření pro přístup ke Code. - Nakupte Kin - Koupit Kin (brzy) - Nákup a prodej měny Kin je aktuálně složitý proces. Časem se tyto procesy zjednoduší. Pokud chcete zjistit, jak nakupovat a prodávat Kin už teď, můžete si projít instruktážní videa níže. - Můžete dát pouze %1$s - Spropitné můžete dát pouze do výše %1$s - Měnu Kin nyní můžete používat ve své aplikaci Code - Poslali jste někomu jejich první Kin! Tady je váš bonus za referenci: - Měna USDC se nyní převádí na měnu KIN. K dokončení procesu by měla stačit asi jedna minuta - Úspěšně jste vložili USDC. Otevřete aplikaci Code a dokončete nákup - Bonus na uvítanou - Zvolit zemi - Již brzy - \@getcode, chci si propojit účet X a dostávat tipy od lidí z celého světa - Odstranit - Ujistěte se, že máte uloženou tajnou frázi pro obnovení, a poté zadejte \"Odstranit\" pro odstranění účtu Code. Tato akce je nevratná. - Nedostali jste kód na %1$s? - Nedostali jste kód? Opakujte zaslání - K zákazu rozpoznávání tváře je nutné ověřit vaši totožnost. - Nemáte aplikaci Code Wallet? - Zatím nemáte žádné kiny. - Povolit rozpoznávání tváře jako další bezpečnostní úroveň transakcí kódu. - Zadejte adresu určení - Zadejte až %1$s - Získejte Kin v hodnotě 5 USD zdarma, když přimějete kamaráda, aby se zaregistroval do služby Code, a pošlete mu jeho první Kin. - Code používá pro platby kryptoměnu Kin. Zde je několik způsobů, jak získat více Kinů do vaší peněženky s kódem. - Získejte první Kin v hodnotě 1 USD zdarma - K darování kinu si nechejte ověřit totožnost. - Uložit kin do kódové peněženky odesláním kinu na níže uvedenou ukládací adresu. Klepnutím zkopírujete. - Získejte další Kin a pak zkuste zaplatit znovu - Neplatný cílový účet - Prosím ujistěte se, že adresa, na kterou vybíráte, byla inicializována poskytovatelem peněženky. Rychlá cesta, jak toho dosáhnout, je nejprve vyměnit malé množství SOL za Kin v peněžence, na kterou se snažíte poslat. - Kód pozvánky - Code je v současné době pouze pro zvané. K přístupu do aplikace budete potřebovat pozvánku. - %1$d přizvání - Code je nová aplikace ke kryptoměnové peněžence, která momentálně funguje pouze pro zvané. Code si stáhnete na %1$s - Zjistit více - Vaše telefonní číslo je propojeno s tímto kódovým účtem. Přátelé vás pomocí tohoto čísla najdou. - Propojuji svůj účet X s @getcode, abych mohl dostávat spropitné od lidí z celého světa. - Načítání zůstatků a historie transakcí - Zkontrolujte snímky k přístupovému kódu, který jste si uložili při prvním vytvoření účtu. - Momentálně jste přihlášeni k účtu. Než budete pokračovat, ujistěte se, že jste uložili svůj přístupový klíč. Chcete se odhlásit a přihlásit pomocí nového účtu? - Chybí propojené\ntelefonní číslo - Nemáte telefonní číslo propojené s tímto kódovým účtem. Propojte si je, aby vás přátelé našli. - Bez připojení k internetu - Na Code - Otevřete aplikaci Code a nasměrujte svůj fotoaparát tak, abyste tuto hotovost získali. - Organizují se vám kontakty - Toto telefonní číslo není ve vašich kontaktech. Přesto jej můžete pozvat do Code. - Zadejte telefonní číslo i s mezinárodní předvolbou. Dbejte na to, abyste uvedli shodné telefonní číslo, které obdrželo pozvánku. - Poháněno - Nyní můžete požádat o spropitné - Bonus za doporučení obdržen - Odeslat %1$s - Zde je %1$s - Požádat o nový za %1$s - Naskenujte tento QR kód pomocí fotoaparátu telefonu a stáhněte si aplikaci Code Wallet. - Aplikaci Code Wallet\nsi stáhnete naskenováním kódu - Vyhledávat měny - Hledání kontaktů - Na vaše telefonní číslo byla odeslána zpráva SMS s ověřovacím kódem. Zadejte prosím výše uvedený ověřovací kód. - Někdo vám poslal hotovost - Někdo vám dal spropitné - Pro naskenování kódů musíte spustit svůj fotoaparát - Vaše karta spropitného vám umožňuje přijímat spropitné od uživatelů služby Code z celého světa. Pro přístup ke kartě Tip Card připojte svou identitu X. - Tip Card vám umožňuje přijímat spropitné od uživatelů Code z celého světa. Přístup k Tip Card získáte po zveřejnění příspěvku na X. - Vaše Tipová karta vám umožňuje přijímat tipy od uživatelů kódu z celého světa. Chcete-li získat přístup ke své kartě tipů, napište zprávu @getcode na X. - Povolte aplikaci Code, aby vám zasílala oznámení, když obdržíte spropitné od jiných uživatelů aplikace Code. - Napište \"%1$s\" - Udělali jsme jistá zdokonalení užívání. Abyste mohli kód i nadále využívat, budete si muset aktualizovat aplikaci. - Platný účet vlastníka - Platný tokenový účet - Hodnota změn kinů. - vám bylo vráceno - Kam byste si chtěli vybrat kin? - K výběru kinu si nechejte ověřit totožnost. - Uložili jste - Dali jste - Máte %1$d přizvání - Kód je momentálně pouze na přizvání. Zbývá vám jen %1$d. - Zaplatili jste - Dali jste - Obdrželi jste - Poslali jste - Utratili jste - Dali jste někomu spropitné - Vybrali jste - Úspěšně jste si vybrali peníze. - Váš účet X byl úspěšně připojen k účtu Code. Nyní můžete požádat o spropitné. - Výběr proběhl úspěšně - X účet úspěšně připojen - Přístup vypršel - Přístupový klíč - Nastavení aplikace - Automatické spuštění fotoaparátu - Zůstatek - Beta příznaky - Bonus - Kupujte a prodávejte Kin - Platby v hotovosti - Tým Code - Nákupy Kin - Internetové platby - Spropitné - Možnosti odstranění chyb - Uložit kin - Uloženo - Zadat slova přístupového klíče - Zadejte telefonní číslo - Selhalo - Časté dotazy - Darováno - Získat hotovost - Začněte kamarádem používat Code - Získat Kin - Získejte další Kin - Dát hotovost - Darovat kin - Nedostatek prostředků - Přizvat přítele - Časově omezená nabídka - Propojené - Místní měna - Můj účet - Nepropojené - Ostatní měny - Placeno - Čeká - Telefonní číslo - Ochrana soukromí - Zakoupeno - X Účet úspěšně připojen - Získat spropitné - Obdrženo - Poslední měny - Doporučte kamaráda a získejte 5 USD - Bonus za doporučení - Žádost o hotovost - Požádat o Kin - Požádat o spropitné - Vyžaduje Face ID - Vyžaduje kód - Vyžaduje Touch ID - Výsledky - Vráceno - Vybrat účet - Vybrat zemi - Zvolit měnu - Odesláno - Utraceno - Přepnout účet - Všeobecné obchodní podmínky - Karta tipu - Dát Kin spropitné - Zapnutí oznámení pro Code - Neznámé - Je nutná aktualizace - Ověřit telefonní číslo - Uvítací bonus - Vybrat kin - Staženo - Váš přístupový klíč - Klepnutím na logo můžete sdílet odkaz ke stažení aplikace - diff --git a/apps/codeApp/src/main/res/values-da/strings-localized.xml b/apps/codeApp/src/main/res/values-da/strings-localized.xml deleted file mode 100644 index fd0077f66..000000000 --- a/apps/codeApp/src/main/res/values-da/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Tilføj kontanter med et betalingskort - Tillad kameraadgang - Tillad adgang til kontakter - Tillad pushnotifikationer - Saldo - Køb Kin - Køb flere Kin - Slet - Annuller send - Chat - Indsaml disse kontanter - Bekræft - Opret forbindelse til X - Fortsæt - Kopieret - Kopier - Kopier adresse - Opret konto - Opret en ny Code-konto - Slet konto - Færdig - Download den nu - Aktiver Face ID - Aktiver Touch ID - Forlad - Giv - Giv Kin - Inviter - Invitationer - Skriv dig på ventelisten - Senere - Lær hvordan du køber Kin - Lær hvordan du sælger Kin - Link et telefonnummer - Log ind - Log ud - Send en besked med @getcode for at oprette forbindelse - Dæmp - Næste - Nej, prøv igen - Ikke nu - OK - Åbn Indstillinger - Indsæt - Indsæt fra udklipsholder - Slå noget op for at forbinde konto - Læg i Wallet - Modtag - Genskab eksisterende konto - Husk - Fjern telefonnummer - Fjern dit telefonnummer - Anmod om drikkepenge - Gem adgangskode i Mine billeder - Gem i billeder - Send - Send verifikationskode - Del - Del som en URL - Del downloadlink - Del denne video - Vis mit drikkepengekort - Start kamera - Abonner - Swipe for at logge ind - Swipe for at betale - Swipe for at give drikkepenge - Prøv en anden Code-konto - Tweet til dem - Lås op for Code - Fjern dæmpning - Afmeld abonnement - Opdater - Se adgangskode - Hæv Kin - Skrev du i stedet de 12 ord ned? - Ja - Ja, hæv Kin - Ja, jeg skrev det ned - og - Kin - i Kin - Alle oplysninger som er tilknyttet din konto fra Codes servere bliver fjernet (telefonnummer, kontakter, transaktionshistorik) - Du kan få adgang til din konto i andre cryptotegnebogs-apps via din hemmelige gendannelsessætning. Du vil ikke kunne bruge din konto på Code - Fjerning af din konto fra blockchain - Hvad sletning indebærer - Hvad vil der ske - Hvad sletning ikke indebærer - Denne Kin blev samlet af en anden. - Denne Kin er allerede indsamlet af nogen. - Tillad adgang til kamera i Indstillinger for at bruge Code. - Disse penge er udløbet. - Genindtast dit telefonnummer, og prøv igen - Tillad venligst adgang til kontakter i Indstillinger for at sende invitationer. - Code er i øjeblikket ikke tilgængeligt i dit land. - Vi kan ikke understøtte din enhed i øjeblikket - Understøttelse af eSims kommer sandsynligvis i en fremtidig version af Code. - Der gik noget galt. Disse Kin kunne ikke indsamles. - Vi forventede ikke, at det ville ske. Noget gik galt. Prøv at oprette kontoen igen. - Tillad Code adgang til billeder i Indstillinger for at gemme din adgangsnøgle. - Der gik noget galt. Sørg for, at dit telefonnummer er indtastet korrekt. - Noget gik galt. Prøv igen. - Det lykkedes ikke at hæve penge. Noget gik galt, prøv at hæve igen. - Code er designet til små hverdagstransaktioner på %1$s eller mindre. - Code er designet til små hverdagstransaktioner. Din daglige givegrænse vil blive forhøjet i morgen. - For at finde ud af, hvordan du får flere Kin, kan du gå til FAQ i Indstillinger - Indtast en anden invitationskode og prøv igen. - Indtast venligst et gyldigt telefonnummer og prøv igen. - Dette er et ugyldigt drikkepengekort. - Indtast en gyldig kode og prøv igen. - Beklager, vi har et netværksproblem. Prøv at invitere din ven igen. - Denne Kin blev automatisk returneret til afsenderen, fordi den ikke blev afhentet inden for 24 timer. Bed dem venligst om at sende Kin igen. - Genindtast dit telefonnummer, og prøv igen - Prøv at tage et billede, der viser koden mere tydeligt. - Tjek din internetforbindelse eller prøv igen senere - Code er i øjeblikket kun pr invitation. Vi giver dig besked, når der er flere tilgængelige invitationer. - For at komme på ventelisten, gå til %1$s - Du kan højst købe for %1$s. Indtast venligst et mindre beløb. - Du kan mindst købe for %1$s. Indtast venligst et større beløb. - Din adgangsnøgle har startet en oplåsning. Derfor vil du ikke længere være i stand til at bruge denne adgangsnøgle i kode. - Send et tweet til denne person for at aktivere deres Tip Card. - Det største beløb, du kan give i drikkepenge, er %1$s. Indtast venligst et mindre beløb. - Det mindste beløb, du kan give i drikkepenge, er %1$s. Indtast venligst et større beløb. - Du kan kun oprette et vist antal konti pr dag. - Code er i øjeblikket begrænset til én konto per enhed. Understøttelse af flere konti kommer sandsynligvis i en fremtidig version af Code. - Code er i øjeblikket begrænset til én konto per telefonnummer. Understøttelse af flere konti kommer sandsynligvis i en fremtidig version af Code. - Kin allerede samlet - Kin allerede samlet - Kameraadgang påkrævet - Penge udløbet - Verifikationskoden udløb - Adgang til kontakter kræves - Land understøttes ikke - Enhed understøttes ikke - eSims understøttes i øjeblikket ikke - Kunne ikke indsamle - Kunne ikke oprette konto - Kunne ikke gemme - Kunne ikke sende - Kunne ikke bekræfte - Transaktion mislykkedes - Givegrænsen er nået - Daglig grænse er nået - Ikke nok Kin - Invitationskode ugyldig eller udløbet - Ugyldigt telefonnummer - Ugyldigt drikkepengekort - Ugyldig kode - Invitation mislykkedes - Link udløbet - Maksimalt antal forsøg nået - Ingen kode fundet - Ingen internetforbindelse - Du har ingen invitationer - Du er endnu ikke blevet inviteret - Beløbet er for højt - Beløbet er for lavt - Adgangsnøgle kan ikke længere bruges i kode - Tip Card ikke aktiveret endnu - Beløbet, der blev givet i drikkepenge, var for stort - Beløbet, der blev givet i drikkepenge, var for lille - For mange konti blev oprettet - Konto allerede oprettet - Konto allerede oprettet - Vi mener, at betalinger skal være enkle, effektive og globale. Code bygger med avanceret blokkædeteknologi, hvilket gør det muligt at tilbyde funktioner, som traditionelle betalingsapps ikke kan, f.eks. globale peer to peer-overførsler, mikrobetalinger, der låser op for individuelle artikler, og gebyrløse smådonationer til dine yndlingskreatører. - Kin er en kryptovaluta ligesom Bitcoin, men er også designet til hurtige, billige betalinger. - Ligesom med Bitcoin er der kun et begrænset antal Kin tilgængeligt. Hvis flere personer køber Kin, stiger værdien, og hvis flere personer sælger Kin, falder værdien. Denne dynamik giver alle, der ejer Kin, mulighed for at veksle til en højere værdi, hvis brugen af Kin vokser. - Du kan købe Kin med dit debetkort. Dette er tilgængeligt på fanen Køb Kin. - Ja, det kan du. Salg af Kin understøttes på flere forskellige kryptovalutabørser. - Der er tre overordnede måder, du kan hjælpe på: Tal om din Code-oplevelse på sociale medier, foreslå dine venner selv at prøve Code, og foreslå dine foretrukne websites at integrere Code-betalinger ved at bede dem om at se på [getcode.com](https://getcode.com). - Hvad er Code? - Hvorfor foretages Code-betalinger i Kin? - Hvorfor ændrer værdien af Kin sig? - Hvordan køber jeg flere Kin? - Kan jeg sælge Kin? - Hvordan kan jeg hjælpe? - accepterer du vores - Ved at trykke på \"Opret en konto\" eller \"Log ind\" - Dit kamera bruges til at modtage Kin. Tillad kameraadgang for at fortsætte. - Vi har brug for push-beskeder for at sende dig rettidig information om din konto. - Udbetalinger er irreversible og kan ikke fortrydes, når de først er startet. - Alle midler på kontoen vil gå tabt. Sletning af din konto er permanent og kan ikke fortrydes. Er du sikker på, at du vil slette denne konto? - Enhver Kin, der ikke er indsamlet inden for 24 timer, vil automatisk blive returneret til din saldo. - Du skal genstarte kontooprettelse og bekræfte dit telefonnummer igen. - Du kan komme tilbage til denne konto ved hjælp af din adgangsnøgle - Du vil ikke blive informeret om nye beskeder fra %1$s. Du kan slå lyden fra når som helst. - Kun konti oprettet via Code understøttes i øjeblikket. - Dine venner vil ikke længere kunne finde dig med dette telefonnummer. - Du vil blive informeret om alle nye beskeder fra %1$s. Du kan slå lyden fra når som helst. - Du vil ikke modtage nogen beskeder fra %1$s, før du betaler dem igen. - Din adgangsnøgle giver adgang til din Codekonto. Opbevar den privat og sikkert. - Disse 12 ord er den eneste måde at gendanne din Codekonto på. Sørg for at skrive dem ned, og hold dem private og sikre. - Er du sikker? - Er du sikker på, at du vil slette denne konto? - Sendte du linket? - Ønsker du at forlade? - Vil du logge ud? - Slå lyd fra for %1$s? - Ikke en Codekonto - Er du sikker? - Slå lyd til for %1$s? - Afmeld %1$s? - Vil du se din adgangsnøgle? - Er du sikker? - %1$s Kin blev indsat på din konto. - De %1$s, du sendte i går, blev ikke opsamlet. De er automatisk blevet returneret til din saldo. - Få en ven i gang med Code og få $5 - Send kontanter gennem enhver messengerapp - Du modtog %1$s af Kin for at sende nogen deres første Kin. - Du kan nu anmode om donationer. - Indbetaling modtaget - Kin returneret - Ny på Code - Henvisningsbonus modtaget - X-konto forbundet - Din invitation til at få Code er udløbet. Du kan logge ud og bruge en anden konto med en gyldig invitationsstatus. - Du kan kun få adgang til dine midler ved hjælp af en adgangskode. Sørg for at gemme den sikkert og privat. - Bekræft din identitet for at se din adgangskode - Tryk på Google Lens-ikonet for at åbne QR-koden og logge ind på Code. Alternativt kan du logge på manuelt ved at indtaste de 12 ord i log ind-skærmen i Code. - Advarsel! Dette billede giver adgang til alle de funds, du har i Code. Del ikke dette billede med andre. Hold det sikkert og beskyttet. - Code giver dig mulighed for at modtage Kin, ved at rette dit kamera mod den digitale regning på en anden brugers telefon - Du skal tillade kameraadgang for at modtage Kin - Godkend for at få adgang til Code. - Køb Kin - Køb Kin (kommer snart) - Køb og salg af Kin er i øjeblikket en kompleks proces. Disse processer vil blive enklere med tiden. Hvis du vil lære at købe og sælge Kin i dag, kan du se videoerne nedenfor. - Du kan kun give op til %1$s - Du kan kun give drikkepenge op til %1$s - Dine Kin er nu klar til brug i din Code-app - Du har sendt nogen deres første Kin! Her er din henvisningsbonus: - Dine USDC bliver konverteret til Kin. Dette burde tage omkring et minut. - Du har indbetalt USDC. Åbn Code-appen for at afslutte dit køb. - Velkomstbonus - Vælg land - Kommer snart - \@getcode Jeg vil gerne forbinde min X-konto, så jeg kan modtage donationer fra folk over hele verden - Slet - Sørg for at du har gemt din hemmelige gendannelsessætning og tast derefter \"Slet\" for at slette din Code-konto. Denne handling kan ikke fortrydes. - Fik du ikke en kode i %1$s? - Modtog du ikke en kode? Gensend - Deaktivering af Face ID kræver, at du bekræfter din identitet. - Har du ikke Code Wallet-appen? - Du har endnu ingen Kin. - Aktiver Face ID for yderligere at forbedre sikkerheden ved transaktioner i Code. - Indtast destinationsadresse - Indtast op til %1$s - Få $5 i Kin gratis, når du får en ven til at tilmelde sig Code, og du sender dem deres første Kin. - Code bruger kryptovalutaen Kin til betalinger. Her er nogle måder, hvorpå du kan få flere Kin i din Code-pung. - Få din første $1 af Kin gratis - Bekræft din identitet for at give Kin - Indsæt Kin i din Code wallet ved at sende Kin til din deponeringsadresse nedenfor. Tap for at kopiere. - Få venligst flere Kin, og prøv derefter at betale igen - Ugyldig destinationskonto - Sørg for, at den adresse, du hæver til, er blevet initialiseret af din tegnebogsudbyder. En genvej til at opnå dette er først at veksle en lille smule SOL til Kin i den tegnebog, du prøver at sende til. - Invitationskode - Koden kan i øjeblikket kun fås ved invitation. Du skal bruge en invitationskode for at få adgang til appen. - %1$d invitationer - Code er en ny crypto wallet-app, der i øjeblikket kun er tilgængelig ved invitation. For at downloade Code gå til %1$s - Læs mere - Dit telefonnummer er linket med denne Code-konto. Venner kan finde dig ved at anvende dette telefonnummer. - Jeg forbinder min X-konto med @getcode, så jeg kan modtage drikkepenge fra folk over hele verden. - Indlæser din saldo og transaktionshistorik - Tjek dine billeder for at hente den adgangskode, du gemte, da du oprettede din konto. - Du er i øjeblikket logget ind på en konto. Sørg for, at du har gemt din adgangsnøgle, før du fortsætter. Vil du logge ud og logge på med en ny konto? - Intet linket telefonnummer - Du har ikke et telefonnummer linket med denne Code-konto. Link et, så dine venner kan finde dig. - Ingen netværksforbindelse - På kode - Åbn Code-appen og peg dit kamera for at få disse kontanter - Organiser dine kontakter - Dette telefonnummer er ikke i dine kontakter. Du kan stadig invitere dem til Code. - Indtast dit telefonnummer inklusiv landekode. Sørg for at anvende det telefonnummer, der modtog invitationen. - Leveres af - Du kan nu anmode om drikkepenge - Henvisningsbonus modtaget - Send %1$s - Her er %1$s - Anmod om en ny i %1$s - Scan denne QR-kode med din telefons kamera for at downloade Code Wallet-appen - Scan for at downloade\nCode Wallet-appen - Søg valutaer - Søg efter kontakter - En sms blev sendt til dit telefonnummer med en bekræftelseskode. Indtast bekræftelseskoden ovenfor. - Nogen har sendt dig kontanter - Nogen har givet dig drikkepenge - Du skal starte dit kamera for at scanne koder - Med dit drikkepengekort kan du modtage drikkepenge fra Code-brugere over hele verden. For at få adgang til dit drikkepengekort skal du forbinde din X-identitet.\n - Dit Tip Card lader dig modtage donationer fra Code-brugere over hele verden. Slå noget op på X for at tilgå dit Tip Card. - Dit tipkort giver dig mulighed for at modtage tips fra kodebrugere over hele verden. For at få adgang til dit tipkort-skal du skrive til @getcode på X. - Tillad, at Code sender dig notifikationer, når du modtager tip fra andre Code-brugere. - Tast \"%1$s\" - Vi har foretaget nogle ændringer for at forbedre oplevelsen. Du skal opdatere appen for at fortsætte med at anvende Code. - Gyldig ejerkonto - Gyldig tokenkonto - Værdien af ​​Kin ændres. - blev returneret til dig - Hvortil ønsker du at hæve dine Kin? - Bekræft din identitet for at hæve Kin - Du har indsat - Du gav - Du har %1$d invitationer - Code er i øjeblikket kun pr invitation. Du har %1$d tilbage. - Du betalte - Du gav - Du modtog - Du sendte - Du brugte - Du har givet nogen drikkepenge - Du har trukket - Dine midler blev udbetalt - Din X-konto er blevet forbundet med din Code-konto. Du kan nu anmode om drikkepenge. - Udbetaling lykkedes - X Konto forbundet - Adgang udløbet - Adgangskode - Appindstillinger - Start automatisk kamera - Saldo - Betaflag - Bonus - Køb og Sælg Kin - Kontante betalinger - Kode-team - Køb af Kin - Betalinger på nettet - Drikkepenge - Fejlfindingsindstillinger - Indsæt Kin - Deponerede - Indtast adgangskode - Indtast telefonnummer - Mislykkedes - FAQ - Gav - Få kontanter - Få en ven i gang med Code - Få Kin - Få flere Kin - Giv kontanter - Giv Kin - Ikke nok penge - Inviter en ven - Tidsbegrænset tilbud - Tilknyttet - Lokalvaluta - Min konto - Ikke tilknyttet - Andre valutaer - Betalt - Afventer - Telefonnummer - Privatlivspolitik - Købt - X-konto forbundet - Modtag tips - Modtog - Seneste valutaer - Henvis en ven, få $5 - Henvisningsbonus - Anmod om kontanter - Anmod om Kin - Anmod om drikkepenge - Kræv Face ID - Kræv adgangskode - Kræv Touch ID - Resultater - Returneret - Vælg en konto - Vælg et land - Vælg en valuta - Sendt - Brugt - Skift konti - Servicebetingelser - Tipkort - Giv drikkepenge i Kin - Slå notifikationer for Code til - Ukendt - Opdatering nødvendig - Bekræft telefonnummer - Velkomstbonus - Hæv Kin - Hævede - Din adgangskode - Tryk på logoet for at dele download-linket til appen - diff --git a/apps/codeApp/src/main/res/values-de/strings-localized.xml b/apps/codeApp/src/main/res/values-de/strings-localized.xml deleted file mode 100644 index 2ae8d2555..000000000 --- a/apps/codeApp/src/main/res/values-de/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Bargeld mit einer Debitkarte hinzufügen - Kamera-Zugriff erlauben - Zugriff auf Kontakte zulassen - Push-Benachrichtigungen zulassen - Saldo - Kin kaufen - Mehr Kin kaufen - Abbrechen - Senden abbrechen - Chat - Erhalten Sie Ihr Guthaben - Bestätigen - Mit X verbinden - Fortfahren - Kopiert - Kopieren - Adresse kopieren - Ein Konto erstellen - Ein neues Code-Konto erstellen - Konto löschen - Fertig - Jetzt herunterladen - Face ID aktivieren - Touch ID aktivieren - Beenden - Geben - Kin überweisen - Einladen - Einladen - In Warteliste eintragen - Später - Erfahren Sie, wie man Kin kauft.\n - Erfahren Sie, wie man Kin verkauft.\n - Eine Telefonnummer verknüpfen - Einloggen - Abmelden - Schreibe @getcode, um Dich zu verbinden - Stummschalten - Weiter - Nein, erneut versuchen - Nicht jetzt - OK - Einstellungen öffnen - Einfügen - Aus Zwischenablage einfügen - Posten, um das Konto zu verbinden - In Wallet legen - Erhalten - Bestehendes Konto wiederherstellen - Erinnern - Telefonnummer entfernen - Ihre Telefonnummer entfernen - Trinkgeld anfordern - Zugriffsschlüssel in Meine Fotos speichern - In Fotos speichern - Senden - Verifizierungscode senden - Teilen - Als URL teilen - Download-Link teilen - Dieses Video teilen\n - Meine Trinkgeldkarte anzeigen - Kamera starten - Abonnieren - Zum Anmelden wischen - Zum Bezahlen wischen\n - Wischen, um Trinkgeld zu geben - Versuchen Sie es mit einem anderen Code-Konto - Twittern Sie sie - Code freischalten - Stummschaltung aufheben - Abbestellen - Aktualisieren - Zugriffsschlüssel anzeigen - Kin abheben - Wurden die 12 Wörter stattdessen aufgeschrieben? - Ja - Ja, Kin abheben - Ja, ich habe sie aufgeschrieben - und - Kin - über Kin - Es werden alle mit Ihrem Konto verbundenen Informationen von den Servern von Code entfernt (Telefonnummer, Kontakte, Transaktionsverlauf). - Sie können mit Ihrer geheimen Wiederherstellungsphrase in anderen Krypto-Wallet-Apps auf Ihr Konto zugreifen. Sie können Ihr Konto nicht in Code verwenden. - Ihr Konto wird nicht von der Blockchain entfernt - Was das Löschen bewirkt - Was passieren wird - Was das Löschen nicht bewirkt - Dieses Kin wurde bereits von jemand anderem abgeholt. - Dieses Kin wurde bereits von jemandem abgerufen. - Bitte erlauben Sie in den Einstellungen Zugriff auf die Kamera, um Code nutzen zu können. - Dieses Bargeld ist verfallen. - Bitte geben Sie Ihre Telefonnummer erneut ein und versuchen Sie es noch einmal. - Bitte erlauben Sie in den Einstellungen den Zugriff auf die Kontakte, um Einladungen zu versenden. - Code ist in Ihrem Land aktuell nicht verfügbar. - Wir können Ihr Gerät aktuell nicht unterstützen - Unterstützung von eSims wird es vermutlich in einer zukünftigen Version von Code geben. - Etwas ist schief gelaufen. Dieses Kin konnte nicht erfasst werden. - Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie, dieses Konto erneut zu erstellen. - Bitte erlauben Sie Code in den Einstellungen Zugriff auf Fotos, um Ihren Zugangsschlüssel speichern zu können. - Etwas ist schiefgegangen. Bitte stellen Sie sicher, dass Sie Ihre Telefonnummer richtig eingegeben haben. - Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut. - Die Abhebung Ihres Guthabens ist fehlgeschlagen. Etwas ist schief gelaufen. Bitte versuchen Sie die Abhebung erneut vorzunehmen. - Code ist für kleine, alltägliche Transaktionen gedacht, die sich auf %1$s oder weniger belaufen. - Code ist für kleine, alltägliche Transaktionen gedacht. Ihr tägliches Limit wird morgen erhöht. - Wie Sie mehr Kin erhalten können, erfahren Sie in den FAQ unter Einstellungen. - Bitte geben Sie einen anderen Einladungscode ein und versuchen Sie es erneut. - Bitte geben Sie eine gültige Telefonnummer ein und versuchen Sie es erneut. - Diese Trinkgeldkarte ist ungültig. - Bitte geben Sie einen gültigen Code ein und versuchen Sie es erneut. - Leider ist ein Netzwerkproblem aufgetreten. Bitte versuchen Sie erneut, Ihre Freunde einzuladen. - Dieses Kin wurde automatisch an den Absender zurückgeschickt, weil es nicht innerhalb von 24 Stunden abgeholt wurde. Bitte bitten Sie den Absender, das Kin erneut zu senden. - Bitte geben Sie Ihre Telefonnummer erneut ein und versuchen Sie es noch einmal. - Bitte versuchen Sie, ein Bild zu bekommen, das den Code deutlicher zeigt. - Bitte überprüfen Sie Ihre Internetverbindung oder versuchen Sie es später noch einmal. - Code kann momentan nur mit Einladung genutzt werden. Wir werden Sie benachrichtigen, sobald weitere Einladungen vorhanden sind. - Gehen Sie zu %1$s, um sich in die Warteliste eintragen zu lassen. - Der Höchstbetrag, den Sie kaufen können, beträgt %1$s. Bitte geben Sie einen kleineren Betrag ein. - Der Mindestbetrag, den Sie kaufen können, beträgt %1$s. Bitte geben Sie einen höheren Betrag ein. - Ihr Zugangsschlüssel hat eine Freischaltung ausgelöst. Infolgedessen können Sie diesen Zugangsschlüssel nicht mehr in Code verwenden. - Senden Sie einen Tweet an diese Person, um ihre Tipp-Karte zu aktivieren. - Sie können maximal %1$s Trinkgeld geben. Bitte geben Sie einen niedrigeren Betrag ein. - Der Mindestbetrag für Trinkgeld ist %1$s. Bitte geben Sie einen höheren Betrag ein. - Sie können nur eine bestimmte Anzahl an neuen Konten pro Tag erstellen. - Code ist aktuell auf ein Konto pro Gerät beschränkt. Unterstützung mehrerer Konten wird es vermutlich in einer zukünftigen Version von Code geben. - Code ist aktuell auf ein Konto pro Telefonnummer begrenzt. Unterstützung mehrerer Konten wird es vermutlich in einer zukünftigen Version von Code geben. - Kin bereits abgeholt - Bereits abgerufene Kin - Kamerazugriff erforderlich - Bargeld Verfallen\n - Verifizierungscode ist abgelaufen - Zugriff auf Kontakte erforderlich - Das Land wird nicht unterstützt - Das Gerät wird nicht unterstützt - eSims wird aktuell nicht unterstützt - Sammeln fehlgeschlagen - Konto konnte nicht erstellt werden - Speichern fehlgeschlagen - Senden fehlgeschlagen - Bestätigung fehlgeschlagen - Transaktion fehlgeschlagen - Give-Limit erreicht - Tägliches Limit erreicht - Nicht genug Kin - Der Einladungscode ist ungültig oder abgelaufen - Ungültige Telefonnummer - Ungültige Trinkgeldkarte - Ungültiger Code - Einladung fehlgeschlagen - Link Abgelaufen - Maximale Anzahl an Versuchen erreicht - Kein Code gefunden - Keine Internetverbindung - Sie haben keine Einladungen - Sie haben noch keine Einladung erhalten - Kaufbetrag zu groß - Kaufbetrag zu klein - Zugangsschlüssel nicht mehr in Code verwendbar - Tipp-Karte noch nicht aktiviert - Trinkgeldbetrag zu hoch - Trinkgeldbetrag zu niedrig - Zu viele Konten erstellt - Das Konto wurde bereits erstellt - Das Konto wurde bereits erstellt - Wir meinen, dass Zahlungen einfach, umfassend und global sein sollten. Durch den Aufbau mit fortschrittlicher Blockchain-Technologie bietet Code mehr Funktionen als herkömmliche Zahlungs-Apps, wie globale Peer-to-Peer-Überweisungen, Micropayments, die einzelne Artikel online freischalten, und gebührenfreie Tipps für Ihre bevorzugten Entwickler. - Wie Bitcoin ist auch Kin eine Kryptowährung, aber ebenfalls für schnelle, kostengünstige Zahlungen konzipiert. - Wie bei Bitcoin gibt es nur eine begrenzte Menge an Kin. Wenn mehr Menschen Kin kaufen, steigt der Wert, und wenn mehr Menschen Kin verkaufen, sinkt der Wert. Diese Dynamik ermöglicht es jedem Kin-Inhaber an der Wertschöpfung teilzuhaben, sobald die Akzeptanz von Kin wächst. - Kin können Sie mit Ihrer Debitkarte kaufen. Möglich ist dies über die Registerkarte „Kin kaufen\". - Ja, das können Sie. Der Verkauf von Kin wird von zahlreichen Kryptowährungsbörsen unterstützt. - Im Wesentlichen können Sie auf drei Arten helfen: Berichten Sie in den sozialen Medien von Ihren Erfahrungen mit Code, empfehlen Sie Ihren Freunden, Code selbst auszuprobieren, und ermutigen Sie Ihre Lieblingswebsites, Code-Zahlungen zu integrieren, indem Sie sie bitten, sich [getcode.com](https://getcode.com) anzuschauen. - Was ist Code? - Warum lauten die Zahlungen in Code auf Kin? - Warum ändert sich der Wert von Kin? - Wie kaufe ich mehr Kin? - Kann ich Kin verkaufen? - Wie kann ich helfen? - geben Sie die Zustimmung zu unseren - Mit dem Tippen auf \"Konto erstellen\" oder \"Anmelden\" - Ihre Kamera wird benötigt, um Kin zu empfangen. Bitte erlauben Sie den Zugriff auf die Kamera, um fortfahren zu können. - Wir verwenden Push-Benachrichtigungen, um Ihnen zeitgerecht Informationen über Ihr Konto zukommen zu lassen. - Abhebungen sind entgültig und können nicht rückgängig gemacht werden. - Das gesamte Guthaben auf diesem Konto geht damit verloren. Die Löschung Ihres Kontos kann nicht rückgängig gemacht werden. Sind Sie sicher, dass Sie dieses Konto löschen wollen? - Jedes Kin, das nicht innerhalb von 24 Stunden abgeholt wird, wird automatisch auf Ihr Guthaben zurückgebucht. - Sie müssen die Kontoerstellung neu starten und Ihre Telefonnummer erneut verifizieren. - Sie können sich mit Ihrem Zugangsschlüssel wieder bei diesem Konto anmelden. - Sie werden nicht über neue Nachrichten von %1$s benachrichtigt. Sie können die Stummschaltung jederzeit zurücknehmen. - Derzeit werden nur über Code erstellte Konten unterstützt. - Ihre Freunde werden Sie nicht mehr unter dieser Telefonnummer finden können. - Sie werden über alle neuen Nachrichten von %1$s benachrichtigt. Sie können den Chat jederzeit stummschalten. - Sie werden keine Nachrichten von %1$s erhalten, bis Sie erneut eine Zahlung an diesen Empfänger senden. - Ihr Zugangsschlüssel gewährt Ihnen Zugang zu Ihrem Code-Konto. Halten Sie Ihn deshalb geheim und wahren Sie ihn sicher auf. - Diese 12 Wörter sind die einzige Möglichkeit, wie Sie Ihr Code-Konto wiederherstellen können. Schreiben Sie sie deshalb auf und bewahren Sie sie sicher auf. - Sind Sie sich sicher? - Sind Sie sicher, dass Sie dieses Konto löschen möchten? - Haben Sie den Link geschickt? - Sind Sie sicher, dass Sie beenden wollen? - Sind Sie sicher, dass Sie sich abmelden möchten? - %1$s stummschalten? - Kein Code-Konto - Sind Sie sicher? - %1$s nicht mehr stummschalten? - %1$s nicht mehr abonnieren? - Ihren Zugangsschlüssel anzeigen? - Sind Sie sicher? - %1$s Kin wurden auf Ihr Konto eingezahlt. - Die %1$s, die Sie gestern gesendet haben, sind nicht abgeholt worden und wurden automatisch auf Ihr Konto zurückgebucht. - Bringen Sie einen Freund zu Code und erhalten Sie 5 $ dafür - Senden Sie Guthaben über jede beliebige Messenger-App - Sie haben %1$s an Kin erhalten, weil Sie jemandem die ersten Kin geschickt haben. - Sie können jetzt Tipps anfordern. - Einzahlung erhalten - Kin zurückgesendet - Neu bei Code - Empfehlungsbonus erhalten - X-Konto verbunden - Ihre Einladung für den Zugang zu Code ist abgelaufen. Sie können sich abmelden und ein anderes Konto mit einem gültigen Einladungsstatus verwenden. - Ihr Zugangsschlüssel ist die einzige Möglichkeit, wie Sie auf Ihr Geld zugreifen können. Bitte bewahren Sie ihn geschützt und sicher auf. - Verifizieren Sie Ihre Identität, um Ihren Zugangsschlüssel anzuzeigen. - Tippen Sie auf das Google-Lens-Symbol, um den QR-Code für die Anmeldung bei Code zu öffnen. Alternativ können Sie sich auch manuell anmelden, indem Sie die 12 Wörter auf dem Bildschirm „Code-Anmeldung\" eingeben. - Achtung! Dieses Bild ermöglicht Ihnen den Zugriff auf alle Gelder, die Sie in „Code\" haben. Teilen Sie dieses Bild nicht mit anderen. Bewahren Sie es sicher auf. - Mit Code können Sie Kin empfangen, indem Sie Ihre Kamera auf die digitale Rechnung auf dem Telefon eines anderen Benutzers richten. - Sie müssen den Zugriff auf die Kamera erlauben, um Kin empfangen zu können. - Authentifizierung für den Zugriff auf Code. - Kin kaufen - Kin kaufen (demnächst verfügbar) - Der Kauf und Verkauf von Kin ist derzeit ein komplexer Vorgang. Diese Vorgänge werden im Laufe der Zeit einfacher werden. Wenn Sie lernen möchten, wie man Kin kauft und verkauft, können Sie sich die folgenden Video-Leitführer ansehen.\n - Sie können nur bis zu %1$s geben - Sie können maximal %1$s Trinkgeld geben - Ihre Kin sind jetzt für die Verwendung in Ihrer Code-App verfügbar - Sie haben jemandem seinen ersten Kin geschickt! Hier ist Ihr Empfehlungsbonus: - USDC wird in Kin umgewandelt. Dieser Vorgang dauert etwa eine Minute - Sie haben erfolgreich USDC eingezahlt. Öffnen Sie die Code-App, um Ihren Kauf abzuschließen. - Willkommensbonus - Land auswählen - Demnächst erhältlich - \@getcode Ich möchte mein X-Konto verknüpfen, damit ich Tipps von Menschen auf der ganzen Welt erhalte. - Löschen - Vergewissern Sie sich, dass Sie Ihre geheime Wiederherstellungsphrase gespeichert haben, und geben Sie dann „Löschen\" ein, um Ihr Code-Konto zu löschen. Dieser Vorgang kann nicht rückgängig gemacht werden. - Sie haben keinen Code bei %1$s erhalten? - Keinen Code erhalten? Erneut senden - Wenn Sie Face ID deaktivieren wollen, müssen Sie Ihre Identität verifizieren. - Sie haben die Code-Wallet-App noch nicht installiert? - Sie haben noch keine Kin. - Aktivieren Sie Face ID, um die Sicherheit von Transaktionen in Code weiter zu erhöhen. - Zieladresse eingeben - Geben Sie bis zu %1$s ein. - Erhalten Sie 5 $ Kin gratis, wenn Sie einen Freund oder eine Freundin dazu bringen, sich bei Code anzumelden, und Sie ihm oder ihr die ersten Kin schicken. - Code verwendet die Kryptowährung Kin für Zahlungen. Hier sind einige Möglichkeiten, mehr Kin in Ihre Code-Geldbörse zu bekommen. - Erhalten Sie Ihren ersten 1 $ Kin gratis - Verifizieren Sie Ihre Identität, um Kin zu überweisen. - Sie können Code-Wallet mit Kin aufladen, indem Sie Kin an Ihre unten angegebene Einzahlungsadresse senden. Zum Kopieren einfach Tippen. - Bitte mehr Kin besorgen und dann erneut versuchen zu bezahlen. - Ungültiges Empfängerkonto - Bitte vergewissern Sie sich, dass die Adresse, an die Sie abheben wollen, von Ihrem Wallet-Anbieter initialisiert wurde. Sie können dies schneller erreichen, wenn Sie zuerst eine kleine Menge SOL gegen Kin in der Wallet tauschen, an die Sie senden möchten. - Einladungscode - Code ist derzeit nur mit Einladung nutzbar. Sie benötigen einen Einladungscode, um auf die App zuzugreifen. - %1$d Einladungen - Code ist eine neue Krypto-Wallet-App, die derzeit nur für eingeladene Benutzer verfügbar ist. Um Code herunterzuladen, gehen Sie zu %1$s - Mehr erfahren - Ihre Telefonnummer ist mit diesem Code-Konto verknüpft. Freunde können Sie über diese Telefonnummer finden. - Ich verbinde mein X-Konto mit @getcode, damit ich von Menschen auf der ganzen Welt Trinkgeld erhalten kann. - Laden des Saldos und Transaktionsverlaufs - Durchsuchen Sie Ihre Fotos nach dem Zugangsschlüssel, den Sie bei der erstmaligen Erstellung Ihres Kontos gespeichert haben. - Sie sind derzeit bei einem Konto angemeldet. Bitte stellen Sie sicher, dass Sie Ihren Zugangsschlüssel gespeichert haben, bevor Sie fortfahren. Möchten Sie sich abmelden und mit einem neuen Konto anmelden? - Keine Telefonnummer vernüpft - Dieses Code-Konto wurde noch nicht mit einer Telefonnummer verknüpft. Verknüpfen Sie eine Telefonnummer, damit Ihre Freunde Sie finden können. - Keine Netzwerkverbindung - Über Code - Öffnen Sie einfach die Code-App und halten Sie Ihre Kamera darauf, um Ihr Guthaben zu erhalten. - Ihre Kontakte organisieren - Diese Telefonnummer ist nicht in Ihren Kontakten enthalten. Sie können diese Person trotzdem zu Code einladen. - Geben Sie Ihre Telefonnummer einschließlich der Landesvorwahl ein. Vergewissern Sie sich, dass Sie dieselbe Telefonnummer verwenden, unter der Sie die Einladung erhalten haben. - Unterstützt durch - Sie können nun Trinkgeld anfordern - Empfehlungsbonus erhalten - %1$s senden - Hier ist %1$s - Fordern Sie einen neuen Code in %1$s an - Scannen Sie mit Ihrer Handy-Kamera diesen QR-Code, um die Code-Wallet-App herunterzuladen - Zum Herunterladen der Code-Wallet-App scannen - Währungen suchen - Kontaktsuche - Eine SMS-Nachricht mit einem Verifizierungscode wurde an Ihre Telefonnummer gesendet. Bitte geben Sie den Verifizierungscode oben ein. - Jemand hat Ihnen Guthaben geschickt - Jemand hat Ihnen Trinkgeld gegeben - Starten Sie für einen Codes-Scan bitte Ihre Kamera - Mit Ihrer Trinkgeldkarte können Sie von Code-Nutzerinnen und -Nutzern auf der ganzen Welt Trinkgeld erhalten. Verbinden Sie Ihr X-Konto, um auf Ihre Trinkgeldkarte zuzugreifen. - Mit Ihrer Tipp-Karte können Sie Tipps von Code-Benutzern aus der ganzen Welt erhalten. Um auf Ihre Tipp-Karte zuzugreifen, posten Sie an X. - Mit Ihrer Tip Card können Sie Tipps von Code-Benutzern aus aller Welt erhalten. Um auf Ihre Tip Card zuzugreifen, senden Sie eine Nachricht an @getcode unter X. - Erlauben Sie Code, Ihnen Benachrichtigungen zu senden, wenn Sie Tipps von anderen Code-Nutzern erhalten. - Geben Sie „%1$s\" ein - Wir haben einige Änderungen vorgenommen, um das Nutzererlebnis zu verbessern. Die App muss aktualisiert werden, um Code weiterhin nutzen zu können. - Gültiges Inhaberkonto - Gültiges Token-Konto - Der Wert des Kins kann schwanken. - wurde an Sie zurückgegeben - Wohin möchten Sie Ihre abgehobenen Kin überweisen? - Verifizieren Sie Ihre Identität, um Kin abzuheben. - Sie haben eingezahlt - Sie haben gegeben - Sie haben %1$d Einladungen - Code kann momentan nur mit Einladung genutzt werden. Sie haben noch %1$d übrig. - Sie haben bezahlt - Sie haben gegeben - Sie erhielten - Sie haben gesendet - Sie haben ausgegeben - Sie haben jemandem Trinkgeld gegeben - Sie haben abgehoben - Die Abhebung Ihres Guthabens war erfolgreich. - Ihr X-Konto wurde erfolgreich mit Ihrem Code-Konto verbunden. Sie können nun Trinkgeld anfordern. - Abhebung erfolgreich - X-Konto erfolgreich verbunden - Zugang abgelaufen - Zugriffsschlüssel - App-Einstellungen - Kamera automatisch starten - Saldo - Beta-Flaggen - Bonus - Kin kaufen & verkaufen\n - Barzahlungen - Code-Team - Kin-Käufe - Online-Zahlungen - Trinkgeld - Debug-Optionen - Kin einzahlen - Eingezahlt - Zugangsschlüssel-Wörter eingeben - Telefonnummer eingeben - Fehlgeschlagen - FAQ - Überwiesen - Bargeld erhalten\n - Gewinne einen deiner Freunde für Code - Kin erhalten - Mehr Kin erhalten\n - Geld geben - Kin überweisen - Unzureichende Mittel\n - Einen Freund einladen - Nur für eine begrenzte Zeit - Verknüpft - Örtliche Währung - Mein Konto - Nicht verknüpft - Weitere Währungen - Bezahlt - Offen - Telefonnummer - Datenschutzrichtlinien - Gekauft - X-Konto erfolgreich verbunden - Tipps erhalten - Empfangen - Letzte Währungen - Empfehle Sie uns an einen Freund oder eine Freundin weiter und erhalten Sie 5 $ dafür - Empfehlungsbonus - Bargeld anfordern - Kin anfordern - Trinkgeld anfordern - Face ID erforderlich - Passcode erforderlich - Touch ID erforderlich - Ergebnisse - Zurückgeschickt - Wählen Sie ein Konto - Wählen Sie ein Land - Wählen Sie eine Währung - Gesendet - Ausgegeben - Konten wechseln - Servicebedingungen - Tippkarte - Kin-Trinkgeld geben - Benachrichtigungen für Code einschalten - Unbekannt - Aktualisierung erforderlich - Telefonnummer verifizieren - Willkommensbonus - Kin abheben - Abgehoben - Ihr Zugangsschlüssel - Tippen Sie das Logo an, um den Download-Link der App zu teilen - diff --git a/apps/codeApp/src/main/res/values-el/strings-localized.xml b/apps/codeApp/src/main/res/values-el/strings-localized.xml deleted file mode 100644 index b88871c2d..000000000 --- a/apps/codeApp/src/main/res/values-el/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Προσθέστε μετρητά με χρεωστική κάρτα - Να Επιτρέπεται η Πρόσβαση στην Κάμερα - Να Επιτρέπεται η Πρόσβαση στις Επαφές - Να Επιτρέπονται οι Ειδοποιήσεις Push - Υπόλοιπο - Αγορά Kin - Αγοράστε περισσότερα Kin - Ακύρωση - Ακύρωση αποστολής - Συνομιλία - Σύλλεξε Αυτά τα Μετρητά - Επιβεβαίωση - Σύνδεση με X - Συνέχεια - Αντιγράφηκε - Αντιγραφή - Αντιγραφή Διεύθυνσης - Δημιουργία Λογαριασμού - Δημιουργία Νέου Λογαριασμού Code - Διαγραφή Λογαριασμού - Ολοκληρώθηκε - Κάνε λήψη Τώρα - Ενεργοποιήστε το Face ID - Ενεργοποιήστε το Touch ID - Έξοδος - Δώσε \n - Δώστε Kin - Πρόσκληση - Προσκλήσεις - Εγγραφή στη Λίστα Αναμονής - Αργότερα - Μάθετε Πώς Να Αγοράζετε Kin - Μάθετε Πώς Να Πουλάτε Kin - Σύνδεση Αριθμού Τηλεφώνου - Σύνδεση - Αποσύνδεση - Στείλε μήνυμα στο @getcode για Σύνδεση - Σίγαση - Επόμενο - Όχι, δοκιμάστε ξανά - Όχι Τώρα - Εντάξει - Άνοιγμα Ρυθμίσεων - Επικόλληση - Επικόλληση από Πρόχειρο - Στείλε Μήνυμα για Σύνδεση του Λογαριασμού - Τοποθέτηση σε Πορτοφόλι - Λάβε - Ανάκτηση Υπάρχοντος Λογαριασμού - Υπενθύμιση - Κατάργηση Αριθμού Τηλεφώνου - Κατάργηση Αριθμού Τηλεφώνου Σας - Ζητήστε Φιλοδώρημα - Αποθήκευση Κλειδιού Πρόσβασης στις Φωτογραφίες Μου - Αποθήκευση σε Φωτογραφίες - Αποστολή - Αποστολή Κωδικού Επαλήθευσης - Κοινοποίηση - Κοινή χρήση ως URL(Ενιαίος Εντοπιστής Πόρων) - Μοιράσου το Σύνδεσμο Λήψης - Μοιραστείτε Αυτό το Βίντεο - Κοινοποίηση της Κάρτας Φιλοδωρημάτων μου - Εκκίνηση Κάμερας - Εγγραφή - Σαρώστε για να συνδεθείτε - Σύρετε για Πληρωμή - Σύρετε για Φιλοδώρημα - Δοκιμάστε έναν Διαφορετικό Λογαριασμό Code - Στείλε τους Μήνυμα - Ξεκλείδωμα του Code - Κατάργηση σίγασης - Κατάργηση συνδρομής - Ενημέρωση - Προβολή Κλειδιού Πρόσβασης - Ανάληψη Kin - Γράψατε τις 12 λέξεις; - Ναι - Ναι, ανάληψη Kin - Ναι, τις έγραψα - και - Kin - σε Kin - Θα διαγραφούν όλες τις πληροφορίες που σχετίζονται με τον λογαριασμό σας από τους διακομιστές του Code (αριθμός τηλεφώνου, επαφές, ιστορικό συναλλαγών) - Θα μπορείτε να έχετε πρόσβαση στον λογαριασμό σας σε άλλες εφαρμογές πορτοφολιού κρυπτονομισμάτων χρησιμοποιώντας τη μυστική φράση ανάκτησης. Δεν θα μπορείτε να χρησιμοποιήσετε τον λογαριασμό σας στο Code - Δεν θα αφαιρέσει τον λογαριασμό σας από την αλυσίδα μπλοκ - Τι θα κάνει η διαγραφή - Τι θα συμβεί - Τι δεν θα κάνει η διαγραφή - Αυτό το Kin είχε ήδη παραληφθεί από κάποιον άλλον. - Αυτό το Kin το συνέλεξε ήδη κάποιος. - Επιτρέψτε στις Ρυθμίσεις την πρόσβαση στην κάμερα για να χρησιμοποιήσετε το Code. - Αυτά τα μετρητά έχουν λήξει. - Εισαγάγετε ξανά τον αριθμό τηλεφώνου σας και δοκιμάστε πάλι. - Παρακαλούμε επιτρέψτε πρόσβαση στις επαφές από τις Ρυθμίσεις για αποστολή προσκλήσεων. - Προς το παρόν το Code δεν διατίθεται στη χώρα σας. - Δεν είμαστε σε θέση να υποστηρίξουμε τη συσκευή σας τη δεδομένη στιγμή - Η υποστήριξη eSims αναμένεται πιθανότατα σε μελλοντική έκδοση του Code. - Κάτι πήγε στραβά. Δεν μπορέσαμε να εισπράξουμε αυτό το Kin. - Δεν περιμέναμε να συμβεί αυτό. Κάτι πήγε στραβά. Προσπαθήστε να δημιουργήσετε ξανά αυτόν τον λογαριασμό. - Επιτρέψτε στις Ρυθμίσεις την πρόσβαση του Code στις Φωτογραφίες για να αποθηκεύσετε το Κλειδί Πρόσβασης. - παρουσιάστηκε σφάλμα. Βεβαιωθείτε ότι ο αριθμός τηλεφώνου που εισαγάγατε είναι σωστός. - Κάτι πήγε στραβά. Δοκιμάστε πάλι. - Αποτυχία ανάληψης των κεφαλαίων σας. Κάτι πήγε στραβά, δοκιμάστε ξανά την ανάληψη. - Το Code είναι σχεδιασμένο για μικρές, καθημερινές συναλλαγές που είναι για %1$s ή λιγότερο. - Το Code είναι σχεδιασμένο για μικρές, καθημερινές συναλλαγές. Το ημερήσιο όριο παραχώρησής σας θα αυξηθεί αύριο. - Για να μάθετε πώς να αποκτήσετε περισσότερα Kin μεταβείτε στις Συχνές Ερωτήσεις, στις Ρυθμίσεις. - Εισαγάγετε έναν διαφορετικό κωδικό πρόσκλησης και προσπαθήστε ξανά. - Παρακαλούμε γράψτε έναν έγκυρο αριθμό τηλεφώνου και δοκιμάστε ξανά. - Αυτή η Κάρτα Φιλοδωρημάτων είναι μη έγκυρη. - Εισαγάγετε έναν έγκυρο κωδικό και δοκιμάστε πάλι. - Λυπούμαστε, αντιμετωπίσαμε ένα πρόβλημα δικτύου. Προσπαθήστε να καλέσετε ξανά τον φίλο σας. - Αυτό το Kin επιστράφηκε αυτόματα στον αποστολέα επειδή δεν παραλήφθηκε εντός 24 ωρών. Παρακαλείστε να τους ζητήσετε να στείλουν ξανά το Kin. - Εισαγάγετε ξανά τον αριθμό τηλεφώνου σας και δοκιμάστε πάλι. - Παρακαλούμε να προσπαθήσεις να λάβεις μια εικόνα που να δείχνει τον κωδικό πιο καθαρά. - Ελέγξτε τη σύνδεσή σας στο διαδίκτυο ή δοκιμάστε ξανά αργότερα. - Το Code είναι προς το παρόν μόνο για όσους έχουν προσκληθεί. Θα σας ειδοποιήσουμε όταν υπάρξουν περισσότερες διαθέσιμες προσκλήσεις. - Για να εγγραφείτε στη λίστα αναμονής μεταβείτε στο %1$s - Το μέγιστο ποσό που μπορείτε να αγοράσετε είναι %1$s. Εισαγάγετε ένα μικρότερο ποσό. - Το ελάχιστο ποσό που μπορείτε να αγοράσετε είναι %1$s. Εισαγάγετε ένα μεγαλύτερο ποσό. - Το Κλειδί Πρόσβασής σας έχει δρομολογήσει ένα ξεκλείδωμα. Ως αποτέλεσμα, δεν θα μπορείτε πλέον να χρησιμοποιήσετε αυτό το Κλειδί Πρόσβασης στο Code. - Στείλε ένα μήνυμα σ\' αυτό το άτομο για να ενεργοποιηθεί η Κάρτα Συμβουλών τους. - Το μέγιστο φιλοδώρημα που μπορείτε να δώσετε είναι %1$s. Εισαγάγετε ένα μικρότερο ποσό. - Το ελάχιστο φιλοδώρημα που μπορείτε να δώσετε είναι %1$s. Εισαγάγετε ένα μεγαλύτερο ποσό. - Μπορείτε να δημιουργήσετε μόνο τόσους νέους λογαριασμούς κάθε μέρα. - Προς το παρόν το Code έχει περιορισμό έναν λογαριασμό ανά συσκευή. Η υποστήριξη για πολλαπλούς λογαριασμούς θα έρθει πιθανότατα σε μια μελλοντική έκδοση του Code. - Προς το παρόν το Code έχει περιορισμό έναν λογαριασμό ανά συσκευή. Η υποστήριξη για πολλαπλούς λογαριασμούς θα έρθει πιθανότατα σε μια μελλοντική έκδοση του Code. - Kin ήδη παραλήφθηκε - Kin που Συλλέχθηκαν Ήδη - Απαιτείται Πρόσβαση Κάμερας - Τα Μετρητά Έληξαν - Ο Κωδικός Επαλήθευσης Έληξε - Απαιτείται Πρόσβαση στις Επαφές - Η χώρα δεν υποστηρίζεται - Η συσκευή δεν υποστηρίζεται - Προς το παρόν δεν υποστηρίζονται eSims - Αποτυχία Είσπραξης - Αποτυχία Δημιουργίας Λογαριασμού - Αποτυχία Αποθήκευσης - Αποτυχία Αποστολής - Αποτυχία Επιβεβαίωσης - Αποτυχημένη Συναλλαγή - Το Όριο Παραχώρησης Επιτεύχθηκε - Το Ημερήσιο Όριο Επιτεύχθηκε - Ανεπαρκή Kin - Κωδικός πρόσκλησης άκυρος ή έχει λήξει - Μη Έγκυρος Αριθμός Τηλεφώνου - Μη Έγκυρη Κάρτα Φιλοδωρημάτων - Μη Έγκυρος Κωδικός - Πρόσκληση Απέτυχε - Σύνδεσμος έληξε - Υπέρβαση Αριθμού Προσπαθειών - Δεν Βρέθηκε το Code - Δεν Υπάρχει Σύνδεση Στο Διαδίκτυο - Δεν Έχετε Προσκλήσεις - Δεν Έχετε Προσκληθεί Ακόμα - Η αγορά είναι υπερβολικά μεγάλη - Η αγορά είναι υπερβολικά μικρή - Το κλειδί πρόσβασης δεν μπορεί πλέον να χρησιμοποιηθεί στο Code - Η Κάρτα Συμβουλών Δεν Έχει Ενεργοποιηθεί Ακόμα - Φιλοδώρημα Πολύ Μεγάλο - Φιλοδώρημα Πολύ Μικρό - Δημιουργήθηκαν πάρα πολλοί λογαριασμοί - Ο λογαριασμός έχει ήδη δημιουργηθεί - Ο λογαριασμός έχει ήδη δημιουργηθεί - Πιστεύουμε ότι οι πληρωμές πρέπει να είναι απλές, ισχυρές και παγκόσμιες. Δουλεύοντας με την προηγμένη τεχνολογία blockchain, το Code προσφέρει χαρακτηριστικά που δεν μπορούν να προσφέρουν οι παραδοσιακές εφαρμογές πληρωμών, όπως παγκόσμιες μεταφορές peer to peer, μικροπληρωμές που ξεκλειδώνουν μεμονωμένα άρθρα στο διαδίκτυο και zero fee tips για τους αγαπημένους σου δημιουργούς. - Το Kin είναι ένα κρυπτονόμισμα όπως το Bitcoin αλλά έχει επίσης σχεδιαστεί για γρήγορες και φθηνές πληρωμές. - Όπως και με το Bitcoin υπάρχει μόνο περιορισμένος αριθμός διαθέσιμων Kin. Όσοι περισσότεροι άνθρωποι αγοράζουν Kin, η αξία ανεβαίνει, και αν περισσότεροι άνθρωποι πωλούν Kin, η αξία μειώνεται. Αυτή η δυναμική επιτρέπει σε όλους όσοι κατέχουν Kin να συμμετέχουν στη δημιουργία αξίας αν η υιοθέτηση του Kin αυξηθεί. - Μπορείς να αγοράσεις Kin με τη χρεωστική σου κάρτα. Αυτό γίνεται από την καρτέλα Get Kin. - Ναι, μπορείς. Η πώληση Kin υποστηρίζεται από πολλές ανταλλαγές κρυπτονομισμάτων. - Υπάρχουν τρεις βασικοί τρόποι με τους οποίους μπορείς να βοηθήσεις: μίλησε για την εμπειρία σου με το Code στα μέσα κοινωνικής δικτύωσης, ενθάρρυνε τους φίλους σου να δοκιμάσουν το Code και ενθάρρυνε τους αγαπημένους σου ιστότοπους να ενσωματώσουν τις πληρωμές Code, ζητώντας τους να μεταβούν στο [getcode.com](https://getcode.com). - Τι είναι το Code; - Γιατί οι πληρωμές στο Code γίνονται με Kin; - Γιατί αλλάζει η αξία του Kin; - Πώς αγοράζω περισσότερα Kin; - Μπορώ να πουλήσω τα Kin; - Πώς μπορώ να βοηθήσω; - συμφωνείτε με - Πατώντας \"Δημιουργία Λογαριασμού\" ή \"Σύνδεση\" - Η κάμερά σας χρησιμοποιείται για τη λήψη Kin. Επιτρέψτε την πρόσβαση στην κάμερα για να συνεχίσετε. - Χρειαζόμαστε τις ειδοποιήσεις push για να σας στέλνουμε έγκαιρα πληροφορίες σχετικά με τον λογαριασμό σας. - Οι αναλήψεις είναι μη αναστρέψιμες και δεν μπορούν να αναιρεθούν αφού ξεκινήσουν. - Όλα τα κεφάλαια σε αυτόν τον λογαριασμό θα χαθούν. Η διαγραφή του λογαριασμού σας είναι μόνιμη και δεν μπορεί να ανακληθεί. Σίγουρα θέλετε να διαγράψετε αυτόν τον λογαριασμό; - Κάθε Kin που δεν έχει παραληφθεί εντός 24 ωρών θα επιστρέφεται αυτόματα στο υπόλοιπό σας. - Θα χρειαστεί να ξεκινήσετε εκ νέου τη δημιουργία λογαριασμού και να επαληθεύσετε ξανά τον αριθμό τηλεφώνου σας. - Μπορείτε να επιστρέψετε σε αυτόν τον λογαριασμό χρησιμοποιώντας το κλειδί πρόσβασής σας - Δεν θα ειδοποιηθείτε για νέα μηνύματα από %1$s. Μπορείτε να απενεργοποιήσετε τη σίγαση ανά πάσα στιγμή. - Προς το παρόν υποστηρίζονται μόνο οι λογαριασμοί που έχουν δημιουργηθεί μέσω Code. - Οι φίλοι σας δεν θα μπορούν πλέον να σας βρίσκουν με αυτόν τον αριθμό τηλεφώνου. - Θα ειδοποιείστε για όλα τα νέα μηνύματα από %1$s. Μπορείτε να κάνετε σίγαση ανά πάσα στιγμή. - Δεν θα λαμβάνετε μηνύματα από %1$s μέχρι να τους πληρώσετε ξανά. - Το Κλειδί Πρόσβασης θα σας παρέχει πρόσβαση στον λογαριασμό σας Code. Κρατήστε το απόρρητο και ασφαλές. - Αυτές οι 12 λέξεις είναι ο μόνος τρόπος για να ανακτήσετε τον λογαριασμό σας Code. Βεβαιωθείτε ότι τις έχετε καταγράψει και κρατήστε τις απόρρητες και ασφαλείς. - Σίγουρα; - Σίγουρα θέλετε να διαγράψετε αυτόν τον λογαριασμό; - Στείλατε τον σύνδεσμο; - Σίγουρα θέλετε έξοδο; - Σίγουρα θέλετε να αποσυνδεθείτε; - Σίγαση %1$s; - Δεν είναι Λογαριασμός Code - Σίγουρα; - Κατάργηση σίγασης %1$s; - Κατάργηση εγγραφής από %1$s; - Προβολή του Κλειδιού Πρόσβασής σας; - Σίγουρα; - %1$s Kin κατατέθηκαν στο λογαριασμό σου. - Τα %1$s που έστειλες χθες δε συλλέχθηκαν. Έχουν επιστραφεί αυτόματα στο λογαριασμό σου. - Φέρε έναν φίλο να ξεκινήσει στο Code και λάβε $5 - Στείλε μετρητά μέσω οποιασδήποτε εφαρμογής μηνυμάτων - Έλαβες %1$s Kin επειδή έστειλες σε κάποιον τα πρώτα του Kin. - Τώρα μπορείς να ζητήσεις συμβουλές. - Η Κατάθεση Ελήφθη - Kin που έχουν Επιστραφεί - Νέος στο Code - Λήφθηκε Δώρο Σύστασης Φίλου - Ο Χ Λογαριασμός Συνδέθηκε - Η πρόσκλησή σας για πρόσβαση στο Code έχει λήξει. Μπορείτε να αποσυνδεθείτε και να χρησιμοποιήσετε έναν άλλο λογαριασμό με έγκυρη πρόσκληση. - Το Κλειδί Πρόσβασης είναι ο μόνος τρόπος πρόσβασης στα κεφάλαιά σας. Κρατήστε το απόρρητο και ασφαλές. - Επαληθεύστε την ταυτότητά σας για να δείτε το Κλειδί Πρόσβασής σας. - Πατήστε το εικονίδιο Google Lens για να ανοίξετε τον κωδικό QR για να συνδεθείτε στο Code. Εναλλακτικά, μπορείτε να συνδεθείτε χειροκίνητα εισάγοντας τις 12 λέξεις στην οθόνη σύνδεσης του Code. - Προειδοποίηση! Αυτή η εικόνα παρέχει πρόσβαση σε όλα τα χρήματα που έχεις στον Κώδικα. Μη μοιραστείς αυτή την εικόνα με κανέναν άλλο. Διατήρησέ τη σε ασφάλεια. - Το Code σάς επιτρέπει να λαμβάνετε Kin στρέφοντας την κάμερά σας στον ψηφιακό λογαριασμό στο τηλέφωνο ενός άλλου χρήστη. - Πρέπει να επιτρέψετε την πρόσβαση στην κάμερα για να μπορείτε να λαμβάνετε Kin. - Πιστοποίηση ταυτότητας για πρόσβαση στο Code. - Αγόρασε Kin - Αγοράστε Kin (προσεχώς) - Η αγορά και πώληση Kin είναι προς το παρόν μια πολύπλοκη διαδικασία. Αυτές οι διαδικασίες θα απλοποιηθούν με την πάροδο του χρόνου. Αν θέλετε να μάθετε πώς να αγοράζετε και να πουλάτε Kin σήμερα, μπορείτε να παρακολουθήσετε τα παρακάτω κατατοπιστικά βίντεο. - Μπορείτε να παραχωρήσετε μόνο μέχρι %1$s - Μπορείτε να δώσετε φιλοδώρημα μόνο μέχρι %1$s - Τα Kin σας είναι τώρα διαθέσιμα για χρήση στην εφαρμογή Code - Στείλατε σε κάποιον το πρώτο του Kin! Εδώ είναι το μπόνους παραπομπής σας: - Τα USDC σας μετατρέπονται σε Kin. Αυτή η διαδικασία θα διαρκέσει περίπου ένα λεπτό - Η κατάθεση USDC ολοκληρώθηκε με επιτυχία. Ανοίξτε την εφαρμογή Code για να ολοκληρώσετε την αγορά σας - Μπόνους καλωσορίσματος - Επιλέξτε χώρα - Έρχεται Σύντομα - \@getcode θα ήθελα να συνδέσω τον λογαριασμό μου X, ώστε να μπορώ να λαμβάνω συμβουλές από άτομα σε όλο τον κόσμο - Διαγραφή - Βεβαιωθείτε ότι έχετε αποθηκεύσει τη Μυστική Φράση Ανάκτησης και, στη συνέχεια, πληκτρολογήστε \"Διαγραφή\" για να διαγράψετε τον λογαριασμό σας στο Code. Αυτή η ενέργεια είναι μη αναστρέψιμη. - Δεν λάβατε κωδικό στο %1$s; - Δεν λάβατε τον κωδικό; Νέα αποστολή - Για την απενεργοποίηση του Αναγνωριστικού Προσώπου απαιτείται επαλήθευση της ταυτότητάς σας. - Δεν έχεις την εφαρμογή Code Wallet (Κωδικός Πορτοφόλι); - Δεν έχετε ακόμα Kin. - Ενεργοποιήστε το Αναγνωριστικό Προσώπου για να ενισχύσετε περαιτέρω την ασφάλεια των συναλλαγών στο Code. - Εισαγάγετε τη διεύθυνση προορισμού - Εισαγάγετε έως %1$s - Λάβε $5 σε Kin δωρεάν όταν ένας φίλος σου εγγραφεί στο Code και τους στείλεις το πρώτο τους Kin. - Το Code χρησιμοποιεί το κρυπτονόμισμα Kin για πληρωμές. Ακολουθούν μερικοί τρόποι για να βάλετε περισσότερο Kin στο πορτοφόλι σας Code. - Λάβε το Πρώτο Σου $1 Kin Δωρεάν - Επαληθεύστε την ταυτότητά σας για να δώσετε Kin. - Καταθέστε Kin στο πορτοφόλι σας Code στέλνοντας Kin στην παρακάτω Διεύθυνση Κατάθεσης. Πατήστε για αντιγραφή. - Αποκτήστε περισσότερα Kin και, στη συνέχεια, δοκιμάστε ξανά να πληρώσετε - Μη έγκυρος λογαριασμός προορισμού - Παρακαλούμε να βεβαιωθείς ότι η διεύθυνση στην οποία κάνεις ανάληψη έχει αρχικοποιηθεί από τον πάροχο του πορτοφολιού σου. Μια συντόμευση για να το πετύχεις αυτό είναι να ανταλλάξεις πρώτα μια μικρή ποσότητα SOL ( Solana) με Kin στο πορτοφόλι στο οποίο προσπαθείς να στείλεις. - Κωδικός Πρόσκλησης - Το Code είναι προς το παρόν μόνο με πρόσκληση. Θα χρειαστείτε έναν κωδικό πρόσκλησης για να αποκτήσετε πρόσβαση στην εφαρμογή. - %1$d Προσκλήσεις - Το Code είναι μια νέα εφαρμογή πορτοφολιού κρυπτονομισμάτων, που επί του παρόντος μπορεί να χρησιμοποιηθεί μόνο κατόπιν πρόσκλησης. Για να κατεβάσετε το Code, μεταβείτε στο %1$s - Μάθετε περισσότερα - Ο αριθμός τηλεφώνου σας είναι συνδεδεμένος με αυτόν τον λογαριασμό Code. Οι φίλοι μπορούν να σας βρουν χρησιμοποιώντας αυτόν τον αριθμό τηλεφώνου. - Συνδέω τον λογαριασμό μου X με το @getcode, ώστε να μπορώ να λαμβάνω φιλοδωρήματα από άτομα σε όλο τον κόσμο. - Φόρτωση του υπολοίπου σας και του ιστορικού συναλλαγών - Ελέγξτε τις φωτογραφίες σας για το Κλειδί Πρόσβασης που αποθηκεύσατε όταν δημιουργήσατε για πρώτη φορά τον λογαριασμό σας. - Είστε συνδεδεμένοι σε έναν λογαριασμό. Βεβαιωθείτε ότι έχετε αποθηκεύσει το κλειδί πρόσβασής σας πριν προχωρήσετε. Θα θέλατε να αποσυνδεθείτε και να συνδεθείτε με νέο λογαριασμό; - Όχι Συνδεδεμένος\nΑριθμός τηλεφώνου - Δεν έχετε αριθμό τηλεφώνου συνδεδεμένο με αυτόν τον λογαριασμό Code. Συνδέστε έναν, ώστε οι φίλοι σας να μπορούν να σας ανακαλύψουν. - Δεν υπάρχει σύνδεση δικτύου - Στο Code - Άνοιξε την εφαρμογή Code και στρέψτε τη φωτογραφική σου μηχανή για να πάρεις αυτά τα μετρητά - Οργάνωση Επαφών Σας - Αυτός ο αριθμός τηλεφώνου δεν βρίσκεται στις Επαφές σας. Μπορείτε ακόμα να τους προσκαλέσετε στο Code. - Πληκτρολογήστε τον αριθμό τηλεφώνου σας, συμπεριλαμβανομένου του κωδικού χώρας. Βεβαιωθείτε ότι χρησιμοποιείτε τον ίδιο αριθμό τηλεφώνου στον οποίο λάβατε την πρόσκληση. - Με την υποστήριξη του - Μπορείτε τώρα να ζητήσετε φιλοδωρήματα - Λήφθηκε δώρο σύστασης φίλου - Αποστολή %1$s - Ορίστε %1$s - Ζητήστε έναν νέο στο %1$s - Σάρωσε αυτόν τον κωδικό QR (Γρήγορης Απόκρισης) με την κάμερα του τηλεφώνου σου για να κατεβάσεις την εφαρμογή Code Wallet - Σαρώστε για λήψη\nτου Code Wallet - Αναζήτηση νομισμάτων - Αναζήτηση επαφών - Ένα μήνυμα SMS εστάλη στον αριθμό τηλεφώνου σας με έναν κωδικό επαλήθευσης. Παρακαλούμε εισαγάγετε τον κωδικό επαλήθευσης παραπάνω. - Κάποιος σου έστειλε μετρητά - Κάποιος σας έδωσε φιλοδώρημα - Πρέπει να κάνεις εκκίνηση της κάμερας για να σαρώσεις το Codes - Η Κάρτα Φιλοδωρημάτων σας επιτρέπει να λαμβάνετε φιλοδωρήματα από χρήστες του Code σε όλο τον κόσμο. Για να αποκτήσετε πρόσβαση στην Κάρτα Φιλοδωρημάτων σας συνδέστε την ταυτότητά σας X. - Η Κάρτα Συμβουλών σου επιτρέπει να λαμβάνεις συμβουλές από χρήστες του Code σε ολόκληρο τον κόσμο. Για πρόσβαση στην Κάρτα Συμβουλών σου στείλε μήνυμα στο Χ. - Η Κάρτα Συμβουλών σου επιτρέπει να λαμβάνεις συμβουλές από χρήστες του Code σ\' όλο τον κόσμο. Για πρόσβαση στην Κάρτα Συμβουλών στείλε μήνυμα στο @getcode για το X. - Να επιτρέπεται στο Code να σου στέλνει ειδοποιήσεις όταν λαμβάνεις Συμβουλές από άλλους χρήστες του Code. - Πληκτρολογήστε \"%1$s\" - Κάναμε κάποιες αλλαγές για να βελτιώσουμε την εμπειρία. Θα πρέπει να ενημερώσετε την εφαρμογή για να συνεχίσετε να χρησιμοποιείτε το Code. - Έγκυρος λογαριασμός ιδιοκτήτη - Έγκυρος λογαριασμός - Η αξία του Kin αλλάζει. - σας επιστράφηκε - Πού θέλετε να κάνετε ανάληψη τα Kin σας; - Επαληθεύστε την ταυτότητά σας για να αποσύρετε Kin. - Καταθέσατε - Δώσατε - Έχετε %1$d προσκλήσεις - Το Code είναι προς το παρόν μόνο με πρόσκληση. Έχετε ακόμα %1$d. - Πληρώσατε - Δώσατε - Λάβατε - Στείλατε - Ξοδέψατε - Δώσατε φιλοδώρημα σε κάποιον - Αποσύρατε - Έγινε ανάληψη των κεφαλαίων σας με επιτυχία. - Ο λογαριασμός σας X έχει συνδεθεί επιτυχώς με τον λογαριασμό σας Code. Μπορείτε τώρα να ζητήσετε φιλοδωρήματα. - Επιτυχής Ανάληψη - Επιτυχής Σύνδεση Λογαριασμού X - Πρόσβαση Έληξε - Κλειδί Πρόσβασης - Ρυθμίσεις Εφαρμογής - Αυτόματη Εκκίνηση Κάμερας - Υπόλοιπο - Δείκτες που καθορίζουν νέα χαρακτηριστικά του λογισμικού μιας εφαρμογής - Μπόνους - Αγορά και Πώληση Kin - Πληρωμές με μετρητά - Ομάδα κωδικού - Αγορές Kin - Ηλεκτρονικές πληρωμές - Φιλοδωρήματα - Επιλογές Εντοπισμού Σφαλμάτων - Κατάθεση Kin - Κατατέθηκε - Εισαγάγετε τις Λέξεις Κλειδιού Πρόσβασης - Εισαγάγετε αριθμό τηλεφώνου - Απέτυχε - Αριθμός Τηλεφώνου - Δόθηκε - Λάβετε μετρητά - Βρες ένα Φίλο να Ξεκινήσει στο Code - Λάβετε Kin - Αποκτήστε Περισσότερα Kin - Δώσε Μετρητά - Δώστε Kin - Ανεπαρκή Κεφάλαια - Προσκαλέστε έναν Φίλο - Προσφορά Περιορισμένου Χρόνου - Συνδεδεμένο - Τοπικό Νόμισμα - Ο Λογαριασμός Μου - Μη Συνδεδεμένο - Άλλα Νομίσματα - Πληρώθηκε - Εκκρεμεί - Αριθμός Τηλεφώνου - Πολιτική Απορρήτου - Αγοράστηκε - Επιτυχής Σύνδεση Λογαριασμού X - Λάβε Συμβουλές - Παραλήφθηκε - Πρόσφατα Νομίσματα - Σύστησε ένα Φίλο, Λάβε $5 - Δώρο Σύστασης Φίλου - Ζητήστε μετρητά - Ζητήστε Kin - Ζητήστε Φιλοδώρημα - Απαιτείται Αναγνώριση Προσώπου - Απαιτείται Κωδικός Πρόσβασης - Απαιτείται Αναγνώριση Αφής - Αποτελέσματα - Επιστράφηκε - Επιλέξτε Λογαριασμό - Επιλέξτε Χώρα - Επιλέξτε Νόμισμα - Στάλθηκε - Δαπανημένα - Εναλλαγή Λογαριασμών - Όροι Χρήσης - Συμβουλή - Φιλοδώρημα Kin - Ενεργοποίησε τις Ειδοποιήσεις για το Code - Άγνωστο - Απαιτείται Ενημέρωση - Επαλήθευση Αριθμού Τηλεφώνου - Δώρο Καλωσορίσματος - Ανάληψη Kin - Ανάληψη - Κλειδί Πρόσβασής Σας - Πατήστε στο λογότυπο για να μοιραστείτε τον σύνδεσμο λήψης της εφαρμογής - diff --git a/apps/codeApp/src/main/res/values-en-rGB/strings-localized.xml b/apps/codeApp/src/main/res/values-en-rGB/strings-localized.xml deleted file mode 100644 index f0384a2b7..000000000 --- a/apps/codeApp/src/main/res/values-en-rGB/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Add Cash with a Debit Card - Allow Camera Access - Allow Access to Contacts - Allow Push Notifications - Balance - Buy Kin - Buy More Kin - Cancel - Cancel Send - Chat - Collect This Cash - Confirm - Connect to X - Continue - Copied - Copy - Copy Address - Create an Account - Create a New Code Account - Delete Account - Done - Download It Now - Enable Face ID - Enable Touch ID - Exit - Give - Give Kin - Invite - Invites - Join the Waitlist - Later - Learn how to buy Kin - Learn how to sell Kin - Link a Phone Number - Log In - Log Out - Message @getcode to Connect - Mute - Next - No, Try Again - Not Now - OK - Open Settings - Paste - Paste From Clipboard - Post to Connect Account - Put in Wallet - Receive - Recover Existing Account - Remind - Remove Phone Number - Remove Your Phone Number - Request a Tip - Save Access Key to My Photos - Save to Photos - Send - Send Verification Code - Share - Share as a URL - Share Download Link - Share this video - Show My Tip Card - Start Camera - Subscribe - Swipe to Login - Swipe to pay - Swipe to Tip - Try a Different Code Account - Tweet Them - Unlock Code - Unmute - Unsubscribe - Update - View Access Key - Withdraw Kin - Wrote the 12 Words Down Instead? - Yes - Yes, Withdraw Kin - Yes, I Wrote Them Down - and - Kin - of Kin - Remove all information associated with your account from Code\'s servers (phone number, contacts, transaction history) - You can access your account in other crypto wallet apps using your Secret Recovery Phrase. You won\'t be able to use your account in Code - Remove your account from the blockchain - What deleting will do - What will happen - What deleting won\'t do - This Kin was already collected by someone else. - This Kin was already collected by someone. - Please allow access to the camera in Settings to use Code. - This cash has expired. - Please re-enter your phone number and try again. - Please allow access to the contacts in Settings to send invites. - Code is currently unavailable in your country. - We are unable to support your device at this time - Support for eSims will likely come in a future version of Code. - Something went wrong. This Kin could not be collected. - We weren\'t expecting that to happen. Something went wrong. Please try creating this account again. - Please allow Code access to Photos in Settings in order to save your Access Key. - Something went wrong. Please ensure that your phone number is entered correctly. - Something went wrong. Please try again. - Failed to withdraw your funds. Something went wrong, please attempt your withdrawal again. - Code is designed for small, everyday transactions that are for %1$s or less. - Code is designed for small, every day transactions. Your daily give limit will increase tomorrow. - To learn how to get more Kin go to the FAQ in Settings. - Please enter a different Invite Code and try again. - Please enter a valid phone number and try again. - This is an invalid Tip Card. - Please enter a valid code and try again. - Sorry, we experienced a network issue. Please try inviting your friend again. - This Kin was automatically returned to the sender because it wasn\'t collected within 24 hours. Please ask them to send the Kin again. - Please re-enter your phone number and try again. - Please try to get an image that shows the code more clearly. - Please check your internet connection or try again later. - Code is currently invite-only. We will notify you when there are more invites available. - To join the waitlist go to %1$s - The maximum you can purchase is %1$s. Please enter a smaller amount. - The minimum you can purchase is %1$s. Please enter a larger amount. - Your Access Key has initiated an unlock. As a result, you will no longer be able to use this Access Key in Code. - Send a tweet to this person to activate their Tip Card. - The maximum you can tip is %1$s. Please enter a smaller amount. - The minimum you can tip is %1$s. Please enter a larger amount. - You can only create so many new accounts each day. - Code is currently limited to one account per device. Support for multiple accounts will likely come in a future version of Code. - Code is currently limited to one account per phone number. Support for multiple accounts will likely come in a future version of Code. - Kin Already Collected - Kin Already Collected - Camera Access Required - Expired cash - Verification Code Timed Out - Contacts Access Required - Country Not Supported - Device Not Supported - eSims Not Currently Supported - Failed to Collect - Failed to Create Account - Failed to Save - Failed to Send - Failed to Confirm - Transaction Failed - Give Limit Reached - Daily Limit Reached - Insufficient Kin - Invite Code Invalid or Expired - Invalid Phone Number - Invalid Tip Card - Invalid Code - Invitation Failed - Link Expired - Maximum Attempts Reached - No code found - No Internet Connection - You Have No Invites - You Haven\'t Been Invited Yet - Purchase too large - Purchase too small - Access Key No Longer Usable in Code - Tip Card Not Activated Yet - Tip Too Large - Tip Too Small - Too many accounts created - Account Already Created - Account Already Created - We believe payments should be simple, powerful, and global. By building with advanced blockchain technology Code offers features that traditional payments apps can\'t, like global peer to peer transfers, micropayments that unlock individual articles online, and zero fee tips for your favourite creators. - Kin is a cryptocurrency like Bitcoin, but is also designed for fast, inexpensive payments. - Like Bitcoin there is only a limited amount of Kin available. If more people buy Kin the value goes up, and if more people sell Kin the value goes down. This dynamic allows everyone who holds Kin to share in the value creation if adoption of Kin grows. - You can buy Kin with your debit card. This is accessible in the Get Kin tab. - Yes you can. Selling Kin is supported on a number of cryptocurrency exchanges. - There are three main ways you can help: talk about your Code experience on social media, encourage your friends to try out Code for themselves, and encourage your favourite websites to integrate Code payments by asking them to check out [getcode.com](https://getcode.com). - What is Code? - Why are Code payments denominated in Kin? - Why does the value of Kin change? - How do I buy more Kin? - Can I sell Kin? - How can I help? - agree to our - By tapping \"Create an Account\" or \"Log In\" you - Your camera is used to receive Kin. Please allow access to the camera to proceed. - We need push notifications to send you timely information about your account. - Withdrawals are irreversible and cannot be undone once initiated. - All funds in this account will be lost. Deleting your account is permanent and cannot be undone. Are you sure you want to delete this account? - Any Kin that isn\'t collected within 24 hours will be automatically returned to your balance. - You will need to restart account creation and verify your phone number again. - You can get back into this account using your Access Key - You will not be notified of any new messages from %1$s. You can unmute at any time. - Only accounts created through Code are currently supported. - Your friends will no longer be able to find you with this phone number. - You will be notified of all new messages from %1$s. You can mute at any time. - You will not receive any messages from %1$s until you pay them again. - Your Access Key will grant access to your Code account. Keep it private and safe. - These 12 words are the only way to recover your Code account. Make sure you wrote them down, and keep them private and safe. - Are you sure? - Are you sure you want to delete this account? - Did you send the link? - Are you sure you want to exit? - Are you sure you want to log out? - Mute %1$s? - Not a Code Account - Are you sure? - Unmute %1$s? - Unsubscribe from %1$s? - View Your Access Key? - Are You Sure? - %1$s Kin was deposited into your account. - The %1$s you sent yesterday wasn\'t collected. It has been automatically returned to your balance. - Get a friend started on Code and get $5 - Send cash through any messenger app - You received %1$s of Kin for sending someone their first Kin. - You can now request tips. - Deposit Received - Kin Returned - New on Code - Referral Bonus Received - X Account Connected - Your invitation to access Code has expired. You can log out and use a different account with a valid invitation status. - Your Access Key is the only way to access your funds. Please keep it private and safe. - Verify your identity to view your Access Key. - Tap the Google Lens icon to open the QR code to log into Code. Alternatively you can log in manually by entering the 12 words in the Code Log In screen. - Advarsel! Dette billede giver adgang til alle de funds, du har i Code. Del ikke dette billede med andre. Hold det sikkert og beskyttet. - Code enables you to receive Kin by pointing your camera at the digital bill on another user\'s phone - You need to allow camera access to be able to receive Kin - Authenticate to access Code. - Buy Kin - Buy Kin (Coming Soon) - Buying and selling Kin is currently a complex process. These processes will become simpler over time. If you would like to learn how to buy and sell Kin today, you can watch the walkthrough videos below. - You can only give up to %1$s - You can only tip up to %1$s - Your Kin is now available for use in your Code App - You sent someone their first Kin! Here is your referral bonus: - Thank you for opening Code, your purchase is now being completed. Your Kin should be available soon. - You\'re almost there! Open the Code app to complete your purchase - Welcome Bonus - Choose a country - Coming Soon - \@getcode I’d like to connect my X account so I can receive tips from people all over the world - Delete - Make sure you have your Secret Recovery Phrase saved, and then enter \"Delete\" to delete your Code account. This action is irreversible. - Didn\'t get a code at %1$s? - Didn\'t get the code? Resend - Disabling Face ID requires you to verify your identity. - Don\'t have the Code Wallet app? - You don\'t have any Kin yet. - Enable Face ID to further enhance the security of transaction in Code. - Enter destination address - Enter up to %1$s - Get $5 of Kin for free when you get a friend to sign up for Code and you send them their first Kin. - Code uses the cryptocurrency Kin for payments. Here are some ways to get more Kin into your Code wallet. - Get Your First $1 of Kin for Free - Verify your identity to give kin. - Deposit Kin into your Code wallet by sending Kin to your Deposit Address below. Tap to copy. - Please get more Kin and then try to pay again - Invalid destination account - Please make sure the address you\'re withdrawing to has been initialised by your wallet provider. A shortcut to achieve this is to first swap a tiny amount of SOL for Kin in the wallet you\'re trying to send to. - Invite Code - Code is currently by invite only. You will need an Invite Code to access the app. - %1$d Invites - Code is a new crypto wallet app. Currently, it\'s invite only. To download Code, go to %1$s - Learn more - Your phone number is linked with this Code account. Friends can find you using this phone number. - I\'m connecting my X account with @getcode so I can receive tips from people all over the world. - Loading your balance and transaction history - Check your photos for the Access Key you saved when you first created your account. - You\'re currently logged into an account. Please ensure you have saved your Access Key before proceeding. Would you like to log out and log in with a new account? - No Linked\nPhone Number - You don\'t have a phone number linked to this Code account. Link one so that your friends can discover you. - No network connection - On Code - Open the Code app and point your camera to grab this cash - Organising Your Contacts - This phone number is not in your Contacts. You can still invite them to Code. - Enter your phone number including the country code. Make sure you use the same phone number that received the invite. - Powered by - You can now request tips - Referral bonus received - Send %1$s - Here\'s %1$s - Request a new one in %1$s - Scan this QR code with your phone\'s camera to download the Code Wallet app - Scan to download the\nCode Wallet app - Search currencies - Search for contacts - An SMS message was sent to your phone number with a verification code. Please enter the verification code above. - Someone sent you cash - Someone tipped you - You need to start your camera to scan Codes - Your Tip Card lets you receive tips from Code users all over the world. To access your Tip Card connect your X identity. - Your Tip Card lets you receive tips from Code users all over the world. To access your Tip Card post to X. - Your Tip Card lets you receive tips from Code users all over the world. To access your Tip Card, message @getcode on X. - Allow Code to send you notifications when you receive Tips from other Code users. - Type \"%1$s\" - We\'ve made some changes to improve the experience. You\'ll need to update the app to keep using Code. - Valid owner account - Valid token account - The value of Kin changes. - was returned to you - Where would you like to withdraw your Kin to? - Verify your identity to withdraw kin. - You deposited - You gave - You Have %1$d Invites - Code is currently invite only. You have %1$d remaining. - You paid - You gave - You received - You sent - You spent - You tipped someone - You withdrew - Your funds have been successfully withdrawn. - Your X account has been successfully connected to your Code account. You can now request Tips. - Withrawal Successful - X Account Successfully Connected - Access Expired - Access Key - App Settings - Auto Start Camera - Balance - Beta Flags - Bonus - Buy & sell Kin - Cash Payments - Code Team - Kin Purchases - Web Payments - Tips - Debug Options - Deposit Kin - Deposited - Enter Access Key Words - Enter Phone Number - Failed - FAQ - Gave - Get Cash - Get a Friend Started on Code - Get Kin - Get more Kin - Give Cash - Give Kin - Insufficient funds - Invite a Friend - Limited Time Offer - Linked - Local Currency - My Account - Not Linked - Other Currencies - Paid - Pending - Phone Number - Privacy Policy - Purchased - X Account Successfully Connected - Receive Tips - Received - Recent Currencies - Refer a Friend and Get $5 - Referral Bonus - Request Cash - Request Kin - Request a Tip - Require Face ID - Require Passcode - Require Touch ID - Results - Returned - Select an Account - Select a Country - Select a Currency - Sent - Spent - Switch Accounts - Terms of Service - Tip Card - Tip Kin - Turn On Notifications for Code - Unknown - Update Required - Verify Phone Number - Welcome Bonus - Withdraw Kin - Withdrew - Your Access Key - Tap the logo to share the app download link - diff --git a/apps/codeApp/src/main/res/values-es/strings-localized.xml b/apps/codeApp/src/main/res/values-es/strings-localized.xml deleted file mode 100644 index d177af915..000000000 --- a/apps/codeApp/src/main/res/values-es/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Añadir efectivo con una tarjeta de débito - Permitir acceso a cámara - Permitir acceso a contactos - Permitir notificaciones Push - Saldo - Comprar Kin - Comprar más Kin - Cancelar - Cancelar envío - Chat - Cobra este dinero - Confirmar - Conectar con X - Continuar - Copiado - Copiar - Copiar dirección - Crear una cuenta - Crear una cuenta Code nueva - Borrar cuenta - Hecho - Descárgala ya - Habilitar identificación facial - Habilitar identificación táctil - Salir - Dar - Dar Kin - Invitar - Invitaciones - Unirse a la lista de espera - Después - Aprenda cómo comprar Kin - Aprenda cómo vender Kin - Vincular un número de teléfono - Iniciar sesión - Cerrar sesión - Envía un mensaje a @getcode para conectarte - Silenciar - Siguiente - No, inténtalo de nuevo - Ahora no - OK - Abrir ajustes - Pegar - Pegar desde el portapapeles - Publica para conectar la cuenta - Guardar en el monedero - Recibir - Recuperar cuenta existente - Recordar - Eliminar número de teléfono - Eliminar tu número de teléfono - Solicitar una propina - Guardar clave de acceso en mis fotos - Guardar en fotos - Enviar - Enviar código de verificación - Compartir - Compartir como URL - Compartir enlace de descarga - Compartir este vídeo - Mostrar mi tarjeta de propinas - Abrir cámara - Suscribirse - Deslizar para iniciar sesión - Deslizar para pagar - Deslizar para dar propina - Probar con una cuenta Code diferente - Tuitéales - Desbloquear Code - Anular silencio - Cancelar suscripción - Actualizar - Ver clave de acceso - Retirar Kin - ¿Has anotado las 12 palabras en su lugar? - - Sí, retirar Kin - Sí, las he anotado - y - Kin - de Kin - Eliminar toda la información asociada a tu cuenta de los servidores de Code (número de teléfono, contactos, historial de transacciones). - Puedes acceder a tu cuenta en otras aplicaciones de monedero criptográfico utilizando tu frase secreta de recuperación. No podrás usar tu cuenta en Code - Elimine tu cuenta de la cadena de bloques - Qué se consigue con la eliminación - Qué ocurrirá - Lo que no se consigue con la eliminación - Este Kin ya fue recogido por otra persona. - Alguien ya ha recogido este Kin. - Permite el acceso a la cámara en Ajustes para utilizar Code. - Este dinero ha caducado. - Vuelve a introducir tu número de teléfono e inténtalo de nuevo. - Para enviar invitaciones, permite el acceso a los contactos en \"Configuración\". - Code no está disponible actualmente en tu país. - No podemos dar asistencia a tu dispositivo en este momento - Es probable que la asistencia para eSims se implemente en una versión futura de Code. - Algo ha salido mal. Este Kin no se ha podido recibir. - Eso no nos lo esperábamos. Algo ha ido mal. Intenta crear la cuenta de nuevo. - Permite que Code acceda a las fotos en Ajustes para guardar tu clave de acceso. - Se ha producido un error. Asegúrate de haber introducido correctamente tu teléfono. - Algo ha salido mal. Inténtalo de nuevo. - No se han podido retirar los fondos debido a un fallo. Inténtalo de nuevo. - Code está diseñado para pequeñas transacciones cotidianas de %1$s o menos. - Code está diseñado para pequeñas transacciones diarias. Tu límite diario aumentará mañana. - Para saber cómo conseguir más Kin, consulta las FAQ en Ajustes. - Introduzca otra invitación diferente e inténtelo de nuevo. - Introduce un número de teléfono válido e inténtalo de nuevo. - Esta tarjeta de propinas no es válida. - Introduce un código válido e inténtalo de nuevo. - Lo sentimos, hemos tenido un problema con la red. Intenta invitar de nuevo a tu amigo. - Este Kin fue devuelto automáticamente al remitente porque no fue recogido en 24 horas. Pídeles que vuelvan a enviar el Kin. - Vuelve a introducir tu número de teléfono e inténtalo de nuevo. - Intenta conseguir una imagen que muestre el código de forma más clara. - Comprueba tu conexión a internet o inténtalo más tarde. - Actualmente, Code está disponible solo mediante invitación. Te avisaremos cuando haya más invitaciones disponibles. - Para unirte a la lista de espera, ve a %1$s - El máximo que puedes comprar es %1$s. Introduce una cantidad menor. - El mínimo que puedes comprar es %1$s. Introduce una cantidad mayor. - Su Clave de Acceso ha iniciado un desbloqueo. Por consiguiente, ya no podrá utilizar esta clave de acceso en Code. - Envía un tuit a esta persona para activar su tarjeta de propinas. - La propina máxima es %1$s. Debes introducir una cantidad menor. - La propina mínima es %1$s. Debes introducir una cantidad mayor. - Solo puedes crear un número limitado de cuentas nuevas cada día. - Code está actualmente limitado a una cuenta por dispositivo. Es probable que la asistencia para múltiples cuentas se implemente en una versión futura de Code. - Code está actualmente limitado a una cuenta por número de teléfono. Es probable que la asistencia para múltiples cuentas se implemente en una versión futura de Code. - Kin Ya recogido - Kin ya recogido - Acceso a la cámara necesario - El efectivo ha caducado - Código de verificación caducado - Se requiere acceso a los contactos - País no admitido - Dispositivo no compatible - eSims no compatibles actualmente - Fallo en la recepción - No se ha podido crear la cuenta - No se ha podido guardar - No se ha podido enviar - No se ha podido confirmar - Transacción fallida - Límite alcanzado - Límite diario alcanzado - Kin insuficientes - La invitación a Code no es válida o ha caducado - Número de teléfono no válido - Tarjeta de propinas no válida - Código no válido - Invitación fallida - Enlace caducado - Se ha alcanzado el número máximo de intentos - No se encuentra ningún código - No hay conexión a internet - No tienes invitaciones - Aún no te han invitado - La compra es demasiado grande - La compra es demasiado pequeña - Esta clave de acceso ya no se podrá utilizar en Code - Tarjeta de propinas todavía no activada - Propina demasiado grande - Propina demasiado pequeña - Has creado demasiadas cuentas - Cuenta ya creada - Cuenta ya creada - Creemos que los pagos deben ser sencillos, poderosos y globales. Code emplea tecnología avanzada de cadena de bloques para ofrecer funciones que las aplicaciones de pago tradicionales no pueden, como transferencias globales entre particulares, micropagos que desbloquean artículos individuales en línea y propinas sin comisiones para tus creadores de contenido favoritos. - Kin es una criptomoneda como Bitcoin, pero también está diseñada para pagos rápidos y económicos. - Al igual que Bitcoin, la cantidad de Kin disponible es limitada. Si más personas compran Kin, su valor aumenta, pero si más personas venden Kin, su valor disminuye. Esta dinámica permite a todos los titulares de Kin participar en la creación de valor si crece la adopción de Kin. - Puedes comprar Kin con tu tarjeta de débito. Puedes hacerlo en la pestaña \"Get Kin\". - Sí. Puedes vender Kin en varios mercados de criptomonedas. - Puedes ayudar de tres formas principales: habla de tu experiencia con Code en las redes sociales, anima a tus amigos a que prueben Code por sí mismos y anima a tus sitios web favoritos a integrar los pagos con Code pidiéndoles que visiten [getcode.com](https://getcode.com). - ¿Qué es Code? - ¿Por qué los pagos de Code están denominados en Kin? - ¿Por qué cambia el valor de Kin? - ¿Cómo puedo comprar más Kin? - ¿Puedo vender Kin? - ¿Cómo puedo ayudar? - aceptas nuestros - Al pulsar \"Crear una cuenta\" o \"Iniciar sesión\", - Tu cámara se emplea para recibir Kin. Permite el acceso a la cámara para continuar. - Necesitamos notificaciones Push para enviarte información puntual sobre tu cuenta. - Las retiradas son irreversibles y no pueden deshacerse una vez iniciadas. - Todos los fondos que haya en esta cuenta se perderán. La eliminación es permanente y no se puede deshacer. ¿Seguro que quieres eliminarla? - Los Kin que no se cobren en un plazo de 24 horas se devolverán automáticamente a tu saldo. - Tendrás que reanudar la creación de la cuenta y verificar de nuevo tu número de teléfono. - Puedes volver a entrar en esta cuenta con tu clave de acceso - No recibirás ninguna notificación de nuevos mensajes de %1$s. Puedes anular el modo silencio en cualquier momento. - Actualmente, solo se admiten las cuentas creadas a través de Code. - Tus amigos ya no podrán encontrarte con este número de teléfono. - Recibirás todas las notificaciones de nuevos mensajes de %1$s. Puedes silenciarlo en cualquier momento. - No recibirás ningún mensaje de %1$s hasta que pagues otra vez. - Tu clave de acceso te permitirá acceder a tu cuenta Code. Guárdala de forma privada y segura. - Estas 12 palabras son la única manera de recuperar tu cuenta Code. Asegúrate de anotarlas y guardarlas de forma privada y segura. - ¿Seguro? - ¿Seguro que quieres borrar esta cuenta? - ¿Enviaste el enlace? - ¿Seguro que quieres salir? - ¿Seguro que quieres cerrar la sesión? - ¿Silenciar %1$s? - No es una cuenta Code - ¿Seguro? - ¿Dejar de silenciar %1$s? - ¿Cancelar suscripción de %1$s? - ¿Ver tu clave de acceso? - ¿Seguro? - Se ha(n) depositado %1$s Kin en tu cuenta - El %1$s que enviaste ayer no se recogió. Se ha devuelto automáticamente a tu saldo. - Inicia a un amigo en Code y consigue 5 $ - Envía dinero a través de cualquier aplicación de mensajería - Has recibido %1$sde Kin por enviar a alguien su primer Kin. - Ahora puedes pedir propinas. - Depósito recibido - Kin devuelto - Nuevo en Code - Bono por recomendación recibido - Cuenta de X conectada - Tu invitación de acceso a Code ha caducado. Puedes cerrar la sesión y utilizar una cuenta diferente con una invitación válida. - Tu clave de acceso es la única forma de acceder a tus fondos. Asegúrate de guardarla de forma privada y segura. - Verifica tu identidad para ver tu clave de acceso. - Toca el icono de Google Lens para abrir el código QR para iniciar sesión en Code. También puedes iniciar sesión manualmente introduciendo las 12 palabras en la pantalla de inicio de sesión de Code. - ¡Atención! Esta imagen da acceso a todos los fondos que tienes en Code. No compartas esta imagen con nadie. Guárdala de forma segura. - Code te permite recibir Kin con solo dirigir tu cámara a la factura digital del teléfono de otro usuario - Debes permitir el acceso a la cámara para poder recibir Kin - Autentificarse para acceder a Code. - Comprar Kin - Comprar Kin (muy pronto) - Actualmente, comprar y vender Kin es un proceso complejo. Estos procesos se simplificarán con el tiempo. Si desea aprender hoy mismo a comprar y vender Kin, puede ver los siguientes vídeos explicativos. - Solo puedes dar hasta %1$s - Solo puedes dar propinas hasta %1$s - Tu Kin ya está disponible para su uso en la aplicación Code - ¡Has enviado a alguien su primer Kin! Aquí tienes tu bonificación por recomendación: - Tus USDC se están convirtiendo a Kin. Esto debería tardar aproximadamente un minuto en completarse. - Has depositado USDC correctamente. Abre la aplicación Code para completar la compra - Bonificación de bienvenida - Elegir un país - Próximamente - \@getcode Me gustaría conectar mi cuenta de X para poder recibir consejos de personas de todo el mundo - Eliminar - Asegúrate de tener guardada tu frase secreta de recuperación y, a continuación, introduce «Eliminar» para borrar tu cuenta de Code. Esta acción es irreversible. - ¿No has recibido un código en %1$s? - ¿No has recibido el código? Volver a enviar. - Para desactivar la identificación facial, es necesario que verifiques tu identidad. - ¿No tienes la aplicación Code Wallet? - Aún no tienes Kin. - Habilita la identificación facial para reforzar aún más la seguridad de las transacciones en Code. - Introducir dirección de destino - Introduce hasta %1$s - Obtén 5 $ de Kin gratis cuando consigas que un amigo se registre en Code y le envíes su primer Kin. - Code utiliza la criptomoneda Kin para los pagos. Aquí tienes algunas formas de conseguir más Kin en tu monedero Code. - Consigue gratis tu primer dólar de Kin - Verifica tu identidad para dar Kin. - Deposita Kin en tu monedero Code enviando Kin a tu dirección de depósito que aparece a continuación. Toca para copiar. - Consiga más Kin e intente pagar de nuevo. - Cuenta de destino no válida - Asegúrese de que el proveedor de su monedero ha inicializado la dirección a la que está retirando dinero. Un método rápido para conseguirlo es cambiar primero una pequeña cantidad de SOL por Kin en el monedero al que intenta enviar. - Invitar a Code - Actualmente, Code es solo por invitación. Necesitará una invitación para poder acceder. - %1$d invitaciones - Code es una nueva aplicación de monedero criptográfico a la que sólo se puede acceder por invitación. Para descargar Code, visita %1$s. - Más información - Tu número de teléfono está vinculado a esta cuenta Code. Tus amigos pueden utilizarlo para localizarte. - Estoy conectando mi cuenta de X con @getcode para poder recibir propinas de gente de todo el mundo. - Cargando tu saldo e historial de transacciones - Busca en tus fotos la clave de acceso que guardaste al crear tu cuenta. - Actualmente estás conectado a una cuenta. Asegúrate de haber guardado tu clave de acceso antes de continuar. ¿Deseas desconectarte e iniciar sesión con una nueva cuenta? - Número de teléfono\nno vinculado - No tienes ningún número de teléfono vinculado a esta cuenta Code. Vincula uno para que tus amigos puedan encontrarte. - Sin conexión a la red - En Code - Abre la aplicación Code y apunta con tu cámara para cobrar este dinero - Organizando tus contactos - Este número de teléfono no está en tus contactos. Pero puedes invitarle a Code. - Introduce tu número de teléfono incluyendo el código de país. Asegúrate de utilizar el mismo número en el que recibiste la invitación. - Desarrollado por - Ahora puedes solicitar propinas - Bono por recomendación recibido - Enviar %1$s - Aquí está %1$s - Solicitar uno nuevo en %1$s - Escanea este código QR con la cámara de tu móvil para descargar la aplicación Code Wallet - Escanea para descargar la\naplicación Code Wallet - Buscar divisas - Buscar contactos - Se ha enviado un mensaje SMS con un código de verificación a su número de teléfono. Introduzca aquí dicho código. - Alguien te ha enviado dinero - Alguien te ha dado una propina - Debes abrir tu cámara para escanear Codes - Tu tarjeta de propinas te permite recibir propinas de usuarios de Code de todo el mundo. Para poder acceder a tu tarjeta de propinas, conecta con tu identidad de X. - Con tu tarjeta de propinas puedes recibir propinas de usuarios de Code de todo el mundo. Para acceder a tu tarjeta de propinas, publica en X. - Tu Tip Card te permite recibir consejos de usuarios de Code de todo el mundo. Para acceder a tu Tip Card envía un mensaje @getcode en X. - Permita que Code le envíe notificaciones cuando reciba Sugerencias de otros usuarios de Code. - Escribe \"%1$s\" - Hemos realizado algunos cambios para mejorar la experiencia. Tendrás que actualizar la aplicación para seguir utilizando Code. - Cuenta de titular válida - Cuenta de token válida - El valor de Kin cambia. - se te ha devuelto - ¿Adónde quieres retirar tus Kin? - Verifica tu identidad para retirar Kin. - Has depositado - Has dado - Tienes %1$d invitaciones - Actualmente, Code está disponible solo mediante invitación. Te quedan %1$d. - Has pagado - Has dado - Has recibido - Has enviado - Has gastado - Le has dado una propina a alguien - Has retirado - Tus fondos se han retirado con éxito. - Tu cuenta de X se ha conectado correctamente a tu cuenta de Code. Ahora puede solicitar propinas. - Retirada realizada con éxito - Cuenta de X conectada correctamente - Acceso caducado - Clave de acceso - Configuración de la aplicación - Abrir automáticamente la cámara - Saldo - Banderas beta - Bono - Comprar y vender Kin - Pagos en efectivo - Equipo de código - Compras de Kin - Pagos por Internet - Propinas - Opciones de depuración - Depositar Kin - Depositó - Introducir palabras de clave de acceso - Introducir número de teléfono - Fallido - FAQ - Dio - Obtener efectivo - Inicia a un amigo en Code - Obtener Kin - Obtenga más Kin - Dar efectivo - Dar Kin - Fondos insuficientes - Invitar a un amigo - Oferta por tiempo limitado - Vinculado - Divisa local - Mi cuenta - No vinculado - Otras divisas - Pagado - Pendiente - Número de teléfono - Política de privacidad - Comprado - Cuenta de X conectada correctamente - Recibir sugerencias - Recibió - Divisas recientes - Recomienda a un amigo y consigue 5 $ - Bono por recomendación - Solicitar efectivo - Solicitar Kin - Solicitar una propina - Requerir Face ID - Requerir contraseña - Requerir Touch ID - Resultados - Devuelto - Seleccionar una cuenta - Seleccionar un país - Elegir una divisa - Enviado - Gastado - Cambiar de cuenta - Términos y condiciones - Tarjeta de propina - Dar Kin de propina - Activar las notificaciones de código - Desconocido - Actualización requerida - Verificar número de teléfono - Bono de bienvenida - Retirar Kin - Retiró - Tu clave de acceso - Pulsa el logotipo para compartir el enlace de descarga de la aplicación - diff --git a/apps/codeApp/src/main/res/values-fi/strings-localized.xml b/apps/codeApp/src/main/res/values-fi/strings-localized.xml deleted file mode 100644 index 92e6d33e2..000000000 --- a/apps/codeApp/src/main/res/values-fi/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Lisää rahaa pankkikortilla - Salli kameran käyttöoikeudet - Salli yhteystietojen käyttöoikeudet - Salli palveluilmoitukset - Saldo - Osta kinejä - Osta lisää Kin-klikkoja - Peruuta - Peruuta lähettäminen - Keskustelu - Ota käteisrahat vastaan - Vahvista - Yhdistä X:ään - Jatka - Kopioitu - Kopioi - Kopioi osoite - Luo tili - Luo uusi kooditili - Poista tili - Valmis - Lataa nyt - Ota kasvotunnistus käyttöön - Ota kosketustunnistus käyttöön - Poistu - Anna - Anna kinejä - Kutsu - Kutsut - Liity odotuslistalle - Myöhemmin - Opi, kuinka kinejä ostetaan - Opi, kuinka kinejä myydään - Yhdistä puhelinnumero - Kirjaudu sisään - Kirjaudu ulos - Yhdistä lähettämällä viesti @getcode - Hiljennä - Seuraava - Ei, yritä uudelleen - Ei nyt - OK - Avaa asetukset - Liitä - Liitä leikepöydältä - Julkaise yhdistettävälle tilille - Pane lompakkoon - Ota vastaan - Palauta olemassa oleva tili - Muistuta - Poista puhelinnumero - Poista puhelinnumerosi - Pyydä tippi - Tallenna käyttöoikeusavain omiin valokuviini - Tallenna valokuviin - Lähetä - Lähetä vahvistuskoodi - Jaa - Jaa URL-osoitteena - Jaa latauslinkki - Jaa tämä video - Näytä tippikorttini - Käynnistä kamera - Tilaa - Kirjaudu sisään pyyhkäisemällä - Maksa pyyhkäisemällä - Anna tippi pyyhkäisemällä - Kokeile eri kooditiliä - Twiittaa hänelle - Avaa Code - Poista mykistys - Lopeta tilaus - Päivitä - Näytä käyttöoikeusavain - Nosta kinejä - Kirjoitettiinko 12 sanaa sen sijaan muistiin? - Kyllä - Kyllä, nosta kinejä - Kyllä, kirjoitin ne muistiin - ja - Kin - kiniä - Kaikki käyttäjätiliisi liittyvät tiedot poistetaan Coden palvelimilta (puhelinnumero, yhteystiedot, siirtohistoria) - Voit käyttää tiliäsi muissa kryptolompakkosovelluksissa salaisen palautuslauseesi avulla. Codessa et voi käyttää tiliäsi - Poistaa käyttäjätiliäsi lohkoketjusta - Mitä poistamisesta seuraa - Mitä tapahtuu - Mitä poistamisella ei voi tehdä - Joku toinen oli jo lunastanut tämän kinin. - Joku on jo lunastanut tämän kinin - Salli kameran käyttöoikeudet asetuksista käsin voidaksesi käyttää Codea. - Tämä raha on vanhentunut. - Syötä puhelinnumerosi uudelleen ja yritä taas. - Voit lähettää kutsuja sallimalla asetuksissa yhteystietojen käyttöoikeuden. - Code ei ole tällä hetkellä saatavilla maassasi. - Emme voi tukea laitettasi juuri nyt - eSIMien tuki tulee todennäköisesti saataville Coden tulevassa versiossa. - Jotakin meni pieleen. Tätä Kiniä ei onnistuttu keräämään. - Emme odottaneet näin käyvän. Jotakin meni vikaan. Kokeile tämän tilin luomista uudestaan. - Anna Codelle valokuvien käyttöoikeudet asetuksista käsin, jotta voit tallentaa käyttöoikeusavaimesi. - Jokin meni vikaan. Varmistathan, että puhelinnumerosi on syötetty oikein. - Jotakin meni vikaan. Yritä uudestaan. - Rahojesi nostaminen epäonnistui. Jotakin meni vikaan. Yritä nostoa uudestaan. - Code on suunniteltu arjen pieniin siirtoihin, jotka ovat summaltaan %1$s tai alle. - Code on suunniteltu arjen pieniin siirtoihin. Päivittäinen antamisen rajasi nousee huomenna. - Lue asetusten UKK-osastosta, miten saat lisää kinejä. - Syötä eri kutsukoodi ja yritä uudelleen. - Kirjoita puhelinnumero oikeassa muodossa ja yritä sitten uudelleen. - Tämä on virheellinen tippikortti. - Syötä voimassaoleva koodi ja yritä uudestaan. - Kärsimme valitettavasti verkko-ongelmasta. Kokeile ystäväsi kutsumista uudestaan. - Tämä kin palautettiin automaattisesti lähettäjälle, koska sitä ei lunastettu 24 tunnin kuluessa. Pyydä häntä lähettämään kin uudelleen. - Syötä puhelinnumerosi uudelleen ja yritä taas. - Koeta ottaa kuva, jossa koodi näkyisi selvemmin. - Tarkista Internet-yhteytesi tai yritä myöhemmin uudestaan. - Code on tällä hetkellä käytettävissä vain kutsusta. Ilmoitamme sinulle, kun lisää kutsuja on saatavilla. - Jos haluat liittyä odotuslistalle, mene tänne: %1$s - Voit ostaa enintään %1$s. Syötä pienempi summa. - Sinun täytyy ostaa vähintään %1$s. Syötä suurempi summa. - Käyttöoikeusavaimesi on aloittanut avaamistapahtuman. Et sen vuoksi pysty enää käyttämään Codessa tätä käyttöoikeusavainta. - Lähetä tälle henkilölle twiitti aktivoidaksesi hänen tippikorttinsa. - Suurin mahdollinen tippi on %1$s. Syötä pienempi summa. - Pienin mahdollinen tippi on %1$s. Syötä suurempi summa. - Voit luoda vain tietyn määrän uusia tilejä päivässä. - Code on tällä hetkellä rajoitettu yhteen tiliin laitetta kohden. Useampien tilien tuki tulee saataville todennäköisesti Coden tulevassa versiossa. - Code on tällä hetkellä rajoitettu yhteen tiliin puhelinnumeroa kohden. Useampien tilien tuki tulee saataville todennäköisesti Coden tulevassa versiossa. - Kin jo lunastettu - Kin jo lunastettu - Kameran käyttöoikeus vaaditaan - Raha vanhentui - Vahvistuskoodi aikakatkaistu - Yhteystietojen käyttöoikeus tarvitaan - Maata ei tueta - Laitetta ei tueta - eSIMejä ei tueta juuri nyt - Kerääminen epäonnistui - Tilin luominen epäonnistui - Tallentaminen epäonnistui - Lähettäminen epäonnistui - Vahvistaminen epäonnistui - Tapahtuma epäonnistui - Antamisen raja saavutettu - Päivittäinen raja saavutettu - Ei tarpeeksi kinejä - Kutsukoodi virheellinen tai vanhentunut - Virheellinen puhelinnumero - Virheellinen tippikortti - Virheellinen koodi - Kutsu epäonnistui - Linkki vanhentunut - Yritysten enimmäismäärä täyttynyt - Koodia ei löytynyt - Ei Internet-yhteyttä - Sinulla ei ole kutsuja - Sinua ei ole vielä kutsuttu - Liian suuri ostos - Liian pieni ostos - Käyttöoikeusavain ei enää käytettävissä Codessa - Tippikortti ei vielä aktivoitu - Tippi liian iso - Liian pieni tippi - Luotu liian monta tiliä - Tili on jo luotu - Tili on jo luotu - Uskomme, että maksujen pitäisi olla yksinkertaisia, tehokkaita ja maailmanlaajuisia. Code nojaa edistyneeseen lohkoketjutekniikkaan ja tarjoaa näin ominaisuuksia, joihin perinteiset maksusovellukset eivät pysty, kuten globaaleja vertaisverkkosiirtoja, mikromaksuja, jotka avaavat yksittäisiä artikkeleita verkossa, ja tippejä suosikkitekijöillesi ilman välityspalkkiota. - Kin on bitcoinin kaltainen kryptovaluutta, mutta se on myös tarkoitettu nopeisiin ja edullisiin maksuihin. - Bitcoinin tavoin myös Kiniä on saatavilla vain rajallinen määrä. Jos useammat ihmiset ostavat Kiniä, sen arvo nousee, ja jos useammat ihmiset myyvät Kiniä, sen arvo laskee. Tämä dynamiikka antaa kaikille Kinin omistajille mahdollisuuden päästä osalliseksi arvonmuodostuksesta, jos Kiniä ryhdytään käyttämään enemmän. - Voit ostaa Kiniä pankkikortilla. Tämä on mahdollista Osta Kiniä -välilehdellä. - Kyllä voit. Kinin myyntiä tuetaan useissa kryptovaluuttapörsseissä. - Voit auttaa kolmella tavalla: kerro omasta Coden käyttökokemuksestasi sosiaalisessa mediassa, rohkaise ystäviäsi kokeilemaan Codea itse ja rohkaise suosikkisivustojasi integroimaan Code-maksut pyytämällä niitä tutustumaan sivustoon [getcode.com](https://getcode.com). - Mikä on Code? - Miksi Code-maksujen valuuttana on Kin? - Miksi Kinin arvo muuttuu? - Miten ostan lisää Kiniä? - Voinko myydä Kiniä? - Miten voin auttaa? - hyväksyt - Napauttamalla painiketta Luo tili tai Kirjaudu sisään - Kameraasi käytetään kinien vastaanottamiseen. Salli kameran käyttöoikeudet voidaksesi jatkaa. - Tarvitsemme palveluilmoitukset, jotta voimme lähettää sinulle ajan tasalla olevia tietoja tilistäsi. - Nostot ovat peruuttamattomia, eikä niitä voi kumota sen jälkeen, kun ne on kerran aloitettu. - Kaikki tällä tilillä olevat rahat menetetään. Tilisi poistetaan lopullisesti, eikä tekoa voi kumota. Haluatko varmasti poistaa tämän tilin? - Kaikki kinit, joita ei lunasteta 24 tunnin kuluessa, palautetaan automaattisesti saldoosi. - Sinun on aloitettava tilin luominen uudelleen ja vahvistettava puhelinnumerosi uudestaan. - Voit päästä takaisin tälle tilille käyttämällä käyttöoikeusavaintasi - Sinulle ei ilmoiteta uusista viesteistä, joita %1$s lähettää. Voit poistaa mykistyksen koska tahansa. - Tällä hetkellä tuetaan vain Coden kautta luotuja tilejä. - Ystäväsi eivät enää pysty löytämään sinua tämän puhelinnumeron perusteella. - Sinulle ilmoitetaan kaikista uusista viesteistä, joita %1$s lähettää. Voit mykistää koska tahansa. - %1$s ei lähetä sinulle enää viestejä ennen kuin maksat sille uudelleen. - Käyttöoikeusavaimesi antaa Code-tilisi käyttöoikeudet. Pidä se yksityisenä ja turvassa. - Nämä 12 sanaa ovat ainoa tapa palauttaa Code-tilisi. Varmista, että kirjoitit ne muistiin, ja pidä ne yksityisinä ja turvassa. - Oletko varma? - Haluatko varmasti poistaa tämän tilin? - Lähetitkö linkin? - Haluatko varmasti poistua? - Haluatko varmasti kirjautua ulos? - Mykistetäänkö %1$s? - Ei Code-tili - Oletko varma? - %1$s on mykistetty. Poistetaanko mykistys? - Peruutetaanko tämän tilaus: %1$s? - Näytetäänkö käyttöoikeusavaimesi? - Oletko varma? - %1$s kiniä talletettin tilillesi. - Eilen lähettämääsi %1$s ei lunastettu. Se on palautettu automaattisesti saldoosi. - Auta kaveri alkuun Coden parissa ja nappaa 5 $ - Lähetä käteistä minkä tahansa viestisovelluksen läpi - Olet saanut %1$s kiniä lähetettyäsi jollekin hänen ensimmäiset kininsä. - Voit nyt pyytää tippejä. - Talletus vastaanotettu - Kin palautettu - Uutta Codessa - Suositusbonus vastaanotettu - X-tili yhdistetty - Coden käyttöoikeuskutsusi ei ole enää voimassa. Voit kirjautua ulos ja käyttää eri tiliä, jonka kutsutila on voimassa. - Käyttöoikeusavaimesi on ainoa keino käyttää rahojasi. Pidä se yksityisenä ja turvassa. - Vahvista henkilöllisyytesi nähdäksesi käyttöoikeusavaimesi. - Napauta Google Lens -ikonia ja avaa QR-koodi kirjautuaksesi Codeen. Voit myös kirjautua sisään manuaalisesti syöttämällä 12 sanaa Code-kirjautumisruudulle. - Varoitus! Tämä kuva antaa pääsyn kaikkiin Code-varoihisi. Älä jaa tätä kuvaa kenenkään muun kanssa. Säilytä se turvallisesti tallessa. - Code antaa sinun vastaanottaa kinejä osoittamalla kamerasi toisen käyttäjän puhelimessa olevaan digitaaliseen seteliin - Sinun on annettava kameran käyttöoikeudet voidaksesi saada kinejä - Todenna henkilöllisyytesi, jotta voit käyttää Codea. - Osta kiniä - Osta kinejä (tulossa pian) - Kinejen ostaminen ja myyminen on monimutkainen prosessi. Nämä prosessit yksinkertaistuvat ajan mittaan. Jos haluat tietää jo tänään, kuinka kinejä voi ostaa ja myydä, kannattaa katsoa alta löytyvät opastusvideot. - Voit antaa korkeintaan %1$s - Voit antaa tippiä enintään %1$s - Kin-kolikkosi ovat nyt käytettävissä Code-sovelluksessa - Lähetit jollekulle ensimmäisen Kinisi! Tässä suosittelijabonuksesi: - USDC-kolikkosi muunnetaan nyt Kin-kolikoiksi. Tämän tulisi kestää noin minuutin. - USDC-kolikkojen talletus onnistui. Avaa Code-sovellus viimeistelläksesi ostoksesi. - Tervetulobonus - Valitse maa - Tulossa pian - \@getcode Haluaisin yhdistää X-tiliini, jotta voin saada tippejä ihmisiltä kaikkialta maailmassa - Poista - Varmista, että olet tallentanut salaisen palautuslauseesi ja kirjoita sitten \"Poista\" Code-tilisi poistamiseksi. Tätä toimintoa ei voi perua. - Etkö saanut koodia numeroon %1$s? - Etkö saanut koodia? Lähetä uudelleen - Kasvojentunnistuksen poistaminen käytöstä vaatii sen, että vahvistat henkilöllisyytesi. - Eikö sinulla ole Code-lompakkosovellusta? - Sinulla ei ole vielä kinejä. - Ota kasvojentunnistus käyttöön parantaaksesi Coden maksutapahtumien turvallisuutta entisestään. - Syötä kohdeosoite - Syötä enintään %1$s - Nappaa 5 $ edestä kiniä ilmaiseksi saadessasi ystävän rekisteröitymään Codeen ja lähettämällä hänelle hänen ensimmäiset kininsä. - Code käyttää maksuihin Kin-kryptovaluuttaa. Tässä on eräitä tapoja saada lisää kinejä Code-kukkaroosi. - Saa ensimmäinen 1 $ kiniä ilmaiseksi - Vahvista henkilöllisyytesi antaaksesi kinejä. - Talleta kinejä Code-kukkaroosi lähettämällä kinejä talletusosoitteeseesi tämän alla. Kopioi napauttamalla. - Hanki lisää kinejä ja yritä sitten maksaa uudelleen - Virheellinen kohdetili - Varmista, että lompakkopalvelujen tarjoajasi on alustanut osoitteen, josta nostat varoja. Tämä onnistuu nopeasti vaihtamalla ensin hyvin pienen määrän solanoita Kiniksi siinä lompakossa, johon yrität lähettää varoja. - Kutsukoodi - Code on tällä hetkellä vain kutsukäytössä. Tarvitset kutsukoodin voidaksesi käyttää sovellusta. - %1$d kutsua - Code on uusi kryptolompakkosovellus, joka on käytettävissä toistaiseksi vain kutsusta. Lataa Code täältä: %1$s - Lue lisää - Puhelinnumerosi on yhdistetty tähän Code-tiliin. Ystäväsi voivat löytää sinut käyttämällä tätä puhelinnumeroa. - X-tilini ja @getcode yhdistetään, jotta voin saada tippejä ihmisiltä kaikkialta maailmassa. - Ladataan saldoasi ja maksuhistoriaasi - Etsi valokuvistasi käyttöoikeusavain, jonka tallensit tilisi luomisen yhteydessä. - Olet tällä hetkellä kirjautunut tilille. Varmista ennen jatkamista, että olet tallentanut käyttöavaimesi. Haluatko kirjautua ulos ja kirjautua sisään uudella tilillä? - Ei yhdistettyä\npuhelinnumeroa - Sinulla ei ole tähän Code-tiliin yhdistettyä puhelinnumeroa. Yhdistä sellainen, jotta ystäväsi voivat löytää sinut. - Ei verkkoyhteyttä - Codessa - Nappaa käteisrahat avaamalla Code-sovellus ja osoittamalla kamerallasi - Organisoidaan yhteystietojasi - Tämä puhelinnumero ei ole yhteystiedoissasi. Voit silti kutsua hänet Codeen. - Syötä puhelinnumerosi, myös maatunnus. Varmista, että käytät samaa puhelinnumeroa, johon sait kutsun. - Palvelun tarjoaa - Voit nyt pyytää tippejä - Suositusbonus vastaanotettu - Lähetä %1$s - Tässä on %1$s - Pyydä uusi, kun aikaa on kulunut %1$s - Lataa Code-lompakkosovellus skannaamalla tämä QR-koodi puhelimesi kameralla - Skannaa ladataksesi\nCode Wallet -sovelluksen - Hae valuuttoja - Hae yhteystietoja - Vahvistuskoodi lähetettiin puhelinnumeroosi tekstiviestinä. Syötä vahvistuskoodi tämän yläpuolelle. - Sinulle on lähetetty käteistä - Joku antoi sinulle tipin - Sinun on käynnistettävä kamerasi voidaksesi lukea Codeja - Tippikorttisi avulla voit vastaanottaa tippejä Coden käyttäjiltä kaikkialta maailmasta. Yhdistä X-henkilöllisyytesi, jotta voit käyttää tippikorttiasi. - Tippikorttisi avulla voit vastaanottaa tippejä Coden käyttäjiltä kaikkialla maailmassa. Pääset käyttämään tippikorttiasi tekemällä julkaisun X-palvelussa. - Tippikortin avulla voit saada tippejä Code-käyttäjiltä ympäri maailman. Saat tippikortin käyttöösi lähettämällä X:ssä viestiä @getcode. - Salli Coden lähettää ilmoituksia, kun saat tippiä muilta Coden käyttäjiltä. - Kirjoita \"%1$s\" - Olemme tehneet eräitä muutoksia parantaaksemme käyttökokemusta. Sinun on päivitettävä sovellus jatkaaksesi Coden käyttämistä. - Pätevä omistajan tili - Pätevä token-tili - Kinin muutosten arvo. - palautettiin sinulle - Minne haluaisit nostaa kinejäsi? - Vahvista henkilöllisyytesi nostaaksesi kinejä. - Talletit - Annoit - Sinulla on %1$d kutsua - Code on tällä hetkellä käytössä vain kutsusta. Sinulla on jäljellä %1$d. - Maksoit - Annoit - Sait - Lähetit - Käytit - Annoit jollekulle tipin - Nostit - Rahojesi nostaminen onnistui. - X-tilisi on nyt onnistuttu yhdistämään Code-tiliisi. Voit nyt pyytää tippejä. - Nosto onnistui - X-tilin yhdistäminen onnistui - Käyttöoikeus päättynyt - Käyttöoikeusavain - Sovelluksen asetukset - Käynnistä kamera automaattisesti - Saldo - Beta-liput - Bonus - Osta ja myy kinejä - Käteismaksut - Code-tiimi - Kin-ostokset - Verkkomaksut - Tipit - Virheenkorjauksen vaihtoehdot - Talleta kinejä - Talletti - Syötä käyttöoikeusavaimen sanat - Syötä puhelinnumero - Epäonnistui - UKK - Antoi - Nosta rahaa - Auta kaveri alkuun Coden parissa - Hae Kin - Hanki lisää kinejä - Anna rahaa - Antoi kinejä - Riittämättömästi varoja - Kutsu ystävä - Rajoitetun ajan tarjous - Yhdistetty - Paikallinen valuutta - Käyttäjätilini - Ei yhdistetty - Muut valuutat - Maksettu - Odottaa - Puhelinnumero - Tietosuojakäytäntö - Ostettu - X-tilin yhdistäminen onnistui - Vastaanota tippejä - Sai - Viimeaikaiset valuutat - Suosita ystävää, saat 5 $ - Suositusbonus - Pyydä rahaa - Pyydä kinejä - Pyydä tippi - Vaadi kasvotunnistus - Vaadi tunnuskoodi - Vaadi sormenjälkitunnistus - Tulokset - Palautettu - Valitse käyttäjätili - Valitse maa - Valitse valuutta - Lähetetty - Käytetty - Vaihda tilejä - Palveluehdot - Tippikortti - Anna kinejä tippinä - Ota Coden ilmoitukset käyttöön - Tuntematon - Päivitys vaaditaan - Vahvista puhelinnumero - Tervetuliaisbonus - Nosta kinejä - Nosti - Oma käyttöoikeusavaimesi - Napauta logoa jakaaksesi sovelluksen latauslinkin - diff --git a/apps/codeApp/src/main/res/values-fr-rCA/strings-localized.xml b/apps/codeApp/src/main/res/values-fr-rCA/strings-localized.xml deleted file mode 100644 index 38149639b..000000000 --- a/apps/codeApp/src/main/res/values-fr-rCA/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Ajouter des liquidités avec une carte de débit - Autoriser l\'accès à la caméra - Autoriser l\'accès aux contacts - Autoriser les notifications poussées - Solde - Acheter des Kin - Acheter plus de Kin - Annuler - Annuler l\'envoi - Clavardage - Encaissez cet argent - Confirmer - Connexion à X - Continuer - Copié - Copier - Copier l\'adresse - Créer un compte - Créer un nouveau compte de code - Supprimer le compte - Terminé - Téléchargez-la maintenant - Activer Face ID - Activer Touch ID - Sortir - Donner - Donner des Kin - Inviter - Invitations - Rejoindre la liste d\'attente - Plus tard - Apprenez à acheter du Kin - Apprenez à vendre du Kin - Lier un numéro de téléphone - Connexion - Se déconnecter - Envoyer un message à @getcode pour connecter - Muet - Suivant - Non, réessayez - Pas maintenant - OK - Ouvrir les paramètres - Coller - Coller depuis le presse-papiers - Envoyez un message à Connecter le compte - Mettre dans le portefeuille - Recevoir - Récupérer un compte existant - Rappeler - Supprimer le numéro de téléphone - Supprimer votre numéro de téléphone - Demander un pourboire - Enregistrer la clé d\'accès dans Mes photos - Enregistrer dans Photos - Envoyer - Envoyer le code de vérification - Partager - Partager comme adresse URL - Partager le lien de téléchargement - Partagez cette vidéo - Montrer ma carte de pourboire - Lancer la caméra - S\'abonner - Balayez pour vous connecter - Balayez pour payer - Glisser pour donner un pourboire - Essayez un autre compte de code - Les contacter via X - Déverrouiller Code - Désactiver le son - Se désabonner - Mise à jour - Afficher la clé d\'accès - Retirer Kin - A écrit les 12 mots à la place? - Oui - Oui, retirer Kin - Oui, je les ai écrits - et - Kin - du Kin - Supprimer toutes les informations associées à votre compte des serveurs de Code (numéro de téléphone, contacts, historique des transactions) - Vous pouvez accéder à votre compte dans d\'autres applications de portefeuille de crypto en utilisant votre phrase de récupération secrète. Vous ne serez pas en mesure d\'utiliser votre compte dans Code - Supprimez votre compte de la blockchain - Ce que la suppression fera - Ce qui se passera - Ce que la suppression ne fera pas - Ce Kin a déjà été collecté par quelqu\'un d\'autre. - Ce Kin a déjà été collecté par quelqu’un. - Veuillez autoriser l\'accès à la caméra dans les paramètres pour utiliser Code. - Ces liquidités ont expiré. - Veuillez saisir à nouveau votre numéro de téléphone et réessayer. - Veuillez autoriser l\'accès aux contacts dans les paramètres pour envoyer des invitations. - Le code n’est actuellement pas disponible dans votre pays. - Nous ne sommes pas en mesure de prendre en charge votre appareil pour le moment - La prise en charge des cartes eSIM sera probablement proposée dans une version ultérieure de Code. - Quelque chose s’est mal passé. Ce Kin n’a pas pu être collecté. - Nous ne nous attendions pas à ce que cela se produise. Quelque chose s\'est mal passé. Veuillez réessayer de créer ce compte. - Veuillez autoriser Code à accéder aux photos dans les Paramètres afin d\'enregistrer votre clé d\'accès. - Une erreur s’est produite. Veuillez vous assurer que votre numéro de téléphone est correctement saisi. - Quelque chose s\'est mal passé. Veuillez réessayer. - Échec du retrait de vos fonds. Quelque chose s\'est mal passé, veuillez réessayer votre retrait. - Code est conçu pour les petites transactions quotidiennes dont le montant est inférieur ou égal à %1$s. - Code est conçu pour les petites transactions quotidiennes. Votre limite de dons quotidiens augmentera demain. - Pour savoir comment obtenir plus de Kin, consultez la FAQ dans les paramètres. - Veuillez saisir un autre code d\'invitation et réessayer. - Veuillez saisir un numéro de téléphone valide et réessayer. - Il s’agit d’une carte de pourboire invalide. - Veuillez saisir un code valide et réessayer. - Désolé, nous avons rencontré un problème de réseau. Veuillez réessayer d\'inviter votre ami(e). - Ce Kin a été automatiquement renvoyé à l\'expéditeur car il n\'a pas été collecté dans les 24 heures. Veuillez leur demander d\'envoyer à nouveau le Kin. - Veuillez saisir à nouveau votre numéro de téléphone et réessayer. - Veuillez essayer d\'obtenir une image qui montre le code plus clairement. - Veuillez vérifier votre connexion Internet ou réessayer plus tard. - Code est actuellement sur invitation uniquement. Nous vous informerons lorsqu\'il y aura plus d\'invitations disponibles. - Pour rejoindre la liste d\'attente, rendez-vous sur %1$s - Votre plafond d\'achat est de %1$s. Veuillez entrer un montant inférieur. - Votre montant minimal d\'achat est de %1$s. Veuillez entrer un montant supérieur. - Votre clé d\'accès a déclenché un déverrouillage. Par conséquent, vous ne pourrez plus utiliser cette clé d\'accès dans le code. - Envoyez un message à cette personne pour activer la carte conseil. - Le pourboire maximum que vous pouvez donner est de %1$s. Veuillez saisir un montant inférieur. - Le pourboire minimum que vous pouvez donner est de %1$s. Veuillez saisir un montant plus élevé. - Vous ne pouvez créer qu\'un certain nombre de nouveaux comptes chaque jour. - Code est actuellement limité à un compte par appareil. La prise en charge de plusieurs comptes sera probablement proposée dans une version ultérieure de Code. - Code est actuellement limité à un compte par numéro de téléphone. La prise en charge de plusieurs comptes sera probablement proposée dans une version ultérieure de Code. - Kin déjà collecté - Kin déjà collecté - L\'accès à la caméra est requis - Liquidités périmées - Le code de vérification a expiré - Accès aux contacts requis - Pays non pris en charge - Appareil non pris en charge - Les cartes eSIM ne sont pas prises en charge actuellement - Échec de la collecte - Échec de la création du compte - Échec de l\'enregistrement - Échec de l\'envoi - Échec de la confirmation - La transaction a échoué - Limite de dons atteinte - Limite quotidienne atteinte - Kin insuffisant - Code d\'invitation invalide ou expiré - Numéro de téléphone invalide - Carte de pourboire invalide - Code invalide - Échec de l\'invitation - Lien expiré - Nombre maximal de tentatives atteint - Aucun code trouvé - Pas de connexion Internet - Vous n\'avez aucune invitation - Vous n\'avez pas encore été invités - Montant d\'achat trop important - Montant d\'achat trop bas - La clé d\'accès n\'est plus utilisable dans le code - La carte à puce n\'a pas encore été activée - Pourboire trop élevé - Pourboire trop bas - Trop de comptes créés - Compte déjà créé - Compte déjà créé - We believe payments should be simple, powerful, and global. By building with advanced blockchain technology Code offers features that traditional payments apps can\'t, like global peer to peer transfers, micropayments that unlock individual articles online, and zero fee tips for your favorite creators. - Kin est une crypto-monnaie comme Bitcoin, mais est également conçue pour des paiements rapides et peu coûteux. - À l\'instar du Bitcoin, il n\'y a qu\'une quantité limitée de Kin disponible. Si davantage de personnes achètent des Kin, la valeur augmente, et si davantage de personnes vendent des Kin, la valeur diminue. Cette dynamique permet à tous ceux qui détiennent des Kin de partager la création de valeur si l\'adoption de Kin croît. - Il est possible d\'acheter des Kin avec votre carte de débit. Cette option est disponible dans l\'onglet Obtenir des Kin. - Oui. La vente de Kin est prise en charge sur un certain nombre d’échanges de crypto-monnaies. - Vous pouvez aider de trois façons principales : parlez de votre expérience avec Code sur les réseaux sociaux, encouragez vos amis à essayer Code et incitez vos sites web préférés à intégrer les paiements Code en leur demandant de visiter getcode.com. - Qu’est-ce qu\'est Code ? - Pourquoi les paiements Code sont-ils libellés en Kin ? - Pourquoi la valeur de Kin change-t-elle ? - Comment acheter plus de Kin ? - Puis-je vendre des Kin ? - Comment puis-je aider ? - accepte notre - En appuyant sur « Créer un compte » ou « Connexion », vous - Votre caméra est utilisée pour recevoir Kin. Veuillez autoriser l\'accès à la caméra pour continuer. - Nous avons besoin de notifications poussées pour vous envoyer des informations opportunes sur votre compte. - Les retraits sont irréversibles et ne peuvent pas être annulés une fois initiés. - Tous les fonds de ce compte seront perdus. La suppression de votre compte est définitive et irréversible. Voulez-vous vraiment supprimer ce compte? - Tout Kin qui n\'est pas collecté dans les 24 heures sera automatiquement reversé sur votre solde. - Vous devrez recommencer la création du compte et vérifier à nouveau votre numéro de téléphone. - Vous pouvez accéder à nouveau à ce compte à l\'aide de votre clé d\'accès - Vous ne serez pas informé des nouveaux messages de %1$s. Vous pouvez rétablir le son à tout moment. - Seuls les comptes créés via Code sont actuellement pris en charge. - Vos amis ne pourront plus vous trouver avec ce numéro de téléphone. - Vous serez informé de tous les nouveaux messages de %1$s. Vous pouvez couper le son à tout moment. - Vous ne recevrez plus de messages de %1$s jusqu\'à ce que vous les payiez à nouveau. - Votre clé d\'accès vous donnera accès à votre compte Code. Gardez-le en privé et en sécurité. - Ces 12 mots sont le seul moyen de récupérer votre compte Code. Assurez-vous de les avoir notés et de les garder en privé et en sécurité. - Êtes-vous sûr? - Voulez-vous vraiment supprimer ce compte? - Avez-vous envoyé le lien ? - Êtes-vous sûr de vouloir quitter? - Êtes-vous sûr de vouloir vous déconnecter? - Couper le son de %1$s ? - Pas un compte Code - Êtes-vous sûr? - Rétablir le son de %1$s ? - Se désabonner de %1$s ? - Afficher votre clé d\'accès? - Êtes-vous sûr? - %1$s Kin a été déposé sur votre compte. - Le %1$s que vous avez envoyé hier n’a pas été collecté. Il a été automatiquement reversé sur votre solde. - Faites démarrer un ami sur Code et obtenez 5 $. - Envoyer de l’argent par le biais de n’importe quelle application de messagerie - Vous avez reçu %1$s de Kin pour avoir envoyé à quelqu’un son premier Kin. - Vous pouvez à présent demander des conseils. - Dépôt reçu - Kin reversé - Nouveau sur le Code - Prime de parrainage reçue - Compte X connecté - Votre invitation à accéder à Code est expirée. Vous pouvez vous déconnecter et utiliser un autre compte avec un statut d\'invitation valide. - Votre clé d\'accès est le seul moyen d\'accéder à vos fonds. Veuillez le garder en privé et en sécurité. - Vérifiez votre identité pour voir votre clé d\'accès. - Appuyez sur l\'icône Google Lens pour ouvrir le code QR permettant de se connecter à Code. Vous pouvez également vous connecter manuellement en saisissant les 12 mots sur l\'écran de connexion au Code. - Attention! Cette image donne accès à tous les fonds dont vous disposez dans Code. Ne partagez pas cette image avec qui que ce soit. Gardez-la en sécurité. - Code vous permet de recevoir Kin en pointant votre appareil photo vers la facture numérique sur le téléphone d\'un autre utilisateur - Vous devez autoriser l\'accès à la caméra pour pouvoir recevoir Kin - Authentifiez-vous pour accéder à Code. - Acheter Kin - Achetez Kin (bientôt disponible) - L’achat et la vente de Kin sont actuellement des processus complexes. Ces processus se simplifieront au fil du temps. Si vous souhaitez apprendre à acheter et à vendre du Kin dès aujourd’hui, vous pouvez regarder les vidéos de présentation ci-dessous. - Vous ne pouvez abandonner que jusqu’à %1$s - Vous ne pouvez donner un pourboire que jusqu\'à %1$s - Votre Kin est désormais disponible pour utilisation dans votre appli Code - Vous avez envoyé à une personne son premier Kin! Voici votre prime de parrainage : - Votre USDC est en cours de conversion en Kin. Cette opération devrait prendre environ une minute - Vous avez déposé des USDC avec succès. Ouvrez l\'appli Code pour terminer votre achat - Prime de bienvenue - Choisissez un pays - Bientôt disponible - \@getcode J\'aimerais connecter mon compte X afin de recevoir des conseils de personnes du monde entier. - Supprimer - Assurez-vous d\'avoir sauvegardé votre phrase secrète de récupération, puis saisissez « Supprimer » pour supprimer votre compte Code. Cette action est irréversible. - Vous n\'avez pas reçu de code à %1$s? - Vous n\'avez pas reçu le code? Renvoyer - La désactivation de Face ID nécessite que vous vérifiiez votre identité. - Vous n\'avez pas l\'application Code Wallet ? - Vous n\'avez pas encore de Kin. - Activez Face ID pour améliorer davantage la sécurité des transactions dans Code. - Entrez l\'adresse de destination - Entrez jusqu\'à %1$s - Obtenez 5 $ de Kin gratuitement lorsque vous incitez un ami à s’inscrire à Code et que vous lui envoyez son premier Kin. - Code utilise la crypto-monnaie Kin pour les paiements. Voici quelques moyens de faire entrer plus de Kin dans votre porte-monnaie Code. - Obtenez gratuitement votre premier 1 $ de Kin - Vérifiez votre identité pour donner Kin. - Déposez Kin dans votre portefeuille Code en envoyant Kin à votre adresse de dépôt ci-dessous. Appuyez pour copier. - Réapprovisionnez-vous en Kin et essayez de payer à nouveau - Compte de destination invalide - Veuillez vous assurer que l\'adresse vers laquelle vous effectuez le retrait a été initialisée par votre fournisseur de portefeuille. Un raccourci pour y parvenir consiste à échanger d\'abord une petite quantité de SOL contre des Kin dans le portefeuille vers lequel vous essayez d\'envoyer des fonds. - Code d\'invitation - Le code est actuellement réservé aux invités. Vous aurez besoin d\'un code d\'invitation pour accéder à l\'application. - %1$d Invitations - Code est une nouvelle application de portefeuille cryptographique qui est actuellement accessible uniquement sur invitation. Pour télécharger Code, rendez-vous sur %1$s - En savoir plus - Votre numéro de téléphone est lié à ce compte Code. Vos amis peuvent vous trouver en utilisant ce numéro de téléphone. - Je connecte mon compte X à @getcode afin de recevoir des pourboires de personnes du monde entier. - Chargement de votre solde et historique de transaction - Vérifiez vos photos pour la clé d\'accès que vous avez enregistrée lorsque vous avez créé votre compte pour la première fois. - Vous êtes actuellement connecté à un compte. Assurez-vous d\'avoir enregistré votre clé d\'accès avant de poursuivre. Souhaitez-vous vous déconnecter et vous connecter avec un nouveau compte ? - Non lié\nNuméro de téléphone - Vous n\'avez pas de numéro de téléphone lié à ce compte Code. Liez-en un pour que vos amis puissent vous retrouver. - Pas de connexion au réseau - Sur Code - Ouvrez l’application Code et pointez votre appareil photo pour récupérer cet argent - Organisation de vos contacts - Ce numéro de téléphone ne figure pas dans vos contacts. Vous pouvez toujours les inviter à Code. - Entrez votre numéro de téléphone, y compris l\'indicatif du pays. Assurez-vous d\'utiliser le même numéro de téléphone que celui qui a reçu l\'invitation. - Optimisé par - Vous pouvez désormais demander des pourboires - Prime de parrainage reçue - Envoyer %1$s - Voici %1$s - Demande d\'un nouveau code dans %1$s - Scannez ce code QR avec l’appareil photo de votre téléphone pour télécharger l’application Code Wallet - Balayer pour télécharger l’application\nCode Wallet - Rechercher des devises - Rechercher des contacts - Un message SMS a été envoyé à votre numéro de téléphone avec un code de vérification. Veuillez saisir le code de vérification ci-dessus. - Quelqu’un vous a envoyé de l’argent - Quelqu\'un vous a donné un pourboire - Vous devez lancer votre caméra pour scanner les codes - Votre carte de pourboire vous permet de recevoir des pourboires de la part des utilisateurs de Code dans le monde entier. Pour accéder à votre carte de pourboire, connectez votre identité X. - Votre carte de conseils vous permet de recevoir des conseils de la part d\'utilisateurs de Code dans le monde entier. Pour avoir accès à votre carte de conseils, envoyez un message par X. - Votre Carte de conseils vous permet de recevoir des conseils de la part d’utilisateurs de Code du monde entier. Pour accéder à votre Carte de conseils, envoyez un message à @getcode sur X. - Autorisez Code à vous envoyer des notifications lorsque vous recevez des conseils d\'autres utilisateurs de Code. - Tapez \"%1$s\" - Nous avons apporté quelques modifications pour améliorer l\'expérience. Vous devrez mettre à jour l\'application pour continuer à utiliser Code. - Compte propriétaire valide - Compte de jetons valide - La valeur de Kin change. - vous a été rendu - Où aimeriez-vous retirer votre Kin? - Vérifiez votre identité pour retirer Kin. - Vous avez effectué un dépôt - Vous avez donné - Vous avez %1$d invitations - Le code est actuellement sur invitation uniquement. Il vous reste %1$d. - Vous avez payé - Vous avez donné - Vous avez reçu - Vous avez envoyé - Vous avez dépensé - Vous avez donné un pourboire à quelqu\'un - Vous avez effectué un retrait - Vos fonds ont été retirés avec succès. - Votre compte X a été connecté avec succès à votre compte Code. Vous pouvez désormais demander des pourboires. - Retrait réussi - Compte X connecté avec succès - Accès expiré - Clé d\'accès - Paramètres de l\'application - Lancement automatique de la caméra - Solde - Drapeaux bêta - Bonus - Achetez et vendez du Kin - Paiements en espèces - Équipe Code - Achats de Kin - Paiements Web - Pourboires - Options de débogage - Déposer Kin - Déposé - Saisir les mots clés d\'accès - Saisir le numéro de téléphone - Échoué - FAQ - A donné - Obtenir des liquidités - Faire démarrer un ami sur Code - Obtenez Kin - Obtenez plus de Kin - Donner de l\'argent - Donner Kin - Fonds insuffisants - Inviter un ami - Offre limitée dans le temps - Lié - Monnaie locale - Mon compte - Non lié - Autres devises - Payé - En attente - Numéro de téléphone - Politique de confidentialité - Acheté - Compte X connecté avec succès - Recevoir des astuces - Reçu - Devises récentes - Parrainez un ami, recevez 5 $ - Prime de parrainage - Demander des liquidités - Demander des Kin - Demander un pourboire - Exiger Face ID - Exiger un code d’accès - Exiger Touch ID - Résultats - Renvoyé - Sélectionner un compte - Sélectionner un pays - Sélectionnez une devise - Envoyé - Dépensé - Changer de compte - Conditions d\'utilisation - Carte à puce - Donner un pourboire en Kin - Activer les notifications pour le code - Inconnu - Mise à jour requise - Vérifier le numéro de téléphone - Bonus de bienvenue - Retirer Kin - Retiré - Votre clé d\'accès - Touchez le logo pour partager le lien de téléchargement de l’application - diff --git a/apps/codeApp/src/main/res/values-fr/strings-localized.xml b/apps/codeApp/src/main/res/values-fr/strings-localized.xml deleted file mode 100644 index 7fcb12e1e..000000000 --- a/apps/codeApp/src/main/res/values-fr/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Ajouter des liquidités avec une carte de débit - Autoriser l\'accès à la caméra - Autoriser l\'accès aux contacts - Autoriser les notifications push - Solde - Acheter des Kin - Acheter davantage de Kin - Annuler - Annuler l’envoi - Discussion - Collecter cet argent - Confirmer - Se connecter à X - Continuez - copié - Copier - Copier l\'adresse - Créer un compte - Créer un nouveau compte Code - Supprimer le compte - Terminé - Téléchargez-la maintenant - Activez Face ID - Activez Touch ID - Quitter - Donner - Donner Kin - Inviter - Invitations - S\'inscrire sur la liste d\'attente - Plus tard - Apprenez à acheter du Kin - Apprenez à vendre du Kin - Associer un numéro de téléphone - Se connecter - Se déconnecter - Envoyez un message à @getcode pour le connecter - Mettre en sourdine - Suivant - Non, réessayez - Pas maintenant - OK - Ouvrir les paramètres - Coller - Coller depuis le presse-papiers - Publier sur le compte connecté - Mettre dans le portefeuille - Recevoir - Récupérer le compte existant - Rappeler - Supprimer le numéro de téléphone - Supprimer votre numéro de téléphone - Demander un pourboire - Sauvegarder la clé d\'accès à mes photos - Enregistrer dans Photos - Envoyer - Envoyer le code de vérification - Partager - Partager comme URL - Partagez le lien de téléchargement - Partager cette vidéo - Afficher ma carte de pourboires - Activer la caméra - S\'abonner - Faites glisser pour vous connecter - Balayez pour payer - Balayez pour donner un pourboire - Essayer un autre compte Code - Envoyez un tweet - Déverrouiller Code - Rétablir le son - Se désabonner - Mettre à jour - Afficher la clé d\'accès - Retirer Kin - Avez-vous plutôt noté les 12 mots ? - Oui - Oui, Retirer Kin - Oui, je les ai notés - et - Kin - de Kin - La suppression de toutes les informations associées à votre compte sur les serveurs de Code (numéro de téléphone, contacts, historique des transactions). - Vous pouvez accéder à votre compte dans d\'autres applications de portefeuille de crypto en utilisant votre phrase de récupération secrète. Vous ne pourrez plus utiliser votre compte dans Code. - La suppression de votre compte de la blockchain - Ce que la suppression entraînera - Que se passera-t-il ? - Ce que la suppression n\'entraînera pas - Ce Kin a déjà été retiré par quelqu’un d’autre. - Quelqu\'un a déjà collecté ce Kin. - Veuillez autoriser l\'accès à la caméra dans les Paramètres pour utiliser Code. - Ce cash a expiré. - Veuillez saisir à nouveau votre numéro de téléphone et réessayer. - Veuillez autoriser l\'accès aux contacts dans les paramètres pour envoyer des invitations. - Code est actuellement indisponible dans votre pays. - Nous ne sommes pas en mesure de prendre en charge votre appareil pour le moment. - La prise en charge des eSims sera probablement disponible dans une prochaine version de Code. - Une erreur est survenue. Ce Kin n\'a pas pu être encaissé. - Nous ne nous attendions pas à ce que cela se produise. Un problème est survenu. Veuillez réessayer de créer ce compte. - Veuillez autoriser Code à accéder à Photos dans les Paramètres afin d\'enregistrer votre clé d\'accès. - Une erreur s\'est produite. Assurez-vous que votre numéro de téléphone est correctement saisi. - Un problème est survenu. Veuillez réessayer. - Le retrait de vos fonds a échoué. Un problème est survenu, veuillez réessayer de retirer vos fonds. - Le code est conçu pour les petites transactions quotidiennes dont le montant est inférieur ou égal à %1$s. - Le code est conçu pour les petites transactions quotidiennes. Votre limite quotidienne de dons augmentera demain. - Pour savoir comment obtenir plus de Kin, consultez la FAQ dans les Paramètres. - Veuillez saisir un autre code d\'invitation et réessayer. - Veuillez entrer un numéro de téléphone valide et réessayer. - Cette carte de pourboires n\'est pas valide. - Veuillez entrer un code valide et réessayer. - Désolé, nous avons rencontré un problème de réseau. Veuillez réessayer d\'inviter votre ami. - Ce Kin a été automatiquement renvoyé à l’expéditeur car il n’a pas été retiré dans les 24 heures. Veuillez lui demander d’envoyer à nouveau le Kin. - Veuillez saisir à nouveau votre numéro de téléphone et réessayer. - Veuillez essayer d\'obtenir une image qui montre le code plus clairement. - Veuillez vérifier votre connexion Internet ou réessayer plus tard. - Code n\'est actuellement disponible que sur invitation. Nous vous informerons lorsque d\'autres invitations seront disponibles. - Pour rejoindre la liste d\'attente, rendez-vous sur %1$s. - Vous pouvez acheter %1$s au maximum. Veuillez saisir un montant inférieur. - Vous pouvez acheter %1$s au minimum. Veuillez saisir un montant plus important. - Votre clé d\'accès a déclenché un déverrouillage. Par conséquent, vous ne pourrez plus utiliser cette clé d\'accès dans le code. - Envoyez un tweet à cette personne pour activer sa Tip Card. - Le pourboire maximum que vous pouvez donner est de %1$s. Veuillez saisir un montant inférieur. - Le pourboire minimum que vous pouvez donner est de %1$s. Veuillez saisir un montant plus élevé. - Vous ne pouvez créer qu\'un nombre limité de nouveaux comptes par jour. - Code est actuellement limité à un compte par appareil. La prise en charge de comptes multiples sera probablement proposée dans une prochaine version de Code. - Code est actuellement limité à un compte par numéro de téléphone. La prise en charge de comptes multiples sera probablement proposée dans une prochaine version de Code. - Kin déjà retiré - KIn déjà collecté - Accès à la caméra requis - Cash expiré - Le code de vérification a expiré - Accès aux contacts requis - Pays non pris en charge - Appareil non pris en charge - eSims non pris en charge actuellement - Encaissement échoué - Échec de la création du compte - Échec de l\'enregistrement - Échec de l\'envoi - Échec de la confirmation - La transaction a échoué - Limite de don atteinte - Limite quotidienne atteinte - Kin insuffisant - Code d\'invitation invalide ou expiré - Numéro de téléphone invalide - Carte de pourboires non valide - Code non valide - Échec de l\'invitation - Le lien a expiré - Nombre maximal de tentatives atteint - Aucun code trouvé - Pas de connexion Internet - Vous n\'avez pas d\'invitations - Vous n\'avez pas encore été invité(e) - Achat trop important - Achat trop faible - La clé d\'accès n\'est plus utilisable dans le code - Tip Card pas encore activée - Pourboire trop élevé - Pourboire trop bas - Trop de comptes créés - Compte déjà créé - Compte déjà créé - Nous pensons que les paiements doivent être simples, efficaces et universels. En s\'appuyant sur la technologie avancée de la blockchain, Code offre des fonctionnalités que les applications de paiement traditionnelles n\'ont pas, comme les transferts de pair à pair, les micropaiements pour débloquer des articles individuels en ligne, et les contributions sans frais à vos créateurs favoris. - Le Kin est une cryptomonnaie comme le Bitcoin, mais il est également conçu pour des paiements rapides et de faible valeur. - Comme le Bitcoin, il n\'existe qu\'un nombre limité de Kin disponible. Si davantage de personnes achètent du Kin, sa valeur augmente, et si plus de personnes en vendent, sa valeur diminue. Cette dynamique permet à tous ceux qui détiennent des Kin de participer à la création de sa valeur si l\'adoption du Kin se répand. - Vous pouvez acheter du Kin avec votre carte bancaire. Cette fonction est accessible via l\'onglet Obtenir du Kin. - Oui, vous pouvez. Vendre du Kin est possible via certains échanges de cryptomonnaies. - Il y a trois principales façons de soutenir Code : partager votre expérience avec Code sur les réseaux sociaux, inciter vos amis à tester Code pour eux-mêmes, et inciter vos sites web favoris à intégrer les paiements avec Code en leur demandant de visiter [getcode.com](https://getcode.com). - En quoi consiste Code ? - Pourquoi les paiements avec Code sont-ils libellés en Kin ? - Pourquoi la valeur du Kin évolue ? - Comment puis-je acheter plus de Kin ? - Puis-vendre du Kin ? - Comment puis-je promouvoir Code ? - accepter notre - En appuyant sur \"Créer un compte\" ou \"Se connecter\" vous - Votre caméra est utilisée pour recevoir des Kin. Veuillez autoriser l\'accès à la caméra pour continuer. - Nous avons besoin des notifications push pour vous envoyer des informations opportunes sur votre compte. - Les retraits sont irréversibles et ne peuvent être annulés une fois lancés. - Tous les fonds de ce compte seront perdus. La suppression de votre compte est permanente et ne peut être annulée. Êtes-vous sûr de vouloir supprimer ce compte ? - Tout Kin n\'ayant pas été pas retiré dans les 24 heures sera automatiquement reversé sur votre solde. - Vous devrez recommencer la création du compte et vérifier à nouveau votre numéro de téléphone. - Vous pouvez vous reconnecter à ce compte en utilisant votre clé d\'accès. - Vous ne serez pas averti des nouveaux messages de %1$s. Vous pouvez annuler la mise en sourdine à tout moment. - Seuls les comptes créés sur Code sont actuellement pris en charge. - Vos amis ne seront plus en mesure de vous trouver avec ce numéro de téléphone. - Vous serez informé de tous les nouveaux messages de %1$s. Vous pouvez activer la mise en sourdine à tout moment. - Vous ne recevrez plus de messages de %1$s jusqu\'à ce que vous payiez à nouveau. - Votre clé d\'accès vous permet d\'accéder à votre compte Code. Gardez-la confidentielle et en sécurité. - Ces 12 mots sont le seul moyen de récupérer votre compte Code. Assurez-vous de les noter et de les conserver en lieu sûr et confidentiel. - Êtes-vous sûr ? - Voulez-vous vraiment supprimer ce compte ? - Avez-vous envoyé le lien ? - Voulez-vous vraiment quitter ? - Voulez-vous vraiment vous déconnecter ? - Mettre en sourdine %1$s ? - Pas un compte Code - Êtes-vous sûr ? - Annuler la mise en sourdine de %1$s ? - Se désabonner de %1$s ? - Afficher votre clé d\'accès ? - Êtes-vous sûr ? - %1$s Kin ont été déposés sur votre compte. - Le %1$s que vous avez envoyé hier n\'a pas été collecté. Il a été automatiquement retourné sur votre solde. - Invitez un ami à s\'inscrire sur Code et recevez 5 $ - Envoyez de l\'argent via n\'importe quelle appli de messagerie - Vous avez reçu %1$s de Kin pour avoir envoyé à quelqu\'un son premier Kin. - Vous pouvez maintenant demander des conseils. - Acompte reçu - KIn retourné - Nouveau sur Code - Bonus de parrainage crédité - Compte X connecté - Votre invitation à accéder à Code a expiré. Vous pouvez vous déconnecter et utiliser un autre compte avec un statut d\'invitation valide. - Votre clé d\'accès est le seul moyen d\'accéder à vos fonds. Veuillez la conserver en toute confidentialité et en toute sécurité. - Vérifier votre identité pour afficher votre clé d\'accès. - Appuyez sur l\'icône de Google Lens pour ouvrir le code QR et vous connecter à Code. Vous pouvez également vous connecter manuellement en saisissant les 12 mots dans l\'écran de connexion de Code. - Attention ! Cette image permet d\'accéder à vos fonds dans Code. Ne partagez pas cette image avec d\'autres personnes. Gardez-la en toute sécurité. - Code vous permet de recevoir Kin en pointant votre appareil photo sur la facture numérique du téléphone d\'un autre utilisateur - Vous devez autoriser l\'accès à la caméra pour pouvoir recevoir des Kin - Authentifiez-vous pour accéder à Code. - Acheter des KIn - Acheter des Kin (bientôt disponible) - L\'achat et la vente de Kin est actuellement un processus complexe. Ces processus se simplifieront avec le temps. Si vous souhaitez apprendre à acheter et à vendre de la Kin aujourd\'hui, vous pouvez regarder les vidéos de présentation ci-dessous. - Vous ne pouvez donner que jusqu\'à %1$s - Le montant de votre pourboire ne peut excéder %1$s - Vous pouvez désormais utiliser vos Kin sur votre appli Code - Vous avez envoyé un premier Kin à quelqu\'un ! Voici votre bonus de parrainage : - La conversion de vos USDC en Kin est en cours. Cette opération devrait prendre environ une minute. - Vous avez bien déposé vos USDC. Ouvrez l\'appli Code pour finaliser votre achat - Bonus de bienvenue - Sélectionner un pays - À venir - \@getcode J\'aimerais connecter mon compte X pour pouvoir recevoir des versements de personnes du monde entier - Supprimer - Assurez-vous d\'avoir enregistré votre phrase secrète de récupération, puis tapez « Supprimer » pour supprimer votre compte Code. Cette action est irréversible. - Vous n\'avez pas reçu de code à %1$s ? - Vous n\'avez pas reçu le code ? Renvoyez-le à - La désactivation de Face ID vous oblige à vérifier votre identité. - Vous n\'avez pas l\'application Code Wallet ? - Vous ne disposez pas d\'abord de Kin. - Activer Face ID pour renforcer encore la sécurité de la transaction dans Code. - Entrer l\'adresse de destination - Entrer jusqu\'à %1$s - Recevez gratuitement 5 $ de Kin lorsqu\'un ami s\'inscrit sur Code et que vous lui envoyez son premier Kin. - Code utilise la crypto-monnaie Kin pour les paiements. Voici quelques moyens d’obtenir plus de Kin dans votre portefeuille Code. - Recevez gratuitement votre premier $ de Kin - Vérifier votre identité pour donner des Kin. - Déposer Kin dans votre porte-monnaie Code en envoyant Kin à votre adresse de dépôt ci-dessous. Taper pour copier. - Veuillez obtenir plus de Kin et essayez de payer à nouveau - Compte de destination invalide - Assurez-vous que l\'adresse de retrait a été validée par votre fournisseur de portefeuille. Pour ce faire, il suffit d\'échanger un peu de SOL contre du kin dans le portefeuille vers lequel vous essayez d\'envoyer de l\'argent. - Code d\'invitation - Le code est actuellement réservé aux invités. Vous aurez besoin d\'un code d\'invitation pour accéder à l\'application. - %1$d d\'invitations - Code est une nouvelle application de portefeuille de cryptomonnaie qui n\'est actuellement accessible que sur invitation. Pour télécharger Code, rendez-vous sur %1$s - En savoir plus - Votre numéro de téléphone est associé à ce compte Code. Vos amis peuvent vous trouver en utilisant ce numéro de téléphone. - Je connecte mon compte X à @getcode afin de recevoir des pourboires de la part de personnes du monde entier. - Chargement de votre solde et de l\'historique de vos transactions - Vérifier vos photos pour la clé d\'accès que vous avez enregistrée lors de la création de votre compte. - Vous êtes actuellement connecté à un compte. Veuillez vous assurer que vous avez sauvegardé votre clé d’accès avant de continuer. Souhaitez-vous vous déconnecter et vous connecter avec un nouveau compte ? - Non associé\nNuméro de téléphone - Vous n\'avez pas de numéro de téléphone associé à ce compte Code. Associez-en un pour que vos amis puissent vous identifier. - Pas de connexion réseau - Sur Code - Ouvrez l\'application Code et pointez votre caméra pour récupérer cet argent - Organiser vos contacts - Ce numéro de téléphone ne figure pas dans vos contacts. Vous pouvez quand même l\'inviter à rejoindre Code. - Entrer votre numéro de téléphone, y compris l\'indicatif du pays. Assurez-vous que vous utilisez le même numéro de téléphone que celui qui a reçu l\'invitation. - Alimenté par - Vous pouvez désormais demander des pourboires - Bonus de parrainage crédité - Envoyer %1$s - Voici %1$s - Demander un nouveau code à %1$s - Scannez ce code QR avec la caméra de votre téléphone pour télécharger l\'application Code Wallet - Scannez pour télécharger l\'application\nApplication Code Wallet - Rechercher des devises - Rechercher des contacts - Un message SMS a été envoyé à votre numéro de téléphone avec un code de vérification. Veuillez saisir le code de vérification ci-dessus. - Quelqu\'un vous a envoyé de l\'argent - Quelqu\'un vous a donné un pourboire - Vous devez activer votre caméra pour scanner les Codes - Votre carte de pourboires vous permet de recevoir des pourboires de la part des utilisateurs de Code dans le monde entier. Pour accéder à votre carte de pourboires, connectez votre identifiant X. - Votre Tip Card vous permet de recevoir des conseils d\'utilisateurs de Code du monde entier. Publiez sur X pour accéder à Tip Card. - Votre carte de pourboires vous permet de recevoir des pourboires de la part des utilisateurs de Code du monde entier. Pour accéder à votre carte de pourboires, envoyez un message à @getcode sur X. - Autoriser Code à vous envoyer des notifications lorsque vous recevez des conseils d\'autres utilisateurs de l\'application. - Saisissez « %1$s » - Nous avons effectué quelques changements pour améliorer l\'expérience. Vous devrez mettre à jour l\'application pour continuer à utiliser Code. - Propriétaire du compte invalide - Jeton de compte invalide - La valeur de Kin change. - vous a(ont) été retourné(s) - Vers où voulez-vous retirer votre Kin ? - Vérifier votre identité pour retirer Kin. - Vous avez déposé - Vous avez donné - Vous avez %1$d d\'invitations - Code est actuellement disponible sur invitation uniquement. Il vous reste %1$d. - Vous avez payé - Vous avez donné - Vous avez reçu - Vous avez envoyé - Vous avez dépensé - Vous avez donné un pourboire à quelqu\'un - Vous avez retiré - Vos fonds ont été retirés avec succès. - Votre compte X a été connecté avec succès à votre compte Code. Vous pouvez désormais demander des pourboires. - Retrait réussi - Compte X connecté avec succès - Accès expiré - Clé d\'accès - Paramètres de l\'application - Activer automatiquement la caméra - Solde - Fanions Bêta - Bonus - Achetez et vendez du Kin - Paiements comptants - Équipe Code - Achats de Kin - Paiements en ligne - Pourboires - Options de débogage - Déposer Kin - Déposé - Entrer les mots de la clé d\'accès - Saisir le numéro de téléphone - Échoué - FAQ - Donné - Obtenir des liquidités - Invitez un ami à s\'inscrire sur Code - Obtenir des Kin - Obtenir plus de Kin - Donner de l\'argent - Donner Kin - Fonds insuffisants - Inviter un ami - Offre à durée limitée - Associé - Devises locales - Mon compte - Non associé - Autres devises - Payé - En attente - Numéro de téléphone - Politique de confidentialité - Acheté - Compte X connecté avec succès - Obtenir des conseils - Reçu - Devises récentes - Parrainez un ami, recevez 5 $ - Bonus de parrainage - Demander des liquidités - Demander des Kin - Demander un pourboire - Nécessite Face ID - Nécessite Passcode - Nécessite Touch ID - Résultats - Renvoyé - Sélectionnez un compte - Sélectionnez un pays - Sélectionner une devise - Envoyé - Dépensé - Échanger des comptes - Conditions d\'utilisation - Carte conseil - Donner un pourboire en Kin - Activer les notifications de Code - Inconnu - Mise à jour requise - Vérifier le numéro de téléphone - Bonus de bienvenue - Retirer Kin - Retiré - Votre clé d\'accès - Appuyez sur le logo pour partager le lien de téléchargement de l\'application - diff --git a/apps/codeApp/src/main/res/values-he/strings-localized.xml b/apps/codeApp/src/main/res/values-he/strings-localized.xml deleted file mode 100644 index a9653308f..000000000 --- a/apps/codeApp/src/main/res/values-he/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - הוספת כסף באמצעות כרטיס חיוב - אפשר גישה למצלמה - אפשר גישה לאנשי הקשר - אפשר הודעות דחיפה - מצב חשבון - רכישת Kin - לקנות kin יותר - ביטול - בטל שליחה - צ\'אט - אסוף את המזומן הזה - אישור - התחבר ל-X - המשך - הועתק - העתק - העתק כתובת - צור חשבון - צור חשבון קוד חדש - מחיקת חשבון - בוצע - הורד אותה עכשיו - הפוך Face ID לזמין - הפוך Touch ID לזמין - יציאה - תן - תן קין - הזמן - הזמנות - הצטרף לרשימת ההמתנה - מאוחר יותר - למדו כיצד לקנות Kin - למדו כיצד למכור Kin - קשר מספר טלפון - התחברות - יציאה מהחשבון - שלחו הודעה ל-@getcode כדי להתחבר - השתק - הבא - לא, נסה שוב - לא עכשיו - בסדר - פתח הגדרות - הדבק - הדבק מהלוח - יש להעלות פוסט כדי לחבר את החשבון - שים בארנק - קבל - שחזר חשבון קיים - הזכר - הסר מספר טלפון - הסר את מספר הטלפון שלך - בקשת טיפ - שמור מפתח גישה בתמונות שלי - שמור בתמונות - שליחה - שלח קוד אימות - שיתוף - שיתוף כ-URL - שתף קישור להורדה - שתפו סרטון זה - הצג את כרטיס הטיפים שלי - הפעל מצלמה - הרשמה - גלגל להתחבר - החליקו כדי לשלם - החליקו כדי לתת טיפ - נסה חשבון קוד אחר - שלחו ציוץ - שחרר נעילת Code - בטל השתקה - בטל מנוי - עדכן - צפה במפתח הגישה - משוך קין - האם כתבת את 12 המילים במקום? - כן - כן, משוך קין - כן, כתבתי אותן - ו - קין - של קין - תסיר את כל המידע המשויך לחשבונך מהשרתים של Code (מספר טלפון, אנשי קשר, היסטוריית עסקאות) - באפשרותך לגשת לחשבונך באפליקציות ארנקי קריפטו אחרות באמצעות ביטוי השחזור הסודי שלך. לא תוכל/י להשתמש בחשבון שלך ב-Code - תסיר את חשבונך מהבלוקצ\'יין - מה המחיקה תעשה - מה יקרה - מה המחיקה לא תעשה - Kin זה כבר נאסף על ידי מישהו אחר. - ה-Kin הזה כבר נאסף על ידי מישהו. - אנא אפשר בהגדרות גישה למצלמה שלך כדי להשתמש בקוד. - מזומן זה אינו בתוקף. - אנא הכנס מחדש את מספר הטלפון שלך ונסה שנית. - נא לאפשר גישה לאנשי הקשר בהגדרות על מנת לשלוח הזמנות. - Code כרגע אינו זמין במדינתך. - לא ניתן לתמוך במכשירך בשלב זה - ייתכן ובגרסה עתידית של Code תהיה תמיכה ב-eSims. - משהו השתבש. לא ניתן היה לאסוף Kin זה. - לא ציפינו שזה יקרה. משהו השתבש. אנא נסה שנית ליצור את החשבון. - אנא אפשר לקוד גישה לתמונות בהגדרות כדי לשמור את מפתח הגישה שלך. - משהו השתבש. נא לוודא שמספר הטלפון הוזן נכון. - משהו השתבש. אנא נסה שנית - משיכת הכספים שלך נכשלה. משהו השתבש, אנא נסה למשוך שוב. - Code מיועדת לעסקאות קטנות ויומיומיות של %1$s או פחות. - Code מיועדת לעסקאות קטנות ויומיומיות. מגבלת הנתינה היומית שלך תגדל מחר. - כדי לגלות איך להשיג עוד קין לך לשאלות נפוצות בעמוד ההגדרות. - אנא הזן קוד הזמנה אחר ונסה שוב. - נא להזין מספר טלפון חוקי ולנסות שוב. - זהו כרטיס טיפים לא חוקי. - אנא הכנס קוד חוקי ונסה שנית. - מצטערים, חווינו בעיית תקשורת. אנא נסה שנית להזמין את חברך. - Kin זה הוחזר אוטומטית לשולח מכיוון שהוא לא נאסף תוך 24 שעות. נא לבקש מהם לשלוח שוב את ה-Kin. - אנא הכנס מחדש את מספר הטלפון שלך ונסה שנית. - אנא נסו לצלם תמונה שמראה את הקוד יותר בבירור. - אנא בדוק את חיבור האינטרנט שלך או נסה שנית מאוחר יותר. - קוד היא כרגע בהזמנה בלבד. נודיע לך כשיהיו הזמנות פנויות נוספות. - כדי להצטרף לרשימת ההמתנה עבור ל-%1$s - המקסימום שתוכל לרכוש הוא %1$s. נא להזין סכום קטן יותר. - המינימום שניתן לרכוש הוא %1$s. נא להזין סכום גדול יותר. - מפתח הגישה שלך יזם ביטול נעילה. כתוצאה מכך, לא תוכל עוד להשתמש במפתח גישה זה ב-Code. - שלחו ציוץ לאדם זה כדי להפעיל את ה\"טיפקארד\" שלו. - המקסימום שאתם יכולים לתת כטיפ הוא %1$s. נא להזין סכום קטן יותר. - המינימום שאתם יכולים לתת כטיפ הוא %1$s. נא להזין סכום גדול יותר. - אתה יכול ליצור כמות מוגבלת של חשבונות חדשים בכל יום. - Code כרגע מוגבל לחשבון אחד למספר טלפון. ייתכן ובגרסה עתידית של Code ניתן יהיה לתמוך בכמה חשבונות. - Code כרגע מוגבל לחשבון אחד למספר טלפון. ייתכן ובגרסה עתידית של Code ניתן יהיה לתמוך בכמה חשבונות. - ה-Kin כבר נאסף - ה-Kin כבר נאסף - נדרשת גישה למצלמה - מזומן אינו בתוקף - פג תוקף קוד אימות - נדרשת גישה לאנשי הקשר - אין תמיכה במדינה - המכשיר אינו נתמך - eSims אינם נתמכים כרגע - האיסוף נכשל - יצירת החשבון נכשלה - השמירה נכשלה - השליחה נכשלה - האימות נכשל - העסקה נכשלה - הגעת למגבלת הנתינה - הגעת למגבלה היומית - אין מספיק קין - קוד ההזמנה לא חוקי או פג תוקף - מספר טלפון לא חוקי - כרטיס טיפים לא חוקי - קוד לא חוקי - ההזמנה נכשלה - פג תוקף הקישור - מספר הניסיונות הגיע לערך המקסימלי האפשרי - לא נמצא קוד - אין חיבור לאינטרנט - אין לך הזמנות - עדיין לא הוזמנת - רכישה גדולה מדי - קנייה קטנה מדי - מפתח הגישה אינו שמיש יותר ב-Code - ה\"טיפקארד\" לא הופעל עדיין - הטיפ גדול מדי - הטיפ קטן מדי - יותר מדי חשבונות נוצרו - החשבון כבר נוצר - החשבון כבר נוצר - אנו מאמינים שתשלומים צריכים להיות פשוטים, עוצמתיים וגלובליים. על ידי בנייה עם טכנולוגיית בלוקצ\'יין מתקדמת, Code מציע תכונות שאפליקציות תשלומים מסורתיות אינן יכולות, כמו העברות עמית לעמית גלובליות, מיקרו-תשלומים שפותחים מאמרים יחידים באינטרנט וטיפים ללא עמלה ליוצרים האהובים עליך. - Kin הוא מטבע קריפטוגרפי כמו ביטקוין, אך הוא מיועד גם לתשלומים מהירים ולא יקרים. - כמו ביטקוין, יש רק כמות מוגבלת של Kin זמין. אם יותר אנשים רוכשים Kin הערך עולה, ואם יותר אנשים מוכרים Kin הערך יורד. הדינמיקה הזו מאפשרת לכל מי שמחזיק ב-Kin להשתתף ביצירת הערך אם האימוץ של Kin גדל. - ניתן לרכוש Kin עם כרטיס החיוב שלך. זה נגיש בלשונית \'קניית Kin\'. - כן, זה ניתן. מכירת Kin נתמכת במספר בורסות של מטבעות קריפטוגרפיים. - ישנן שלוש דרכים עיקריות בהן ניתן לסייע: לדבר על חווית השימוש שלך ב-Code במדיה חברתית, לעודד את חבריך לנסות את Code בעצמם, ולעודד את האתרים האהובים עליך לשלב תשלומים ב-Code על ידי בקשה מהם לעיין ב-[getcode.com](https ://getcode.com). - מהו Code? - מדוע תשלומים ב-Code נקובים ב-Kin? - מדוע הערך של Kin משתנה? - איך ניתן לרכוש עוד Kin? - האם ניתן למכור Kin? - איך ניתן לסייע? - מסכים ל - בלחיצה על \"צור חשבון\" או \"התחברות\" אתה - המצלמה שלך משמשת לקבלת קין. אנא אפשר גישה למצלמה כדי להמשיך. - אנו צריכים לשלוח הודעות דחיפה כדי להעביר לך מידע על החשבון שלך בזמן אמת. - משיכות הן בלתי הפיכות ולא ניתן לבטל אותן ברגע שהחלו. - כל הכספים בחשבון זה יאבדו. מחיקת החשבון שלך היא קבועה ולא ניתנת לביטול.\nאתה בטוח שברצונך למחוק חשבון זה? - כל Kin שלא נאסף תוך 24 שעות יוחזר אוטומטית ליתרה שלך. - תצטרך להתחיל מחדש את יצירת החשבון ולאמת שוב את מספר הטלפון שלך. - אתה יכול לחזור לחשבון הזה באמצעות שימוש במפתח הגישה שלך - לא תקבלו התראה על הודעות חדשות מ-%1$s. אתם יכולים לבטל את ההשתקה בכל עת. - רק חשבונות שנוצרו באמצעות קוד נתמכים כרגע. - החברים שלך לא יוכלו יותר למצוא אותך באמצעות מספר הטלפון הזה. - תקבלו התראה על כל ההודעות החדשות מ-%1$s. אתם יכולים להשתיק זאת בכל עת. - לא תקבלו הודעות כלשהן מ-%1$s עד שתשלמו להם שוב. - מפתח הגישה שלך נותן גישה לחשבון הקוד שלך. שמור עליו בסוד ובמקום בטוח. - 12 המילים האלה הן הדרך היחידה לשחזר את חשבון הקוד שלך. וודא שרשמת אותן ושמור עליהן בסוד ובמקום בטוח. - אתה בטוח? - אתה בטוח שברצונך למחוק את החשבון הזה? - שלחת את הקישור? - אתה בטוח שברצונך לצאת? - אתה בטוח שברצונך לצאת מהחשבון? - להשתיק את %1$s? - לא חשבון קוד - אתה בטוח? - לבטל את השתקת %1$s? - לבטל את הרישום ל-%1$s? - צפייה במפתח הגישה שלך? - אתה בטוח? - %1$s Kin הופקדו בחשבון שלך. - ה-%1$s ששלחת אתמול לא נאסף. זה הוחזר באופן אוטומטי ליתרת החשבון שלך. - עודד חבר להתחיל להשתמש ב-Code וקבל 5$ - שלח מזומן דרך כל אפליקציית מסנג\'ר - קיבלת %1$s של Kin על כך ששלחת למישהו את ה-Kin הראשון שלו. - כעת ניתן לבקש טיפים. - ההפקדה התקבלה - ה-Kin הוחזר - חדש ב-Code - בונוס ההפניה התקבל - חשבון X מחובר - פג תוקף ההזמנה שלך לקוד. אתה יכול לצאת מהחשבון ולהשתמש בחשבון אחר עם הזמנה בתוקף. - מפתח הגישה שלך הוא הדרך היחידה לגשת לכספים שלך. אנא שמור אותו בסוד ובמקום בטוח. - אמת את הזהות שלך כדי לצפות במפתח הגישה שלך. - - אזהרה! תמונה זו מעניקה גישה לכל הכספים שיש לך ב-Code. אין לשתף תמונה זו עם אף אחד אחר. יש לשמור אותה מאובטחת ובטוחה. - קוד מאפשרת לך לקבל קין באמצעות כיוון המצלמה שלך לשטר דיגיטלי בטלפון של משתמש אחר - אתה צריך לאפשר גישה למצלמה כדי שתוכל לקבל קין. - יש לאמת כדי לגשת ל-Code. - קנה Kin - רכישת Kin (בקרוב) - קנייה ומכירה של Kin הוא תהליך מורכב. תהליכים אלו יעשו פשוטים יותר עם הזמן. אם אתם רוצים ללמוד כיצד לקנות ולמכור Kin היום, ניתן לצפות בסרטוני ההדרכה למטה. - באפשרותך לתת רק עד %1$s - אתם יכולים לתת טיפ רק עד %1$s - ה-Kin שלך זמין כעת לשימוש באפליקציית הקוד שלך - שלחת למישהו את ה-Kin הראשון שלו! הנה בונוס ההפניה שלך: - ה-USDC שלך מומר ל-Kin. תהליך זה אמור להימשך כדקה אחת. - הפקדת בהצלחה USDC. פתח את אפליקציית Code כדי להשלים את הרכישה - בונוס מבורך - בחר מדינה - יגיע בקרוב - \@getcode, אני רוצה לחבר את חשבון X שלי כדי שאוכל לקבל טיפים מאנשים בכל רחבי העולם - מחק - יש לוודא ששמרת את ביטוי השחזור הסודי שלך, ולאחר מכן להזין \"מחק\" כדי למחוק את חשבון Code שלך. פעולה זו היא בלתי הפיכה. - לא קיבלת קוד ב-%1$s? - לא קיבלת את הקוד? שלח מחדש - השבתת זיהוי פנים דורשת ממך לאמת את הזהות שלך. - אין לך את אפליקציית Code Wallet? - עדיין אין לך קין. - אפשר זיהוי פנים כדי להגביר את אבטחת העסקאות בקוד. - הכנס כתובת יעד - הכנס כמות עד %1$s - קבל 5$ של Kin בחינם כאשר אתה מביא חבר להירשם ל-Code ושולח לו את ה-Kin הראשון שלו. - הקוד משתמש במטבע הקריפטו Kin לתשלומים. הנה כמה דרכים להוסיף עוד Kin לארנק הקוד שלך. - קבל את ה-$1 של Kin הראשון שלך בחינם - אמת את הזהות שלך כדי להעביר קין. - הפקד קין לארנק הקוד שלך על ידי שליחת קין לכתובת ההפקדות שלך המצוינת מטה. הקש להעתקה. - אנא השיגו עוד Kin ואז נסו לשלם שוב - חשבון יעד לא חוקי - נא ודאו שהכתובת שאליה אתם מושכים אותחלה על ידי ספק הארנק שלכם. דרך קצרה לעשות זאת היא להחליף תחילה כמות קטנה של SOL ל-Kin בארנק שאליו אתם מנסים לשלוח. - קוד הזמנה - Code היא כרגע בהזמנה בלבד. תזדקק לקוד הזמנה כדי לגשת לאפליקציה. - %1$d הזמנות - Code היא אפליקציית ארנק קריפטו חדשה שכרגע על בסיס הזמנה בלבד. להורדת Code יש לגשת ל: %1$s - למד עוד - מספר הטלפון שלך מקושר עם חשבון הקוד הזה. חברים יכולים למצוא אותך באמצעות שימוש במספר הטלפון הזה. - אני מחבר/ת את חשבון X שלי עם @getcode כדי שאוכל לקבל טיפים מאנשים מכל רחבי העולם. - טוען את היתרה והיסטוריית העסקאות שלך - בדוק את התמונות של מפתח הגישה ששמרת כשיצרת לראשונה את החשבון. - הנך מחובר/ת כעת לחשבון. נא לוודא ששמרת את מפתח הגישה שלך בטרם תמשיכ/י. האם ברצונך להתנתק ולהתחבר עם חשבון חדש? - אין מספר\nטלפון מקושר - אין לך מספר טלפון המקושר לחשבון הקוד הזה. קשר מספר טלפון כדי שהחברים שלך יוכלו למצוא אותך. - אין חיבור רשת - אודות קוד - פתח את אפליקציית Code וכוון את המצלמה שלך כדי לקחת את המזומן הזה - ארגון אנשי הקשר שלך - מספר הטלפון הזה אינו נמצא באנשי הקשר שלך. באפשרותך עדיין להזמין אותם ל-Code. - הכנס את מספר הטלפון שלך כולל קידומת מדינה. וודא שאתה משתמש במספר טלפון זהה לזה שדרכו קיבלת את ההזמנה. - מופעל ע\"י - עכשיו אתם יכולים לבקש טיפים - בונוס ההפניה התקבל - שלח %1$s - הנה %1$s - בקש קוד חדש תוך %1$s - סרוק קוד QR זה עם מצלמת הטלפון שלך כדי להוריד את אפליקציית Code Wallet - סרקו להורדת אפליקציית ה-Code Wallet - חפש סוגי מטבע - חפש אנשי קשר - הודעת SMS נשלחה למספר הטלפון שלך עם קוד אימות. נא להזין את קוד האימות למעלה. - מישהו שלח לך מזומן - מישהו נתן לכם טיפ - עליך להפעיל את המצלמה כדי לסרוק קודים - כרטיס הטיפים שלכם מאפשר לכם לקבל טיפים ממשתמשי Code מכל רחבי העולם. כדי לגשת לכרטיס הטיפים שלכם חברו את זהות X שלכם. - ה\"טיפקארד\" שלך מאפשר לך לקבל טיפים ממשתמשי קוד בכל רחבי העולם. כדי לגשת ל\"טיפקארד\" שלך, עליך להעלות פוסט ל-X. - כרטיס ה-Tip שלכם מאפשר לכם לקבל טיפים ממשתמשי Code בכל רחבי העולם. כדי לגשת לכרטיס ה-Tip שלכם, שלחו @getcode ב-X.\n\n - אפשרו ל-Code לשלוח לכם הודעות כשאתם מקבלים טיפים ממשתמשי Code אחרים. - יש להקליד \"%1$s\" - עשינו כמה שינויים כדי לשפר את חווית המשתמש. תצטרך לעדכן את האפליקציה כדי להמשיך להשתמש בקוד. - חשבון בעלים תקף - חשבון אסימון תקף - ערך הקין משתנה. - הוחזר אליך - לאן אתה רוצה למשוך את הקין שלך? - אמת את הזהות שלך כדי למשוך קין. - הפקדת - נתת - יש לך %1$d הזמנות - קוד היא כרגע בהזמנה בלבד. נשארו לך %1$d הזמנות. - שילמת - נתת - קיבלת - שלחת - הוצאת - נתתם למישהו טיפ - משכת - הכספים שלך נמשכו בהצלחה. - חשבון X שלכם חובר בהצלחה לחשבון Code שלכם. עכשיו אתם יכולים לבקש טיפים. - המשיכה עברה בהצלחה - חשבון X חובר בהצלחה - פג תוקף הגישה - מפתח גישה - הגדרות אפליקציה - הפעלה אוטומטית של המצלמה - מצב חשבון - דגלי בטא - בונוס - קנו ומכרו Kin - תשלומים במזומן - צוות קוד - רכישות Kin - תשלום באינטרנט - טיפים - אפשרויות תיקון שגיאות - הפקד קין - הפקדות שבוצעו - הקלד מילים ממפתח הגישה - הכנס מספר טלפון - נכשל - שאלות נפוצות - העברות שבוצעו - קבלת כסף - עודד חבר להתחיל להשתמש ב-Code - השגת Kin - השיגו עוד Kin - תן מזומן - העבר קין - אין מספיק כספים - הזמן חבר - הצעה לזמן מוגבל - מקושר - מטבע מקומי - החשבון שלי - לא מקושר - סוגי מטבע אחרים - שולם - ממתין - מספר טלפון - מדיניות פרטיות - נרכש - חשבון X חובר בהצלחה - קבלו טיפים - הפקדות שהתקבלו - סוגי מטבע בשימוש לאחרונה - הפנה חבר, קבל 5$ - בונוס הפניה - בקשת כסף - בקשת Kin - בקשת טיפ - נדרש Face ID - נדרשת סיסמה - נדרש Touch ID - תוצאות - הוחזר - בחר חשבון - בחר מדינה - בחר סוג מטבע - נשלח - הוצאו - החלף חשבון - תנאי שימוש - כרטיס טיפ - מתן Kin כטיפ - הפעילו הודעות עבור Code - לא ידוע - נדרש עדכון - אימות מספר טלפון - בונוס קבלת פנים - משוך קין - משיכות שבוצעו - מפתח הגישה שלך - לחצו על הלוגו כדי לשתף את קישור הורדת האפליקציה - diff --git a/apps/codeApp/src/main/res/values-hi-rIN/strings-localized.xml b/apps/codeApp/src/main/res/values-hi-rIN/strings-localized.xml deleted file mode 100644 index 5d1457511..000000000 --- a/apps/codeApp/src/main/res/values-hi-rIN/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - डेबिट कार्ड से कैश जोड़ें - कैमरा एक्सेस की अनुमति दें - संपर्कों को एक्सेस करने दें - पुश नोटिफिकेशन की अनुमति दें - शेष राशि - Kin खरीदें - ज्यादा Kin खरीदें - कैंसिल करें - सेंड करना कैंसिल करें - चैट - यह नकदी इकट्ठा कर लें - कन्फर्म करें - X से कनेक्ट करें - जारी रखें - कॉपी किया गया - कॉपी - पता कॉपी करें - एक अकाउंट बनाएँ - एक नया कोड अकाउंट बनाएं - अकाउंट को डिलीट करें - हो गया - इसे अभी डाउनलोड करें - Face ID चालू करें - Touch ID चालू करें - बाहर निकलें - दें - Kin दे - आमंत्रित करें - आमंत्रण - प्रतीक्षा सूची में शामिल हों - बाद में - Kin खरीदना सीखें - Kin बेचना सीखें - एक फोन को नंबर लिंक करें - लॉग इन करें - लॉग आउट करें - कनेक्ट करने के लिए @getcode पर मेसेज भेजें - म्यूट करें - अगला - नहीं, दोबारा कोशिश करें - अभी नहीं - ठीक है - सेटिंग्स को ओपन करें - पेस्ट करें - क्लिपबोर्ड से पेस्ट करें - अकाउंट कनेक्ट करने के लिए पोस्ट करें - वॉलेट में डालें - पाएँ - वर्तमान अकाउंट को पुनर्प्राप्त करें\n - याद दिलाएं - फ़ोन नंबर हटाएं - अपना फ़ोन नंबर हटाएं - एक टिप का अनुरोध करें - मेरी फ़ोटो में एक्सेस की को सेव करें - फ़ोटो में सेव करें - सेंड - वेरिफिकेशन कोड भेजें - शेयर करें - URL के रूप में शेयर करें - डाउनलोड लिंक शेयर करें - इस वीडियो को शेयर करें - मेरा टिप कार्ड दिखाएँ - कैमरा शुरू करें - सदस्यता लें - लॉगिन करने के लिए स्वाइप करें - भुगतान करने के लिए स्वाइप करें - टिप देने के लिए स्वाइप करें - एक अलग कोड अकाउंट को आज़माएं - उन्हें ट्वीट करें - Code अनलॉक करें - म्यूट से हटाएँ - अनसब्सक्राइब करें - अपडेट करें - एक्सेस की को देखें - Kin वापस लें - इसके बजाय नीचे 12 शब्द लिखे? - हाँ - हाँ, Kin वापस लें - हाँ, मैंने उन्हें लिख दिया - और - निकट संबंधी - Kin का - Code के सर्वर से अपने खाते से जुड़ी सभी जानकारी हटाएं, जैसे कि फ़ोन नंबर, संपर्क, लेन-देन क् इतिहास - आप सीक्रेट रिकवरी फ्रेज का इस्तेमाल करके अन्य क्रिप्टो वॉलेट पर अपना खाता एक्सेस कर सकते हैं. आप Code पर अपना खाता इस्तेमाल नहीं कर पाएंगे - ब्लॉकचैन से अपना खाता हटाएं - खाता मिटाने से क्या होगा - इसका असर क्या होगा - खाता मिटाने से क्या नहीं होगा - इस Kin को पहले ही किसी और ने कलेक्ट कर लिया था. - यह Kin पहले ही किसी के द्वारा कलेक्ट कर लिया जा चुका है. - कृपया कोड का उपयोग करने के लिए सेटिंग में कैमरे तक एक्सेस की अनुमति दें. - यह नकदी एक्सपायर हो गई है. - कृपया अपना फ़ोन नंबर पुनः एंटर करें और पुनः प्रयास करें. - आमंत्रण भेजने के लिए सेटिंग में जाकर संपर्क सूची का एक्सेस दें. - फ़िलहाल आपके देश में Code उपलब्ध नहीं है. - इस समय हम आपके डिवाइस के इस्तेमाल को सपोर्ट करने में असमर्थ हैं - मुमकिन है कि ई-सिम का इस्तेमाल Code के आने वाले वर्शन में आएगा. - कुछ दिक़्क़त हुई है. इस Kin को एकत्र नहीं किया जा सका. - हमें ऐसा होने की उम्मीद नहीं थी. कुछ गलत हो गया. कृपया इस अकाउंट को पुन: बनाने का प्रयास करें. - अपनी एक्सेस की को सेव करने के लिए कृपया सेटिंग में कोड को फ़ोटो तक एक्सेस की अनुमति दें. - कुछ दिक़्क़त हुई है. कृपया सुनिश्चित करें कि आपका फ़ोन नंबर सही दर्ज किया गया है. - कुछ गलत हो गया. कृपया पुन: प्रयास करें. - आप धन को निकालने में विफल हो गए. कुछ गलत हो गया है, कृपया धन निकालने का पुन: प्रयास करें. - Code को छोटी और रोज़ाना किए जाने वाले लेन-देन के लिए बनाया गया है, जिसकी राशि %1$s या इससे कम हो सकती है. - Code को छोटी और रोज़ाना किए जाने वाले लेन-देन के लिए बनाया गया है. आपकी दैनिक सीमा कल फिर से बहाल हो जाएगी. - अधिक Kin कैसे प्राप्त करें, यह जानने के लिए सेटिंग में अक्सर पूछे जाने वाले प्रश्न पर जाएं. - कृपया दूसरा आमंत्रण कोड डालें और दोबारा प्रयास करें. - कृपया मान्य फ़ोन नंबर डालकर फिर से प्रयास करें. - यह एक अमान्य टिप कार्ड है. - कृपया एक वैलिड कोड एंटर करें और पुनः प्रयास करें. - क्षमा करें, हमें एक नेटवर्क समस्या का सामना करना पड़ा. कृपया अपने मित्र को फिर से आमंत्रित करने का प्रयास करें. - यह Kin आपने आप हीं सेन्डर को वापिस चला गया क्योंकि इसे 24 घंटे के भीतर कलेक्ट नहीं किया गया था. कृपया उन्हें Kin को फिर से भेजने के लिए बोलें. - कृपया अपना फ़ोन नंबर पुनः एंटर करें और पुनः प्रयास करें. - कृपया ऐसी तस्वीर प्राप्त करने का प्रयास करें जो कोड को अधिक स्पष्ट रूप से दिखाए. - कृपया अपना इंटरनेट कनेक्शन चेक करें या बाद में पुन: प्रयास करें. - कोड वर्तमान में केवल आमंत्रण पर ही उपलब्ध है. अधिक आमंत्रण उपलब्ध होने पर हम आपको सूचित करेंगे. - प्रतीक्षा सूची में शामिल होने के लिए %1$s पर जाएं - आप अधिकतम %1$s खरीद सकते हैं। कृपया छोटी रकम दर्ज करें। - आप न्यूनतम %1$s खरीद सकते हैं। कृपया बड़ी रकम दर्ज करें। - आपकी ऐक्सेस कुंजी से अनलॉक करने की प्रक्रिया शुरू की गई है. इसलिए अब आप इस ऐक्सेस कुंजी का उपयोग Code में नहीं कर पाएंगे. - इस व्यक्ति को अपना टिप कार्ड एक्टिवेट करने के लिए एक ट्वीट भेजें. - आप अधिकतम टिप %1$s दे सकते हैं. कृपया छोटा अमाउंट डालें. - आप न्यूनतम टिप %1$s दे सकते हैं. कृपया बड़ा अमाउंट डालें. - आप प्रतिदिन केवल इतने ही नए अकाउंट बना सकते हैं. - वर्तमान में Code प्रति डिवाइस के लिए एक खाते तक सीमित है. आने वाले समय में आने वाले Code के वर्शन में मल्टीपल एकाउंट्स के इस्तेमाल की संभावना है. - वर्तमान में Code प्रति फ़ोन नंबर एक खाते तक सीमित है. आने वाले समय में आने वाले Code के वर्शन में मल्टीपल एकाउंट्स के इस्तेमाल की संभावना है. - Kin को पहले ही कलेक्ट कर लिया था. - Kin पहले ही कलेक्ट किया जा चुका है - कैमरा की एक्सेस आवश्यक है - नकद एक्सपायर हो गया - वेरिफिकेशन कोड की समय सीमा समाप्त हो गयी - संपर्क सूची का एक्सेस देना आवश्यक है - इस देश में सेवाएँ उपलब्ध नहीं हैं - इस डिवाइस पर सेवाएँ उपलब्ध नहीं हैं - वर्तमान में ई-सिम का इस्तेमाल नहीं किया जा सकता है - एकत्र करने में असमर्थ - अकाउंट बनाने में विफल रहा - सेव करने में विफल रहा - भेजने में विफल हुआ - कन्फर्म करने में असफल रहा - ट्रैन्ज़ैक्शन विफल रहा - पैसे देने की सीमा पूरी हो गई है - दैनिक सीमा पूरी हो गई है - अपर्याप्त kin - आमंत्रण कोड अमान्य है या इसकी समयसीमा समाप्त हो गई है - फ़ोन नंबर अमान्य है - अमान्य टिप कार्ड है. - इनवैलिड कोड - आमंत्रण विफल रहा - लिंक एक्सपायर हो गया गया है - अधिकतम प्रयास हो गया - कोई कोड नहीं मिला - कोई इंटरनेट कनेक्शन नहीं - आपको कोई आमंत्रण नहीं है - आपको अब तक आमंत्रित नहीं किया गया है - खरीदारी बहुत बड़ी है - खरीदारी बहुत छोटी है - Code में ऐक्सेस कुंजी का उपयोग अब नहीं किया जा सकता - टिप कार्ड अभी तक एक्टिवेट नहीं हुआ है - टिप बहुत ज्यादा है - \nटिप बहुत कम है - बहुत सारे अकाउंट बन गए हैं - अकाउंट पहले से ही बनाया जा चुका है - अकाउंट पहले से ही बनाया जा चुका है - हमारा मानना ​​है कि भुगतान की प्रक्रिया सरल, शक्तिशाली और वैश्विक होना चाहिए. उन्नत ब्लॉकचेन तकनीक के साथ निर्माण करके Code ऐसी सुविधाएँ प्रदान करता है जो पारंपरिक भुगतान ऐप्स नहीं कर सकते हैं, जैसे ग्लोबल पीयर टू पीयर ट्रांसफ़र, माइक्रोपेमेंट जो व्यक्तिगत आर्टिकल्स को ऑनलाइन अनलॉक करते हैं, और आपके पसंदीदा क्रियेटर्स के लिए बिना किसी शुल्क के टिप्स भी. - Kin बिटकॉइन की तरह एक क्रिप्टोकरेंसी है, लेकिन इसे तेज़, सस्ते भुगतान के लिए भी डिज़ाइन किया गया है. - बिटकॉइन की तरह Kin भी सीमित मात्रा में ही उपलब्ध है. यदि अधिक लोग Kin खरीदते हैं तो मूल्य बढ़ जाता है, और यदि अधिक लोग Kin बेचते हैं तो मूल्य कम हो जाता है. यह डायनामिक उन सभी को अनुमति देती है जो Kin को होल्ड करते हैं, यदि Kin को होल्ड करने वालों की संख्या बढ़ती है तो संपत्ति बढ़ती है. - आप अपने डेबिट कार्ड से Kin खरीद सकते हैं. यह Get Kin टैब में उपलब्ध है. - हाँ आप बीच सकते हैं. कई क्रिप्टोकरेंसी एक्सचेंजों पर Kin को बेचा जा सकता है. - तीन मुख्य तरीके हैं जिनसे आप मदद कर सकते हैं: सोशल मीडिया पर Code के साथ अपने अनुभव के बारे में बात करें, अपने दोस्तों को स्वयं Code आज़माने के लिए प्रोत्साहित करें, और अपनी पसंदीदा वेबसाइटों को Code से भुगतान को स्वीकार करने के लिए प्रोत्साहित करें, उन्हें [getcode.com]\n(https://getcode.com) देखने के लिए कहें. - Code क्या है? - कोड भुगतानों को Kin में क्यों दर्शाया जाता है? - Kin का मूल्य क्यों बदलता है? - मैं और kin कैसे ख़रीदूँ? - क्या मैं Kin बेच सकता हूँ? - मैं कैसे मदद कर सकता हूँ? - हमारी सहमति से - आप टैप करके \"एक खाता बनाएँ\" या \"लॉग इन\" करें - Kin को प्राप्त करने के लिए आपके कैमरे का उपयोग किया जाता है. कृपया आगे बढ़ने के लिए कैमरे का उपयोग करने दें. - आपको आपके खाते के बारे में समय पर जानकारी भेजने के लिए हमें पुश नोटिफिकेशन की आवश्यकता है. - निकासी अपरिवर्तनीय है और एक बार शुरू करने के बाद इसे पूर्ववत नहीं किया जा सकता है. - इस अकाउंट की सभी धनराशि नष्ट हो जाएगी. आपके अकाउंट का हटना स्थायी है और इसे पूर्ववत नहीं किया जा सकता है. क्या आप वाकई इस अकाउंट को हटाना चाहते हैं? - कोई भी परिजन जो 24 घंटों के भीतर कलेक्ट नहीं किया जाता है, वह अपने आप हीं आपके बैलेंस में वापस आ जाएगा. - आपको अकाउंट निर्माण फिर से शुरू करना होगा और अपना फ़ोन नंबर फिर से सत्यापित करना होगा. - आप अपनी एक्सेस कुंजी का उपयोग करके इस अकाउंट में वापस आ सकते हैं - आपको %1$s द्वारा भेजे गए किसी भी नए मैसेज के बारे में सूचित नहीं किया जाएगा. आप किसी भी समय अनम्यूट कर सकते हैं. - केवल कोड के माध्यम से बनाए गए अकाउंट ही वर्तमान में समर्थित हैं. - आपके मित्र अब आपको इस फ़ोन नंबर से नहीं ढूंढ पाएंगे. - आपको %1$s द्वारा भेजे गए सभी नए मैसेज की सूचना दी जाएगी. आप किसी भी समय म्यूट कर सकते हैं. - जब तक आप उन्हें दोबारा भुगतान नहीं करेंगे तब तक आपको %1$s से कोई मैसेज प्राप्त नहीं होगा. - आपकी एक्सेस कुंजी आपके कोड अकाउंट तक आपको एक्सेस प्रदान करेगी. इसे निजी और सुरक्षित रखें. - ये 12 शब्द आपके कोड अकाउंट को पुनर्प्राप्त करने का एकमात्र तरीका हैं. सुनिश्चित करें कि आपने उन्हें लिखा है, और उन्हें निजी और सुरक्षित रखा है. - क्या आपको यकीन है? - क्या आप वाकई यह अकाउंट हटाना चाहते हैं? - क्या आपने लिंक को भेजा? - क्या आप वाकई बाहर निकलना चाहते हैं? - क्या आप वाकई लॉग आउट करना चाहते हैं? - %1$s को म्यूट करें? - कोई कोड अकाउंट नहीं - क्या आपको यकीन है? - %1$s को अनम्यूट करें? - %1$s से सदस्यता समाप्त करें? - अपनी एक्सेस कुंजी देखें? - क्या आपको यकीन है? - %1$s Kin आपके खाते में जमा कर दिया जा चुका है. - आपके द्वारा कल भेजा गया %1$s कलेक्ट नहीं किया गया था. यह अपने आप आपके बैलेंस में वापस कर दिया गया है. - किसी दोस्त से Code इस्तेमाल करना शुरू करवाएं और 5$ पाएं - किसी भी मैसेंजर ऐप से कैश भेजें - किसी को अपना पहला Kin भेजने के लिए आपको %1$s Kin मिलें हैं. - अब आप सुझावों के लिए अनुरोध कर सकते हैं. - डिपॉजिट मिला - Kin लौटा दिया गया - Code पर नए हैं - रेफरल बोनस प्राप्त हुआ - X अकाउंट जोड़ा गया - कोड को एक्सेस करने के लिए आपके आमंत्रण की समय-सीमा समाप्त हो गई है. आप लॉग आउट कर सकते हैं और एक वैलिड आमंत्रण स्थिति के साथ किसी अलग अकाउंट का उपयोग कर सकते हैं. - आपकी एक्सेस की आपके फंड तक पहुंचने का एकमात्र तरीका है. कृपया इसे गोपनीय और सुरक्षित रखें. - अपनी एक्सेस कुंजी देखने के लिए अपनी पहचान वेरीफाई करें - अगर आपको Code ऐप में लॉग इन करना है, तो QR कोड खोलने के लिए Google Lens आइकॉन पर टैप करें. इसके अलावा, आप Code की लॉग इन स्क्रीन में 12 शब्द दर्ज करके मैन्युअल रूप से लॉग इन कर सकते हैं. - चेतावनी! यह इमेज कोड में आपके पास मौजूद सभी फंडों तक पहुंच प्रदान करती है. इस इमेज को किसी और के साथ शेयर न करें. इसे बचा कर और सुरक्षित रखें. - कोड आपको अपने कैमरे को किसी अन्य उपयोगकर्ता के फ़ोन में डिजिटल बिल पर पॉइंट करके Kin को प्राप्त करने में इनेबल करता है - Kin प्राप्त करने में इनेबल होने के लिए आपको कैमरा एक्सेस की अनुमति देनी होगी - Code को एक्सेस करने के लिए प्रमाणित करें. - Kin खरीदें - Kin खरीदें (जल्द ही आ रहा है) - Kin को खरीदना और बेचना वर्तमान में एक जटिल प्रक्रिया है. समय के साथ ये प्रक्रियाएँ सरल होती जाएँगी. अगर आप सीखना चाहते हैं कि आज Kin को कैसे खरीदें और बेचें तो आप नीचे दिए गए वीडियो देख सकते हैं. - आप अधिकतम %1$s दे सकते हैं - आप केवल %1$s तक टिप दे सकते हैं - अब आपका Kin आपके Code App में इस्तेमाल के लिए उपलब्ध है - आपने किसी को उनका पहला Kin भेजा! यह रहा आपका रेफ़रल बोनस है: - आपका USDC Kin में परिवर्तित किया जा रहा है। इसे पूरा होने में लगभग एक मिनट का समय लगेगा - आपने सफलतापूर्वक USDC डिपोजिट कर दिया है। अपनी खरीदारी पूरी करने के लिए Code App खोलें - वेलकम बोनस - एक कंट्री चुनें - जल्द आ रहा है - \@getcode मैं अपना X अकाउंट कनेक्ट करना चाहता हूं ताकि मैं दुनिया भर के लोगों से टिप्स प्राप्त कर सकूं - मिटाएं - सुनिश्चित करें कि आपके पास अपना सीक्रेट रिकवरी फ्रेज सेव है. इसके बाद अपना Code खाता मिटाने के लिए \"Delete\" टाइप करें. इस कार्रवाई को पहले जैसा नहीं किया जा सकता. - %1$s पर कोड प्राप्त नहीं हुआ? - क्या कोड नहीं मिला? फिर से भेजें - फेस आईडी को डिसएबल करने के लिए आपको अपनी पहचान वेरीफाई करनी होगी - क्या आपके पास कोड वॉलेट ऐप नहीं है? - आपका अभी तक कोई Kin नहीं है. - कोड में लेनदेन की सुरक्षा को और बढ़ाने के लिए फेस आईडी को इनेबल करें. - डेस्टिनेशन एड्रेसें एंटर करें - %1$s तक दर्ज करें - जब आप किसी दोस्त को Code पर साइन अप करने के लिए कहते हैं और आप उन्हें उनका पहला Kin भेजते हैं तो आपको $5 के बराबर Kin फ्री में मिलेगा. - कोड पेमेंट के लिए क्रिप्टोकरेंसी किन का उपयोग करता है. अपने कोड वॉलेट में और परिजनों को लाने के कुछ तरीके यहां दिए गए हैं. - अपना पहला $1 के बराबर Kin फ्री में प्राप्त करें - परिजनों को देने के लिए अपनी पहचान की वेरीफाई करें. - नीचे अपने जमा पते पर Kin को भेजकर Kin को अपने कोड वॉलेट में जमा करें. कॉपी करने के लिए टैप करें. - कृपया अधिक Kin पाएं और फिर दोबारा भुगतान करने का प्रयास करें - अमान्य डेस्टिनेशन अकाउंट - कृपया सुनिश्चित करें कि जिस एड्रेस पर आप पैसे निकाल रहे हैं, उसे आपके वॉलेट प्रोवाइडर द्वारा आरंभीकृत किया गया है. इसे पाने का एक शॉर्टकट यह है कि आप जिस वॉलेट में पैसे भेजने की कोशिश कर रहे हैं, उसमें पहले SOL की एक छोटी राशि को Kin में स्वैप करें. - आमंत्रण कोड - Code को केवल आमंत्रण पाने वाले लोग ही उपयोग कर सकते हैं. ऐप को ऐक्सेस करने के लिए आपके पास एक आमंत्रण कोड होना चाहिए. - %1$d आमंत्रित करें - कोड एक नया क्रिप्टो वॉलेट ऐप है जो सिर्फ़ फिलहाल इन्वाइट है। कोड डाउनलोड करने के लिए %1$s पर जाएं - और अधिक जानें - आपका फ़ोन नंबर इस कोडअकाउंट से लिंक है. मित्र आपको इस फ़ोन नंबर का उपयोग करके ढूंढ सकते हैं. - मैं अपने X अकाउंट को @getcode से जोड़ रहा हूँ ताकि मैं दुनिया भर के लोगों से टिप्स लें सकूँ. - आपका बैलेंस और लेन-देन इतिहास लोड हो रहा है - एक्सेस की के लिए अपने उस फोटो को चेक करें जिसे आपने पहली बार अपना अकाउंट बनाते समय सेव किया था. - आप अभी एक अकाउंट में लॉग इन हैं. आगे बढ़ने से पहले कृपया सुनिश्चित करें कि आपने अपनी एक्सेस की सेव कर ली है. क्या आप लॉगआउट करके नए अकाउंट से लॉगिन करना चाहेंगे? - कोई लिंक नहीं है\nफ़ोन नंबर - आपके पास इस कोड अकाउंट से लिंक किया गया कोई फ़ोन नंबर नहीं है. किसी एक को लिंक करें ताकि आपके मित्र आपको खोज सकें. - कोई नेटवर्क कनेक्शन नहीं है - कोड पर - कोड ऐप खोलें और इस नकदी को लेने के लिए अपने कैमरे को पॉइंट करें - अपने कॉन्टेक्ट्स को व्यवस्थित करना - यह फ़ोन नंबर आपके संपर्क में मौजूद नहीं है. इसके बावजूद आप उन्हें Code पर आमंत्रित कर सकते हैं. - देश कोड के साथ अपना फोन नंबर दर्ज करें. सुनिश्चित करें कि आप उसी फ़ोन नंबर का उपयोग करते हैं जिसे आमंत्रण प्राप्त हुआ था. - द्वारा संचालित - अब आप टिप्स का अनुरोध कर सकते हैं - रेफरल बोनस प्राप्त हुआ - भेजें %1$s - ये रहा %1$s - %1$s में एक नए के लिए अनुरोध करें - कोड वॉलेट ऐप डाउनलोड करने के लिए इस QR कोड को अपने फोन के कैमरे से स्कैन करें - Code वॉलेट ऐप डाउनलोड करने के लिए स्कैन करें - अकाउंट सर्च करें - कॉन्टेक्ट्स के लिए सर्च करें - आपके फ़ोन नंबर पर एक वेरिफ़िकेशन कोड वाला एक मैसेज भेजा गया है. कृपया वह कोड ऊपर डालें. - किसी ने आपको नकद भेजा है - किसी ने आपको टिप दी - कोड्स स्कैन करने के लिए आपको अपना कैमरा शुरू करना होगा - आपका टिप कार्ड आपको दुनिया भर के Code उपयोगकर्ताओं से टिप्स प्राप्त करने की सुविधा देता है. अपने टिप कार्ड को एक्सेस करने के लिए अपनी X की पहचान को कनेक्ट करें. - आपका टिप कार्ड आपको दुनिया भर के Code उपयोगकर्ताओं से सुझाव प्राप्त करने देता है. अपने टिप कार्ड को एक्सेस करने के लिए X पर पोस्ट करें. - आपका Tip Card आपको दुनिया भर के Code यूज़र से सुझाव प्राप्त करने देता है. अपने Tip Card तक पहुँचने के लिए X पर @getcode मेसेज भेजें. - जब आप अन्य Code यूज़र से सुझाव प्राप्त करते हैं तो Code को आपको सूचनाएं भेजने की अनुमति दें. - टाइप करें \"%1$s\" - अनुभव को बेहतर बनाने के लिए हमने कुछ बदलाव किए हैं. कोड का उपयोग जारी रखने के लिए आपको ऐप को अपडेट करना होगा. - वैलिड मालिक अकाउंट - वैलिड टोकन अकाउंट - Kin का मान बदल जाता है. - आपको वापस कर दिया गया - आप अपने Kin को कहाँ वापस लेना चाहेंगे? - परिजनों को वापस लेने के लिए अपनी पहचान की वेरीफाई करें. - आपने डिपॉज़िट किया - आपने दिया - आपके पास है %1$d आमंत्रण हैं - कोड वर्तमान में केवल आमंत्रित है. आपके पास %1$d शेष है. - आपने पेमेंट किया - आपने दिया - आपने प्राप्त किया - आपने भेजा - आपने ख़र्च किया - आपने किसी को टिप दिया - आपने विथड्रॉ किया - आपकी धनराशि सफलतापूर्वक निकाल ली गई है. - आपका X अकाउंट आपके Code अकाउंट से सफलतापूर्वक कनेक्ट हो गया है. अब आप टिप्स का अनुरोध कर सकते हैं. - धन निकालने में सफल रहे - X अकाउंट सफलतापूर्वक कनेक्ट हो गया - एक्सेस की समय सीमा समाप्त हो गयी है - एक्सेस की - ऐप की सेटिंग्स - कैमरा ऑटो स्टार्ट करें - शेष राशि - बीटा झंडे - बोनस - Kin खरीदें और बेचें - काश पेमेंट्स - कोड टीम - Kin की खरीदारी - वेब पेमेंट्स - टिप्स - डीबग विकल्प - डिपाजिट Kin - जमा किया गया - एक्सेस की वर्ड को एंटर करें - फ़ोन नंबर डालें - असफल - अक्सर पूछे जाने वाले प्रश्न - दिया - कैश पाएं - किसी दोस्त से Code इस्तेमाल करना शुरू करवाएं - Kin पाएं - अधिक Kin पाएं - कैश दें - Kin दे - अपर्याप्त फंड - एक दोस्त को आमंत्रित करें - सीमित समय ऑफर - लिंक्ड है - स्थानीय मुद्रा - मेरा खाता - लिंक्ड नहीं है \n - अन्य मुद्राएं - भुगतान किया गया - पेंडिंग - फ़ोन नंबर - गोपनीयता नीति - खरीदा - X अकाउंट सफलतापूर्वक कनेक्ट हो गया - सुझाव प्राप्त करें - रिसीव हुआ - हाल की मुद्राएं - किसी दोस्त को रेफ़र करें, $5 प्राप्त करें - रैफरल बोनस - कैश के लिए रिक्वेस्ट करें - Kin के लिए अनुरोध करें - एक टिप का अनुरोध करें - फेस ID की ज़रूरत है - पासकोड की ज़रूरत है - टच ID की ज़रूरत है - परिणाम - वापिस कर दिया - कोई खाता चुनें - कोई देश चुनें - एक करेंसी को सेलेक्ट करें - भेज दिया - खर्च किया - अकाउंट स्विच करें - सेवा की शर्तें - टिप कार्ड - Kin को टिप दें - Code के लिए नोटिफ़िकेशन चालू करें - अज्ञात - अपडेट की आवश्यकता है - फोन नंबर की पुष्टि करें - वेलकम बोनस - Kin को वापस लें - वापस ले लिया - आपकी एक्सेस की - ऐप डाउनलोड लिंक शेयर करने के लिए लोगो पर टैप करें - diff --git a/apps/codeApp/src/main/res/values-hu/strings-localized.xml b/apps/codeApp/src/main/res/values-hu/strings-localized.xml deleted file mode 100644 index 69958cc93..000000000 --- a/apps/codeApp/src/main/res/values-hu/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Adj hozzá készpént egy bankkártyával - Kamera-hozzáférés engedélyezése - Hozzáférés engedélyezése a névjegyekhez - Push értesítések engedélyezése - Egyenleg - Kin vásárlása - További Kinek vásárlása - Mégsem - Küldés törlése - Csevegés - Gyűjtsd össze ezt a pénzt - Megerősít - Csatlakozás X-hez - Folytatás - Bemásolva - Másol - Cím másolása - Fiók létrehozása - Hozzon létre egy új kódfiókot - Fiók törlése - Kész - Töltsd le most - Arc azonosítás bekapcsolása - Érintéses azonosítás bekapcsolása - Kilépés - Adj - Kin adása - Meghív - Meghívottak - Csatlakozás a várólistához - Később - Tudja meg, hogyan kell Kin-t vásárolni - Tudja meg, hogyan kell eladni a Kin-t - Kapcsoljon telefonszámot - Bejelentkezés - Kijelentkezés - Üzenj a @getcode-nak a csatlakozáshoz - Némítás - Következő - Nem, próbáld újra - Ne most - OK - Beállítások megnyitása - Beilleszt - Beillesztés a vágólapról - Írj bejegyzést, hogy csatlakoztasd a fiókodat - Walletba helyez - Kapj - Meglévő fiók helyreállítása - Emlékeztet - Telefonszám eltávolítása - Távolítsa el a telefonszámát - Kérj borravalót - Mentse a hozzáférési kulcsot a Saját fotók közé - Mentés a Fotók közé - Küldés - Ellenőrző kód küldése - Megosztás - Megosztás URL-ként - Letöltési hivatkozás megosztása - Ossza meg ezt a videót - Borravalós kártyám mutatása - Kamera indítása - Feliratkozás - Húzza el az ujját a bejelentkezéshez - Fizessen húzással - Lapozás a borravalóhoz - Próbáljon ki egy másik kódfiókot - Tweetelj nekik - Code kioldása - Némítás feloldása - Leiratkozás - Frissítés - Hozzáférési kulcs megtekintése - Kin felvétele - A 12 szót írtad le helyette? - Igen - Igen, vedd fel a Kint - Igen, leírtam őket - és - Kin - összegű Kin - A fiókodhoz kapcsolódó minden információ (telefonszám, kapcsolatok, tranzakciós előzmények) eltávolítása a Code szervereiről - El tudod érni a fiókodat más kripto pénztárca alkalmazásokban a Titkos Helyreállítási Kifejezés használatával. Nem fogod tudni használni a fiókodat a Code-ban - Távolítsd el a fiókod a blokkláncról - Ami nem törlődik - Mi fog történni - Ami nem törlődik - Ezt a Kin-t már valaki más felvette. - Ezt a Kint már begyűjtötte valaki. - A kód használatához engedélyezze a kamerához való hozzáférést a Beállításokban. - Ez a készpénz lejárt. - Kérjük, adja meg újra a telefonszámát, és próbálja újra. - Meghívók küldéséhez engedélyezd a hozzáférést a kapcsolataidhoz. - A Code jelenleg nem érhető el az országodban. - Jelenleg nem áll módunkban támogatni a készülékedet. - A Code egy későbbi verziójában valószínűleg támogatni fogja az eSims-et. - Valami rosszul sült el. Ezt a Kin-t nem sikerült begyűjteni. - Nem számítottunk rá, hogy ez megtörténik. Valami elromlott. Kérjük, próbálja meg újra létrehozni ezt a fiókot. - A hozzáférési kulcs mentéséhez engedélyezze a kódhoz való hozzáférést a Beállításokban a Fotókhoz. - Valami hiba történt. Ellenőrizd, hogy helyesen adtad-e meg a telefonszámodat. - Valami elromlott. Kérem, próbálja újra. - Nem sikerült kivenni az összeget. Hiba történt. Kérjük, próbálja meg újra a visszavonást. - A Code kis, mindennapi tranzakciókra lett tervezve, ami %1$s vagy kevesebb. - A Code kis, mindennapi tranzakciókra lett tervezve. A napi kiadási limit holnap fog megnőni. - Ha többet szeretne megtudni a Kin beszerzéséről, keresse fel a Beállítások menü GYIK részét. - Kérjük, adjon meg egy másik meghívó kódot, és próbálja újra. - Adj meg egy érvényes telefonszámot és próbáld újra. - Ez egy érvénytelen borravalós kártya. - Adjon meg egy érvényes kódot, és próbálja újra. - Sajnáljuk, hálózati problémát tapasztaltunk. Kérjük, próbálja meg újra meghívni barátját. - Ez a Kin automatikusan visszaérkezett a küldőhöz, mert 24 órán belül nem vették fel. Légyszíves kérdezd meg őket, hogy elküldjed-e ismét. - Kérjük, adja meg újra a telefonszámát, és próbálja újra. - Próbáljon meg egy olyan képet szerezni, amin jobban látszik a kód. - Kérjük, ellenőrizze internetkapcsolatát, vagy próbálja újra később. - A Code-ra jelenleg csak meghívni lehet embereket. Értesíteni fogjuk, ha további meghívók elérhetővé válnak. - A várólistához való csatlakozáshoz menjen a %1$s - A maximálisan megvásárolható összeg %1$s. Adjon meg kisebb összeget - A minimális vásárlási összeg %1$s. Adjon meg nagyobb összeget. - A hozzáférési kulcsa feloldást kezdeményezett. Ennek eredményeként a továbbiakban nem fogja tudni használni ezt a hozzáférési kulcsot a kódban. - Küldj egy tweetet ennek a személynek, hogy aktiválja a Borravalós kártyáját. - A maximálisan adható borravaló %1$s. Adj meg egy kisebb összeget. - A minimálisan adható borravaló %1$s. Adj meg egy nagyobb összeget. - Naponta csak annyi új fiókot hozhat létre. - A Code jelenleg készülékenként egy fiókra van korlátozva. A Code egy későbbi verziójában valószínűleg támogatni fogja a több fiókot is. - A Code jelenleg telefonszámonként egy fiókra van korlátozva. A Code egy későbbi verziójában valószínűleg támogatni fogja a több fiókot is. - A Kin-t már felvették - A Kin már összegyűjtve - Kamera hozzáférés szükséges - Készpénz lejárt - Ellenőrző kód időtúllépés - A kapcsolatok elérése kötelező - Az ország nem támogatott - A készülék nem támogatott - Az eSims jelenleg nem támogatott - Nem sikerült a begyűjtés - Nem sikerült létrehozni a fiókot - Nem sikerült a mentés - Nem sikerült elküldeni - Nem sikerült megerősíteni - Sikertelen tranzakció - Add meg az elért limitet - A napi limitet elérted - Nem elegendő kin - A meghívó kódja érvénytelen vagy lejárt - Hibás telefonszám - Érvénytelen borravalós kártya - Érvénytelen kód - Meghívás sikertelen - A link lejárt - Elérte a kísérletek maximális számát - Nem található kód - Nincs internetkapcsolat - Nincsenek meghívói - Még nem kapott meghívást - A vásárlási összeg túl nagy - A vásárlási összeg túl kicsi - A hozzáférési kulcs már nem használható a kódban - Borravalós kártya még nincs aktiválva - A borravaló túl sok - A borravaló túl kevés - Túl sok fiók jött létre - A fiók már létre lett hozva - A fiók már létre lett hozva - Hisszük, hogy a fizetéseknek egyszerűnek, hatékonynak és globálisnak kell lenniük. A fejlett blokklánc-technológiára építve a Code olyan funkciókat kínál, amelyekre a hagyományos fizetési alkalmazások nem képesek, például globális peer-to-peer átutalásokat, az egyes cikkeket online feloldó mikrofizetéseket és a kedvenc alkotóknak szóló nulla díjas tippeket. - A Kin a Bitcoinhoz hasonló kriptopénz, amit gyors és olcsó fizetésekre terveztek. - A Bitcoinhoz hasonlóan csak korlátozott mennyiségű Kin áll rendelkezésre. Ha többen vásárolnak Kin-t, az értéke emelkedik, ha pedig többen adnak el Kin-t, az értéke csökken. Ez a dinamika lehetővé teszi, hogy mindenki, akinek Kin-je van, részesüljön az értékteremtésből, ha a Kin elfogadottsága növekszik. - A Kin-t bankkártyával vásárolhatja meg. Ez a Get Kin fülön érhető el. - Igen, megteheti. A Kin-nel számos kriptopénz tőzsdén kereskedhet. - Három fő módon segíthet: beszéljen a közösségi médiában a Code-dal kapcsolatos tapasztalatairól, bátorítsa barátait, hogy próbálják ki maguk is a Code-ot, és bátorítsa kedvenc weboldalait a Code-fizetések integrálására, kérve őket, hogy nézzék meg a [getcode.com](https://getcode.com) oldalt. - Mi a Code? - Miért vannak a Code fizetések Kinben denominálva? - Miért változik a Kin értéke? - Hogyan vásárolhatok még több Kin-t? - Eladhatom a Kin-t? - Miben segíthetek? - egyetért a mi - A \"Fiók létrehozása\" vagy a \"Bejelentkezés\" gomb megérintésével - Az Ön kamerája a Kin fogadására szolgál. A folytatáshoz engedélyezze a kamerához való hozzáférést. - Push értesítésekre van szükségünk, hogy időben információkat küldhessünk a fiókjáról. - A visszavonások visszafordíthatatlanok, és a kezdeményezést követően nem vonhatók vissza.\n - A számlán lévő összes pénz elvész. A fiók törlése végleges, és nem vonható vissza. Biztosan törli ezt a fiókot? - Minden Kin, amit 24 órán nem vesznek fel automatikusan visszaérkezik az egyenlegedre. - Újra kell indítania a fiók létrehozását, és újra igazolnia kell telefonszámát. - Hozzáférési kulcsa segítségével visszaléphet ebbe a fiókba - Nem kapsz értesítést %1$s új üzeneteiről. A némítás bármikor visszavonható. - Jelenleg csak a Code segítségével létrehozott fiókok támogatottak. - Ismerősei többé nem találják meg ezzel a telefonszámmal. - Értesítést kapsz %1$s új üzeneteiről. Bármikor elnémíthatod. - Nem kapsz %1$s üzenetet, amíg újra nem fizetsz nekik. - Hozzáférési kulcsa hozzáférést biztosít kódfiókjához. Tartsa privátban és biztonságban. - Ez a 12 szó az egyetlen módja a Code-fiók helyreállításának. Ügyeljen arra, hogy felírta őket, és kezelje bizalmasan és biztonságosan. - Biztos benne? - Biztosan törli ezt a fiókot? - Elküldted a linket? - Biztos ki akar lépni? - Biztosan ki akar jelentkezni? - %1$s némítása? - Nem kódszámla - Biztos benne? - Visszavonod %1$s némítását? - %1$s elhagyása? - Megnézi a hozzáférési kulcsát? - Biztos benne? - %1$s A Kint jóváírtuk a számládon. - A tegnap elküldött %1$s nem került összegyűjtésre. Ez automatikusan visszakerült az egyenlegedre. - Kezdd el egy barátoddal a Code használatát, és kapj 5 dollárt - Küldjön készpénzt bármely messenger alkalmazáson keresztül - %1$s Kint kaptál, amiért elküldted valakinek az első Kinét. - Most már kérhetsz borravalót. - Befizetés beérkezett - Kin visszatért - Új a Code-on - Ajánlói bónusz megérkezett - X fiók csatlakoztatva - A kód hozzáférési meghívója lejárt. Kijelentkezhet, és egy másik fiókot használhat érvényes meghívási állapottal. - A hozzáférési kulcs az egyetlen módja annak, hogy hozzáférjen pénzeszközeihez. Kérjük, tartsa privátban és legyen óvatos. - A hozzáférési kulcs megtekintéséhez igazolja személyazonosságát. - Koppintson a Google Lens ikonra a QR-kód megnyitásához a Code-ba való bejelentkezéshez. Alternatív megoldásként manuálisan is bejelentkezhet úgy, hogy beírja a 12 szót a Kódbejelentkezés képernyőn. - Figyelem! Ez a kép hozzáférést biztosít a Code-ban lévő összes pénzeszközéhez. Ne ossza meg ezt a képet senki mással. Tartsa biztonságban. - A Code lehetővé teszi a Kin fogadását, ha kameráját egy másik felhasználó telefonján lévő digitális számlára irányítja - A Kin fogadásához engedélyeznie kell a kamerához való hozzáférést - Azonosítsa magát a Code eléréséhez. - Vásároljon Kint - Vásárolj Kin-t (Hamarosan) - A Kin vétele és eladása jelenleg bonyolult folyamat. Ezek a folyamatok idővel egyszerűsödni fognak. Ha szeretné megtanulni, hogyan vásároljon és adjon el Kin-t még ma, akkor nézze meg az alábbi videókat. - Legfeljebb ennyit adhatsz: %1$s - Legfeljebb %1$s borravalót adhatsz - Mostantól használhatja Kinjeit a Code alkalmazásban - Elküldte valakinek az első Kin-jét! Itt van az ajánlói bónusza: - USDC-it átváltjuk Kinre. A művelet körülbelül egy percet vesz igénybe - USDC sikeresen befizetve. Nyissa meg a Code alkalmazást a vásárlás befejezéséhez - Üdvözlő bónusz - Válasszon egy országot - Jön hamarosan - \@getcode Csatlakoztatni akarom az X fiókomat, hogy borravalót kaphassak bárhonnan a világon. - Törlés - Győződj meg róla, hogy a Titkos Helyreállítási Kifejezés el van mentve, majd nyomj \"Törlés\"-t a Code fiókod törléséhez. Ez a művelet nem visszavonható. - Nem kapott kódot itt: %1$s? - Nem kapta meg a kódot? Küldés újra - Az arcfelismerés letiltásához igazolnia kell személyazonosságát. - Nincs a Code Wallet alkalmazásod? - Még nincs rokonod. - Engedélyezze a Face ID-t, hogy tovább fokozza a tranzakciók biztonságát a Code-ban. - Írja be a cél címét - Adjon meg legfeljebb %1$s - 5 USD Kint kapsz ingyen, ha felkéred egy barátod, hogy regisztráljon a Code szolgáltatásra, és te elküldöd neki az első Kint. - A Code a Kin-t használja fizetésre. Íme néhány mód, hogy több Kin kerüljön a Code tárcádba. - Szerezd meg első 1 dolláros Kint ingyen - Igazolja személyazonosságát ahhoz, hogy kint adhasson. - Fizesse be Kint Code tárcájába úgy, hogy elküldi a Kint az alábbi befizetési címére. Koppintson a másoláshoz. - Kérjük, szerezzen több Kin-t, majd próbáljon újra fizetni - Érvénytelen célfiók - Kérjük, győződjön meg róla, hogy a cím, amelyre a kifizetést végzi, inicializálva lett a pénztárca szolgáltatója által. Ezt úgy érheti el, ha először egy kevés SOL-t Kin-re vált a tárcában, ahová küldeni szeretne. - Meghívó kód - A kód jelenleg csak meghívásra használható. Az alkalmazás eléréséhez meghívókódra lesz szüksége. - %1$d meghívó - A Code egy új kriptotárca-alkalmazás, amely jelenleg csak meghívásra használható. A Code letöltéséhez menj ide: %1$s - Tudj meg többet - Az Ön telefonszáma ehhez a Code fiókhoz van kapcsolva. Ismerősei megtalálhatják Önt ezen a telefonszámon. - Az X-fiókomat összekötöm a @getcode fiókkal, hogy borravalót kaphassak a világ minden tájáról. - Egyenleg és tranzakciós előzmények betöltése - Nézze meg fotóin a hozzáférési kulcsot, amelyet a fiók létrehozásakor mentett. - Jelenleg be vagy jelentkezve egy fiókkal. Mielőtt tovább lépsz, győződj meg róla, hogy a Hozzáférési kulcsodat elmentetted. Kijelentkezel és bejelentkezel egy új fiókkal? - Nincs linkelve\nTelefonszám - Önnek nincs telefonszáma ehhez a Code-fiókhoz társítva. Párosítson össze vele egyet, hogy barátai felfedezhessék. - Nincs hálózati kapcsolat - A kódon - Nyisd meg a Code alkalmazást, és irányítsd a kamerát a pénz megragadásához - Névjegyek rendezése - Ez a telefonszám nincs a kapcsolataid között. Továbbra is meghívhatod a Code alkalmazásba. - Adja meg telefonszámát az országkóddal együtt. Győződjön meg arról, hogy ugyanazt a telefonszámot használja, amelyre a meghívót kapta. - A következő jóvoltából: - Most már kérhetsz borravalót. - Ajánlói bónusz megérkezett - %1$s küldés - Itt van %1$s - Kérjen újat itt: %1$s - A Code Wallet alkalmazás letöltéséhez olvasd be ezt a QR-kódot a telefonod kamerájával - Olvasd be a Code Wallet \nalkalmazás letöltéséhez - Pénznemek keresése - Névjegyek keresése - SMS-t küldtünk a telefonszámára egy ellenőrző kóddal. Kérjük, írja be a fenti ellenőrző kódot. - Valaki pénzt küldött neked - Valaki borravalót adott neked - El kell indítania a fényképezőgépet a Code-ok beolvasásához. - A Borravalós kártya lehetővé teszi, hogy borravalót kapj a Code felhasználóitól a világ minden tájáról. A Borravalós kártyához való hozzáféréshez csatlakoztasd az X azonosítódat. - A Borravalós kártyával borravalót kaphatsz a Code felhasználóitól a világ minden tájáról. A Borravalós kártyát eléréséhez írj az X-re. - A Borravalós kártyával borravalót kaphat Code felhasználóktól a világ minden tájáról. A Borravalós kártya eléréséhez üzenjen a @getcode-nak az X-en. - Engedélyezze, hogy a Code értesítést küldjön, amikor borravalót kap más Code felhasználóktól. - Írd be: \"%1$s\" - Néhány változtatást eszközöltünk az élmény javítása érdekében. A Code használatának folytatásához frissítenie kell az alkalmazást. - Érvényes tulajdonosi fiók - Érvényes token fiók - A Kin értéke változik. - viszzaküldve Önnek - Hová szeretné visszavonni rokonát? - Igazolja személyazonosságát annak érdekében, hogy felvehessen kint. - Ön befizetett - Ön adott - %1$d meghívása van - A kód jelenleg csak meghívásra használható. Még %1$d van hátra. - Ön fizetett - Ön adott - Ennyit kaptál - Ön küldött - Ön elköltött - Valakinek borravalót adtál - Ön kivett - Az összeget sikeresen visszavontuk. - Az X-fiókodat sikeresen összekapcsoltuk a Code-fiókoddal. Most már kérhetsz borravalót. - A visszavonás sikeres - X-fiók sikeresen csatlakoztatva - Hozzáférés lejárt - Hozzáférési kulcs - Alkalmazás beállításai - Kamera automatikus indítása - Egyenleg - Béta zászlók - Bónusz - Kin vásárlás és eladás - Készpénzes kifizetések - Code csapat - Kin vásárlások - Webes kifizetések - Borravalók - Bugmentesítő opciók - Kin küldése - Pénzt átutalt - Írja be a hozzáférési kulcsszavakat - Telefonszám megadása - Sikertelen - GYIK - Adott - Kapj készpénzt - Szerezz egy barátot elkezdte használni a Code-ot - Szerezz Kint - Szerezzen több Kin-t - Adj készpénzt - Kin felvétele - Nincs elég tőke - Hívd meg egy barátod - Korlátozott időre szóló ajánlat - Kapcsolatban áll - Helyi pénznem - Fiókom - Nem áll kapcsolatban - Egyéb pénznemek - Kifizetve - Függőben levő - Telefonszám - Adatvédelmi irányelvek - Megvásárolva - X-fiók sikeresen csatlakoztatva - Kapjon borravalót - Fogadva - Legutóbbi pénznemek - Ajánlj egy barátot, kapsz 5 dollárt - Ajánlói bónusz - Kérj készpénzt - Kin kérése - Kérj borravalót - Face ID-t igényel - Jelszót igényel - Touch ID-t igényel - Eredmények - Visszatérítve - Válassz egy fiókot - Válassz országot - Válasszon egy valutát - Elküldve - Elköltött - Fiókcsere - Szolgáltatási feltételek - Borravalós kártya - Kin borravaló - Értesítések bekapcsolása a Code-hoz - Ismeretlen - Frissítés szükséges - Telefonszám ellenőrzése - Kezdő bónusz - Kin felvétele - Felvéve - Az Ön hozzáférési kulcsa - Érintsd meg a logót az alkalmazás letöltési linkjének megosztásához - diff --git a/apps/codeApp/src/main/res/values-id/strings-localized.xml b/apps/codeApp/src/main/res/values-id/strings-localized.xml deleted file mode 100644 index c8b26ff6b..000000000 --- a/apps/codeApp/src/main/res/values-id/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Tambahkan Uang Tunai dengan Kartu Debit - Izinkan Akses Kamera - Izinkan Akses ke Kontak - Izinkan Pemberitahuan Push - Saldo - Beli Kin - Beli Lebih Banyak Kin - Batal - Batal Kirim - Mengobrol - Ambil Uang Ini - Konfirmasi - Sambungkan ke X - Lanjut - Disalin - Salin - Salin Alamat - Buat sebuah Akun - Buat Akun Code Baru - Hapus Akun - Selesai - Unduh Sekarang - Aktifkan Face ID - Aktifkan Touch ID - Keluar - Berikan - Berikan Kin - Undang - Undangan - Bergabung dengan Daftar Tunggu - Nanti - Pelajari Cara Membeli Kin - Pelajari Cara Menjual Kin - Tautkan Nomor Telepon - Masuk - Keluar - Kirim pesan ke @getcode untuk Menghubungkan - Bisukan - Selanjutnya - Tidak, Coba Lagi - Tidak Sekarang - Oke - Buka Pengaturan - Tempel - Tempel Dari Papan Klip - Post untuk Menghubungkan Akun - Masukkan ke Dompet - Terima - Pulihkan Akun Yang Ada - Ingatkan - Hapus Nomor Telepon - Hapus Nomor Telepon Anda - Minta Tip - Simpan Kunci Akses ke Foto Saya - Simpan ke Foto - Kirim - Kirim Kode Verifikasi - Bagikan - Bagikan sebagai URL - Bagikan Tautan Unduhan - Bagikan Video Ini - Tampilkan Kartu Tip Saya - Mulai Kamera - Langganan - Usap untuk Masuk - Geser untuk membayar - Geser untuk memberi Tip - Coba Akun Code Yang Berbeda - Kirimi Tweet - Buka Code - Suarakan - Berhenti langganan - Perbarui - Lihat Kunci Akses - Tarik Kin - Tulis 12 Kata Sebagai Gantinya? - Ya - Ya, Tarik Kin - Ya, Saya Telah Menulisnya - dan - Kin - Kin - Hapus semua informasi terkait akun Anda dari server Code (nomor telepon, kontak, riwayat transaksi) - Anda dapat mengakses akun Anda di aplikasi dompet kripto lainnya menggunakan Frasa Pemulihan Rahasia. Anda tidak dapat menggunakan kode Anda di Code - Hapus akun Anda dari blockchain - Yang akan dilakukan oleh penghapusan - Yang akan terjadi - Yang tidak akan dilakukan oleh penghapusan - Kin ini sudah diambil oleh orang lain. - Kin ini sudah diambil oleh seseorang. - Izinkan akses ke kamera dalam Pengaturan untuk menggunakan Code. - Dana ini sudah kedaluwarsa. - Silakan masukkan kembali nomor telepon Anda dan coba lagi. - Harap berikan akses kontak pada Pengaturan untuk mengirim undangan. - Kode saat ini tidak tersedia di negara Anda. - Kami tidak dapat mendukung perangkat Anda saat ini - Dukungan untuk eSim kemungkinan akan hadir dalam versi Kode mendatang. - Terjadi masalah. Kin ini tidak dapat diambil. - Kami tidak memperkirakan hal itu terjadi. Terjadi masalah. Silakan coba membuat akun ini lagi. - Izinkan Code mengakses Foto di Pengaturan untuk menyimpan Kunci Akses Anda. - Terdapat kesalahan. Pastikan nomor telepon Anda dimasukkan dengan benar. - Terjadi masalah. Harap mencoba lagi. - Gagal menarik dana Anda. Ada yang tidak beres, silakan mencoba mengulangi penarikan. - Code dirancang untuk transaksi kecil harian bernilai %1$s ke bawah. - Code dirancang untuk transaksi kecil harian. Limit pemberian harian Anda akan bertambah besok. - Untuk mempelajari cara mendapatkan lebih banyak Kin, buka Tanya-Jawab di Pengaturan. - Harap memasukkan Kode Undangan yang lain dan mencoba lagi. - Harap masukkan nomor telepon dengan benar dan coba lagi. - Ini adalah Kartu Tip yang tidak valid. - Masukkan kode yang valid dan coba lagi. - Maaf, kami mengalami masalah jaringan. Silakan coba mengundang teman Anda lagi. - Kin ini secara otomatis dikembalikan ke pengirim karena tidak diambil dalam waktu 24 jam. Silakan minta mereka untuk mengirim Kin lagi. - Silakan masukkan kembali nomor telepon Anda dan coba lagi. - Silakan coba untuk mendapatkan gambar yang menampilkan kode lebih jelas. - Silakan periksa koneksi internet Anda atau coba lagi nanti. - Code saat ini hanya untuk undangan. Kami akan memberi tahu Anda ketika ada lebih banyak undangan yang tersedia. - Untuk bergabung dengan daftar tunggu masuklah ke %1$s - Jumlah maksimum yang dapat Anda beli sebesar %1$s. Masukkan jumlah yang lebih kecil. - Jumlah minimum yang dapat Anda beli sebesar %1$s. Masukkan jumlah yang lebih besar. - Kunci Akses Anda telah digunakan untuk membuka. Akibatnya, Anda tak lagi dapat menggunakan Kunci Akses ini di Code. - Kirim tweet ke orang ini untuk mengaktifkan Kartu Tip mereka. - Tip maksimum yang dapat Anda berikan adalah %1$s. Silakan masukkan jumlah yang lebih kecil. - Minimum yang dapat Anda beri tip adalah %1$s. Silakan masukkan jumlah yang lebih besar. - Anda hanya dapat membuat sejumlah akun baru setiap hari. - Kode saat ini terbatas pada satu akun per perangkat. Dukungan untuk banyak akun kemungkinan akan hadir dalam versi Kode mendatang. - Kode saat ini terbatas pada satu akun per perangkat. Dukungan untuk banyak akun kemungkinan akan hadir dalam versi Kode mendatang. - Kin Sudah Diambil - Kin Sudah Diambil - Memerlukan Akses Kamera - Dana kedaluwarsa - Batas Waktu Kode Verifikasi Habis - Akses Kontak Diperlukan - Negara tidak mendukung - Perangkat tidak mendukung - eSim Saat Ini Tidak Mendukung - Gagal Mengambil - Gagal Membuat Akun - Gagal Menyimpan - Gagal Mengirim - Gagal Mengonfirmasi - Transaksi Gagal - Limit Pemberian Tercapai - Limit Harian Tercapai - Kin Tidak Cukup - Kode Undangan Tidak Valid atau Kedaluwarsa - Nomor Telepon Salah - Kartu Tip Tidak Valid - Kode Tidak Valid - Undangan Gagal - Tautan Kedaluwarsa - Upaya Maksimum Tercapai - Kode Tidak Ditemukan - Tidak Ada Sambungan Internet - Anda Tidak Memiliki Undangan - Anda Belum Diundang - Pembelian terlalu besar - Pembelian terlalu kecil - Kunci Akses Tak Lagi Dapat Digunakan di Code - Kartu Tip Belum Diaktivasi - Tip Terlalu Besar - Tip Terlalu Kecil - Terlalu banyak akun yang dibuat - Akun Sudah Dibuat - Akun sudah dibuat - Kami percaya pembayaran itu seharusnya mudah, kuat, dan global. Dengan membangun menggunakan teknologi blockchain tingkat lanjut, Code menawarkan fitur yang tidak dimiliki oleh aplikasi pembayaran tradisional, seperti transfer peer to peer global, micropayment yang membuka artikel individu secara daring, serta nol tip biaya untuk kreator favorit Anda. - Kin adalah mata uang kripto seperti Bitcoin, tetapi juga dirancang untuk pembayaran yang cepat dan terjangkau. - Seperti Bitcoin, jumlah Kin yang tersedia terbatas. Jika lebih banyak orang yang membeli Kin nilainya akan naik, dan jika lebih banyak orang yang menjual Kin nilainya akan turun. Dinamika ini memungkinkan semua orang yang memegang Kin untuk bersama-sama menciptakan nilai jika Kin semakin banyak diadopsi. - Anda bisa membeli Kin dengan kartu debit Anda. Ini dapat diakses pada tab Get Kin. - Ya. Penjualan Kin didukung di sejumlah bursa mata uang kripto. - Ada tiga cara utama Anda dapat membantu: berbicara seputar pengalaman Code Anda di media sosial, ajak teman-teman Anda mencoba Code, dan ajak situs-situs web favorit Anda untuk mengintegrasikan pembayaran Kin dengan memintanya mengunjungi [getcode.com](https://getcode.com). - Apa itu Code? - Kenapa pembayaran Code didenominasikan dalam Kin? - Kenapa nilai Kin berubah? - Bagaimana cara membeli lebih banyak Kin? - Bisakah saya menjual Kin? - Bagaimana saya bisa membantu? - setuju dengan - Dengan mengetuk \"Buat Akun\" atau \"Masuk\" maka Anda - Kamera Anda digunakan untuk menerima Kin. Harap Izinkan akses ke kamera untuk melanjutkan. - Kami membutuhkan pemberitahuan push untuk mengirimi Anda informasi tentang akun Anda secara tepat waktu. - Penarikan tidak dapat diubah dan tidak dapat dibatalkan setelah dimulai. - Semua dana di akun ini akan hilang. Menghapus akun Anda bersifat permanen dan tidak dapat diurungkan. Anda yakin ingin menghapus akun ini? - Setiap Kin yang tidak diambil dalam waktu 24 jam akan secara otomatis dikembalikan ke saldo Anda. - Anda harus memulai ulang pembuatan akun dan memverifikasi nomor telepon Anda lagi. - Anda dapat kembali ke akun ini menggunakan Kunci Akses Anda - Anda tidak akan diberi tahu tentang pesan baru dari %1$s. Anda dapat mengaktifkan suara kapan saja. - Saat ini, hanya akun yang dibuat melalui Code yang didukung. - Teman-teman Anda tidak akan lagi dapat menemukan Anda dengan nomor telepon ini. - Anda akan diberitahu tentang semua pesan baru dari %1$s. Anda dapat membisukan kapan saja. - Anda tidak akan menerima pesan apa pun dari %1$s sampai Anda membayarnya lagi. - Kunci Akses Anda akan memberikan akses ke akun Code Anda. Jaga kerahasiaan dan keamannnya. - 12 kata ini adalah satu-satunya cara untuk memulihkan akun Code Anda. Pastikan Anda menuliskannya, dan menjaganya tetap rahasia dan aman. - Anda yakin? - Anda yakin ingin menghapus akun ini? - Apakah Anda mengirimkan tautannya? - Anda yakin ingin keluar? - Anda yakin ingin keluar? - Bisukan %1$s? - Bukan Akun Code - Anda yakin? - Aktifkan suara %1$s? - Berhenti langganan dari %1$s? - Lihat Kunci Akses Anda? - Anda Yakin? - %1$s Kin disetorkan ke akun Anda. - %1$s yang Anda kirim kemarin tidak diambil. Ini sudah secara otomatis dikembalikan ke saldo Anda. - Ajak teman untuk memulai Code dan dapatkan $5 - Kirim uang tunai melalui aplikasi messenger apa pun - Anda menerima %1$s Kin karena telah mengirimi seseorang Kin pertamanya. - Anda sekarang dapat meminta tip. - Setoran Diterima - Kin Dikembalikan - Baru di Code - Bonus Referensi Diterima - Akun X Terhubung - Undangan untuk mengakses Code telah kedaluwarsa. Anda dapat keluar dan menggunakan akun lain yang memiliki status undangan yang valid. - Kunci Akses Anda adalah satu-satunya cara untuk mengakses dana Anda. Harap jaga kerahasiaan dan keamanannya. - Verifikasi identitas Anda untuk melihat Kunci Akses Anda. - Ketuk ikon Google Lens untuk membuka kode QR untuk masuk ke Code. Atau Anda dapat masuk secara manual dengan memasukkan 12 kata di layar Masuk Code. - Peringatan! Gambar ini memberikan akses ke semua dana yang Anda miliki di Kode. Jangan bagikan gambar ini ke orang lain. Jaga agar tetap aman dan terlindungi. - Code memungkinkan Anda menerima Kin dengan mengarahkan kamera Anda ke tagihan digital di ponsel pengguna lain - Anda perlu mengizinkan akses kamera untuk dapat menerima Kin - Autentikasikan untuk mengakses Code. - Beli Kin - Beli Kin (Segera Hadir) - Membeli dan menjual Kin saat ini merupakan proses yang rumit. Proses akan menjadi lebih sederhana seiring berjalannya waktu. Jika Anda ingin mempelajari cara membeli dan menjual Kin hari ini, Anda dapat menonton video panduan di bawah ini. - Anda hanya bisa memberikan hingga %1$s - Anda hanya dapat memberi tip hingga %1$s - Kin Anda sekarang tersedia untuk digunakan di Code App - Anda mengirim Kin pertama bagi seseorang! Ini adalah bonus referensi Anda: - USDC Anda sedang dikonversi ke Kin. Proses ini dapat memerlukan waktu sekitar satu menit - Anda berhasil menyetor USDC. Buka Code App untuk menyelesaikan pembelian - Bonus Penyambutan - Pilih salah satu negara - Segera Hadir - \@getcode Saya ingin menghubungkan akun X saya agar bisa menerima kiat-kiat dari orang di seluruh dunia - Hapus - Pastikan Anda menyimpan Frasa Pemulihan Rahasia, lalu masukkan \"Delete\" untuk menghapus akun Code Anda. Tindakan ini tidak dapat dibatalkan. - Tidak mendapatkan kode di %1$s? - Tidak mendapatkan kode? Kirim ulang - Menonaktifkan Face ID mengharuskan Anda memverifikasi identitas Anda. - Belum punya aplikasi Code Wallet? - Anda belum punya Kin. - Aktifkan Face ID untuk lebih meningkatkan keamanan transaksi dalam Code. - Masukkan alamat tujuan - Masukkan hingga %1$s - Dapatkan $5 Kin gratis saat Anda mengajak teman untuk mendaftar ke Code dan Anda mengirimi mereka Kin pertamanya. - Code menggunakan cryptocurrency Kin untuk pembayaran. Berikut adalah beberapa cara untuk memasukkan lebih banyak Kin ke dompet Code Anda. - Dapatkan $1 Kin Pertama Anda Secara Gratis - Verifikasi identitas Anda untuk memberi Kin. - Setor Kin ke dompet Code Anda dengan mengirim Kin ke Alamat Setoran Anda di bawah ini. Ketuk untuk menyalin. - Dapatkan lebih banyak Kin dan kemudian coba bayar lagi - Akun tujuan tidak valid - Pastikan alamat penarikan Anda sudah diinisialisasi oleh penyedia dompet Anda. Jalan pintas untuk mencapai in adalah dengan menukar sedikit SOL dengan Kin di dompet tujuan pengiriman Anda.\n - Kode Undangan - Code saat ini hanya untuk undangan. Anda perlu Kode Undangan untuk mengakses aplikasi. - %1$d Undangan - Code adalah aplikasi dompet kripto baru yang saat ini hanya untuk undangan. Untuk mengunduh Code, silakan datang ke %1$s - Pelajari lebih lanjut - Nomor telepon Anda ditautkan dengan akun Code ini. Teman-teman dapat menemukan Anda menggunakan nomor telepon ini. - Saya menyambungkan akun X saya dengan @getcode agar saya dapat menerima tip dari orang di seluruh dunia. - Memuat saldo dan riwayat transaksi Anda - Periksa Kunci Akses yang Anda simpan di foto Anda saat pertama kali membuat akun. - Anda saat ini masuk ke akun. Pastikan Anda telah menyimpan Kunci Akses Anda sebelum melanjutkan. Apakah Anda ingin keluar dan masuk dengan akun baru? - Tak Ada Yang Tertaut - Anda tidak memiliki nomor telepon yang ditautkan ke akun Code ini. Tautkan nomor agar teman Anda dapat menemukan Anda. - Tidak ada koneksi jaringan - Pada Code - Buka aplikasi Code dan arahkan kamera Anda untuk mengambil uang ini - Mengatur Kontak Anda - Nomor telepon ini tidak ada dalam Kontak Anda. Anda tetap dapat mengundangnya ke Code. - Masukkan nomor telepon Anda termasuk kode negara. Pastikan Anda menggunakan nomor telepon yang sama dengan yang menerima undangan. - Didukung oleh - Anda sekarang dapat meminta tip - Bonus referensi diterima - Kirim %1$s - Inilah %1$s - Meminta yang baru di %1$s - Pindai kode QR ini dengan kamera ponsel Anda untuk mengunduh aplikasi Code Wallet - Pindai untuk mengunduh aplikasi Code Wallet - Cari mata uang - Cari kontak - Pesan SMS berisi kode verifikasi telah dikirim ke nomor telepon Anda. Harap memasukkan kode verifikasi di atas. - Seseorang mengirim uang kepada Anda - Seseorang memberi tip untuk Anda - Anda perlu memulai kamera Anda untuk memindai Code - Kartu Tip Anda memungkinkan Anda menerima tip dari pengguna Code di seluruh dunia. Untuk mengakses Kartu Tip Anda, hubungkan identitas X Anda. - Kartu Tip Anda memungkinkan Anda menerima tip dari pengguna Code di seluruh dunia. Untuk mengakses Kartu Tip, kirim postingan ke X. - Kartu Tip Anda memungkinkan Anda menerima tip dari pengguna Code di seluruh dunia. Untuk mengakses Kartu Tip Anda, kirim pesan ke @getcode di X. - Izinkan Code mengirimi Anda pemberitahuan saat Anda menerima Tip dari pengguna Code lainnya. - Tikkan \"%1$s\" - Kami telah melakukan beberapa perubahan untuk meningkatkan pengalaman. Anda harus memperbarui aplikasi untuk tetap menggunakan Code. - Akun pemilik yang valid - Akun token yang valid - Nilai Kin berubah. - telah dikembalikan kepada Anda - Ke mana Anda ingin menarik Kin? - Verifikasi identitas Anda untuk menarik Kin. - Anda menyetorkan - Anda memberi - Anda memiliki %1$d Undangan - Code saat ini hanya untuk undangan. Anda memiliki %1$d tersisa. - Anda membayar - Anda memberi - Anda menerima - Anda mengirim - Anda membelanjakan - Anda memberi tip untuk seseorang - Anda menarik - Dana Anda berhasil ditarik. - Akun X Anda telah berhasil tersambung ke akun Code Anda. Anda sekarang dapat meminta Tip. - Penarikan Berhasil - Akun X Berhasil Terhubung - Akses Kedaluwarsa - Kunci Akses - Pengaturan Aplikasi - Mulai Otomatis Kamera - Saldo - Tanda Beta - Bonus - Beli & Jual Kin - Pembayaran Tunai - Tim Code - Pembelian Kin - Pembayaran Web - Tip - Opsi Debug - Deposit Kin - Deposit - Masukkan Kata Kunci Akses - Masukkan Nomor Telepon - Gagal - Tanya-Jawab - Memberi - Dapatkan Uang Tunai - Ajak Teman Memulai Code - Dapatkan Kin - Dapatkan Lebih Banyak Kin - Berikan Uang - Berikan Kin - Dana Tidak Cukup - Undang Teman - Penawaran Terbatas - Tertaut - Mata Uang Lokal - Akun Saya - Tidak Terhubung - Mata Uang Lainnya - Dibayar - Menunggu - Nomor Telepon - Kebijakan Privasi - Dibeli - Akun X Berhasil Tersambung - Terima Tip - Menerima - Mata Uang Terbaru - Ajak Teman, Dapatkan $5 - Bonus Referensi - Minta Uang Tunai - Minta Kin - Minta Tip - Memerlukan Face ID - Memerlukan Kode Sandi - Memerlukan Touch ID - Hasil - Kembali - Pilih Akun - Pilih Negara - Pilih Mata Uang - Terkirim - Terpakai - Beralih Akun - Ketentuan Layanan - Kartu Tip - Beri Tip Kin - Nyalakan Notifikasi untuk Code - Tak Dikenal - Memerlukan Pembaruan - Verifikasi Nomor Telepon - Bonus Selamat Datang - Tarik Kin - Menarik - Kunci Akses Anda - Klik logo untuk membagikan tautan unduhan aplikasi - diff --git a/apps/codeApp/src/main/res/values-it/strings-localized.xml b/apps/codeApp/src/main/res/values-it/strings-localized.xml deleted file mode 100644 index e57d1f3ec..000000000 --- a/apps/codeApp/src/main/res/values-it/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Aggiungi Contanti con una Carta di Debito - Consenti l\'accesso alla fotocamera - Consenti l\'accesso ai contatti - Consenti le notifiche push - Saldo - Compra Kin - Acquista altri Kin - Annulla - Cancella invio - Chatta - Ritira questi soldi - Conferma - Connetti a X - Continua - Copiato - Copia - Copia indirizzo - Crea un account - Crea un nuovo account Code - Elimina account - Fatto - Scaricalo adesso - Abilita Face ID - Abilita Touch ID - Esci - Dare - Dona Kin - Invita - Inviti - Unisciti alla lista d\'attesa - Più tardi - Impara ad acquistare Kin - Impara a vendere Kin - Collega un numero di telefono - Accedi - Disconnetti - Trasmetti @getcode per connetterti - Silenzia - Avanti - No, riprova - Non adesso - OK - Apri le impostazioni - Incolla - Incolla dagli appunti - Pubblica per Connettere l\'Account - Metti nel portafoglio - Ricevere - Recupera un account esistente - Ricorda - Rimuovi numero di telefono - Rimuovi il tuo numero di telefono - Richiedi una mancia - Salva chiave d\'accesso nelle mie foto - Salva nelle foto - Invia - Invia codice di verifica - Condividi - Condividi come URL - Condividi link per il download - Condividi questo video - Moatra la mia Tip Card - Apri la fotocamera - Iscriviti - Scorri per accedere - Scorri per pagare - Scorri per dare la mancia - Prova con un account Code diverso - Twitta loro - Codice di sblocco - Riattiva audio - Annulla l\'iscrizione - Aggiorna - Visualizza chiave d\'accesso - Preleva Kin - Hai preferito annotare le 12 parole? - - Sì, preleva Kin - Sì, le ho annotate - e - Kin - di Kin - Rimuovere tutte le informazioni associate al tuo account dai server di Code (numero di telefono, contatti, cronologia transazioni) - Puoi accedere al tuo account in altre app crypto wallet usando la tua frase segreta di recupero. Non potrai usare il tuo account su Code - Rimuovere il tuo account dalla blockchain - Cosa farà la cancellazione - Cosa accadrà - Cosa non farà la cancellazione - Questo Kin è già stato riscosso da qualcun\'altro. - Questo Kin è già stato acquisito da qualcuno. - Consenti l\'accesso alla fotocamera nelle Impostazioni per poter utilizzare Code. - Questo denaro è scaduto. - Inserisci nuovamente il tuo numero di telefono e riprova. - Sei pregato di consentire l\'accesso ai contatti nelle Impostazioni per mandare gli inviti. - Attualmente Code non è disponibile nel tuo paese. - Attualmente non possiamo supportare il tuo dispositivo - Probabilmente il supporto per le eSim sarà aggiunto in una versione futura di Code. - Qualcosa è andato storto. Non è stato possibile riscuotere questo Kin. - Questo è un imprevisto. Qualcosa è andato storto. Prova a creare nuovamente questo account. - Consenti l\'accesso alle foto da parte di Code nelle Impostazioni per poter salvare la tua chiave d\'accesso. - Qualcosa è andato storto. Assicurati che il tuo numero di telefono sia stato inserito correttamente. - Qualcosa è andato storto. Si prega di riprovare. - Non è stato possibile prelevare i tuoi fondi. Qualcosa è andato storto. Ritenta il prelievo. - Code è disegnato per transazioni piccole e quotidiane che ammontano a %1$s o meno. - Code è disegnato per transazioni piccole e quotidiane. Il tuo limite giornaliero aumenterà domani. - Per scoprire come ottenere altri Kin, consulta le Domande frequenti nelle Impostazioni. - Immetti per favore un diverso Codice di Invito e riprova. - Sei pregato di inserire un numero di telefono valido e di riprovare. - Questa Tip Card non è valida - Inserisci un codice valido e riprova. - Spiacenti, si è verificato un errore di rete. Prova a invitare di nuovo il tuo amico. - Questo Kin è stato automaticamente rimandato al mittente perché non è stato riscosso entro le 24 ore. Chiedi di inviare di nuovo il Kin. - Inserisci nuovamente il tuo numero di telefono e riprova. - Cerca di ottenere un\'immagine che mostri il codice più chiaramente. - Controlla la tua connessione Internet o riprova più tardi. - Code è attualmente solo a invito. Ti avviseremo quando ci saranno altri inviti disponibili. - Per unirti alla lista d\'attesa, vai su %1$s - La quantità massima acquistabile è %1$s. Inserisci un importo inferiore. - La quantità minima acquistabile è %1$s. Inserisci un importo superiore. - La tua Chiave di Accesso ha avviato uno sblocco. Di conseguenza, non potrai più usare questa Chiave di Accesso in Code. - Invia un tweet a questa persona per attivare la loro Tip Card. - La mancia massima che puoi lasciare è %1$s. Inserisci un importo inferiore. - La mancia minima che puoi lasciare è %1$s. Inserisci un importo maggiore. - Puoi creare solo un certo numero di nuovi account al giorno. - Attualmente Code è limitato ad un account per dispositivo. Probabilmente il supporto per più account sarà aggiunto in una versione futura di Code. - Attualmente Code è limitato ad un account per numero di telefono. - Kin già riscosso - Kin già acquisiti - Accesso alla fotocamera richiesto - Denaro scaduto - Codice di verifica scaduto - Richiesto l\'accesso ai contatti - Paese non supportato - Dispositivo non supportato - eSim attualmente non supportate - Riscossione fallita - Creazione dell\'account non riuscita - Salvataggio non riuscito - Invio non riuscito - Conferma non riuscita - Transazione non riuscita - Limite di erogazione raggiunto - Limite giornaliero raggiunto - Kin insufficienti - Codice di Invito Non Valido o Scaduto - Numero di telefono non valido - Tip Card non valida - Codice non valido - Invito non riuscito - Link scaduto - Limite di tentativi raggiunto - Nessun codice trovato - Nessuna connessione Internet - Non hai nessun invito - Non sei stato ancora invitato - Acquisto troppo grande - Acquisto troppo piccolo - Chiave di Accesso Non Più Utilizzabile in Code - Tip Card Non Ancora Attivata - Mancia troppo grande - Mancia troppo piccola - Troppi account creati - Account già creato - Account già creato - Crediamo che i pagamenti dovrebbero essere semplici, potenti e globali. Essendo costruito con tecnologia blockchain avanzata, Code offre funzionalità che le app di pagamento tradizionali non possono offrire, come trasferimenti peer to peer globali, micropagamenti che sbloccano singoli articoli online e mance a costo zero per i tuoi creatori preferiti. - Il Kin è una criptovaluta come il Bitcoin, ma progettata per pagamenti ridotti e rapidi. - Come per il Bitcoin, è disponibile solo una quantità limitata di Kin. Se più persone acquistano Kin il valore aumenta, se più persone vendono Kin il valore diminuisce. Questa dinamica consente a tutti coloro che detengono Kin di partecipare alla creazione di valore se l\'adozione di Kin cresce. - Per comprare Kin basta una carta di debito. Per farlo, vai alla scheda Ottieni Kin. - Sì, è possibile. La vendita di Kin è supportata da diversi scambi di criptovalute. - Ci sono tre modi principali in cui ci puoi aiutare: parlare della tua esperienza con Code sui social media, incoraggiare i tuoi amici a provare Code in prima persona e incoraggiare i tuoi siti web preferiti a integrare i pagamenti con Code chiedendo loro di visitare [getcode.com](https://getcode.com). - Cos\'è Code? - Perché i pagamenti su Code sono denominati in Kin? - Perché il valore dei Kin varia? - Come si comprano altri Kin? - È possibile vendere i Kin? - Posso essere d\'aiuto? - accetti i nostri - Toccando \"Crea un account\" o \"Accedi\", - La tua fotocamera viene utilizzata per ricevere Kin. Consenti l\'accesso alla fotocamera per procedere. - Abbiamo bisogno delle notifiche push per inviarti informazioni tempestive sul tuo account. - I prelievi sono irreversibili e non possono essere annullati una volta innescati. - Tutti i fondi che si trovano in questo account andranno persi. L\'eliminazione del tuo account è permanente e non può essere annullata. Sei sicuro di voler eliminare questo account? - Qualsiasi Kin non riscosso entro le 24 ore sarà immediatamente rimandato al tuo saldo. - Dovrai riavviare la creazione dell\'account e verificare nuovamente il tuo numero di telefono. - Potrai accedere nuovamente a questo account usando la tua chiave d\'accesso - Non riceverai notifiche di eventuali nuovi messaggi da %1$s. Puoi riattivare l\'audio in qualsiasi momento. - Al momento sono supportati solo gli account creati tramite Code. - I tuoi amici non potranno più trovarti con questo numero di telefono. - Riceverai una notifica di tutti i nuovi messaggi da %1$s. Puoi disattivare l\'audio in qualsiasi momento. - Non riceverai alcun messaggio da %1$s finché non la pagherai di nuovo. - La tua chiave d\'accesso ti consentirà di accedere al tuo account Code. Tienila privata e al sicuro. - Queste 12 parole sono l\'unico modo per recuperare il tuo account Code. Assicurati di annotarle e di tenerle private e al sicuro. - Sei sicuro? - Sei sicuro di voler eliminare questo account? - Hai inviato il link? - Sei sicuro di voler uscire? - Sei sicuro di volerti disconnettere? - Disattivare l\'audio a %1$s? - Non è un account Code - Sei sicuro? - Riattivare l\'audio a %1$s? - Annullare l\'iscrizione a %1$s? - Vuoi visualizzare la tua chiave d\'accesso? - Sei sicuro? - %1$s Kin sono stata depositati sul tuo conto. - %1$s che hai inviato ieri non sono stati ritirati. Sono stati automaticamente riaccreditati sul tuo saldo. - Fai iniziare un amico su Code e ricevi 5 $ - Invia denaro tramite qualsiasi app di messaggistica - Hai ricevuto %1$s di Kin per aver inviato a qualcuno il suo primo Kin. - Ora puoi richiedere mance. - Deposito ricevuto - Kin restituito - Nuovo su Code - Bonus referral ricevuto - Account X Connesso - Il tuo invito per l\'accesso a Code è scaduto. Puoi disconnetterti e usare un account diverso con uno stato di invito valido. - La tua chiave d\'accesso è l\'unico modo tramite cui puoi accedere ai tuoi fondi. Tienila privata e al sicuro. - Verifica la tua identità per poter visualizzare la tua chiave d\'accesso. - Tocca l\'icona di Google Lens per aprire il codice QR per accedere a Code. In alternativa puoi effettuare il login manualmente inserendo le 12 parole nella schermata di login di Code. - Attenzione! Questa immagine dà accesso a tutti i fondi che hai in Code. Non condividere questa immagine con nessun altro. Mantieni l\'immagine protetta e al sicuro.\n\n\n\n\n\n\n - Code ti consente di ricevere Kin puntando la tua fotocamera sulla banconota digitale sul telefono di un altro utente - Devi consentire l\'accesso alla fotocamera per poter ricevere Kin - Effettua l\'autenticazione per accedere a Code. - Acquista Kin - Compra Kin (in arrivo) - La compravendita di Kin è attualmente un processo complesso. Con il tempo queste procedure diventeranno più semplici. Se vuoi imparare ad acquistare e vendere Kin oggi stesso, puoi guardare i video esplicativi sottostanti. - Puoi dare solo fino a %1$s - Puoi lasciare una mancia solo fino a %1$s - I tuoi Kin sono ora pronti all\'uso nella tua app Code - Hai inviato a qualcuno il loro primo Kin! Ecco il tuo bonus di riferimento: - Stiamo convertendo i tuoi USDC in Kin. L\'operazione dovrebbe richiedere circa un minuto - Il deposito di USDC è riuscito. Apri l\'app Code per completare il tuo acquisto - Bonus di benvenuto - Scegli un paese - Prossimamente - \@getcode Vorrei connettere al mio account X in modo da poter ricevere suggerimenti da persone di tutto il mondo - Cancella - Assicurati di aver salvato la tua frase di recupero segreta e poi inserisci \"Cancella\" per cancellare il tuo account Code. Questa azione è irreversibile. - Non hai ricevuto un codice al %1$s? - Non hai ricevuto il codice? Invia di nuovo - La disattivazione di Face ID richiede la verifica della tua identità. - Non hai l\'applicazione Code Wallet? - Non possiedi ancora nessun Kin. - Abilita Face ID per aumentare ulteriormente la sicurezza delle transazioni in Code. - Inserisci l\'indirizzo di destinazione - Inserisci fino a %1$s - Ottieni 5 $ di Kin gratis quando fai iscrivere un amico a Code e gli invii il suo primo Kin. - Code usa la criptovaluta Kin per effettuare pagamenti. Questi sono alcuni modi per immettere più Kin nel tuo Code wallet. - Ottieni il tuo primo 1 $ di Kin gratis - Verifica la tua identità per poter donare Kin. - Deposita Kin nel tuo portafoglio Code inviando Kin al tuo indirizzo di deposito qui sotto. Tocca per copiare. - Ottieni più Kin e poi prova a pagare di nuovo - Conto di destinazione non valido - Assicurati che l\'indirizzo da cui stai effettuando il ritiro sia stato inizializzato dal fornitore del tuo portafoglio. Una scorciatoia per raggiungere questo obiettivo è scambiare prima una piccola quantità di SOL con Kin nel portafoglio a cui stai tentando di inviare. - Codice di Invito - Code è attualmente solo su invito. Avrai bisogno di un Codice di Invito per accedere all\'app. - %1$d inviti - Code è una nuova app per criptovalute, al momento accessibile solo su invito. Per scaricare Code vai su %1$s - Maggiori informazioni - Il tuo numero di telefono è collegato a questo account Code. Gli amici possono trovarti tramite questo numero di telefono. - Sto collegando il mio account X con @getcode così posso ricevere mance da persone di tutto il mondo. - Caricamento di saldo e cronologia delle transazioni - Controlla nelle tue foto per trovare la chiave d\'accesso che hai salvato quando hai creato per la prima volta il tuo account. - In questo momento hai effettuato l\'accesso ad un account. Controlla di aver salvato la chiave d\'accesso prima di procedere. Desideri uscire e accedere con un nuovo account? - Nessun numero di telefono collegato - Non hai un numero di telefono collegato a questo account Code. Collegane uno in modo che i tuoi amici possano trovarti. - Nessuna connessione di rete - Su Code - Apri l\'app Code e punta la tua fotocamera per prendere questi soldi. - Organizzare i tuoi contatti - Questo numero di telefono non è tra i tuoi contatti. Puoi ancora invitarlo su Code. - Inserisci il tuo numero di telefono, incluso il prefisso internazionale. Assicurati di usare lo stesso numero di telefono che ha ricevuto l\'invito. - Realizzato da - Ora puoi richiedere mance - Bonus referral ricevuto - Invia %1$s - Ecco %1$s - Richiedine un altro tra %1$s - Scansiona questo codice QR con la fotocamera del tuo telefono per scaricare l\'applicazione Code Wallet - Scansiona per scaricare la\napp Code Wallet - Cerca valute - Cerca contatti - È stato inviato un SMS al tuo numero telefonico con un codice di verifica. Immetti per favore il codice di verifica qui sopra. - Qualcuno ti ha inviato denaro - Qualcuno ti ha dato una mancia - È necessario aprire la fotocamera per scansionare i codici - La tua Tip Card ti consente di ricevere mance dagli utenti di Code in tutto il mondo. Per accedere alla tua Tip Card collega la tua identità X. - La tua Tip Card ti consente di ricevere mance dagli utenti di Code in tutto il mondo. Per accedere alla tua Tip Card, pubblica su X. - La tua Tip Card ti consente di ricevere suggerimenti dagli utenti Code di tutto il mondo. Per accedere alla Tip Card invia un messaggio a @getcode su X. - Consenti a Code di inviarti notifiche quando ricevi mance da altri utenti di Code. - Digita \"%1$s\" - Abbiamo apportato alcune modifiche per migliorare l\'esperienza. Dovrai aggiornare l\'app per poter continuare a utilizzare Code. - Account proprietario valido - Conto token valido - Il valore dei Kin varia. - è stato restituito a te - Dove desideri prelevare i tuoi Kin? - Verifica la tua identità per poter prelevare Kin. - Hai depositato - Hai dato - Hai %1$d inviti - Code è attualmente solo a invito. Ne hai %1$d rimanenti. - Hai pagato - Hai dato - Hai ricevuto - Hai inviato - Hai speso - Hai dato la mancia a qualcuno - Hai prelevato - I tuoi fondi sono stati prelevati correttamente. - Il tuo account X è stato collegato con successo al tuo account Code. Ora puoi richiederea mance. - Prelievo effettuato correttamente - Account X connesso correttamente - Accesso scaduto - Chiave d\'accesso - Impostazioni app - Apri la fotocamera automaticamente - Saldo - Bandiere beta - Bonus - Acquista e vendi Kin - Pagamenti in denaro - Team Code - Acquista Kin - Pagamenti web - Mance - Opzioni di debug - Deposita Kin - Depositati - Inserisci le parole della chiave d\'accesso - Inserisci Numero di Telefono - Non riuscito - Domande frequenti - Donati - Ottieni Contanti - Fai iniziare un amico su Code - Ottieni Kin - Ottieni più Kin - Dare denaro - Dona Kin - Fondi insufficienti - Invita un amico - Offerta a tempo limitato - Collegato - Valuta locale - Il mio account - Non collegato - Altre valute - Pagamenti - In attesa - Numero di telefono - Informativa sulla privacy - Acquisti - Account X connesso con successo - Ricevi mance - Ricevuti - Valute recenti - Invita un amico, ricevi 5 $ - Bonus referral - Richiedi Contanti - Richiedi Kin - Richiedi una mancia - Richiedi Face ID - Richiedi password - Richiedi Touch ID - Risultati - Rimandato - Seleziona un account - Seleziona un paese - Seleziona una valuta - Inviato - Spesi - Cambia account - Termini di servizio - Carta suggerimento - Mancia Kin - Attiva le notifiche per Code - Sconosciuto - Aggiornamento richiesto - Verifica numero di telefono - Bonus di benvenuto - Preleva Kin - Prelevati - La tua chiave d\'accesso - Tocca il logo per condividere il link di download dell\'app - diff --git a/apps/codeApp/src/main/res/values-ja/strings-localized.xml b/apps/codeApp/src/main/res/values-ja/strings-localized.xml deleted file mode 100644 index 7686e19d8..000000000 --- a/apps/codeApp/src/main/res/values-ja/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - デビットカードで現金を追加する - カメラへのアクセスを許可する - 連絡先へのアクセスを許可する - プッシュ通知を許可する - 残高 - Kinを購入する - さらにKinを購入 - キャンセル - 送信をキャンセル - チャット - このお金を受け取る - 確認する - Xに接続する - 続行 - コピー済み - コピー - アドレスをコピーする - アカウントを作成する - 新規Codeアカウントを作成する - アカウントを削除する - 完了 - 今すぐダウンロード - Face IDを有効にする - Touch IDを有効にする - 終了する - 渡す - Kinを与える - 招待 - 招待 - 順番待ちリストに参加する - 後で - Kinの購入方法を知る - Kinの売却方法を知る - 電話番号をリンクする - ログイン - ログアウト - \@getcode にメッセージを送って連携する - ミュート - 次へ - いいえ,もう一度試してください - 後で - OK - [設定]を開く - 貼り付ける - クリップボードから貼り付ける - 投稿してアカウントを接続 - ウォレットに入れる - 受け取る - 既存のアカウントを復元する - リマインド - 電話番号を削除する - お客様の電話番号を削除する - チップをリクエストする - アクセスキーをマイフォトに保存する - フォトに保存する - 送信 - 確認コードを送信する - 共有 - URLとして共有する - ダウンロードリンクを共有 - この動画をシェアする - チップカードを表示する - カメラを起動 - 購読 - スワイプしてログイン - スワイプして支払う - スワイプしてチップを支払う - 別のCodeアカウントを試す - ツイートする - Codeをアンロック - ミュート解除 - 登録解除 - 更新 - アクセスキーを表示する - Kinを引き出す - 代わりに12ワードを書き留めましたか? - はい - はい,Kinを引き出します - はい,書き留めました - および - Kin(キン) - 相当のKin - アカウントに関連付けられたすべての情報をCodeのサーバーから削除します(電話番号,連絡先,取引履歴) - シークレットリカバリフレーズを使用して,他のクリプトウォレットアプリでアカウントにアクセスできます。Codeでアカウントを使用できなくなります - ブロックチェーンからアカウントを削除する - 削除するとどうなるか - どうなるか - 削除できないこと - このKinはすでに誰かが収集したものです。 - このKinはすでに誰かに受け取られています。 - Codeを使用するには,[設定]でカメラへのアクセスを許可してください。 - このキャッシュの有効期限は切れています。 - 電話番号を再入力して,再試行してください。 - 招待を送信するには,「設定」で連絡先へのアクセスを許可してください。 - 現在,お住まいの国ではCodeをご利用いただけません。 - 現時点ではお使いのデバイスをサポートできません - eSimsには,Codeの将来のバージョンで対応の予定です。 - なにか問題が発生しました。このKinを回収できませんでした。 - そんなことが起こるとは思っていませんでした。不具合が発生しました。このアカウントをもう一度作成してみてください。 - アクセスキーを保存するために,[設定]で写真へのCodeアクセスを許可してください。 - なにか問題が発生しました。電話番号が正しく入力されていることをご確認ください。 - 不具合が発生しました。再試行してください。 - 資金の引き出しに失敗しました。不具合が発生しましたので,もう一度引き出しを試みてください。 - コードは,%1$s 以下の小規模な毎日のトランザクション用に設計されています。 - Codeは,毎日の小さなトランザクション用に設計されています。 明日,1日のギブ上限が引き上げられます。 - より多くのKinを取得する方法については,[設定]のよくある質問(FAQ)を参照してください。 - 別の招待コードを入力して,もう一度お試しください。 - 有効な電話番号を入力して,もう一度お試しください。 - このチップカードは無効です。 - 有効なコードを入力して,再試行してください。 - 申し訳ありませんが,ネットワークの問題が発生しました。もう一度お友達を招待してみてください。 - このKinは24時間以内に収集されなかったため,自動的に送信者に返送されました。もう一度Kinを送ってもらうようリクエストしてください。 - 電話番号を再入力して,再試行してください。 - コードがもっとはっきり見える画像を入手してください。 - インターネット接続を確認するか,後で再試行してください。 - Codeは現在招待制です。利用できる追加の招待がありましたら,お知らせいたします。 - 順番待ちリストに参加するには,%1$sにアクセスします - 購入最大金額は%1$sです。もっと少ない金額を入力してください。 - 購入最小金額は%1$sです。もっと大きい金額を入力してください。 - アクセスキーがロック解除を開始しました。 その結果,このアクセスキーをCodeで使用できなくなります。 - この人のチップカードを有効化するにはこの人にツイートを送信してください。 - チップの最大額は%1$sです。もっと小さな額を入力してください。 - チップの最小額は%1$sです。もっと大きな額を入力してください。 - 1日に作成できる新規アカウントの数は限られています。 - Codeは現在,デバイスごとに1つのアカウントに制限されています。複数のアカウントには,Codeの将来のバージョンで対応の予定です。 - Codeは現在,電話番号ごとに1つのアカウントに制限されています。複数のアカウントには,Codeの将来のバージョンで対応の予定です。 - Kinはすでに収集されています - すでに受取済みのKin - カメラへのアクセスが必要です - キャッシュ有効期限切れ - 確認コードがタイムアウトしました - 連絡先へのアクセスが必要 - サポートされていない国 - サポートされていないデバイス - eSimsは現在サポートされていません - 回収できませんでした - アカウントの作成に失敗しました - 保存に失敗しました - 送信に失敗しました - 確認に失敗しました - 取引に失敗しました - ギブの上限に達しました - 1日の上限に達しました - Kinが不足しています - 招待コードが無効または期限切れです - 無効な電話番号 - 無効なチップ カードです - コードが無効です - 招待に失敗しました - リンクの有効期限が切れています - 最大試行回数に達しました - コードが見つかりません - インターネット接続なし - お客様には招待がありません - お客様はまだ招待されていません - 購入が大きすぎます - 購入が少なすぎます - Codeで使用できなくなったアクセス キー - チップカードが有効化されていません - チップ額が多すぎます - チップ額が少なすぎます - 作成されたアカウントが多すぎます - すでにアカウント作成済み - すでにアカウント作成済み - 私たちは,決済はシンプルかつパワフルで,グローバルであるべきだと考えています。高度なブロックチェーン技術で構築されたCodeは,グローバルなピアツーピア送金,オンラインの個々の品目をアンロックするマイクロペイメント,お気に入りのクリエイターへの手数料ゼロのチップなど,従来の決済アプリにはない機能を提供しています。 - Kinはビットコインのような暗号通貨ですが,高速かつ低額の支払い向けにも設計されています。 - ビットコインと同様に,利用できるKinの量には限りがあります。Kinを購入する人が増えれば価値は上がり,Kinを売る人が増えれば価値は下がります。このダイナミクスにより,Kinの普及が進めば,Kinを保有するすべての人が価値創造を共有できます。 - デビットカードでKinを購入できます。これは,「Get Kin」タブからアクセスできます。 - はい,できます。多くの暗号通貨取引所がKinの販売に対応しています。 - あなたが力になれる主な方法は3つあります:ソーシャルメディアで自分のCodeでの体験について話す。友だちに自分でCodeを試してみるよう勧める。そして,お気に入りのウェブサイトに [getcode.com](https://getcode.com) をチェックするよう依頼して,Codeの決済を統合するよう促す。 - Codeとは? - Codeの決済はなぜKin建てなのでしょうか? - Kinの価値が変動するのはなぜですか? - Kinを買い足すには? - Kinは売却できますか? - どのように力になれますか? - 同意したことになります - 「アカウントを作成する」または「ログイン」をタップすることで,お客様は - お客様のカメラはKinを受信するために使用されます。続行するには,カメラへのアクセスを許可してください。 - お客様のアカウントに関する情報をタイムリーに送信するために,プッシュ通知が必要です。 - 引き出しは取り消すことができず,一度開始すると元に戻すことはできません。 - このアカウントにあるすべての資金が失われます。アカウントの削除は永久的なものであり,元に戻すことはできません。このアカウントを削除してもよろしいですか? - 24時間以内に収集されなかったKinは自動的に残高に返されます。 - アカウントの作成を再開し,電話番号を再度確認する必要があります。 - アクセスキーを使用して,このアカウントに戻ることができます。 - %1$sからの新着メッセージは通知されません。ミュートはいつでも解除できます。 - Codeで作成されたアカウントのみが現在サポートされています。 - お友達は,この電話番号でお客様を見つけることができなくなります - %1$sからの新着メッセージはすべて通知されます。いつでもミュートにすることができます。 - 再度支払うまで%1$sからのメッセージは受信できません。 - アクセスキーにより,Codeアカウントへのアクセスが許可されます。アクセスキーは公開せずに安全に保管してください。 - この12ワードは,Codeアカウントを復元する唯一の方法です。必ず書き留めて,公開せずに安全に保管してください。 - よろしいですか? - このアカウントを削除してもよろしいですか? - リンクを送信しましたか? - 終了してよろしいですか? - ログアウトしてもよろしいですか? - %1$sをミュートしますか? - Codeアカウントではありません - よろしいですか? - %1$sのミュートを解除しますか? - %1$sの購読を解除しますか? - アクセスキーを表示しますか? - よろしいですか? - %1$sKinがあなたのアカウントに入金されました - 昨日あなたが送金した%1$sは受け取りが行われず,自動的にあなたの残高に返金されました。 - 友だちにCodeを始めさせて5ドルもらおう - 何らかのメッセージアプリで現金を送金 - 誰かに最初のKinを送ったことで%1$s分のKinを受け取りました。 - チップをリクエストできるようになりました。 - 受取済みの入金 - 返金されたKin - Code最新情報 - 受取済みの紹介ボーナス - Xアカウントが接続されました - Codeにアクセスするための招待の有効期限が切れています。ログアウトして,有効な招待ステータスを持つ別のアカウントを使用できます。 - アクセスキーは,お客様の資金にアクセスする唯一の方法です。公表せずに安全に保管してください。 - お客様のアクセスキーを表示するために本人確認を行ってください。 - GoogleレンズのアイコンをタップしてQR コードを開き,Code にログインしてください。または,Codeのログイン画面に12文字を入力することで手動ログインすることもできます。 - 警告!この画像は,あなたがCodeに保有しているすべての資金へのアクセスを提供するものです。この画像を他人と共有しないでください。安全に保管してください。 - Codeを使用すると,他のユーザーの電話のデジタル請求書にカメラを向けることで,Kinを受信することができます - Kinを受信できるようにするには,カメラへのアクセスを許可する必要があります - Codeにアクセスするには認証を行ってください。 - Kinを購入 - Kinを購入する(近日実装予定) - 今はKinの売買プロセスは複雑ですが,時がたつにつれ,プロセスは簡単になります。今すぐKinの売買方法を知りたい場合は,以下の説明動画をご覧ください。 - %1$s まであきらめることができます - チップを支払えるのは%1$sまでです - KinがCodeアプリで使用できるようになりました - あなたは誰かに最初のKinを送りました!紹介ボーナスは次のとおりです: - USDCはKinに変換されます。完了するまでおよそ1分かかります。 - USDCが正常に入金されました。Codeアプリを開くと購入が完了します - ウェルカムボーナス - 国を選択する - もうすぐ登場 - \@getcode Xアカウントと連携して,世界中の人たちからヒントをもらえるようにしたい - 削除 - シークレットリカバリフレーズが保存されていることを確認し,「削除」と入力してCodeアカウントを削除してください。このアクションは元に戻せません。 - %1$sでコードを受け取りませんでしたか? - コードを受け取りませんでしたか? 再送信する - Face IDを無効にするには,本人確認が必要です。 - Codeウォレットアプリをお持ちではありませんか? - お客様はまだKinを持っていません。 - Face IDを有効にすることで,Codeでの取引のセキュリティをさらに高めることができます。 - 送信先アドレスを入力してください - 最大%1$sまで入力してください - 友だちがCodeにサインアップし,その友だちに最初のKinを送ると,5ドル分のKinが無料でもらえます。 - コードは支払いに暗号通貨のKinを使用します。コードウォレットにさらに多くのKinを追加する方法をいくつか紹介します。 - 最初のKin 1ドル分を無料で獲得しよう - Kinを与えるために本人確認を行ってください。 - 以下の入金アドレスにKinを送信することによって,KinをCodeウォレットに入金します。タップしてコピーしてください。 - Kinをもっと入手してから再度お支払いをお試しください - 無効な送金先アカウント - 引き出し先のアドレスがウォレットプロバイダーによりイニシャライズされていることをご確認ください。これを行うための近道は,まず送信先のウォレットで少額のSOLをKinに交換することです。 - 招待コード - Codeは現在招待制です。 アプリにアクセスするには招待コードが必要です。 - %1$dの招待 - Codeは新たな仮想通貨ウォレットアプリで,現在は招待限定となっています。Codeをダウンロードするには%1$sにアクセスしてください - さらに詳しく - お客様の電話番号は,このCodeアカウントとリンクしています。お友達はこの電話番号を使ってお客様を見つけることができます。 - Xアカウントを@getcodeに接続して,世界中の人々からチップを受け取れるようにしています。 - 残高および取引履歴を読み込んでいます - 最初にアカウントを作成したときに保存したアクセスキーが存在するかどうか写真を確認してください。 - 現在アカウントにログインしています。続行する前に,アクセスキーを保存していることを確認してください。ログアウトして新しいアカウントでログインしますか? - リンクされた\n電話番号がありません - このCodeアカウントにリンクされた電話番号がありません。お友達がお客様を見つけることができるように電話番号をリンクしてください。 - ネットワーク接続がありません - Code上で - Codeアプリを開き,カメラを向けてこのお金を受け取ってください - 連絡先の整理 - この電話番号は連絡先にありません。彼らをCodeに招待することはできます。 - 国番号を含めた電話番号を入力します。招待を受け取ったのと同じ電話番号を使用していることを確認してください。 - 提供: - チップをリクエストできるようになりました - 受取済みの紹介ボーナス - %1$sを送信 - こちらが%1$sです - %1$s以内に新しいものをリクエストする - 電話機のカメラでこのQRコードをスキャンし,Codeウォレットアプリをダウンロードしてください - スキャンしてCode Wallet\nアプリをダウンロード - 通貨を検索する - 連絡先を検索する - 確認コードを含むSMSメッセージが電話番号に送信されました。 上記の確認コードを入力してください。 - 誰かがあなたに送金しました - 誰かがあなたにチップを支払いました - カメラを起動してコードをスキャンする必要があります - チップカードを使用すると,世界中のCodeユーザーからチップを受け取ることができます。チップカードにアクセスするには,XのIDを接続してください。 - チップカードを使用すると世界中のCodeユーザーからチップを受け取れます。チップカードにアクセスするにはXに投稿してください。 - Tip Cardを使用すると,世界中のCodeユーザーからヒントを受け取ることができます。Tip Cardにアクセスするには,Xで@getcodeにメッセージを送ってください。 - 他のCodeユーザーからTipをもらった時にCodeからの通知を受け取れるようにしてください。 - \"%1$s\"と入力 - エクスペリエンスを向上させるために,いくつかの変更を行いました。Codeを使い続けるには,アプリを更新する必要があります。 - 有効なオーナーアカウント - 有効なトークンアカウント - Kinの値が変化します。 - あなたに返された - Kinの引き出し先を教えてください。 - Kinを引き出すために本人確認を行ってください。 - 入金した - 与えた - お客様には%1$dの招待があります - Codeは現在招待制です。残りは%1$dです。 - 支払った - 与えた - 受け取りました - 送信した - 支出した - あなたが誰かにチップを支払いました - 引き落とした - お客様の資金は正常に引き出されました。 - XアカウントがCodeアカウントに正常に接続されました。チップをリクエストできるようになりました。 - 引き出し成功 - Xアカウントが正常に接続されました - アクセスの有効期限が切れました - アクセスキー - アプリ設定 - カメラの自動起動 - 残高 - ベータフラグ - ボーナス - Kinの購入&売却 - 現金支払い - Codeチーム - Kinの購入 - ウェブ支払い - チップ - デバッグオプション - Kinを入金する - 入金済み - アクセスキーワードを入力する - 電話番号を入力 - 失敗しました - よくある質問 - 与えた - 現金を得る - 友だちにCodeを始めさせる - Kinを入手 - Kinをもっと入手する - 現金を渡す - Kinを与える - 資金不足 - 友達を招待する - 期間限定オファー - リンク済み - 現地通貨 - マイアカウント - リンクされていません - その他の通貨 - 支払い済み - 保留中 - 電話番号 - プライバシーポリシー - 購入済み - Xアカウントが正常に接続されました - Tipを受け取る - 受信済み - 最近の通貨 - 友だちを紹介して5ドルもらおう - 紹介ボーナス - 現金を要求する - Kinをリクエスト - チップをリクエストする - Face IDが必要 - パスコードが必要 - Touch IDが必要 - 結果 - 返送済み - アカウントを選択 - 国を選択 - 通貨を選択する - 送信済み - 使用済み - アカウントを切り替える - 利用規約 - チップカード - Kinにチップを支払う - Codeの通知をオンにしてください - 不明 - 更新が必要です - 電話番号を確認する - ウェルカムボーナス - Kinを引き出す - 引き出し済み - お客様のアクセスキー - ロゴをタップしてアプリのダウンロードリンクを共有 - diff --git a/apps/codeApp/src/main/res/values-ko/strings-localized.xml b/apps/codeApp/src/main/res/values-ko/strings-localized.xml deleted file mode 100644 index e88b0be07..000000000 --- a/apps/codeApp/src/main/res/values-ko/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - 직불 카드로 현금 추가 - 카메라 접근 허용 - 연락처 접근 허용 - 푸쉬 알림 허용 - 잔액 - 킨 구매 - 더 많은 Kin 구매 - 취소 - 보내기 취소 - 채팅 - 이 현금 모으기 - 확인 - X에 연결 - 계속 - 복사됨 - 복사 - 주소 복사하기 - 계정 생성하기 - 새로운 Code 계정 생성 - 계정 삭제 - 완료 - 지금 다운로드 - Face ID 활성화 - Touch ID 활성화 - 나가기 - 주기 - Kin 보내기 - 초대 - 초대 - 대기자 명단에 등록하기 - 나중에 - 킨(Kin) 매입 방법 알아보기 - 킨 매도 방법 알아보기 - 전화번호 연결 - 로그인 - 로그아웃 - \@getcode로 메시지를 보내 연결하기 - 음소거 - 다음 - 아니요, 다시 시도하세요. - 지금은 괜찮아요 - 확인 - 설정 열기 - 붙여넣기 - 클립보드에서 붙여넣기 - 계정을 연결하려면 게시하기 - 지갑에 넣기 - 받기 - 기존 계정 복구하기 - 알림 - 전화번호 삭제 - 전화번호를 삭제하세요 - 팁 요청 - 액세스 키를 사진첩에 저장 - 사진첩에 저장 - 보내기 - 인증 코드 보내기 - 공유 - URL로 공유 - 다운로드 링크 공유 - 이 동영상 공유 - 내 팁 카드 표시 - 카메라 시작 - 구독 - 스와이프하여 로그인 - 스와이프하여 결제 - 팁으로 스와이프 - 다른 Code 계정을 시도하세요 - 트윗하기 - Code 잠금 해제 - 음소거 해제 - 구독 취소 - 업데이트 - 액세스 키 보기 - Kin 인출하기 - 대신 12개의 단어를 적으셨나요? - - 네, Kin을 인출합니다 - 네, 적었습니다 - - Kin - 의 Kin - Code 서버에서 모든 계정 관련 정보(휴대전화 번호, 연락처, 거래 이력)가 삭제됩니다. - 복구 문구를 사용해 다른 암호화폐 지갑 앱에서 계정에 접근할 수 있습니다. Code에서는 계정을 사용할 수 없습니다. - 블록체인에서 계정이 삭제되지 않습니다. - 삭제되는 항목 - 계정 삭제 시 - 삭제되지 않는 항목 - 이 Kin은 이미 다른 사람이 수령했습니다. - 이 Kin은 이미 누군가가 수집했습니다. - Code를 사용하려면 설정에서 카메라 접근을 허용하십시오. - 이 현금은 만료되었습니다. - 전화번호를 다시 입력한 후 다시 시도하십시오. - 초대를 보내려면 설정에서 연락처 접근 권한을 허용해 주세요. - 현재 귀하의 국가에서는 Code를 이용할 수 없습니다. - 현재 귀하의 기기를 지원하지 않습니다 - eSim은 추후 Code 버전에서 지원될 예정입니다. - 문제가 발생했습니다. 이 Kin은 획득할 수 없습니다. - 예기치 못한 일이 일어났습니다. 문제가 발생했습니다. 계정 생성을 다시 시도하십시오. - 액세스 키를 저장하려면 설정에서 Code가 사진에 접근하는 것을 허용하십시오. - 문제가 발생했습니다. 휴대전화 번호를 올바르게 입력했는지 확인해 주세요. - 문제가 발생했습니다. 다시 시도해 주세요. - 자금 인출에 실패했습니다. 문제가 발생했습니다. 인출을 다시 시도하십시오. - Code는 %1$s 이하의 소액 거래를 위해 만들어졌습니다. - Code는 소액 거래를 위해 만들어졌습니다. 일일 전송 한도가 내일 증가합니다. - 더 많은 Kin을 얻는 방법은 설정의 자주 묻는 질문에서 알아보십시오. - 다른 초대 코드를 입력하고 다시 시도해 주세요. - 올바른 휴대전화 번호를 입력한 후 다시 시도해 주세요. - 잘못된 팁 카드입니다. - 유효한 코드를 입력한 후 다시 시도하십시오. - 죄송합니다, 네트워크 문제가 발생했습니다. 친구 초대를 다시 시도하십시오. - 이 Kin은 24시간 이내에 수령하지 않았기 때문에 보낸 사람에게 자동으로 반환되었습니다. Kin을 다시 보내달라고 요청하세요. - 전화번호를 다시 입력하고 다시 시도하십시오. - 코드가 더 명확하게 보이는 이미지를 사용해 보세요. - 인터넷 연결 상태를 확인하거나 나중에 다시 시도하십시오. - Code는 현재 초대로만 이용이 가능합니다. 더 많은 초대를 보낼 수 있을 때 알림을 보내드리겠습니다. - 대기자 명단에 등록하려면 %1$s로 이동하세요 - 구매할 수 있는 최대 금액은 %1$s입니다. 더 적은 금액을 입력하세요. - 구매할 수 있는 최소 금액은 %1$s입니다. 더 큰 금액을 입력하세요. - 액세스 키로 잠금 해제를 시작했습니다. 이제 Code에서 이 액세스 키를 더 이상 사용할 수 없습니다. - 해당 팁 카드를 활성화하려면 이 사람에게 트윗을 보내세요. - 팁을 줄 수 있는 최대 금액은 %1$s입니다. 더 작은 금액을 입력하세요. - 팁을 줄 수 있는 최소 금액은 %1$s입니다. 더 큰 금액을 입력하세요. - 하루에 생성할 수 있는 계정 수가 정해져 있습니다. - 현재 Code는 기기당 1개 계정으로 제한됩니다. 복수 계정은 추후 Code 버전에서 지원될 예정입니다. - 현재 Code는 휴대전화 번호당 1개 계정으로 제한됩니다. 복수 계정은 추후 Code 버전에서 지원될 예정입니다. - Kin이 이미 수령됨 - Kin 이미 수집됨 - 카메라 접근 필요 - 현금 만료됨 - 인증 코드 만료됨 - 연락처 권한 필요 - 지원하지 않는 국가 - 지원하지 않는 기기 - 현재 지원하지 않는 eSim - 획득 실패 - 계정 생성 실패 - 저장 실패 - 전송 실패 - 확인 실패 - 거래 실패 - 전송 한도 도달 - 일일 한도 도달 - 부족한 Kin - 초대 코드 유효하지 않음 또는 만료됨 - 올바르지 않은 휴대전화 번호 - 잘못된 팁 카드 - 잘못된 코드 - 초대 실패 - 링크가 만료됨 - 최대 시도 횟수에 도달했습니다. - 코드를 찾을 수 없습니다 - 인터넷에 연결되지 않음 - 초대가 없습니다 - 아직 초대를 받지 못했습니다 - 구매 금액이 너무 큼 - 구매 금액이 너무 작음 - Code에서 더 이상 사용할 수 없는 액세스 키입니다 - 팁 카드가 아직 활성화되지 않음 - 팁이 너무 큼 - 팁이 너무 작음 - 너무 많은 계정이 생성됨 - 이미 생성된 계정 - 이미 생성된 계정 - 결제는 간편하고 강력하며 세계적이어야 합니다. 첨단 블록체인 기술로 개발된 Code는 전 세계 P2P 송금, 온라인에서 개별 게시물을 잠금 해제하는 소액 결제, 좋아하는 크리에이터를 위한 수수료 없는 팁 등, 기존 결제 앱에는 없는 기능을 제공합니다. - Kin은 Bitcoin과 같은 암호화폐이며, 빠르고 저렴한 결제를 위해 설계되었습니다. - Bitcoin과 마찬가지로 Kin의 양은 한정적입니다. 사람들이 Kin을 구매할수록 가치가 상승하고 판매할수록 가치가 하락합니다. 이러한 역학 관계 덕분에 Kin이 채택될수록 Kin을 보유한 모두가 창출되는 가치를 공유할 수 있습니다. - Kin 구매 탭에서 직불 카드로 Kin을 구매할 수 있습니다. - 예, 가능합니다. 여러 암호화폐 거래소에서 Kin을 판매할 수 있습니다. - 도움을 주실 수 있는 방법은 세 가지입니다. 소셜 미디어에서 Code 이용 경험을 공유하고, 친구에게 Code를 사용해 보라고 권유하고, 자주 방문하는 웹사이트에서 [getcode.com](https://getcode.com)을 확인하여 Code 결제를 통합하도록 요청해 주세요. - Code가 무엇인가요? - 왜 Code 결제는 Kin으로 표시되나요? - Kin의 가치는 왜 변하나요? - Kin을 더 구매하려면 어떻게 해야 하나요? - Kin을 판매할 수 있나요? - 어떻게 하면 도움이 될까요? - 에 동의 - \"계정 생성\" 혹은 \"로그인\"을 탭하면 - Kin을 받는 데는 카메라가 사용됩니다. 계속 진행하려면 카메라 접근을 허용하십시오. - 귀하의 계정에 대한 정보를 적시에 보내려면 푸시 알림이 필요합니다. - 인출은 되돌릴 수 없으며 한 번 실행하면 취소할 수 없습니다. - 이 계정의 모든 자금이 손실됩니다. 계정 삭제는 영구적이며 되돌릴 수 없습니다. 정말로 계정을 삭제하시겠습니까? - 24시간 이내에 수령되지 않은 Kin은 내 잔액으로 자동 반환됩니다. - 계정 생성을 다시 시작하고 전화번호를 다시 인증해야합니다. - 액세스 키를 이용하여 다시 이 계정에 들어올 수 있습니다. - %1$s에서 보내는 모든 새 메시지에 대한 알림을 받지 않게 됩니다. 언제든지 음소거를 해제할 수 있습니다. - Code를 통해 생성된 계정만 지원합니다. - 친구들이 이 전화번호를 사용하여 더 이상 귀하를 찾을 수 없게 됩니다. - %1$s에서 보내는 모든 새 메시지에 대한 알림을 받게 됩니다. 언제든지 음소거할 수 있습니다. - 다시 결제하기 전까지는 %1$s에서 보내는 메시지를 받지 않게 됩니다. - 액세스 키는 Code 계정에 접근할 수 있게 합니다. 비공개로 안전하게 보관하십시오. - 이 12개의 단어들은 Code 계정을 복원하는 유일한 방법입니다. 따로 적어서 비공개로 안전하게 보관하십시오. - 확실합니까? - 계정을 삭제하시겠습니까? - 링크를 보내셨습니까? - 나가시겠습니까? - 로그아웃 하시겠습니까? - %1$s을(를) 음소거할까요? - Code 계정이 아님 - 확실합니까? - %1$s의 음소거를 해제할까요? - %1$s 구독을 취소할까요? - 액세스 키 보기? - 확실합니까? - Kin %1$s 이(가) 귀하의 계좌에 입금되었습니다. - 어제 보낸 %1$s이(가) 수집되지 않았습니다. 자동으로 잔액에 반환되었습니다. - 친구가 Code를 시작하게 만들고 $5 받으세요 - 메신저 앱을 통해 현금을 보내세요 - 누군가에게 첫 Kin을 보내주고 Kin %1$s을(를) 받았습니다. - 이제 팁을 요청할 수 있습니다. - 입금한 돈 수령 - Kin 반환됨 - Code에서 새로운 것 - 추천 보너스 받음 - X 계정이 연결됨 - Code를 이용할 수 있는 초대가 만료되었습니다. 로그아웃한 후 유효한 초대 상태가 있는 다른 계정을 이용할 수 있습니다. - 액세스 키는 자금에 접근할 수 있는 유일한 방법입니다. 비공개로 안전하게 보관하십시오. - 액세스 키를 열람하려면 신원을 확인하십시오. - Google 렌즈 아이콘을 탭하여 QR 코드를 열어 Code에 로그인하세요. 아니면 Code 로그인 화면에서 12개의 단어를 입력하여 수동으로 로그인할 수 있습니다. - 경고! 이 이미지를 통해 Code에 보유한 모든 자금에 접근할 수 있습니다. 이 이미지를 다른 사람에게 공유하지 마세요. 안전하게 보관하세요. - Code를 사용하면 다른 사용자의 휴대전화에 있는 디지털 청구서에 카메라를 대고 Kin을 받을 수 있습니다. - Kin을 받으려면 카메라 접근을 허용해야 합니다. - Code에 접근하려면 인증하세요. - Kin 구입하기 - Kin 구매(곧 제공) - 현재 킨의 매입 및 매도 절차가 복잡합니다. 시간이 지남에 따라 이러한 복잡한 절차도 수월해질 것입니다. 오늘 킨의 매입 및 매도 방법을 배우고 싶으시다면 아래 동영상을 시청해 주세요. - 최대 %1$s까지만 보낼 수 있습니다 - 최대 %1$s까지만 팁을 줄 수 있습니다. - 이제 Code 앱에서 Kin을 사용할 수 있습니다 - 누군가에게 첫 Kin을 보냈습니다! 추천 보너스는 다음과 같습니다. - USDC를 Kin으로 변환하는 중입니다. 완료하는 데 약 1분 정도 소요됩니다. - USDC를 성공적으로 입금했습니다. Code 앱을 열어 구매를 완료합니다. - 웰컴 보너스 - 국가 선택 - 곧 출시 - \@getcode 전 세계 사람들에게서 팁을 받을 수 있도록 X 계정을 연결하고 싶습니다. - 삭제 - Code 계정을 삭제하려면 복구 문구를 저장한 다음 \"삭제\"를 입력하세요. 삭제하면 되돌릴 수 없습니다. - %1$s에서 코드를 받지 못했나요? - 코드를 받지 못했나요? 재전송하기 - Face ID를 비활성화하면 본인 확인이 필요합니다. - 코드 지갑 앱이 없으신가요? - 아직 Kin이 없습니다. - Code 내 거래의 보안을 더욱 강화시키려면 Face ID를 활성화하십시오. - 목적지 주소 입력 - 최대 %1$s까지 입력 - 친구를 Code에 가입하게 만들고 친구에게 첫 번째 Kin을 보낼 때 Kin $5를 무료로 받으세요. - Code는 결제에 암호 화폐 Kin을 사용합니다. Code 월렛에 Kin을 더 많이 넣을 수 있는 몇 가지 방법이 있습니다. - 무료로 첫 번째 Kin $1 받기 - Kin을 보내려면 신원을 확인하십시오. - 아래의 입금 주소로 Kin을 전송하여 Code 지갑으로 입금하세요. 복사하려면 탭하세요. - 킨을 더 확보한 후에 다시 결제해 보세요 - 잘못된 대상 계좌 - 인출하려는 대상 주소가 해당 월렛 제공자에 의해 초기화되었는지 확인하세요. 이를 달성하는 지름길은 먼저 보내려는 대상 월렛에서 소액의 SOL을 Kin으로 바꾸는 것입니다. - 초대 코드 - Code는 현재 초대제로 운영되고 있습니다. 앱에 액세스하려면 초대 코드가 필요합니다. - 초대 %1$d - 코드는 현재 초대 전용의 새로운 암호화폐 월렛 앱입니다. 코드를 다운로드하려면 %1$s(으)로 이동하세요. - 더 알아보기 - 귀하의 전화번호가 이 Code 게정과 연결되어 있습니다. 친구들이 이 전화번호를 이용하여 귀하를 찾을 수 있습니다. - 전 세계 사람들로부터 팁을 받을 수 있도록 내 X 계정을 @getcode와 연결합니다. - 잔액 및 거래 기록 로드 중 - 처음 계정을 생성했을 때 사진첩에 저장한 액세스 키를 확인하세요. - 현재 계정에 로그인되어 있습니다. 계속하기 전에 액세스 키를 저장했는지 확인하세요. 로그아웃하고 새 계정으로 로그인하시겠습니까? - 연결되지 않음\n전화번호 - 해당 Code 계정이 연결된 전화번호가 없습니다. 친구들이 검색할 수 있도록 전화번호를 연결하세요. - 네트워크 연결 안 됨 - Code에서 - 이 현금을 받으려면 코드 앱을 열고 카메라를 가리키세요 - 연락처 관리 - 이 휴대전화 번호는 연락처에 없지만, Code로 초대할 수 있습니다. - 국가 코드를 포함한 전화번호를 입력하세요. 초대를 받은 번호와 같은 번호를 사용하십시오. - 제공자: - 이제 팁을 요청할 수 있습니다. - 추천 보너스 받음 - %1$s 보내기 - %1$s입니다. - %1$s 내로 새로운 코드 요청 - 휴대 전화 카메라로 이 QR 코드를 스캔하여 코드 지갑 앱을 다운로드하세요. - 스캔하여 Code Wallet 앱을\n다운로드하세요 - 통화 검색 - 연락처 검색 - 전화번호로 인증 코드가 포함된 SMS 메시지를 보내드렸습니다. 위에 인증 코드를 입력해 주세요. - 누군가 현금을 보냈습니다 - 누군가 귀하에게 팁을 주었습니다. - Code를 스캔하려면 카메라를 시작해야 합니다 - 팁 카드를 사용하면 전 세계 Code 사용자로부터 팁을 받을 수 있습니다. 팁 카드에 액세스하려면 X-Identity를 연결하세요. - 팁 카드를 사용하면 전 세계 Code 사용자로부터 팁을 받을 수 있습니다. 팁 카드에 액세스하려면 X에 게시하세요. - Tip Card를 사용하면 전 세계 Code 사용자로부터 팁을 받을 수 있습니다. Tip Card에 액세스하려면 X에서 @getcode 계정으로 메시지를 보내주세요. - 다른 Code 사용자로부터 팁을 받을 때 Code가 알림을 보낼 수 있도록 허용합니다. - 유형 \"%1$s\" - 경험 개선을 위해 변경사항을 적용했습니다. Code를 계속 사용하려면 앱을 업데이트 하세요. - 유효한 소유자 계좌 - 유효한 토큰 계좌 - Kin의 변경값. - 반환됨 - Kin을 어디로 인출하시겠습니까? - Kin을 인출하려면 신원을 확인하십시오. - 입금함 - 제공함 - %1$d의 초대가 있습니다 - Code는 현재 초대로만 가능합니다. %1$d이 남아있습니다. - 지불함 - 제공함 - 받음 - 보냄 - 사용함 - 귀하가 누군가에게 팁을 주었습니다. - 출금함 - 자금이 성공적으로 인출되었습니다. - X 계정이 코드 계정에 성공적으로 연결되었습니다. 이제 팁을 요청할 수 있습니다. - 인출 성공 - X 계정이 성공적으로 연결됨 - 접근 만료됨 - 액세스 키 - 앱 설정 - 카메라 자동 시작 - 잔액 - 베타 플래그 - 보너스 - 킨 매입 및 매도 - 현금 결제 - Code 팀 - 킨 구매 - 웹 결제 - - 디버그 옵션 - Kin 입금하기 - 입금 내역 - 액세스 키 단어를 입력하세요 - 전화번호 입력 - 실패함 - 자주 묻는 질문 - 보낸 내역 - 현금 받기 - 친구가 Code를 시작하게 만들기 - Kin 받기 - 킨 더 확보하기 - 현금 주기 - Kin 보내기 - 자금 부족 - 친구 초대하기 - 기간 제한 특별 혜택 - 연결됨 - 현지 통화 - 내 계정 - 연결되지 않음 - 다른 통화 - 결제 완료 - 대기 중 - 전화번호 - 개인정보 보호정책 - 구매 완료 - X 계정이 성공적으로 연결됨 - 팁 받기 - 받은 내역 - 최근 통화 - 친구 추천하고 $5 받으세요 - 추천 보너스 - 현금 요청 - 킨 요청 - 팁 요청 - Face ID 필요 - 암호 필요 - Touch ID 필요 - 결과 - 반환됨 - 계정 선택 - 국가 선택 - 통화 선택 - 보내기 완료 - 지출 - 계정 바꾸기 - 서비스 약관 - 팁 카드 - 팁 킨 - Code에 대한 알림 켜기 - 알 수 없음 - 업데이트 요구됨 - 전화번호 인증 - 웰컴 보너스 - Kin 인출 - 인출 내역 - 나의 액세스 키 - 앱 다운로드 링크를 공유하려면 로고를 누르세요 - diff --git a/apps/codeApp/src/main/res/values-ms/strings-localized.xml b/apps/codeApp/src/main/res/values-ms/strings-localized.xml deleted file mode 100644 index 4bbeb9b0e..000000000 --- a/apps/codeApp/src/main/res/values-ms/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Tambah Wang Tunai dengan Kad Debit - Benarkan Akses Kamera - Benarkan Akses kepada Kenalan - Benarkan Notifikasi Tolak - Baki - Beli Kin - Beli Lagi Kin - Batal - Batal Hantar - Sembang - Kumpul Wang Tunai Ini - Sahkan - Berhubung kepada X - Teruskan - Disalin - Salin - Salin Alamat - Cipta Akaun - Cipta Akaun Code Baharu - Hapuskan Akaun - Selesai - Muat Turun Sekarang - Mengaktifkan ID Wajah - Mengaktifkan ID Sentuhan - Keluar - Beri - Beri Kin - Ajak - Ajakan - Sertai Senarai Tunggu - Sebentar lagi - Belajar Cara Membeli Kin - Belajar Cara Menjual Kin - Pautkan Nombor Telefon - Log Masuk - Log Keluar - Hantar teks kepada @getcode untuk Berhubung - Senyap - Seterusnya - Tidak. Cuba Lagi - Bukan Sekarang - OK - Buka Tetapan - Tampal - Tampal Dari Papan Keratan - Siar untuk Menghubungkan Akaun - Masukkan dalam Dompet - Terima - Pulihkan Akaun Sedia Ada - Ingatkan - Singkirkan Nombor Telefon - Singkirkan Nombor Telefon Anda - Minta Tip - Simpan Kunci Akses pada Gambar Saya - Simpan pada Gambar - Hantar - Hantar Kod Pengesahan - Kongsi - Kongsikan sebagai URL - Kongsi Pautan Muat Turun - Kongsi Video Ini - Tunjuk Kad Tip Saya - Mulakan Kamera - Langgan - Leret untuk Log Masuk - Leret untuk Membayar - Leret untuk beri Tip - Cuba Akaun Code Lain - Tweetkan - Buka Kunci Code - Nyahsenyap - Buang langganan - Kemas kini - Lihat Kunci Akses - Keluarkan Kin - Catat 12 Perkataan Sebagai Ganti? - Ya - Ya, Keluarkan Kin - Ya, Saya Mencatatnya - dan - Kin - dari Kin - Padamkan semua maklumat yang dikaitkan dengan akaun anda daripada pelayan Code (nombor telefon, hubungan, sejarah transaksi) - Anda boleh menggunakan Frasa Pemulihan Rahsia anda untuk mengakses akaun anda dalam aplikasi dompet kripto lain. Anda tidak akan dapat menggunakan akaun anda dalam Code - Padamkan akaun anda daripada blok rantai - Pemadaman akan menyebabkan - Perkara yang akan berlaku - Pemadaman tak akan menyebabkan - Kin ini sudah dikumpulkan oleh orang lain. - Kin ini sudah dikumpul oleh seseorang. - Sila benarkan akses kepada kamera dalam Tetapan untuk menggunakan Code. - Wang tunai ini telah tamat tempoh. - Sila masukkan semula nombor telefon anda dan cuba lagi. - Sila membenarkan akses hubungan dalam Tetapan untuk menghantar jemputan. - Code tidak tersedia di negara anda buat masa ini. - Kami tidak dapat menyokong peranti anda pada masa ini - Sokongan untuk eSims mungkin akan datang dalam versi Code akan datang. - Ralat telah berlaku. Kin ini tidak dapat dikumpulkan. - Kami tidak menjangkakan perkara itu berlaku. Ada sesuatu tidak kena. Sila cuba mencipta akaun ini sekali lagi. - Sila benarkan akses Code kepada Gambar dalam Tetapan untuk menyimpan Kunci Akses anda. - Ada sesuatu yang tidak kena berlaku. Sila pastikan nombor telefon anda dimasukkan dengan betul. - Ada sesuatu tidak kena. Sila cuba lagi. - Gagal mengeluarkan dana anda. Ada sesuatu tidak kena, sila cuba keluarkan dana anda sekali lagi. - Code direka bentuk untuk transaksi harian kecil sebanyak %1$s atau kurang. - Code direka bentuk untuk transaksi harian kecil. Had pemberian harian anda akan meningkat esok. - Untuk mengetahui cara mendapatkan lebih banyak Kin pergi ke FAQ dalam Tetapan. - Sila masukkan Kod Jemputan yang lain dan cuba lagi. - Sila masukkan nombor telefon yang sah dan cuba lagi. - Ini adalah Kad Tip yang tidak sah. - Sila masukkan kod yang sah dan cuba lagi. - Maaf, kami mengalami masalah rangkaian. Sila cuba mengajak rakan anda sekali lagi. - Kin ini telah dikembalikan secara automatik kepada pengirim kerana ia tidak dikumpulkan dalam masa 24 jam. Sila minta mereka menghantar Kin sekali lagi. - Sila masukkan semula nombor telefon anda dan cuba lagi. - Sila cuba untuk mendapatkan imej yang menunjukkan kod itu lebih jelas. - Sila periksa sambungan internet anda atau cuba lagi kemudian. - Pada masa ini Code hanya atas ajakan sahaja. Kami akan memberitahu anda apabila lebih banyak ajakan boleh didapati. - Untuk menyertai senarai tunggu pergi ke %1$s - Amaun maksimum yang boleh anda beli ialah %1$s. Sila masukkan amaun yang lebih kecil. - Amaun minimum yang boleh anda beli ialah %1$s. Sila masukkan amaun yang lebih besar. - Kunci akses anda telah memulakan menyahkunci. Oleh itu, anda tidak akan dapat menggunakan Kunci Akses ini dalam Kod lagi. - Hantar tweet kepada orang ini untuk mengaktifkan Kad Tip mereka. - Maksimum anda boleh memberi tip adalah %1$s. Sila masukkan jumlah yang lebih kecil. - Minimum yang anda boleh tip adalah %1$s. Sila masukkan jumlah yang lebih besar. - Anda tidak boleh mencipta akaun baharu setiap hari. - Code kini terhad kepada satu akaun bagi setiap peranti. Sokongan untuk berbilang akaun mungkin akan datang dalam versi Code akan datang. - Code kini terhad kepada satu akaun bagi setiap nombor telefon. Sokongan untuk berbilang akaun mungkin akan datang dalam versi Code akan datang. - Kin Sudah Diambil - Kin Sudah Dikumpul - Akses Kamera Diperlukan - Wang Tunai Tamat Tempoh - Pengesahan Code Dibatalkan - Akses Hubungan Diperlukan - Negara Tidak Disokong - Peranti Tidak Disokong - eSims Tidak Disokong Buat Masa Ini - Gagal Mengumpul - Gagal Mencipta Akaun - Gagal Disimpan - Gagal Dihantar - Gagal Disahkan - Urus Niaga Gagal - Had Pemberian Dicapai - Had Harian Dicapai - Kin tidak mencukupi - Kod Jemputan Tidak Sah atau Tamat Tempoh - Nombor Telefon Tidak Sah - Kad Tip tidak sah. - Kod Tidak Sah - Ajakan Gagal - Pautan Tamat Tempoh - Percubaan Maksimum Dicapai - Tiada Kod Ditemui - Tiada Sambungan Internet - Anda Tiada Ajakan - Anda Belum Lagi Diajak - Amaun pembelian terlalu besar - Amaun pembelian terlalu kecil - Kunci Akses Tidak Boleh Digunakan dalam Kod lagi - Kad Tip Belum Diaktifkan Lagi - Tip Terlalu Besar - Tip Terlalu Kecil - Terlalu banyak akaun dicipta - Akaun Sudah Dicipta - Akaun Sudah Dicipta - Kami percaya bahawa proses pembayaran hendaklah ringkas, berkuasa dan bersifat global. Dengan membina menggunakan teknologi rangkaian blok termaju, Code menawarkan ciri yang tidak boleh dilakukan oleh aplikasi pembayaran tradisional, seperti pemindahan global dari rakan ke rakan, pembayaran mikro yang membuka kunci artikel individu dalam talian dan petua tanpa bayaran untuk pencipta kegemaran anda. - Kin ialah mata wang kripto seperti Bitcoin, tetapi juga direka untuk pembayaran yang cepat dan murah. - Seperti Bitcoin, jumlah Kin yang boleh didapati adalah terhad. Jika lebih ramai orang membeli Kin nilainya akan meningkat, dan jika lebih ramai orang menjual Kin nilainya akan menurun. Dinamik ini membolehkan semua orang yang memegang Kin untuk berkongsi dalam penghasilan nilai jika penerimaan Kin berkembang. - Anda boleh membeli Kin dengan kad debit anda. Ini boleh diakses dalam tab Dapatkan Kin. - Ya, boleh. Penjualan Kin disokong pada beberapa pertukaran mata wang kripto. - Terdapat tiga cara utama anda boleh membantu: ceritakan tentang pengalaman anda menggunakan Code di media sosial, galakkan rakan anda untuk mencuba sendiri Code dan galakkan laman web kegemaran anda untuk menyepadukan pembayaran menggunakan Code dengan meminta mereka melayari [getcode.com](https ://getcode.com). - Apakah Code? - Mengapakah pembayaran Code menggunakan mata wang Kin? - Mengapakah nilai Kin berubah? - Bagaimanakah cara untuk membeli lebih banyak Kin? - Bolehkah saya menjual Kin? - Bagaimana saya boleh membantu? - bersetuju dengan - Dengan mengetik \"Cipta Akaun\" atau \"Log Masuk\" anda - Kamera anda digunakan untuk menerima Kin. Sila benarkan akses kepada kamera untuk teruskan. - Kami memerlukan notifikasi tolak untuk menghantar maklumat tentang akaun anda tepat pada masanya. - Pengeluaran tidak boleh ditarik balik dan tidak boleh dibuat asal setelah dimulakan. - Semua dana dalam akaun ini akan hilang. Menghapuskan akaun anda kekal dan tidak boleh dibuat asal. Anda pasti anda ingin menghapuskan akaun ini? - Mana-mana Kin yang tidak diambil dalam masa 24 jam akan dikembalikan secara automatik ke baki anda. - Anda perlu memulakan semula penciptaan akaun anda dan sahkan nombor telefon anda sekali lagi. - Anda boleh masuk semula ke akaun ini menggunakan Kunci Akses anda - Anda tidak akan dimaklumkan bagi sebarang mesej baru dari %1$s. Anda boleh nyahredam pada bila-bila masa. - Hanya akaun yang dicipta melalui Code sahaja disokong pada masa ini. - Kawan-kawan anda tidak lagi boleh mencari anda dengan nombor telefon ini. - Anda akan dimaklumkan bagi semua mesej baru dari %1$s. Anda boleh redamkan pada bila-bila masa. - Anda tidak akan menerima sebarang mesej dari %1$s sehingga anda membayar mereka semula. - Kunci Akses anda akan memberikan akses kepada akaun Code anda. Pastikan ia peribadi dan selamat. - 12 perkataan ini adalah satu-satunya cara untuk memulihkan akaun Code anda. Pastikan anda mencatatnya, dan pastikan ia peribadi dan selamat. - Anda pasti? - Anda pasti anda ingin menghapuskan akaun ini? - Adakah anda menghantar pautan? - Anda pasti anda ingin keluar? - Anda pasti anda ingin log keluar? - Redam %1$s? - Bukan Akaun Code - Anda pasti? - Nyahredam %1$s? - Berhenti melanggan dari %1$s? - Lihat Kunci Akses Anda? - Anda Pasti? - %1$s Kin telah didepositkan ke dalam akaun anda. - %1$s yang anda hantar semalam tidak dikumpulkan. Ia telah dikembalikan secara automatik ke baki anda. - Dapatkan rakan untuk bermula menggunakan Code dan dapatkan $5 - Hantar wang tunai melalui mana-mana aplikasi pemesej - Anda menerima %1$s Kin kerana mengirimkan kepada seseorang Kin mereka yang pertama. - Anda kini boleh meminta tip. - Deposit Diterima - Kin Dikembalikan - Baharu tentang Code - Bonus Rujukan Diterima - Akaun X Dihubungkan - Ajakan anda untuk mengakses Code telah tamat. Anda boleh log keluar dan menggunakan akaun lain dengan status ajakan yang sah. - Kunci Akses anda adalah satu-satunya cara untuk mengakses dana anda. Sila pastikan ia peribadi dan selamat. - Sahkan identiti anda untuk melihat Kunci Akses anda. - Tekan ikon Google Lens untuk membuka kod QR untuk mendaftar masuk ke dalam Code. Alternatif lain adalah anda boleh mendaftar masuk secara manual dengan memasukkan 12 perkataan dalam skrin Daftar Masuk Code. - Amaran! Imej ini memberi akses kepada semua dana yang anda ada di dalam Code. Jangan kongsi imej ini dengan sesiapa. Pastikan ia dilindungi dan selamat. - Code membolehkan anda menerima Kin dengan menghalakan kamera anda ke bil digital pada telefon pengguna lain - Anda perlu membenarkan akses kamera untuk menerima Kin - Sahkan untuk mengakses Code. - Beli Kin - Beli Kin (Akan Datang) - Membeli dan menjual Kin merupakan proses yang kompleks buat masa ini. Proses ini akan menjadi lebih mudah dari semasa ke semasa. Jika anda ingin belajar cara membeli dan menjual Kin hari ini, anda boleh menonton video di bawah. - Anda hanya boleh memberi sehingga %1$s - Anda hanya boleh memberi tip sehingga %1$s - Kini, Kin anda tersedia untuk digunakan dalam Code App anda - Anda telah menghantar Kin pertama kepada seseorang! Berikut adalah bonus rujukan anda: - USDC anda sedang ditukarkan kepada Kin. Proses ini sepatutnya mengambil masa kira-kira satu minit untuk diselesaikan - Anda berjaya mendepositkan USDC. Buka Code App untuk melengkapkan pembelian anda - Bonus Selamat Datang - Pilih negara - Akan datang - \@getcode saya ingin menghubungkan akaun X saya supaya saya boleh menerima petua dari seluruh dunia - Padam - Pastikan anda telah menyimpan Frasa Pemulihan Rahsia anda, kemudian masukkan \"Padam\" untuk memadamkan akaun Code anda. Tindakan ini tidak dapat dipulihkan. - Tidak menerima kod di %1$s? - Tidak menerima kod? Hantar semula - Melumpuhkan Face ID memerlukan anda mengesahkan identiti anda. - Tidak mempunyai aplikasi Code Wallet? - Anda belum mempunyai sebarang Kin. - Aktifkan Face ID untuk meningkatkan lagi keselamatan urus niaga dalam Code. - Masukkan alamat destinasi - Masukkan sehingga %1$s - Dapatkan $5 Kin secara percuma apabila anda mendapatkan rakan untuk mendaftar Code dan anda menghantar Kin pertama mereka kepada mereka. - Kod menggunakan mata wang kripto Kin untuk pembayaran. Berikut merupakan beberapa cara untuk memasukkan lebih banyak Kin ke dalam dompet Kod anda. - Dapatkan $1 Kin Pertama Anda secara Percuma - Sahkan identiti anda untuk memberikan kin. - Depositkan Kin ke dompet Code anda dengan menghantar Kin ke Alamat Deposit anda di bawah. Ketik untuk salin. - Sila dapatkan lebih banyak Kin dan kemudian cuba bayar sekali lagi - Akaun destinasi tidak sah - Sila pastikan alamat yang anda tarik balik telah diparamkan oleh pembekal dompet anda. Jalan pintas untuk mencapai ini adalah dengan menukar sejumlah kecil SOL untuk Kin dahulu dalam dompet yang anda cuba hantar. - Kod Jemputan - Kod pada masa ini adalah jemputan sahaja. Anda memerlukan Kod Jemputan untuk mengakses aplikasi itu. - %1$d Ajakan - Code ialah apl dompet kripto yang hanya menerima jemputan sahaja buat masa ini. Untuk memuat turun Code, pergi ke %1$s - Ketahui lebih lanjut - Nombor telefon anda dipautkan dengan akaun Code ini. Kawan-kawan boleh mencari anda menggunakan nombor telefon ini. - Saya mnghubungkan akaun X saya dengan @getcode agar saya boleh menerima tip dari orang ramai di seluruh dunia. - Memuatkan baki dan sejarah transaksi anda - Semak gambar anda untuk Kunci Akses yang disimpan apabila anda mula-mula mencipta akaun. - Anda sekarang telah log masuk ke akaun. Sila pastikan anda telah menyimpan Kunci Akses anda sebelum meneruskan. Adakah anda ingin mengelog keluar dan mengelog masuk dengan akaun baharu? - Tiada Nombor \nTelefon Dipautkan - Anda tidak mempunyai nombor telefon yang dipautkan ke akaun Code. Pautkan nombor telefon supaya kawan-kawan anda boleh menemui anda. - Tiada sambungan rangkaian - Pada Code - Buka aplikasi Code dan halakan kamera anda untuk mendapatkan wang tunai ini - Mengatur Kenalan anda - Nombor telefon ini tiada dalam Hubungan anda. Anda masih boleh menjemput mereka menggunakan Code. - Masukkan nombor telefon anda termasuk kod negara. Pastikan anda menggunakan nombor telefon sama yang menerima ajakan. - Dikuasakan oleh - Anda kini boleh meminta tip - Bonus rujukan diterima - Hantar %1$s - Inilah %1$s - Minta kod baharu dalam %1$s - Imbas kod QR ini menggunakan kamera telefon anda untuk memuat turun aplikasi Code Wallet - Imbas untuk memuat turun aplikasi Code Wallet - Cari mata wang - Cari kenalan - Mesej teks dengan kod pengesahan telah dihantar ke nombor telefon anda. Sila masukkan kod pengesahan di atas. - Seseorang telah menghantar wang tunai kepada anda - Seseorang memberi tip kepada anda - Anda perlu memulakan kamera anda untuk mengimbas Codes - Kad Tip anda membolehkan anda menerima tip dari pengguna Code di seluruh dunia. Untuk mengakses Kad Tip anda sila hubungkan identiti X anda. - Kad Tip anda membolehkan anda menerima tip dari pengguna Kod di serata dunia. Untuk mengakses ke Kad Tip anda sila siarkan ke X. - Kad Upah anda membolehkan anda menerima upah daripada pengguna Code di seluruh dunia. Untuk mengakses Kad Upah anda, hantar mesej kepada @getcode di X. - Benarkan Code untuk menghantar pemberitahuan kepada anda apabila anda menerima Petua daripada pengguna Code lain. - Jenis \"%1$s\" - Kami telah melakukan beberapa perubahan untuk meningkatkan pengalaman. Anda perlu mengemas kini aplikasi untuk terus menggunakan Code. - Akaun pemilik sah - Akaun token yang sah - Nilai Kin berubah. - telah dikembalikan kepada anda - Ke mana anda ingin memasukkan Kin anda? - Sahkan identiti anda untuk mengeluarkan kin. - Anda memasukkan - Anda memberi - Anda Mempunyai %1$d Ajakan - Pada masa ini Code hanya atas ajakan sahaja. Anda tinggal %1$d. - Anda membayar - Anda memberi - Anda menerima - Anda menghantar - Anda membelanja - Anda memberi tip kepada seseorang - Anda mengeluarkan - Dana anda telah berjaya dikeluarkan. - Akaun X anda berjaya dihubungkan kepada akaun Code anda. Anda kini boleh meminta Tip. - Pengeluaran Berjaya - Akaun X Berjaya Dihubungkan - Akses Tamat - Kunci Akses - Tetapan Aplikasi - Mulakan Kamera secara Automatik - Baki - Bendera Beta - Bonus - Beli & Jual Kin - Pembayaran Tunai - Pasukan Kod - Pembelian Kin - Pembayaran Web - Tip - Pilihan Nyahpepijat - Depositkan Kin - Didepositkan - Masukkan Perkataan Kunci Akses - Masukkan Nombor Telefon - Gagal - FAQ - Beri - Dapatkan Wang Tunai - Dapatkan Rakan untuk Bermula menggunakan Code - Dapatkan Kin - Dapatkan Lebih Banyak Kin - Beri Wang - Beri Kin - Dana Tidak Mencukupi - Ajak Kawan - Tawaran Masa Terhad - Dipautkan - Mata Wang Tempatan - Akaun Saya - Tidak Dipautkan - Mata Wang Lain - Dibayar - Belum selesai - Nombor Telefon - Dasar Privasi - Dibeli - Akaun X Berjaya Dihubungkan - Terima Petua - Diterima - Mata Wang Baru - Rujuk Rakan, Dapat $5 - Bonus Rujukan - Minta Wang Tunai - Minta Kin - Minta Tip - Perlukan ID Muka - Perlukan Kod Laluan - Perlukan ID Sentuhan - Hasil - Dikembalikan - Pilih Akaun - Pilih Negara - Pilih Mata Wang - Dihantar - Perbelanjaan - Tukar Akaun - Syarat Perkhidmatan - Kad Petua - Tip Kin - Hidupkan Pemberitahuan untuk Code - Tidak diketahui - Kemas Kini Diperlukan - Sahkan Nombor Telefon - Bonus Alu-aluan - Keluarkan Kin - Keluarkan - Kunci Akses Anda - Ketik logo untuk kongsikan pautan muat turun aplikasi - diff --git a/apps/codeApp/src/main/res/values-nl/strings-localized.xml b/apps/codeApp/src/main/res/values-nl/strings-localized.xml deleted file mode 100644 index cc1544ca3..000000000 --- a/apps/codeApp/src/main/res/values-nl/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Voeg contant geld toe met een bankpas - Toegang tot camera toestaan - Toegang tot contacten toestaan - Pushmeldingen toestaan - Saldo - Kin kopen - Koop meer Kin - Annuleren - Verzenden Annuleren - Chat - Claim dit geld - Bevestigen - Verbinden met X - Doorgaan - Gekopieerd - Kopiëren - Adres kopiëren - Een account aanmaken - Een nieuw Code-account aanmaken - Account verwijderen - Klaar - Download hem nu - Gezichts-ID inschakelen - Touch-ID inschakelen - Afsluiten - Geven - KIN schenken - Uitnodigen - Uitnodigingen - Inschrijven op wachtlijst - Later - Leer hoe Kin te kopen - Leer hoe Kin te verkopen - Een telefoonnummer koppelen - Inloggen - Uitloggen - Stuur een bericht aan @getcode om te verbinden - Dempen - Volgende - Nee, probeer het opnieuw - Nu niet - OK - Instellingen openen - Plakken - Plakken van klembord - Post om het account te verbinden - In wallet plaatsen - Ontvangen - Bestaand account herstellen - Herinneren - Telefoonnummer verwijderen - Je telefoonnummer verwijderen - Een fooi aanvragen - Toegangscode opslaan in Mijn Foto\'s - Opslaan in Foto\'s - Verzenden - Verificatiecode sturen - Delen - Delen als URL - Downloadlink delen - Deel deze video - Toon mijn fooikaart - Start camera - Zich abonneren - Swipe om in te loggen - Veeg om te betalen - Veeg naar fooi geven - Probeer een ander Code-account - Tweet ze - Ontgrendel code - Dempen opheffen - Uitschrijven - Updaten - Toegangscode tonen - KIN opnemen - In plaats daarvan de 12 woorden opgeschreven? - Ja - Ja, KIN opnemen - Ja, ik heb ze opgeschreven - en - KIN - van KIN - Alle aan je account gekoppele informatie verwijderen van de servers van Code (telefoonnummer, contacten, transactiegeschiedenis) - Je hebt toegang tot je account in andere crypto-portemonnee-apps met behulp van je Geheime Herstelzin. Je kunt je account niet gebruiken in Code. - Je account verwijderen van de blockchain - Wat verwijderen zal doen - Wat er zal gebeuren - Wat verwijderen niet zal doen - Deze Kin is al opgehaald door iemand anders. - Deze Kin is al door iemand opgehaald. - Geef toegang tot de camera in Instellingen om Code te gebruiken. - Het contant geld is verlopen. - Voer je telefoonnummer opnieuw in en probeer het opnieuw. - Geef toegang tot de contacten in Instellingen om uitnodigingen te verzenden. - De code is momenteel niet beschikbaar in je land. - We kunnen je apparaat op dit moment niet ondersteunen - Ondersteuning voor eSims komt waarschijnlijk in een toekomstige versie van Code. - Er ging iets mis. Deze Kin kon niet worden geïnd. - We hadden niet verwacht dat dat zou gebeuren. Er is een fout opgetreden. Probeer dit account opnieuw aan te maken. - Geef Code toegang tot Foto\'s in Instellingen om je toegangscode op te slaan. - Er is iets misgegaan. Controleer of je telefoonnummer correct is ingevoerd. - Er is een fout opgetreden. Probeer het later opnieuw. - Geldopname mislukt. Er is een fout opgetreden, probeer het later opnieuw. - Code is ontworpen voor kleine, dagelijkse transacties van %1$s of minder. - Code is ontworpen voor kleine, dagelijkse transacties. Je dagelijkse donatielimiet wordt morgen verhoogd. - Ga naar de Veelgestelde Vragen in Instellingen om te leren hoe je meer KIN kunt krijgen. - Voer een andere uitnodigingscode in en probeer het opnieuw. - Voer een geldig telefoonnummer in en probeer het opnieuw. - Dit is een ongeldige fooikaart. - Voer een geldige code in en probeer het opnieuw. - Het spijt ons, er is een netwerkprobleem opgetreden. Probeer je vriend opnieuw uit te nodigen. - Deze Kin is automatisch teruggestuurd naar de afzender omdat deze niet binnen 24 uur is opgehaald. Vraag ze alsjeblieft om de Kin opnieuw te sturen. - Voer je telefoonnummer opnieuw in en probeer het opnieuw. - Probeer een afbeelding te vinden waarop de code duidelijker te zien is. - Controleer je internetverbinding of probeer het later opnieuw. - Code is momenteel alleen op uitnodiging. We laten het je weten als er meer uitnodigingen beschikbaar zijn. - Ga naar %1$s om je in te schrijven op de wachtlijst - Het maximale bedrag dat je kunt kopen is %1$s. Voer een kleiner bedrag in. - Het minimale bedrag dat je kunt kopen is %1$s. Voer een groter bedrag in. - Je toegangssleutel heeft een ontgrendeling geactiveerd. Hierdoor kun je deze Toegangscode in code niet meer gebruiken. - Stuur een tweet naar deze persoon om zijn/haar fooikaart te activeren. - De maximale fooi is %1$s. Voer een kleiner bedrag in. - De minimale fooi is %1$s. Voer een groter bedrag in. - Je kunt maar een beperkt aantal nieuwe accounts per dag aanmaken. - Code is momenteel beperkt tot één account per apparaat. Ondersteuning voor meerdere accounts komt waarschijnlijk in een toekomstige versie van Code. - Code is momenteel beperkt tot één account per telefoonnummer. Ondersteuning voor meerdere accounts komt waarschijnlijk in een toekomstige versie van Code. - Kin is al verzameld - Kin al opgehaald - Toegang tot camera vereist - Contant geld verlopen - Verificatiecode verlopen - Toegang tot Contacten vereist - Land niet ondersteund - Apparaat niet ondersteund - eSims momenteel niet ondersteund - Kon niet verbinden. - Account aanmaken mislukt - Opslaan mislukt - Sturen mislukt - Bevestigen mislukt - Transactie mislukt - Donatielimiet bereikt - Dagelijkse limiet bereikt - Onvoldoende KIN - Uitnodigingscode ongeldig of verlopen - Ongeldig telefoonnummer - Ongeldige fooikaart - Ongeldige code - Uitnodiging mislukt - Link Verlopen - Maximum aantal pogingen bereikt - Geen code gevonden - Geen internetverbinding - Je hebt geen uitnodigingen - Je hebt nog geen uitnodiging ontvangen - Aankoop te groot - Aankoop te klein - Toegangssleutel niet langer bruikbaar in code - De fooikaart is nog niet geactiveerd - De fooi is te groot - De fooi is te klein - Te veel accounts aangemaakt - Account reeds aangemaakt - Account reeds aangemaakt - Wij vinden dat betalingen eenvoudig, krachtig en wereldwijd bruikbaar moeten zijn. Dankzij geavanceerde blockchaintechnologie heeft Code functies die traditionele betaalapps niet kunnen bieden, zoals wereldwijde peer-to-peer-overschrijvingen, microbetalingen voor het online ontgrendelen van losse artikelen en kosteloze tips voor je favoriete makers. - Kin is een cryptocurrency zoals Bitcoin, maar is ook ontworpen voor snelle, goedkope betalingen. - Net zoals bij Bitcoin is er maar een beperkte hoeveelheid Kin beschikbaar. Als er meer mensen Kin kopen, stijgt de waarde, en als er meer mensen Kin verkopen, dan daalt de waarde. Door deze dynamiek kan iedereen die Kin bezit delen in het creëren van waarde indien het gebruik van Kin groeit. - Je kunt Kin kopen met je bankpas. Dit is toegankelijk via het tabblad Get Kin. - Ja, dat kan. Kin verkopen wordt ondersteund door een aantal cryptocurrency exchanges. - Er zijn drie hoofdmanieren waarop je kunt helpen: praat over Code op social media, moedig je vrienden aan om Code zelf ook te gebruiken, en moedig je lievelingswebsites aan om Codebetalingen te integreren door ze te vragen een kijkje te nemen op [getcode.com](https://getcode.com). - Wat is Code? - Waarom worden Code-betalingen gerekend in Kin? - Waarom verandert de waarde van Kin? - Hoe koop ik meer Kin? - Kan ik Kin verkopen? - Hoe kan ik helpen? - akkoord te gaan met ons - Door te tikken op \'Een account aanmaken\' of \'Inloggen\', bevestig je - Je camera wordt gebruikt om KIN te ontvangen. Geef toegang tot de camera om door te gaan. - We gebruiken pushmeldingen om je tijdig informatie over je account te sturen. - Opnames zijn onomkeerbaar en kunnen niet ongedaan worden gemaakt als ze eenmaal zijn gestart. - Al het geld op dit account gaat verloren. Het verwijderen van je account is definitief en kan niet ongedaan worden gemaakt. Weet je zeker dat je dit account wilt verwijderen? - Elke Kin die niet binnen 24 uur wordt verzameld, wordt automatisch teruggestort op je saldo. - Je dient het aanmaken van een account opnieuw te starten en je telefoonnummer opnieuw te verifiëren. - Je kunt weer toegang krijgen tot dit account met je toegangscode - Je wordt niet geïnformeerd over nieuwe berichten van %1$s. Je kunt het dempen op elk moment opheffen. - Alleen accounts die via Code zijn aangemaakt, worden momenteel ondersteund. - Je vrienden kunnen je niet meer vinden via dit telefoonnummer. - Je wordt geïnformeerd over alle nieuwe berichten van %1$s. Je kunt dempen op elk moment. - Je ontvangt geen berichten van %1$s totdat je ze opnieuw betaalt. - Je toegangscode geeft toegang tot je Code-account. Houd ze privé en sla ze veilig op. - Deze 12 woorden zijn de enige manier om je Code-account te herstellen. Zorg ervoor dat je ze opschrijft, privé houdt en veilig opslaat. - Weet je het zeker? - Weet je zeker dat je dit account wilt verwijderen? - Heb je de link gestuurd? - Weet je zeker dat je wilt afsluiten? - Weet je zeker dat je wilt uitloggen? - Dempen %1$s? - Geen Code-account - Weet je het zeker? - Dempen opheffen %1$s? - Zich uitschrijven voor %1$s? - Je toegangscode tonen? - Weet je het zeker? - %1$s Kin is op je account gestort. - De %1$s die je gisteren hebt verzonden, is niet opgehaald. Het is automatisch teruggestort op je saldo. - Laat een vriend beginnen met Code en ontvang $5 - Stuur geld via een messenger-app van jouw keuze - Je hebt %1$s van Kin ontvangen omdat je iemand zijn of haar eerste Kin hebt gestuurd. - Je kunt nu fooien vragen. - Storting ontvangen - Kin geretourneerd - Nieuw op Code - Verwijzingsbonus ontvangen - X-account verbonden - Je uitnodiging voor toegang tot Code is verlopen. Je kunt uitloggen en een ander account gebruiken met een geldige uitnodigingsstatus. - Je toegangscode is de enige manier om toegang te krijgen tot je geld. Gelieve ze privé te houden en veilig op te slaan. - Je identiteit verifiëren om je toegangscode te zien. - Tik op het Google Lens-pictogram om de QR-code te openen om je aan te melden bij Code. Je kunt ook handmatig inloggen door de twaalf woorden in te voeren in het inlogscherm van Code. - Opgepast! Deze afbeelding geeft toegang tot alle fondsen die je hebt in Code. Deel deze afbeelding met niemand. Houd deze veilig en zeker. - Met Code kun je KIN ontvangen door je camera op de digitale rekening op de telefoon van een andere gebruiker te richten - Je dient toegang tot de camera toe te staan om KIN te kunnen ontvangen - Verifieer voor toegang tot Code. - Koop Kin - Kin kopen (binnenkort beschikbaar) - Het kopen en verkopen van Kin is momenteel een complex proces. Deze processen zullen in de toekomst eenvoudiger worden. Als je wilt leren hoe je op dit moment Kin kunt kopen en verkopen, kun je de onderstaande video\'s bekijken. - Je kunt slechts %1$s geven - Je kunt maximaal %1$s fooi geven - Je Kin is vanaf nu beschikbaar voor gebruik in je Code-app - Je hebt iemand hun eerste Kin gestuurd! Hier is je referentiebonus: - Je USDC wordt geconverteerd naar Kin. Dit duurt ongeveer een minuut - Je hebt succesvol USDC gestort. Open de Code-app om je aankoop af te ronden - Welkomstbonus - Een land kiezen - Binnenkort beschikbaar - \@getcode Ik wil graag mijn X-account verbinden zodat ik tips kan ontvangen van mensen over de hele wereld - Verwijderen - Zorg ervoor dat je je Geheime Herstelzin hebt opgeslagen en voer vervolgens \'Verwijderen\' in om je Code-account te verwijderen. Deze actie is onomkeerbaar. - Geen code ontvangen op %1$s? - De code niet ontvangen? Opnieuw sturen - Als je Face ID uitschakelt, dien je je identiteit te verifiëren. - Heb je de app Code Wallet niet? - Je hebt nog geen KIN. - Schakel Face ID in om de beveiliging van transacties in Code verder te verbeteren. - Adres van bestemming invoeren - Voer maximaal %1$s in - Ontvang $5 aan Kin gratis als je een vriend zich laat aanmelden voor Code en je hem of haar zijn of haar eerste Kin stuurt. - Code gebruikt de cryptocurrency Kin voor betalingen. Hier zijn enkele manieren om meer Kin in je Code-wallet te krijgen. - Krijg je eerste $1 van Kin gratis - Je identiteit verifiëren om KIN te schenken. - Stort KIN in je Code-wallet door KIN naar je stortingsadres hieronder te sturen. Tik om te kopiëren. - Verkrijg meer Kin en probeer opnieuw te betalen - Ongeldig bestemmingsaccount - Zorg ervoor dat het adres waarnaar je geld opneemt is geïnitialiseerd door je portefeuille-provider. Een kortere weg om dit te bereiken is door eerst een kleine hoeveelheid SOL om te ruilen voor Kin in de portefeuille waarnaar je probeert te versturen. - Uitnodigingscode - Code is momenteel alleen op uitnodiging. Je hebt een uitnodigingscode nodig om toegang te krijgen tot de app. - %1$d uitnodigingen - Code is een nieuwe cryptowallet-app die momenteel invite-only is. Om Code te downloaden gaat u naar %1$s - Meer informatie - Je telefoonnummer is gekoppeld aan dit Code-account. Vrienden kunnen je vinden via dit telefoonnummer. - Ik verbind mijn X-account met @getcode zodat ik fooien kan ontvangen van mensen over de hele wereld. - Je saldo en transactiegeschiedenis worden geladen - Controleer je foto\'s voor de toegangscode die je hebt opgeslagen toen je je account voor het eerst aanmaakte. - Je bent momenteel ingelogd op een account. Zorg ervoor dat je je toegangssleutel hebt opgeslagen voordat je doorgaat. Wil je uitloggen en inloggen met een nieuw account? - Geen gekoppeld\ntelefoonnummer - Je hebt geen telefoonnummer gekoppeld aan dit Code-account. Koppel een telefoonnummer zodat je vrienden je kunnen vinden. - Geen netwerkverbinding - Op Code - Open de Code-app en gebruik je camera om dit geld te claimen - Je contacten ordenen - Dit telefoonnummer staat niet in je Contacten. Je kunt deze persoon nog steeds uitnodigen voor Code. - Voer je telefoonnummer in, inclusief de landcode. Zorg ervoor dat je hetzelfde telefoonnummer gebruikt waarop je de uitnodiging hebt ontvangen. - Mogelijk gemaakt door - Je kunt nu fooien aanvragen - Verwijzingsbonus ontvangen - Verstuur %1$s - Hier is %1$s - Vraag een nieuwe code aan binnen %1$s - Scan deze QR-code met de camera van je telefoon om de app Code Wallet te downloaden - Scan om de Code Portemonnee-\napp te downloaden - Valuta zoeken - Zoeken naar contacten - Er is een sms-bericht naar je telefoonnummer verzonden met een verificatiecode. Voer de bovenstaande verificatiecode in. - Iemand heeft je geld gestuurd - Iemand heeft je een fooi gegeven - Je moet je camera opstarten om Codes te scannen. - Met je fooikaart kunt je fooien ontvangen van codegebruikers over de hele wereld. Om toegang te krijgen tot je fooikaart, verbind je X-identiteit. - Met je fooikaart kunt je fooien ontvangen van Code-gebruikers over de hele wereld. Om toegang te krijgen tot je fooikaart, post op X. - Met je Fooikaart kun je fooien ontvangen van Code-gebruikers over de hele wereld. Stuur een bericht naar @getcode op X om toegang te krijgen tot je Fooikaart. - Code toestaan om je meldingen te sturen wanneer je Fooi van andere Code-gebruikers ontvangt. - Typ \'%1$s\' - We hebben enkele wijzigingen doorgevoerd om de ervaring te verbeteren. Je dient de app te updaten om Code te blijven gebruiken. - Geldig eigenaarsaccount - Geldig token-account - De waarde van KIN verandert. - is aan jou teruggestuurd - Waarheen moeten je opgenomen KIN worden overgemaakt? - Je identiteit verifiëren om KIN op te nemen. - Je stortte - Jij gaf - Je hebt %1$d uitnodigingen - Code is momenteel alleen op uitnodiging. Je hebt nog %1$d over. - Je betaalde - Je gaf - Je ontving - Je stuurde - Je hebt uitgegeven - Je hebt iemand een fooi gegeven - Je hebt opgenomen - Je geldopname werd met succes uitgevoerd. - Je X-account is met succes verbonden met je code-account. Je kunt nu fooien aanvragen. - Geldopname gelukt - Het X-account is met succes verbonden - Toegang verlopen - Toegangscode - Appinstellingen - Camera automatisch starten - Saldo - Bèta-vlaggen - Bonus - Kin kopen en verkopen - Betalingen in contanten - Code van team - Kin-aankopen - Online betalingen - Fooien - Debug-opties - KIN storten - Gestort - Woorden van toegangscode invoeren - Telefoonnummer invoeren - Mislukt - Veelgestelde Vragen - Geschonken - Ontvang contant geld - Laat een vriend beginnen met Code - Krijg Kin - Verkrijg meer Kin - Contant geven - KIN schenken - Onvoldoende fondsen - Een vriend uitnodigen - Aanbieding voor beperkte tijd - Gekoppeld - Lokale valuta - Mijn Account - Niet gekoppeld - Andere valuta - Betaald - In behandeling - Telefoonnummer - Privacybeleid - Aangeschaft - Het X-account is met succes verbonden - Fooi ontvangen - Ontvangen - Recente valuta - Verwijs een vriend en ontvang $5 - Verwijzingsbonus - Vraag contant geld aan - Kin aanvragen - Een fooi aanvragen - Vereist gezichts-ID - Vereist toegangscode - Vereist touch-ID - Resultaten - Geretourneerd - Selecteer een Account - Selecteer een Land - Een valuta selecteren - Verstuurd - Besteed - Wisselen van account - Servicevoorwaarden - Fooikaart - Fooi-kin - Schakel meldingen in voor Code - Onbekend - Update vereist - Telefoonnummer verifiëren - Welkomstbonus - KIN opnemen - Opgenomen - Je toegangscode - Tik op het logo om de downloadlink van de app te delen - diff --git a/apps/codeApp/src/main/res/values-no/strings-localized.xml b/apps/codeApp/src/main/res/values-no/strings-localized.xml deleted file mode 100644 index 5d26ad0f7..000000000 --- a/apps/codeApp/src/main/res/values-no/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Sett inn penger med et debetkort - Tillat kameratilgang - Gi tilgang til kontakter - Tillat push-varsler - Saldo - Kjøp Kin - Kjøp mer Kin - Kanseller - Kanseller sending - Chat - Ta i mot disse pengene - Bekreft - Koble til X - Fortsett - Kopiert - Kopier - Kopier adresse - Opprett en konto - Opprett en ny kodekonto - Slett konto - Ferdig - Last den ned nå - Aktiver Face ID - Aktiver Touch ID - Avslutt - Gi - Gi Kin - Inviter - Invitasjoner - Bli med på ventelisten - Senere - Lær hvordan du kjøper Kin - Lær hvordan du selger Kin - Koble til et telefonnummer - Logg inn - Logg ut - Send @getcode for å koble til - Slå av lyden - Neste - Nei, prøv igjen - Ikke nå - OK - Åpne innstillinger - Lim inn - Lim inn fra utklippstavlen - Skriv et innlegg for å koble til kontoen - Legg i lommeboken - Motta - Gjenopprett eksisterende konto - Påminn - Fjern telefonnummer - Fjern telefonnummeret ditt - Be om tips - Lagre tilgangsnøkkel til Bildene mine - Lagre i Bilder - Send - Send bekreftelseskode - Del - Del som nettadresse - Del nedlastingslenke - Del denne videoen - Vis tipskortet mitt - Start kamera - Abonner - Sveip for å logge inn - Sveip for å betale - Sveip for å tipse - Prøv en annen kodekonto - Send dem en tweet - Lås opp Code - Slå på lyden - Avslutt abonnement - Oppdater - Se tilgangsnøkkel - Ta ut Kin - Noterte du de 12 ordene i stedet? - Ja - Ja, ta ut Kin - Ja, jeg noterte dem - og - Kin - av Kin - Fjern all informasjon knyttet til kontoen din fra Codes servere (telefonnummer, kontakter og transaksjonshistorikk) - Du kan få tilgang til kontoen din i andre kryptovaluta-apper med den hemmelige gjenopprettings-setningen din. Du vil ikke kunne bruke kontoen din i Code - Fjern kontoen din fra blokkjeden - Dette fører sletting til - Dette kommer til å skje - Dette fører sletting ikke til - Noen andre har allerede gjort krav på denne Kin. - Denne mengden Kin er allerede hentet av noen. - Gi tilgang til kameraet i «Innstillinger» for å bruke koden. - Disse kontantene er utløpt. - Skriv inn telefonnummeret ditt på nytt og prøv på nytt. - Gi tilgang til kontaktene under innstillinger for å kunne sende invitasjoner. - Code er for øyeblikket ikke tilgjengelig i landet ditt. - Vi kan ikke støtte enheten din for øyeblikket. - Støtte for eSims kommer sannsynligvis i en fremtidig versjon av Code. - Noe gikk galt. Kin kunne ikke samles inn. - Vi hadde ikke forventet at dette skulle skje. Noe gikk galt. Prøv å opprette denne kontoen på nytt. - Vennligst gi kodetilgang til «Bilder» i «Innstillinger» for å lagre tilgangsnøkkelen din. - Noe gikk galt. Sørg for at du har oppgitt riktig telefonnummer. - Noe gikk galt. Vennligst prøv på nytt. - Kunne ikke ta ut pengene dine. Noe gikk galt, prøv uttaket på nytt. - Code er laget for små, hverdagslige transaksjoner på %1$s eller mindre. - Code er laget for små, hverdagslige transaksjoner. Dagsgrensen din for å gi øker i morgen. - For å lære hvordan du får mer Kin gå til «Vanlige spørsmål» i «Innstillinger». - Skriv inn en annen invitasjonskode og prøv på nytt. - Legg inn et gyldig telefonnummer og prøv igjen. - Dette er et ugyldig tipskort. - Vennligst skriv inn en gyldig kode og prøv igjen. - Beklager, vi har hadde et nettverksproblem. Prøv å invitere vennen din på nytt. - Denne Kin ble automatisk returnert til avsenderen fordi den ikke ble hentet innen 24 timer. Be dem sende Kin igjen. - Skriv inn telefonnummeret ditt på nytt og prøv på nytt. - Prøv å ta et bilde som viser koden tydeligere. - Sjekk Internettforbindelsen din eller prøv igjen senere. - Koden gjelder for øyeblikket kun for invitasjoner. Vi vil varsle deg når det er flere invitasjoner tilgjengelig. - For å bli med på ventelisten, gå til %1$s - Det maksimale antallet du kan kjøpe er %1$s. Angi en mindre mengde. - Det minimale antallet du kan kjøpe er %1$s. Angi en større mengde. - Tilgangsnøkkelen din har åpnet for tilgang. Derfor kan du ikke lenger bruke denne tilgangsnøkkelen i Code. - Send en tweet til denne personen for å aktiver personens tipskort. - Det meste du kan tipse er %1$s. Legg inn et mindre beløp. - Det minste du kan tipse er %1$s. Legg inn et større beløp. - Du kan bare opprette et visst antall nye kontoer hver dag. - Code er for øyeblikket begrenset til én konto per enhet. Støtte for flere kontoer kommer sannsynligvis i en fremtidig versjon av Code. - Code er for øyeblikket begrenset til én konto per telefonnummer. Støtte for flere kontoer kommer sannsynligvis i en fremtidig versjon av Code. - Noen gjorde allerede krav på Kin - Kin allerede hentet - Kameratilgang kreves - Kontanter utløpt - Tiden for bekreftelseskoden er utløpt - Tilgang til kontakter påkrevd - Landet støttes ikke. - Enhet støttes ikke. - eSims støttes ikke for øyeblikket. - Kunne ikke samle inn - Kunne ikke opprette konto - Kunne ikke lagre - Mislykket sending - Kunne ikke bekrefte - Transaksjonen mislyktes - Grense for å gi er nådd - Dagsgrense nådd - Utilstrekkelig Kin - Invitasjonskoden er ugyldig eller utløpt - Ugyldig telefonnummer - Ugyldig tipskort - Ugyldig kode - Invitasjon mislyktes - Lenke utløpt - Maksimalt antall forsøk nådd - Ingen kode funnet - Ingen Internettforbindelse - Du har ingen invitasjoner - Du har ikke blitt invitert ennå - Kjøpet er for stort - Kjøpet er for lite - Tilgangsnøkkel kan ikke lenger brukes i Code - Tipskort enda ikke aktivert - For stort tips - For lite tips - For mange kontoer er opprettet - Kontoen er allerede opprettet - Kontoen er allerede opprettet - Vi mener at betalinger bør være enkle, potente og globale. Ved å bygge med avansert blokkjedeteknologi tilbyr Code funksjoner som tradisjonelle betalingsapper ikke kan, som globale overføringer mellom privatpersoner, mikrobetalinger som låser opp individuelle artikler på nettet, og gebyrfri tips til dine favorittskapere. - Kin er en kryptovaluta som Bitcoin, men er også laget for raske og billige betalinger. - Som Bitcoin er det kun en begrenset mengde Kin tilgjengelig. Dersom flere kjøper Kin vil verdien gå opp, og dersom flere selger Kin vil verdien gå ned. Denne dynamikken gjør at alle som eier Kin kan dele på verdien dersom Kin-markedet vokser. - Du kan kjøpe Kin med debetkortet ditt. Dette er tilgjengelig fra fanen \"Get Kin\". - Ja, det kan du. Det er mulig å selge Kin på flere kryptovekslingsplattformer. - Det er tre måter du kan hjelpe på: snakk om din opplevelse av Code på sosiale medier, oppmuntre vennene dine til å teste ut Code selv, og oppmuntre dine favorittnettsider til å integrere Code-betalingsløsninger ved å sjekke ut [getcode.com](https://getcode.com). - Hva er Code? - Hvorfor denomineres Code-betalinger i Kin? - Hvorfor endres verdien av Kin? - Hvordan kjøper jeg mer Kin? - Kan jeg selge Kin? - Hvordan kan jeg hjelpe? - godtar du vår - Ved å trykke på «Opprett en konto» eller «Logg inn» - Kameraet ditt brukes til å motta Kin. Gi tilgang til kameraet for å fortsette. - Vi trenger push-varsler for å sende deg rettidig informasjon om kontoen din. - Uttak er irreversible og kan ikke angres når de først er igangsatt. - Alle midler på denne kontoen vil gå tapt. Sletting av kontoen din er permanent og kan ikke angres. Er du sikker på at du vil slette denne kontoen? - Kin som ingen har gjort krav på innen 24 timer vil automatisk bli returnert til saldoen din. - Du må starte kontoopprettelsen på nytt og bekrefte telefonnummeret ditt på nytt. - Du kan gå tilbake til denne kontoen ved å bruke tilgangsnøkkelen din. - Du varsles ikke om nye meldinger fra %1$s. Du kan slå på lyden igjen når som helst. - Bare kontoer opprettet med en kode støttes for øyeblikket. - Vennene dine kan ikke lenger finne deg med dette telefonnummeret. - Du varsles om alle nye meldinger fra %1$s. Du kan slå av lyden når som helst. - Du mottar ingen meldinger fra %1$s før du betaler dem igjen. - Tilgangsnøkkelen din gir deg tilgang til kodekontoen din. Bevar den privat og trygt. - Disse 12 ordene er den eneste måten å gjenopprette kodekontoen din på. Sørg for at du skrev dem ned, og bevar dem privat og trygt. - Er du sikker? - Er du sikker på at du vil slette denne kontoen? - Har du sendt lenken? - Er du sikker på at du vil avslutte? - Er du sikker på at du vil logge ut? - Slå av lyden på %1$s? - Dette er ikke en kodekonto - Er du sikker? - Slå på lyden på %1$s? - Avslutt abonnement på %1$s? - Vil du se tilgangsnøkkelen din? - Er du sikker? - %1$s Kin ble satt inn på kontoen din. - De %1$s du sendte i går ble ikke hentet. De er automatisk lagt til saldoen din. - Få en venn i gang med Code og få USD 5 - Send kontanter gjennom enhver meldingsapp - Du mottok Kin verdt %1$s for for å ha sendt noen deres første Kin. - Nå kan du be om tips. - Innskudd mottatt - Kin returnert - Ny på Code - Henvisningsbonus mottatt - X-konto tilkoblet - Din invitasjon til å få tilgang til koden er utløpt. Du kan logge ut og bruke en annen konto med en gyldig invitasjonsstatus. - Tilgangsnøkkelen din er den eneste måten å få tilgang til pengene dine på. Bevar den privat og trygt. - Bekreft identiteten din for å se tilgangsnøkkelen din. - Trykk på Google Lens-ikonet for å åpne QR-koden for innlogging i Code. Du kan eventuelt logge inn manuelt ved å skrive inn de 12 ordene på Kodeinnlogging-skjermen. - Advarsel! Dette bildet gir tilgang til alle midlene du har i Code. Ikke del dette bildet med noen andre. Oppbevar det trygt og sikkert. - Med koden kan du motta Kin ved å rette kameraet mot den digitale regningen på en annen brukers telefon - Du må tillate kameratilgang for å kunne motta Kin - Autentiser for å få tilgang til Code. - Kjøp Kin - Kjøp Kin (kommer snart) - Kjøp og salg av Kin er for tiden en kompleks prosess. Disse prosessene vil bli enklere over tid. Hvis du vil lære hvordan du kjøper og selger Kin i dag, kan du se instruksjonsvideoene nedenfor. - Du kan bare gi opptil %1$s - Du kan bare tipse opptil %1$s - Du har nå tilgjengelige Kin i Code-appen - Du har sendt noen sin første Kin! Her er henvisningsbonusen din: - USDC veksles til Kin. Dette tar cirka ett minutt - Du har satt inn USDC. Åpne Code-appen for å fullføre kjøpet - Velkomstbonus - Velg et land - Kommer snart - \@getcode Jeg vil gjerne koble til X-kontoen min slik at jeg kan motta tips fra folk fra hele verden - Slett - Sørg for at du har den hemmelige gjenopprettings-setningen din lagret, og velg deretter «Slett» for å slette Code-kontoen din. Denne handlingen er irreversibel. - Fikk du ikke en kode på %1$s? - Fikk du ikke koden? Send på nytt - Deaktivering av Face-ID krever at du bekrefter identiteten din. - Har du ikke appen Code Wallet? - Du har ennå ikke noen Kin - Aktiver Face-ID for å forbedre sikkerheten for transaksjoner i kode ytterligere. - Angi destinasjonsadresse - Skriv inn opptil %1$s - Få Kin verdt USD 5 gratis når du får en venn til å registrere seg for Code og du sender dem deres første Kin. - Kode bruker kryptovalutaen Kin for betalinger. Her er noen måter for å få flere Kin i kode-lommeboken din. - Få dine første Kin verdt USD 1 gratis - Bekreft identiteten din for å gi Kin. - Deponer Kin i kodelommeboken din ved å sende Kin til innskuddsadressen din nedenfor. Trykk for å kopiere. - Få deg flere Kin og prøv deretter å betale på nytt - Ugyldig destinasjonskonto - Sørg for at adressen du vil overføre til er initialisert av leverandøren av den elektroniske lommeboken. En snarvei for å oppnå dette er å først veksle en liten mengde SOL om til Kin i den elektroniske lommeboken du vil overføre til. - Invitasjonskode - Code er for øyeblikket kun tilgjengelig med en invitasjon. Du trenger en invitasjonskode for å få tilgang til appen. - %1$d invitasjoner - Code er en ny lommebok-app for kryptovaluta. For tiden kun er for inviterte brukere. Gå til %1$s for å laste ned Code. - Lær mer - Telefonnummeret ditt er koblet til denne kodekontoen. Vennene dine kan finne deg ved å bruke dette telefonnummeret. - Jeg kobler X-kontoen min med @getcode slik at jeg kan motta tips fra personer over hele verden. - Vi laster inn saldo og transaksjonshistorikk - Sjekk bildene dine for tilgangsnøkkelen du lagret da du først opprettet kontoen din. - Du er for øyeblikket logget på en konto. Sørg for at du har lagret tilgangsnøkkelen din før du fortsetter. Vil du logge ut og logge på med en ny konto? - Intet koblet telefonnummer - Du har ikke koblet et telefonnummer til denne kodekontoen. Koble til et telefonnummer slik at vennene dine kan finne deg. - Ingen nettverkforbindelse - På kode - Åpne Code-appen og pek med kameraet ditt for å ta i mot disse pengene - Organisere kontaktene dine - Dette telefonnummeret er ikke i kontaktlisten din. Du kan likevel invitere dem til Code. - Skriv inn telefonnummeret ditt inkludert landskoden. Pass på at du bruker samme telefonnummer som mottok invitasjonen. - Driftet av - Du kan nå be om tips - Henvisningsbonus mottatt - Send %1$s - Her er %1$s - Be om en ny i %1$s - Skann denne QR-koden med telefonkameraet ditt for å laste ned appen Code Wallet - Skann for å laste ned\nCode Wallet-appen.\n - Søk etter valutaer - Søk etter kontakter - En SMS-melding ble sendt til telefonnummeret ditt med en bekreftelseskode. Vennligst skriv inn bekreftelseskoden ovenfor. - Noen sendte deg penger - Noen tipset deg - Du må starte kameraet ditt for å skanne koder - Tipskortet ditt lar deg motta tips fra Code-brukere over hele verden. For tilgang til tipskortet ditt koble deg opp til X-identiteten din. - Tipskortet ditt lar deg motta tips fra Code-brukere over hele verden. Skriv et innlegg til X for å få tilgang til tipskortet ditt. - Med tipskortet mottar du tips fra Code-brukere over hele verden. Send meldingen @getcode på X for å få tilgang til tipskortet. - Tillat at Code sender varsler når du mottar tips fra andre Code-brukere. - Skriv \"%1$s\" - Vi har gjort noen endringer for å forbedre opplevelsen. Du må oppdatere appen for å fortsette å bruke koden. - Gyldig eierkonto - Gyldig myntkonto - Kin-verdien forandres - ble returnert til deg - Hvor vil du ta ut Kin til? - Bekreft identiteten din for å ta ut Kin. - Du satte inn - Du ga - Du har %1$d invitasjoner - Koden gjelder for øyeblikket kun for invitasjoner. Du har %1$d igjen. - Du betalte - Du ga - Du mottok - Du sendte - Du brukte - Du tipset noen - Du tok ut - Midlene dine har blitt tatt ut. - X-kontoen din er koblet opp til Code-kontoen din. Du kan nå be om tips. - Uttak lyktes - X-konto koblet opp - Tilgang utløpt - Tilgangsnøkkel - Appinnstillinger - Start kamera automatisk - Saldo - Beta-flagg - Bonus - Kjøp og selg Kin - Kontantbetalinger - Kodeteam - Kin-kjøp - Nettbetalinger - Tips - Feilsøkingsalternativer - Innskudd Kin - Deponerte - Skriv inn tilgangsnøkkelord - Skriv inn telefonnummer - Mislyktes - Vanlige spørsmål - Ga - Få penger - Få en venn i gang med Code - Skaff deg Kin - Få flere Kin - Gi kontanter - Gi Kin - Utilstrekkelige midler - Inviter en venn - Tidsbegrenset tilbud - Koblet - Lokal valuta - Min konto - Ikke koblet - Andre valutaer - Betalt - Utestående - Telefonnummer - Personvernerklæring - Kjøpt - X-kontoen koblet opp - Motta tips - Mottok - Nylige valutaer - Henvis en venn, få USD 5 - Henvisningsbonus - Be om penger - Be om Kin - Be om tips - Krev ansikts-ID - Krev passkode - Krev berørings-ID - Resultater - Returnert - Velg konto - Velg land - Velg en valuta - Sendt - Brukt - Bytt konto - Brukervilkår - Tipskort - Tips Kin - Aktiver varsler for Code - Ukjent - Oppdatering kreves - Bekreft telefonnummer - Velkomstbonus - Ta ut Kin - Tok ut - Tilgangsnøkkelen din - Trykk på logoen for å dele nedlastingslenken til appen - diff --git a/apps/codeApp/src/main/res/values-pl/strings-localized.xml b/apps/codeApp/src/main/res/values-pl/strings-localized.xml deleted file mode 100644 index 565bb7f3d..000000000 --- a/apps/codeApp/src/main/res/values-pl/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Dodaj Cash z użyciem karty debetowej - Zezwól na dostęp do kamery - Zezwól na dostęp do kontaktów - Zezwól na powiadomienia typu push - Saldo - Kup Kin - Kup więcej Kin - Anuluj - Anuluj wysyłanie - Czat - Odbierz te pieniądze - Potwierdź - Połącz z X - Kontynuuj - Skopiowano - Skopiuj - Skopiuj adres - Utwórz konto - Utwórz nowe konto Code - Usuń konto - Gotowe - Pobierz ją teraz - Włącz funkcję Face ID - Włącz funkcję Touch ID - Wyjdź - Podaruj - Daj Kin - Zaproś - Zaproszenia - Dołącz do listy oczekujących - Później - Dowiedz się, jak kupić Kin - Dowiedz się, jak sprzedawać Kin - Linkuj numer telefonu - Zaloguj się - Wyloguj się - Wyślij wiadomość do @getcode, aby się podłączyć - Wycisz - Dalej - Nie, spróbuj ponownie - Nie teraz - OK - Otwórz ustawienia - Wklej - Wklej ze schowka - Wyślij post, aby podłączyć konto - Umieść w portfelu - Otrzymaj - Odzyskaj istniejące konto - Przypomnij - Usuń numer telefonu - Usuń swój numer telefonu - Poproś o napiwek - Zapisz klucz dostępu w sekcji Moje zdjęcia - Zapisz do Zdjęcia - Wyślij - Wyślij kod weryfikacyjny - Udostępnij - Udostępnij URL - Udostępnij odnośnik do pobrania - Udostępnij ten film - Pokaż moją kartę napiwków - Uruchom kamerę - Subskrybuj - Przesuń, aby zalogować - Przeciągnij, aby zapłacić - Przesuń do napiwku - Wypróbuj inne konto Code - Zatweetuj do tej osoby - Odblokuj Code - Wyłącz wyciszenie - Zrezygnuj z subkrypcji - Zaktualizuj - Zobacz klucz dostępu - Wypłać Kin - Zapisałeś zamiast tego 12 słów? - Tak - Tak, wypłać Kin - Tak, zapisałem to - i - Kin - z Kin - Usuń wszystkie informacje powiązane ze swoim kontem z serwerów Code (numer telefonu, kontakty, historia transakcji) - Możesz uzyskać dostęp do swojego konta w innych aplikacjach kryptoportfeli, korzystając ze swojej tajnej frazy odzyskiwania. Nie będziesz w stanie używać swojego konta w aplikacji Code - Usuń swoje konto z łańcucha bloków - Jakie będą skutki usunięcia - Co się stanie - Czego nie spowoduje usunięcie - Kin został już odebrany przez kogoś innego. - Ten Kin został już odebrany przez kogoś innego. - Żeby używać Code, zezwól na dostęp do kamery w ustawieniach. - Ta gotówka wygasła. - Wpisz ponownie numer telefonu i spróbuj jeszcze raz. - Zezwól na dostęp do kontaktów w ustawieniach, aby wysłać zaproszenia. - Aplikacja Code nie jest obecnie dostępna w Twoim kraju. - Tym razem nie jesteśmy w stanie obsłużyć Twojego urządzenia - Wsparcie dla kart eSim pojawi się najprawdopodobniej w przyszłej wersji aplikacji Code. - Coś poszło nie tak. Ten Kin nie mógł zostać pobrany. - Nie spodziewaliśmy się tego. Coś poszło nie tak. Spróbuj jeszcze raz utworzyć to konto. - Żeby zapisać klucz dostępu, zezwól Code na dostęp do zdjęć w ustawieniach. - Coś poszło nie tak. Upewnij się, że Twój numer telefonu został poprawnie wpisany. - Coś poszło nie tak. Spróbuj ponownie. - Wypłacenie środków nie powiodło się. Coś poszło nie tak. Spróbuj jeszcze raz wypłacić środki. - Aplikacja Code powstała z myślą o małych, codziennych transakcjach na kwotę %1$s lub mniejszą. - Aplikacja Code powstała z myślą o małych, codziennych transakcjach. Twój limit dawania powiększy się jutro. - Żeby dowiedzieć się, jak uzyskać więcej Kin, przejdź do sekcji Ustawienia > Często zadawane pytania. - Wpisz inny kod zaproszenia i spróbuj ponownie. - Podaj poprawny numer telefonu i spróbuj ponownie. - To jest nieprawidłowa karta napiwków. - Wpisz prawidłowy kod i spróbuj ponownie. - Przepraszamy, wystąpił problem z siecią. Spróbuj jeszcze raz zaprosić znajomego. - Kin został automatycznie zwrócony do nadawcy, ponieważ nie został odebrany w ciągu 24 godzin. Poproś go o ponowne wysłanie tej kryptowaluty. - Wpisz ponownie numer telefonu i spróbuj jeszcze raz. - Spróbuj uzyskać obraz, na którym będzie wyraźniej widać kod. - Sprawdź połączenie z Internetem lub spróbuj ponownie później. - Code jest aktualnie wyłącznie na zaproszenie. Powiadomimy Cię, kiedy będzie dostępnych więcej zaproszeń. - Żeby dołączyć do listy oczekujących, przejdź do %1$s - Maksymalny zakup to %1$s. Wprowadź mniejszą kwotę. - Minimalny zakup to %1$s. Wprowadź większą kwotę. - Twój klucz dostępu inicjował odblokowanie. W rezultacie nie możesz już używać tego klucza dostępu w kodzie. - Zatweetuj do tej osoby, aby aktywować jej kartę napiwkową. - Maksymalny napiwek, jaki możesz dać, to %1$s. Wprowadź mniejszą kwotę. - Minimalny napiwek, jaki możesz dać, to %1$s. Wprowadź wyższą kwotę. - Liczba kont, które można utworzyć dziennie, jest ograniczona. - Aplikacja Code jest obecnie ograniczona do jednego konta na urządzenie. Wsparcie dla wielu kont pojawi się najprawdopodobniej w przyszłej wersji Code. - Aplikacja Code jest obecnie ograniczona do jednego konta na numer telefonu. Wsparcie dla wielu kont pojawi się najprawdopodobniej w przyszłej wersji Code. - Odebrano już kin - Kin został już odebrany - Wymagany jest dostęp do kamery - Gotówka wygasła - Wygasł kod weryfikacyjny - Wymagany dostęp do kontaktów - Nieobsługiwany kraj - Urządzenie nie jest obsługiwane - Karty eSim nie są obecnie obsługiwane - Nie udało się pobrać - Utworzenie konta nie powiodło się - Zapisanie nie powiodło się - Wysłanie nie powiodło się - Potwierdzenie nie powiodło się - Transakcja nie powiodła się - Osiągnięto limit dawania - Osiągnięto limit dzienny - niewystarczająca ilość Kin - Kod zaproszenia jest nieważny lub wygasł. - Niepoprawny numer telefonu - Nieprawidłowa karta napiwków - Nieprawidłowy kod - Zaproszenie nie powiodło się - Odnośnik wygasł - Osiągnięto maksymalną liczbę prób - Nie znaleziono kodu - Brak połączenia z Internetem - Nie masz zaproszeń - Nie zaproszono Cię jeszcze - Zakup jest zbyt duży - Zakup za mały - Nie da się już użytkować klucza dostępu w kodzie - Karta napiwkowa nie została jeszcze aktywowana - Napiwek jest za duży - Napiwek jest za mały - Utworzono za wiele kont - Konto zostało już utworzone - Konto zostało już utworzone - Wierzymy, że płatności powinny być proste, skuteczne i globalne. Dzięki zaawansowanej technologii blockchain portfel Code oferuje funkcje, których tradycyjne aplikacje płatnicze nie są w stanie zapewnić, takie jak globalne przelewy peer to peer, mikropłatności, które odblokowują poszczególne artykuły online, oraz zerowe opłaty za napiwki dla Twoich ulubionych twórców. - Kin jest kryptowalutą podobną do Bitcoina, ale jest również przeznaczona do szybkich i niedrogich płatności. - Podobnie jak w przypadku Bitcoin, dostępna jest tylko ograniczona ilość waluty Kin. Jeśli więcej osób kupuje Kin, wartość rośnie, a jeśli więcej osób sprzedaje Kin, wartość spada. Ta dynamika pozwala każdemu, kto posiada Kin, uczestniczyć w tworzeniu wartości, jeśli popularność Kin rośnie. - Kin możesz kupić za pomocą karty debetowej. Jest to dostępne w zakładce Pobierz Kin. - Tak, możesz. Sprzedaż Kin jest obsługiwana na wielu giełdach kryptowalut. - Możesz pomóc na trzy główne sposoby: opowiedz o swoich doświadczeniach z Code w mediach społecznościowych, poleć znajomym wypróbowanie portfela Code i zachęcaj swoje ulubione witryny internetowe do integracji płatności Code. Mogą to zrobić, odwiedzając stronę [getcode.com](https://getcode.com). - Czym jest Code? - Dlaczego płatności w portfelu Code dokonuje się w walucie Kin? - Dlaczego wartość waluty Kin się zmienia? - Jak mogę kupić większą ilość waluty Kin? - Czy mogę sprzedawać Kin? - Jak mogę pomóc? - zgadzasz się na nasze - Stukając „Utwórz konto\" lub „Zaloguj się\" - Twoja kamera używana jest do otrzymywania Kin. Aby umożliwić dalsze kroki, prosimy o zezwolenie na dostęp do kamery. - Żeby wysyłać Ci w porę informacje o koncie, potrzebujemy powiadomień typu push. - Wypłaty są nieodwracalne i po ich zainicjowaniu nie można cofnąć tego działania. - Wszystkie środki na koncie zostaną utracone. Usunięcie konta jest nieodwracalne – nie można cofnąć tego działania. Czy na pewno chcesz usunąć konto? - Kin nieodebrany w ciągu 24 godzin zostanie automatycznie zwrócony na Twoje konto. - Trzeba będzie zrestartować tworzenie konta i jeszcze raz zweryfikować numer telefonu. - Możesz wrócić do tego konta przy użyciu swojego klucza dostępu - Nie będziesz otrzymywać powiadomień o nowych wiadomościach od %1$s. Możesz wyłączyć wyciszenie w dowolnym momencie. - Aktualnie wspierane są tylko konta utworzone przez Code. - Twoi znajomi nie będą już mogli Cię znaleźć z użyciem tego numeru telefonu. - Będziesz otrzymywać powiadomienia o wszystkich nowych wiadomościach od %1$s. Możesz je wyciszyć w dowolnym momencie. - Nie będziesz otrzymywać żadnych wiadomości od %1$s, dopóki nie zapłacisz mu ponownie. - Twój klucz dostępu zapewni Ci dostęp do konta Code. Nie udostępniaj go nikomu i przechowuj go bezpiecznie. - Te 12 słów to jedyna metoda odzyskania Twojego konta Code. Pamiętaj, żeby je zapisać, nikomu nie pokazywać i przechowywać w bezpiecznym miejscu. - Czy na pewno? - Czy na pewno chesz usunąć to konto? - Czy wysłałeś(-aś) odnośnik? - Czy na pewno chcesz wyjść? - Czy na pewno chcesz się wylogować? - Wyciszyć %1$s? - Nie konto Code - Czy na pewno? - Wyłączyć wyciszenie %1$s? - Anulować subskrypcję %1$s? - Chcesz zobaczyć swój klucz dostępu? - Czy na pewno? - Na Twoim koncie złożono %1$s Kin. - Wysłane przez Ciebie wczoraj %1$s nie zostało odebrane. Zostało zwrócone automatycznie na Twoje saldo. - Poleć aplikację Code znajomej osobie i otrzymaj 5 $ - Wyślij gotówkę przez dowolny komunikator - Otrzymałeś(-aś) %1$s Kin za wysłanie komuś pierwszego Kin. - Możesz teraz prosić o napiwki. - Otrzymany depozyt - Zwrócony Kin - Nowość w aplikacji Code - Otrzymano bonus za polecenie - Konto X podłączone - Twoje zaproszenie dostępu do Code wygasło. Możęsz się wylogować i użyć innego konta z prawidłowym statusem zarposzenia. - Twój klucz dostępu to jedyny sposób uzyskania dostępu do Twoich środków. Nie przekazuj go nikomu i trzymaj go w bezpiecznym miejscu. - Zweryfikuj tożsamość, żeby zobaczyć Klucz dostępu. - Stuknij ikonę Google Lens, żeby otworzyć kod QR do logowania się do Code. Alternatywnie możesz się zalogować ręcznie, wpisując 12 słów w Code Log na ekranie. - Ostrzeżenie! Ten obraz daje dostęp do wszystkich środków, które posiadasz w Code. Nie udostępniaj tego obrazu nikomu innemu. Przechowuj go w bezpiecznym miejscu. - Code umożliwia otrzymywanie Kin poprzez wskazywanie kamerą na banknot cyfrowy na telefonie innego użytkownika - Żeby móc otrzymywać Kin, trzeba zezwolić na dostęp do kamery - Uwierzytelnij, aby uzyskać dostęp do Code. - Kup Kin - Kup kin (wkrótce) - Kupno i sprzedaż Kin jest obecnie złożonym procesem. Z czasem procesy te staną się prostsze. Jeśli chcesz dowiedzieć się, jak kupować i sprzedawać Kin już dziś, możesz obejrzeć poniższe filmy instruktażowe. - Możesz dać tylko do %1$s - Maksymalny napiwek to %1$s - Kin jest dostępny do użycia w aplikacji Code - Wysłałeś(-aś) komuś pierwszego Kina! Oto Twój bonus za polecenie: - USDC jest w trakcie konwerscji na Kin. Zajmie to około minuty - Udało się przelać USDC. Otwórz aplikację Code, aby dokończyć zakup - Bonus powitalny - Wybierz kraj - Wkrótce - \@getcode Chcę podłączyć moje konto X, aby otrzymywać napiwki od ludzi z całego świata. - Usuń - Upewnij się, że masz zapisaną swoją tajną frazę odzyskiwania, a następnie wprowadź „Usuń\", aby usunąć konto Code. Tego działania nie da się odwrócić. - Nie otrzymałeś kodu w %1$s? - Nie otrzymałeś kodu? Wyślij ponownie - Wyłączenie identyfikatora twarzy wymaga zweryfikwoania tożsamości. - Nie masz aplikacji Code Wallet? - Nie masz jeszcze Kin. - Włącz identyfikator Twarzy, żeby jeszcze bardziej zwiększyć bezpieczeństwo transakcji w Code. - Wpisz adres docelowy - Wpisz do %1$s - Otrzymaj 5 $ Kin za darmo, kiedy Twój znajomy zarejestruje się do aplikacji Code, a Ty wyślesz mu pierwszy Kin. - Code korzysta z kryptowaluty kin do dokonywania płatności. Oto parę sposobów na uzyskanie większej ilości tej kryptowaluty do swojego portfela Code. - Otrzymaj pierwszego 1 $ Kin za darmo - Zweryfikuj tożsamość, żeby otrzymać Kin. - Zdeponuj Kin w swoim portfelu Code, wysyłając Kin na poniższy adres depozytowy. Stuknij, żeby skopiować. - Zdobądź więcej Kin, a następnie spróbuj zapłacić ponownie - Nieprawidłowe konto docelowe - Upewnij się, że wycofywany adres został inicjalizowany przez dostawcę Twojego portfela. Można to osiągnąć na skróty, najpierw wymieniając małą ilość SOL na Kin w portfelu, na który chcesz przesłać środki. - Kod zaproszenia - Kod jest obecnie dostępny wyłącznie na zaproszenie. Żeby uzyskać dostęp do aplikacji, będziesz potrzebować kodu zaproszenia. - %1$d zaproszenia - Code to nowa aplikacja-portfel krypto, obecnie dostępna tylko na zaproszenie. Żeby ją pobrać, przejdź do %1$s - Dowiedz się więcej - Twój numer telefonu jest połączony z tym kontem Code. Znajomi mogą Cię znaleźć, używając tego numeru telefonu. - Łączę moje konto X z @getcode, dzięki czemu mogę otrzymywać napiwki od ludzi z całego świata. - Ładowanie salda i historii transakcji - Sprawdź w zdjęciach klucz dostępu zapisany podczas tworzenia konta. - Obecnie jesteś zalogowany(-a) do konta. Zanim przejdziesz dalej, sprawdź, czy zapisałeś(-aś) klucz dostępu. Czy chcesz się wylogować i zalogować z użyciem nowego konta? - Brak połączonego\nnumeru telefonu - Nie masz numeru telefonu połączonego z tym kontem Code. Połącz numer telefonu, żeby mogli Cię odkryć znajomi. - Brak połączenia z siecią - Na Code - Otwórz aplikację Code i skieruj aparat w odpowiednie miejsce, aby odebrać pieniądze - Organizowanie Twoich kontaktów - Tego numeru telefonu nie ma w twoich kontaktach. Wciąż możesz zaprosić jego użytkownika do aplikacji Code. - Wpisz numer telefonu, w tym kod kraju. Pamiętaj o tym, żeby użyć numeru telefonu, na który przyszło zaproszenie. - Obsługiwane przez - Możesz teraz poprosić o napiwki. - Otrzymano bonus za polecenie - Wyślij %1$s - Oto %1$s - Poproś o nowy w %1$s - Zeskanuj ten kod QR aparatem telefonu, aby pobrać aplikację Code Wallet - Zeskanuj, aby pobrać\naplikację portfela Code - Wyszukaj waluty - Szukaj kontaktów - Na Twój numer telefonu przesłano wiadomość SMS z kodem weryfikacyjnym. Wpisz go powyżej. - Ktoś wysłał ci pieniądze - Ktoś dał Ci napiwek - Aby skanować Code, musisz uruchomić kamerę - Twoja karta napiwków umożliwia otrzymywanie napiwków od użytkowników Code na całym świecie. Aby uzyskać dostęp do karty napiwków, połącz swoją tożsamość X. - Twoja karta napiwkowa pozwala Ci otrzymywać napiwki od użytkowników Code na całym świecie. Aby uzyskać dostęp do karty napiwkowej, wyślij post do X. - Twoja Tip Card umożliwia otrzymywanie napiwków od użytkowników Code na całym świecie. Aby uzyskać dostęp do Tip Card, wyślij wiadomość @getcode na X. - Zezwól aplikacji Code na wysyłanie Ci powiadomień, gdy otrzymasz wskazówki od innych użytkowników aplikacji. - Wpisz „%1$s\" - Dokonaliśmy paru zmian, żeby usprawnić korzystanie z usługi. Żeby nadal używać Code, musisz zaktualizować aplikację. - Prawidłowe konto właściciela - Prawidłowe konto tokenu - Wartość Kin zmienia się. - zwrócono Ci - Dokąd chcesz wypłacić Kin? - Zweryfikuj tożsamość, żeby wypłacić Kin. - Zdeponowałeś(-aś) - Dałeś(-aś) - Masz %1$d zaproszeń - Code jest obecnie tylko na zaproszenie. Pozostało Ci %1$d. - Zapłaciłeś(-aś) - Dałeś(-aś) - Otrzymałeś - Wysłałeś(-aś) - Wydałeś(-aś) - Dałeś komuś napiwek - Wypłaciłeś(-aś) - Twoje środki zostały wypłacone. - Twoje konto X zostało pomyślnie połączone z kontem Code. Możesz teraz poprosić o napiwki. - Wypłacenie powiodło się - Pomyślnie połączono konto X - Dostęp wygasł - Klucz dostępu - Ustawienia aplikacji - Autostart kamery - Saldo - Betaflagi - Bonus - Kupuj i sprzedawaj Kin - Płatności gotówką - Zespół Code - Zakupy Kin - Płatności online - Napiwki - Debuguj opcje - Zdeponuj Kin - Zdeponowane - Wpisz słowa klucza dostępu - Wpisz numer telefonu - NIe powiodło się - Często zadawane pytania - Dał - Otrzymaj Cash - Poleć znajomemu aplikację Code - Uzyskaj kin - Zdobądź więcej Kin - Podaruj gotówkę - Daj Kin - Niewystarczające środki - Zaproś znajomego - Oferta ograniczona w czasie - Linkowane - Miejscowa waluta - Moje konto - Nielinkowane - Inne waluty - Opłacone - W oczekiwaniu - Numer telefonu - Zasady prywatności - Kupione - Pomyślnie połączono konto X - Otrzymaj wskazówki - Otrzymane - Niedawne waluty - Poleć znajomemu i otrzymaj 5 $ - Bonus za polecenie - Poproś o Cash - Poproś o Kin - Poproś o napiwek - Wymagaj Face ID - Wymagaj hasła - Wymagaj Touch ID - Wyniki - Zwrócone - Wybierz konto - Wybierz kraj - Wybierz walutę - Wysłane - Wydano - Przełącz konta - Warunki świadczenia usług - Karta napiwkowa - Napiwek Kin - Włącz powiadomienia dla aplikacji Code - Nieznane - Wymagana aktualizacja - Zweryfikuj numer telefonu - Bonus powitalny - Wypłać Kin - Wycofał - Twój klucz dostępu - Dotknij logo, aby udostępnić link do pobrania aplikacji - diff --git a/apps/codeApp/src/main/res/values-pt/strings-localized.xml b/apps/codeApp/src/main/res/values-pt/strings-localized.xml deleted file mode 100644 index 53e529620..000000000 --- a/apps/codeApp/src/main/res/values-pt/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Adicionar dinheiro com um cartão de débito - Permitir Acesso à Câmara - Permitir Acesso aos Contactos - Permitir Notificações Push - Saldo - Adquirir Kin - Comprar mais Kin - Cancelar - Cancelar envio - Conversar - Recebe este dinheiro - Confirmar - Conectar ao X - Continuar - Copiado - Copiar - Copiar Endereço - Criar uma Conta - Criar uma Nova Conta Code - Eliminar Conta - Concluído - Descarrega agora - Ativar a identificação facial - Ativar a identificação por toque - Sair - Dar - Dar Kin - Convidar - Convites - Aderir à Lista de Espera - Mais tarde - Saiba como comprar Kin - Saiba como vender Kin - Associar um Número de Telefone - Iniciar Sessão - Terminar Sessão - Enviar mensagem a @getcode para Conectar - Silenciar - Próximo - Não, tentar de novo - Agora Não - Ok - Abrir Definições - Colar - Colar a Partir da Área de Transferência - Faça uma publicação para associar conta - Por na Carteira - Receber - Recuperar Conta Existente - Relembrar - Remover Número de Telefone - Remover o Seu Número de Telefone - Solicitar uma recompensa - Guardar Chave de Acesso nas Minhas Fotos - Guardar em Fotos - Enviar - Enviar Código de Verificação - Partilhar - Partilhar como URL - Partilhar Ligação de Download - Partilhar este vídeo - Exibir o meu Cartão de Recompensas - Ativar Câmara - Subscrever - Deslize para autenticar - Deslize para pagar - Deslize para dar recompensa - Tente uma Conta Code Diferente - Enviar tweet - Desbloquear a Code - Ligar o som - Anular subscrição - Atualizar - Ver Chave de Acesso - Levantar Kin - Anotou a 12 Palavras Em Alternativa? - Sim - Sim, Levantar Kin - Sim, Anotei-os - e - Kin - de Kin - Remover todas as informações associadas com a sua conta dos servidores da Code (número de telefone, contactos, histórico de transações) - Poderá aceder à sua conta através de outras aplicações de carteira de criptomoedas usando a sua frase secreta de recuperação. Não poderá utilizar a sua conta da Code - Remover a sua conta da blockchain - O que eliminar irá fazer - O que acontecerá - O que eliminar não fará - Este Kin já foi recebido por outra pessoa. - Este Kin já foi recolhido por alguém. - Permita o acesso às suas Fotos nas Definições para poder utilizar a Code. - Este dinheiro expirou. - Reintroduza o seu número de telemóvel e tente novamente. - Precisa de dar acesso aos contactos nas definições para enviar convites. - A Code está atualmente indisponível no seu país. - Neste momento, não conseguimos suportar o seu dispositivo - A compatibilidade para eSims provavelmente aparecerá numa versão futura da Code. - Algo correu mal. Não foi possível recolher esta Kin. - Não estávamos a contar que isto acontecesse. Algo correu mal. Tente criar esta conta novamente. - Permita que o Code tenha acesso às suas Fotos nas Definições, de forma a poder guardar a sua Chave de Acesso. - Algo correu mal. Garanta que o seu número de telefone está introduzido corretamente. - Algo correu mal. Tente novamente. - O levantamento de fundos falhou. Algo correu mal, tente fazer o seu levantamento novamente. - A Code foi concebida para transações pequenas do dia-a-dia de %1$s ou menos. - A Code foi concebida para transações pequenas do dia-a-dia. O seu limite diário de dádiva será aumentado amanhã. - Para saber como obter mais Kin consulte o Guia FAQ nas Definições. - Introduza um Código de Convite diferente e tente novamente. - Introduza um número de telefone válido e tente novamente. - Este Cartão de Recompensas não é válido. - Introduza um código válido e tente novamente. - Desculpe, surgiu um erro de rede. Tente convidar o seu amigo novamente. - Este Kin foi automaticamente devolvido ao remetente por não ter sido recebido no prazo de 24 horas. Peça ao remetente que envie o Kin de novo. - Reintroduza o seu número de telemóvel e tente novamente. - Tente obter uma imagem que mostre o código mais claramente. - Verifique a sua ligação de internet ou tente novamente mais tarde. - A Code neste momento só pode ser acedida por convite. Iremos notificá-lo quando estiverem disponíveis mais convites. - Para se juntar à lista de espera vá a %1$s - O máximo que pode comprar é %1$s. Insira um montante menor. - O mínimo que pode comprar é %1$s. Insira um montante maior. - A sua Chave de Acesso iniciou um desbloqueio. Por esse motivo não poderá continuar a utilizar esta Chave de Acesso na Code. - Envie um tweet a esta pessoa para ativar o seu Cartão de gorjetas. - O valor máximo de recompensa é %1$s. Por favor, insira um montante inferior. - O valor mínimo de recompensa é %1$s. Por favor, insira um montante superior. - Só pode criar algumas contas por dia. - Atualmente, a Code está limitada a uma conta por dispositivo. A compatibilidade com múltiplas contas provavelmente aparecerá numa versão futura da Code. - Atualmente, a Code está limitada a uma conta por número de telefone. A compatibilidade com múltiplas contas provavelmente aparecerá numa versão futura da Code. - Kin já recebido - Kin já recolhido - É Necessário Acesso à Câmara - Dinheiro expirado - O Código de Verificação Expirou - Acesso aos contactos requerido - País Não Compatível - Dispositivo Não Compatível - eSims Não São Atualmente Compatíveis - Falha na Recolha - A Criação de Conta Falhou - Não foi possível Guardar - Enviou Falhou - Confirmação Falhou - A Transação Falhou - Limite de dádiva alcançado - Limite diário alcançado - Kin Insuficiente - Código de Convite Inválido ou Expirado - Número de telefone inválido - Cartão de Recompensas inválido - Código Inválido - O Convite Falhou - Hiperligação expirada - Número Máximo de Tentativas Atingido - Não foi encontrado nenhum código - Sem Ligação à Internet - Não Tem Convites - Ainda Não Foi Convidado - Compra demasiado grande - Compra demasiado pequena - Chave de Acesso Deixou de Ser Utilizável na Code - Cartão de gorjetas por ativar - O valor da recompensa é demasiado alto - O valor da recompensa é demasiado baixo - Demasiadas contas criadas - Conta Já Criada - Conta Já Criada - Consideramos que os pagamentos devem ser simples, poderosos e globais. Ao desenvolver os seus produtos com tecnologia avançada de blockchain, a Code oferece caraterísticas que as aplicações de pagamento tradicionais não podem oferecer, como, por exemplo, transferências ponto a ponto a nível global, micropagamentos que desbloqueiam artigos individuais online e gorjetas isentas de taxas para os seus criadores favoritos. - A Kin é uma criptomoeda, como a Bitcoin, mas também foi concebida para permitir a realização de pagamentos de forma rápida e económica. - Tal como acontece com a Bitcoin, só se encontra disponível uma quantidade limitada de Kin. Quanto mais pessoas comprarem Kin, mais subirá a sua cotação, e quanto mais pessoas venderem Kin, menor será o seu valor. Esta dinâmica permite que todos os detentores de Kin partilhem entre si os benefícios da criação de valor, se houver um aumento na adoção de Kin. - Pode comprar Kin com o seu cartão de débito. Essa opção encontra-se acessível no separador \"Obter Kin\". - Sim, pode. A venda de Kin é suportada em várias casas de câmbio de criptomoedas. - Existem três formas através das quais pode ajudar: falando sobre a sua experiência com a Code nas redes sociais, incentivando os seus amigos a experimentarem a Code e incentivando os seus sites favoritos a integrarem os pagamentos via Code, pedindo-lhes para consultarem [getcode.com](https://getcode.com). - O que é a Code? - Porque é que os pagamentos da Code são denominados em Kin? - Porque é que se verificam alterações no valor da Kin? - Como é que compro mais Kin? - Posso vender Kin? - Como é que posso ajudar? - está a concordar com o nosso - Ao tocar em \"Criar uma Conta\" ou \"Iniciar Sessão\" - A sua câmara é utilizada para receber Kin. Permita o acesso à câmara para continuar. - São necessárias notificações push para lhe podermos enviar informação pertinente sobre a sua conta. - Os levantamentos são irreversíveis e, uma vez iniciados, não podem ser desfeitos. - Todos os fundos nesta conta serão perdidos. A eliminação da sua conta é permanente e não pode ser desfeita. Tem a certeza que quer eliminar esta conta? - O Kin que não tiver sido recebido no prazo de 24 horas será automaticamente devolvido ao seu saldo. - Terá que reiniciar a criação de conta e verificar novamente o seu número de telemóvel. - Pode voltar a entrar nesta conta utilizando a sua Chave de Acesso - Não irá receber notificações de quaisquer mensagens novas de %1$s. Pode reativar as notificações a qualquer momento. - Neste momento são apenas suportadas contas criadas através da Code. - Os seus amigos já não poderão encontrá-lo através deste número de telemóvel. - Irá receber notificações de todas as novas mensagens de %1$s. Pode desativar as notificações a qualquer momento. - Não irá receber quaisquer mensagens de %1$s até lhe pagar novamente. - A sua Chave de Acesso irá dar-lhe acesso à sua conta Code. Mantenha-a privada e segura. - Estas 12 palavras são a única forma de recuperar a sua conta Code. Certifique-se que as anota e mantenha-as privadas e seguras. - Tem a certeza? - Tem a certeza que quer eliminar esta conta? - Enviou a hiperligação? - Tem a certeza que quer sair? - Tem a certeza que quer terminar sessão? - Silenciar %1$s? - Não é uma Conta Code - Tem a certeza? - Reativar notificações de %1$s? - Anular subscrição de %1$s? - Ver a Sua Chave de Acesso? - Tem a Certeza? - Foi depositado %1$s Kin na sua conta. - O %1$s que enviou ontem não foi recolhido. Foi devolvido automaticamente para o seu saldo. - Inicie um amigo na Code e ganhe 5€ - Envie dinheiro através de qualquer aplicação de mensagens - Recebeu %1$s em Kin por ter enviado o primeiro Kin a alguém. - Agora já pode pedir gorjetas. - Depósito recebido - Kin devolvido - Novidades na Code - Bónus de referência recebido - Conta X associada - O seu convite para ter acesso à Code expirou. Pode terminar sessão ou utilizar uma conta diferente com um estado de convite válido. - A Sua Chave de Acesso é a única forma de aceder aos seus fundos. Mantenha-o privado e seguro. - Verifique a sua identidade para ver a sua Chave de Acesso. - Toque no ícone do Google Lens para ler o código QR e iniciar sessão no Code.\nEm alternativa, pode iniciar sessão manualmente inserindo as 12 letras no ecrã de Início de Sessão do Code.\n - Atenção! Esta imagem dá acesso a todos os fundos que tem no Code. Não partilhe esta imagem com ninguém. Mantenha-a segura e protegida. - A Code permite-lhe receber Kin apontando a sua câmara ao recibo digital no telemóvel de outro utilizador - Tem de permitir o acesso à câmara para ser capaz de receber Kin - Autenticar para aceder à Code. - Comprar Kin - Comprar Kin (brevemente) - Comprar e vender Kin é neste momento um processo complexo. Este irá tornar-se mais simples ao longo do tempo. Se quiser aprender a comprar e vender Kin hoje, pode assistir ao guia nos vídeos abaixo. - Apenas pode dar até %1$s - O valor de recompensa permitido é de, no máximo, %1$s - Os seus Kin estão agora disponíveis para usar na sua aplicação Code - Enviou a alguém o seu primeiro Kin! Aqui está o seu bónus de referência: - Os seus USDC estão a ser convertidos em Kin. Esta operação deve demorar cerca de um minuto - Depositou USDC com sucesso. Abra a aplicação Code para concluir a sua compra - Bónus de boas-vindas - Escolher um país - Brevemente - \@getcode Gostava de ligar a minha conta X para poder receber dicas de pessoas de todo o mundo - Eliminar - Certifique-se de que guardou a sua frase de recuperação secreta, e depois selecione \"Eliminar\" para eliminar a sua conta da Code. Esta ação é irreversível. - Não recebeu um código em %1$s? - Não recebeu o código? Reenviar - Se desativar o Face ID, terá que verifique a sua identidade - Não tens a aplicação Code Wallet? - Ainda não tem qualquer Kin. - Ative o Face ID para melhorar a segurança das suas transações na Code. - Introduzir endereço de destino - Introduza até %1$s - Ganhe 5€ grátis em Kin quando conseguir que um amigo se inscreva na Code e este lhe enviar o seu primeiro Kin. - O Code usa a criptomoeda Kin para efetuar pagamentos. Aqui estão algumas formas de adicionar mais Kin à sua carteira Code. - Ganhe o seu primeiro 1€ em Kin grátis - Verifique a sua identidade para dar kin. - Deposite Kin na sua carteira Code enviando Kin para o seu Endereço de Depósito abaixo. Toque para copiar. - Obtenha mais Kin e tente pagar novamente - Conta de destino inválida - Certifique-se de que o endereço para o qual está a fazer o levantamento foi inicializado pelo seu fornecedor de carteira. Um atalho para conseguir isto é trocar primeiro uma pequena quantidade de SOL por Kin na carteira para a qual está a tentar enviar. - Código de Convite - A Code apenas está disponível através de convite. Irá necessitar de um Código de Convite para aceder à aplicação. - %1$d Convites - A Code é uma nova aplicação de carteira de criptomoedas com acesso exclusivo para pessoas com convite, neste momento. Para descarregar a Code vá a %1$s - Saber mais - O seu número de telemóvel está associado a esta conta Code. Os seus amigos podem encontrá-lo utilizando este número. - Estou a conectar a minha conta X com a @getcode para poder receber recompensas de pessoas de todo o mundo. - A carregar o seu saldo e histórico de transações - Procure nas suas fotos a Chave de Acesso que guardou quando criou a sua conta. - Tem sessão iniciada com uma conta. Certifique-se de que guardou a sua chave de acesso antes de continuar. Pretende terminar a sessão e iniciar sessão com uma nova conta? - Nenhum Número de\nTelemóvel Associado - Não tem nenhum número de telemóvel associado a esta conta Code. Associa um para que os seus amigos o possam encontrar. - Sem ligação de rede - No Code - Abre a aplicação Code e aponta a tua câmara para agarrar este dinheiro - A Organizar os Seus Contactos - Este número de telefone não está nos seu seus contactos. Pode mesmo assim convidá-lo para a Code. - Introduza o seu número de telemóvel, incluindo o código do país. Certifique-se de que utiliza o número de telemóvel no qual recebeu o convite. - Desenvolvido por - Agora pode solicitar recompensas - Bónus de referência recebido - Enviar %1$s - Aqui está %1$s - Pedir um novo em %1$s - Digitaliza este código QR com a câmara do teu telemóvel para descarregar a aplicação Code Wallet - Digitalize para transferir a\naplicação Code Wallet - Procurar moedas - Procurar por contactos - Foi enviada uma mensagem SMS para o seu número de telefone com um código de verificação. Introduza acima o código de verificação. - Alguém enviou-te dinheiro - Recebeu uma recompensa - Para ler códigos, tem de ativar a sua câmara - O seu Cartão de Recompensas permite-lhe receber recompensas de utilizadores da Code de todo o mundo. Para ter acesso ao seu Cartão de Recompensas, conecte a sua identidade X. - O seu Cartão de gorjetas permite-lhe receber gorjetas de utilizadores do Code em todo o mundo. Para aceder ao seu Cartão de gorjetas, faça uma publicação no X. - O seu cartão de gorjetas permite-lhe receber gorjetas de utilizadores do Code em todo o mundo. Para aceder ao seu cartão de gorjetas, envie uma mensagem para @getcode no X. - Permita que o Code lhe envie notificações quando receber gorjetas de outros utilizadores do Code. - Digite \"%1$s\" - Fizemos algumas alterações para melhorar a experiência. Terá que atualizar a aplicação para continuar a usar a Code. - Conta de proprietário válida - Conta de token válida - O valor de Kin muda. - foi-lhe devolvido - Para onde gostaria de levantar o seu Kin? - Verifique a sua identidade para levantar kin. - Depositou - Deu - Tem %1$d Convites - A Code neste momento só pode ser acedida através de convite. Tem %1$d restantes. - Pagou - Deu - Recebeu - Enviou - Gastou - Deu uma recompensa - Levantou - Os seus fundos foram levantados com sucesso. - A sua conta X foi conectada com êxito à sua conta Code. Já pode solicitar Recompensas. - Levantamento Com Sucesso - Conta X conectada com êxito - Acesso Expirado - Chave de Acesso - Definições da Aplicação - Ativar Câmara Automaticamente - Saldo - Bandeiras Beta - Bónus - Comprar e Vender Kin - Pagamentos em dinheiro - Equipa Code - Aquisições de Kin - Pagamentos Web - Recompensas - Opções de Depuração - Depositar Kin - Depositado - Introduza as Palavras Chave de Acesso - Introduza o Número de Telefone - Falhou - Guiar FAQ - Deu - Receber dinheiro - Iniciar um Amigo na Code - Obter Kin - Obter mais Kin - Dar dinheiro - Dar Kin - Fundos insuficientes - Convidar um Amigo - Oferta por tempo limitado - Ligado - Moeda Local - A Minha Conta - Não Ligado - Outras Moedas - Pago - Pendente - Número de Telefone - Política de Privacidade - Adquirido - Conta X conectada com êxito - Receber gorjetas - Recebeu - Moedas Recentes - Refira a uma amigo, ganhe 5€ - Bónus de Referência - Solicitar dinheiro - Solicitar Kin - Solicitar uma recompensa - Pedir Face ID - Pedir Código de Acesso - Pedir Touch ID - Resultados - Devolvido - Selecione uma conta - Selecione um país - Selecione uma Moeda - Enviado - Gastou - Mudar de Conta - Termos de Serviço - Cartão de Dica - Dar recompensa em Kin - Ativar notificações do Code - Desconhecido - Atualização Necessária - Verificar Número de Telefone - Bónus de Boas-vindas - Levantar Kin - Levantou - A Sua Chave de Acesso - Toque no logotipo para partilhar a ligação de transferência da aplicação - diff --git a/apps/codeApp/src/main/res/values-ro/strings-localized.xml b/apps/codeApp/src/main/res/values-ro/strings-localized.xml deleted file mode 100644 index 6b4322362..000000000 --- a/apps/codeApp/src/main/res/values-ro/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Adăugați numerar cu un card de debit - Permite accesul la fotocameră - Permite accesul la contacte - Permite notificările push - Sold - Cumpără monede Kin - Cumpără mai multe monede Kin - Anulare - Anulează trimiterea - Chat - Colectați acești bani - Confirmare - Conectează-te la X - Continuă - Copiat - Copiază - Copiază adresa - Creează un cont - Creează un nou cont Code - Șterge contul - Gata - Descărcați-o acum - Activează Face ID - Activează Touch ID - Ieșire - Trimite - Dă Kin - Invită - Invitații - Alătură-te listei de așteptare - Mai târziu - Află cum să cumperi Kin - Află cum să vinzi Kin - Conectează un număr de telefon - Conectare - Deconectare - Trimite mesaj către @getcode pentru Conectare - Pune în surdină - Următorul - Nu, încearcă din nou - Nu acum - OK - Deschide setările - Lipește - Lipește din clipboard - Postează pentru a conecta contul - Pune în portofel - Primește - Recuperează un cont existent - Reamintește - Elimină numărul de telefon - Elimină numărul tău de telefon - Solicită un bacșiș - Salvează cheia de acces în Fotografiile mele - Salvează în fotografii - Trimite - Trimite verificarea pentru Code - Distribuie - Distribuie ca adresă URL - Partajați link-ul de descărcare - Partajează acest videoclip - Arată cardul meu de bacșiș - Pornește camera - Abonează-te - Glisează pentru a te conecta - Glisează pentru a plăti - Glisează pentru a da bacșiș - Încearcă un alt cont Code - Trimite-i un Tweet - Deblochează Code - Scoate din surdină - Dezabonare - Actualizare - Vizualizează cheia de acces - Retrage Kin - Ai notat în schimb cele 12 cuvinte? - Da - Da, retrage Kin - Da, le-am notat - și - Kin - în Kin - Va șterge de pe serverele Code toate informațiile asociate cu contul tău (număr de telefon, contacte, istoricul tranzacțiilor) - Îți poți accesa contul în alte aplicații de portofel de criptomonedă folosindu-ți Fraza secretă recuperare. Nu-ți vei putea folosi contul în Code. - Nu îți va șterge contul din blockchain - Ce va face ștergerea - Ce se va întâmpla - Ce nu va face ștergerea - Acest kin a fost colectat deja de altcineva. - Acest Kin a fost deja colectat de cineva. - Permite accesul la fotocameră din Setări pentru a folosi Code. - Acest numerar a expirat. - Reintrodu numărul de telefon și încearcă din nou. - Te rugăm să permiți accesul la contacte din Setări pentru a putea trimite invitații. - Code nu este momentan disponibil în țara dumneavoastră. - Nu putem oferi suport pentru dispozitivul dumneavoastră în acest moment - Suportul pentru eSims va veni, probabil, într-o versiune viitoare a Code. - Ceva nu a funcționat bine. Acest Kin nu a putut fi colectat. - Nu ne așteptam să se întâmple așa. Ceva a mers rău. Te rugăm să încerci din nou să creezi acest cont. - Pentru a-ți salva cheia de acces este nevoie ca în Setări să permiți Code să acceseze Fotografiile. - Ceva a mers prost. Vă rugăm să vă asigurați că numărul dumneavoastră de telefon este introdus în mod corect. - Ceva a mers rău. Încearcă din nou. - Retragerea fondurilor tale a eșuat. Ceva nu a mers cum trebuie, te rugăm să încerci din nou să efectuezi retragerea. - Code este conceput pentru tranzacții mici, de toate zilele, în sumă de %1$s sau mai puțin. - Code este conceput pentru tranzacții mici, de zi cu zi. Limita sumelor pe care le poți trimite va fi mărită mâine. - Pentru a afla cum poți obține mai mulți Kin, mergi la Întrebări frecvente în Setări. - Te rugăm să introduci un alt cod de invitație și să încerci din nou. - Te rugăm să introduci un număr de telefon valid și să încerci din nou. - Acesta este un card de bacșiș invalid. - Introdu un cod valid și încearcă din nou. - Ne pare rău, avem probleme de rețea. Te rugăm să încerci din nou să-ți inviți prietenul. - Acest kin a fost returnat automat expeditorului deoarece nu a fost colectat în termen de 24 de ore. Roagă-l să trimită din nou kin-ul. - Introdu din nou numărul de telefon și încearcă din nou. - Vă rugăm să încercați să obțineți o imagine care să prezinte codul mai clar. - Verifică-ți conexiunea la internet sau încearcă din nou mai târziu. - Code este în prezent doar pe bază de invitație. Îți vom da de știre când vor fi disponibile alte invitații. - Alătură-te la lista de așteptare pentru %1$s - Cantitatea maximă pe care o poți achiziționa este %1$s. Te rugăm să introduci o sumă mai mică. - Cantitatea minimă pe care o poți achiziționa este %1$s. Te rugăm să introduci o sumă mai mare. - Cheia ta de acces a inițiat o deblocare. Ca urmare, nu vei mai putea utiliza această cheie de acces în Code. - Trimite un tweet acestei persoane pentru a-i activa Cardul de bacșișuri. - Cantitatea maximă pe care o poți da ca bacșiș este %1$s. Te rugăm să introduci o sumă mai mică. - Cantitatea minimă pe care o poți da ca bacșiș este %1$s. Te rugăm să introduci o sumă mai mare. - Poți crea un număr limitat de conturi în fiecare zi. - În momentul de față, Code este limitat la un singur cont pe dispozitiv. Suportul pentru mai multe conturi va apărea, probabil, într-o versiune viitoare a Code. - În momentul de față, Code este limitat la un singur cont per număr de telefon. Suportul pentru mai multe conturi va apărea, probabil, într-o versiune viitoare a Code. - Kin-ul a fost colectat deja - Kin deja colectat - Este necesar accesul la fotocameră - Numerar expirat - Codul de verificare a expirat - Este necesar accesul la contacte - Țară neacceptată - Dispozitiv neacceptat - eSims nu este acceptat în prezent - Colectare nereușită - Crearea contului a eșuat - Salvarea a eșuat - Trimiterea a eșuat - Confirmarea a eșuat - Tranzacția a așuat - A fost atinsă limita de trimitere - Limita zilnică a fost atinsă - Sumă în Kin insuficientă - Codul de invitație nu este valid sau a expirat - Număr de telefon nevalid - Card de bacșiș invalid - Cod invalid - Invitația a eșuat. - Linkul a expirat - Ai atins numărul maxim de tentative - Nu s-a găsit niciun Code - Fără conexiune la internet - Nu ai nicio invitație. - Nu ai fost încă invitat - Achiziție prea mare - Achiziție prea mică - Cheia de acces nu mai poate fi utilizată în Code - Cardul de bacșișuri nu este activat încă - Bacșiș prea mare - Bacșiș prea mic - Au fost create prea multe conturi - Cont deja creat - Cont deja creat - Considerăm că plățile ar trebui să fie simple, fiabile și globale. Utilizând tehnologia blockchain avansată, Code oferă caracteristici pe care aplicațiile de plată tradiționale nu le pot oferi, cum ar fi transferuri globale peer to peer, microplăți care deblochează articole individuale online și bacșișuri fără taxe pentru creatorii tăi preferați. - Kin este o criptomonedă precum Bitcoin, dar este, de asemenea, concepută pentru plăți rapide și la costuri reduse. - Ca și în cazul Bitcoin, nu este disponibilă decât o cantitate limitată de Kin. Dacă mai multe persoane cumpără Kin, valoarea crește, iar dacă mai multe persoane vând Kin, valoarea scade. Această dinamică permite tuturor celor care dețin Kin să beneficieze de creșterea de valoare în cazul în care Kin este adoptat pe scară largă. - Poți cumpăra Kin cu cardul tău de debit. Această opțiune este accesibilă în fila Get Kin. - Da, o poți face. Vânzarea Kin este acceptată de o serie de exchange-uri de criptomonedă. - Există trei moduri principale prin care poți ajuta: vorbește despre experiența ta cu Code pe rețelele de socializare, încurajează-ți prietenii să încerce și ei Code și încurajează site-urile tale preferate să integreze plățile Code, sugerându-le să viziteze [getcode.com](https://getcode.com). - Ce este Code? - De ce plățile Code sunt exprimate în Kin? - De ce Kin își schimbă valoarea? - Cum pot cumpăra mai mult Kin? - Pot să vând Kin? - Cum pot fi de ajutor? - îți exprimi acordul cu - Apăsând pe „Creează un cont\" sau pe „Conectare\", - Fotocamera este folosită pentru a primi Kin. Te rugăm să permiți accesul la fotocameră pentru a continua. - Avem nevoie de notificări push pentru a-ți putea trimite cu promptitudine informații cu privire la contul tău. - Retragerile sunt ireversibile și nu pot fi anulate odată ce au fost inițiate. - Vei pierde toate fondurile din acest cont. Contul va fi șters permanent și irevocabil. Ești sigur că dorești să ștergi acest cont? - Orice kin care nu este colectat în termen de 24 de ore va fi returnat automat în soldul tău. - Va fi necesar să reîncepi crearea contului și să-ți verifici din nou numărul de telefon. - Poți intra din nou în acest cont utilizând Cheia ta de acces - Nu vei fi notificat cu privire la niciun mesaj nou de la %1$s. Poți reactiva notificările în orice moment. - În prezent sunt suportate doar conturile create prin Code. - Prietenii tăi nu vor mai putea să te găsească folosind acest număr de telefon. - Vei fi notificat cu privire la toate mesajele noi de la %1$s]. Poți dezactiva notificările în orice moment. - Nu vei mai primi niciun mesaj de la %1$s până când nu îi plătești din nou. - Cheia de acces îți permite să intri în contul tău Code. Păstreaz-o secretă și în siguranță. - Aceste 12 cuvinte reprezintă singura modalitate prin care-și poți recupera contul Code. Asigură-te că le-ai notat și că le păstrezi în confidențialitate și siguranță. - Ești sigur? - Ești sigur că dorești să ștergi acest cont? - Ai trimis linkul? - Ești sigur că dorești să ieși? - Ești sigur că dorești să te deconectezi? - Dezactivezi notificările pentru %1$s? - Nu este un cont Code. - Ești sigur? - Reactivezi notificările pentru %1$s? - Te dezabonezi de la %1$s? - Dorești să-ți vizualizezi Cheia de acces? - Ești sigur? - Suma de %1$s Kin a fost depusă în contul dumneavoastră. - %1$s pe care l-ai trimis ieri nu a fost colectat. Acesta a fost returnat automat în soldul dumneavoastră. - Ajutați un prieten să înceapă cu Code și primiți 5 dolari - Trimiteți bani prin orice aplicație de mesagerie - Ați primit %1$s în Kin pentru că ați trimis cuiva primul Kin. - Acum poți cere bacșișuri. - Depozit primit - Kin returnat - Nou pe Code - Bonus de recomandare primit - Cont X conectat - Codul tău de invitație pentru a accesa Code a expirat. Te poți deconecta pentru a folosi un alt cont care are o invitație validă. - Cheia ta de acces reprezintă singura modalitate prin care îți poți accesa fondurile. Asigură-te că o păstrezi în confidențialitate și siguranță. - Verifică-ți identitatea pentru a-ți vizualiza cheia de acces. - Apasă pe pictograma Google Lens pentru a deschide codul QR pentru conectarea la Code. Alternativ, te poți conecta manual introducând cele 12 cuvinte în ecranul de conectare al Code. - Atenție! Această imagine asigură accesul la toate fondurile pe care le aveți în Code. Nu divulgați această imagine nimănui altcuiva. Păstrați-o în condiții de siguranță și protecție. - Code îți permite să primești Kin încadrând cu fotocamera factura digitală de pe telefonul unui alt utilizator - Va fi nevoie să permiți accesul la fotocamera pentru a putea primi Kin - Autentificați-te pentru a accesa Code. - Cumpărați Kin - Cumpără kin (în curând) - În prezent, cumpărarea și vânzarea de Kin constituie un proces complex. Aceste procese se vor simplifica în timp. Dacă dorești să înveți cum să cumperi și să vinzi Kin astăzi, poți viziona videoclipurile de prezentare de mai jos. - Poți da cel mult %1$s - Poți da un bacșiș de până la %1$s - Monedele tale Kin sunt acum disponibile pentru a fi utilizate în aplicația Code - Ai trimis cuiva primul său Kin! Iată bonusul tău de recomandare: - Îți mulțumim că ai deschis Code, achiziția ta este în curs de finalizare. Monedele tale Kin ar trebui să fie disponibile în curând - Ești aproape gata! Deschide aplicația Code pentru a finaliza achiziția - Bonus de bun-venit - Alege o țară - Urmează în curând - \@getcode Vreau să-mi conectez contul X ca să primesc sfaturi de la oameni din toată lumea - Șterge - Asigură-te că ai salvat Fraza ta secretă de recuperare, apoi scrie „Șterge\" pentru a-ți șterge contul Code. Această acțiune este ireversibilă. - Nu ai primit un cod la %1$s? - Nu ai primit codul? Trimite din nou - Este nevoie să-ți verifici identitatea pentru a putea dezactiva Face ID. - Nu aveți aplicația Code Wallet? - Nu ai încă niciun Kin. - Activează Face ID pentru a ameliora securitatea tranzacțiilor efectuate în Code. - Introdu adresa de destinație - Introdu maxim %1$s - Primiți 5 dolari în Kin în mod gratuit atunci când convingeți un prieten să se înscrie la Code și îi trimiteți primul său Kin. - Code folosește criptomoneda kin pentru plăți. Iată câteva modalități de a obține mai mulți kin în portofelul tău Code. - Obțineți primul dumneavoastră 1 dolar în Kin în mod gratuit - Verifică-ți identitatea pentru a trimite Kin. - Depozitează Kin în portofelul tău Code trimițând o sumă în Kin la adresa ta de depozit de mai jos. Apasă pentru a copia. - Te rugăm să obții mai mulţi Kin și apoi să încerci să plătești din nou - Cont de destinație nevalid - Asigură-te că adresa la care retragi bani a fost inițializată de furnizorul portofelului tău. O scurtătură pentru a realiza aceasta este să schimbi mai întâi o cantitate mică de SOL pentru Kin în portofelul către care încerci să transferi. - Cod de invitație - Momentan Code este accesibil doar pe bază de invitație. Vei avea nevoie de un cod de invitație pentru a accesa aplicația. - %1$d invitații - Code este o nouă aplicație de portofel criptografic, care în prezent este doar pe bază de invitație. Pentru a descărca Code, accesați %1$s. - Află mai multe - Numărul tău de telefon este conectat la acest cont Kin. Prietenii te pot găsi folosind acest număr de telefon. - Îmi conectez contul X cu @getcode pentru a putea primi bacșișuri de la oameni din toată lumea. - Se încarcă soldul și istoricul tranzacțiilor - Caută în fotografii Cheia de acces pe care ai salvat-o când ai creat contul. - Momentan ești conectat la un cont. Înainte de a continua, asigură-te că ți-ai salvat cheia de acces. Dorești să te deconectezi și să te conectezi la un cont nou? - Neconectat\nNumăr de telefon - Nu ai un număr de telefon care să fie conectat la acest cond Code. Conectează unul pentru a te asigura că prietenii tăi te pot găsi. - Fără conexiune la rețea - Pe Code - Deschideți aplicația Code și îndreptați camera foto pentru a lua acești bani - Organizează-ți contactele - Acest număr de telefon nu se regăsește printre Contactele tale. Poți totuși invita trimite o invitație pentru Code. - Introdu numărul tău de telefon, inclusiv prefixul internațional. Asigură-te că folosești același număr de telefon la care ai primit invitația. - Realizat de - Acum poți solicita bacșișuri - Bonus de recomandare primit - Trimite %1$s - Iată %1$s - Cere unul nou în %1$s - Scanați acest cod QR cu camera telefonului dumneavoastră pentru a descărca aplicația Code Wallet - Scanați pentru a descărca\naplicația Portofelul Code - Caută valute - Caută în contacte - Un mesaj SMS cu un cod de verificare a fost trimis la numărul tău de telefon. Te rugăm să introduci mai sus codul de verificare. - Cineva v-a trimis bani - Cineva ți-a dat un bacșiș - Trebuie să pornești camera foto pentru a scana Coduri - Cardul tău de bacșiș îți permite să primești bacșișuri de la utilizatorii Code din întreaga lume. Pentru a accesa Cardul tău de bacșiș, conectează-te cu contul tău X. - Cardul tău de bacșișuri îți permite să primești bacșișuri de la utilizatorii Code din întreaga lume. Pentru a-ți accesa Cardul de bacșișuri, postează pe X. - Cardul dumneavoastră de bacșiș (Tip Card) vă permite să primiți bacșișuri de la utilizatorii Code din întreaga lume. Pentru a vă accesa cardul de bacșiș, trimiteți un mesaj @getcode pe X. - Permite Code să-ți trimită notificări atunci când primești Sugestii de la alți utilizatori Code. - Scrie „%1$s\" - Am făcut unele modificări pentru a îmbunătăți experiența de utilizare. Va fi nevoie să actualizezi aplicația pentru a putea folosi mai departe Code. - Cont de proprietar valid - Cont jeton valid - Valoarea Kin-ului se modifică. - ți s-a returnat - Unde ai dori să-ți retragi suma în Kin? - Verifică-ți identitatea pentru a retrage Kin. - Ai depus - Ai dat - Ai %1$d invitații - Accesul la Code este în prezent doar pe bază de invitație. Îți rămân %1$d. - Ai plătit - Ai dat - Ai primit - Ai trimis - Ai cheltuit - Ai dat cuiva un bacșiș - Ai retras - Fondurile tale au fost retrase cu succes. - Contul tău X a fost conectat cu succes la contul tău Code. Acum poți solicita bacșișuri. - Retragerea a avut succes - Contul X a fost conectat cu succes - Accesul a expirat - Cheie de acces - Setări aplicație - Pornirea automată a camerei - Sold - Steaguri Beta - Bonus - Cumpără și vinde Kin - Plăți în numerar - Echipa de programare - Achiziții monede Kin - Plăți web - Bacșișuri - Opțiuni de depanare - Depozitează Kin - Depozitat - Introdu cuvintele cheie de acces - Introdu numărul de telefon - Eșuat - Întrebări frecvente - Trimis - Obțineți numerar - Ajutați un prieten să înceapă cu Code - Obține kin - Obține mai mulţi Kin - Trimite cash - Trimite Kin - Fonduri insuficiente - Invită un prieten - Ofertă limitată în timp - Conectat - Valuta locală - Contul meu - Neconectat - Alte valute - Plătit - În așteptare - Număr de telefon - Politică de confidențialitate - Achiziționat - Contul X a fost conectat cu succes - Primește Sugestii - Primit - Valute folosite recent - Recomandați un prieten, primiți 5 dolari - Bonus de recomandare - Solicitați numerar - Solicită monede Kin - Solicită un bacșiș - Solicită Face ID - Necesită parolă - Solicită Touch ID - Rezultate - Returnat - Selectează un cont - Selectează o țară - Selectează o valută - Trimis - Cheltuiți - Folosește alt cont - Termeni de utilizare - Card de bacșiș - Trimite bacșiș monede Kin - Activează notificările pentru Code - Necunoscut - Este necesară actualizarea - Verifică numărul de telefon - Bonus de bun-venit - Retrage Kin - Retras - Cheia ta de acces - Atingeți logo-ul pentru a partaja link-ul de descărcare a aplicației - diff --git a/apps/codeApp/src/main/res/values-ru/strings-localized.xml b/apps/codeApp/src/main/res/values-ru/strings-localized.xml deleted file mode 100644 index a328961a1..000000000 --- a/apps/codeApp/src/main/res/values-ru/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Добавить наличные с помощью дебетовой карты - Разрешить доступ к камере - Разрешить доступ к контактам - Разрешить push-уведомления - Баланс - Купить кин - Купить больше Kin - Отменить - Отменить отправку - Общаться - Заберите эти деньги - Подтвердить - Подключиться к X - Продолжить - Скопировано - Копировать - Копировать адрес - Создать учетную запись - Создать новую учетную запись Code - Удалить учетную запись - Готово - Скачать сейчас - Включить Face ID - Включить Touch ID - Выйти - Дать - Дать Kin - Пригласить - Приглашения - Присоединиться к списку ожидания - Позже - Узнать, как купить Kin - Узнать, как продать Kin - Привязать номер телефона - Войти - Выйти - Отправьте сообщение @getcode для привязки - Отключить звук - Далее - Нет, попробовать еще раз - Не сейчас - Хорошо - Открыть настройки - Вставить - Вставить из буфера обмена - Опубликуйте пост, чтобы подключить аккаунт - Положить в кошелек - Получить - Восстановить существующую учетную запись - Напомнить - Удалить номер телефона - Удалить ваш номер телефона - Запросить чаевые - Сохранить ключ доступа в мои фото - Сохранить в фото - Отправить - Отправить код проверки - Поделиться - Поделиться в виде URL - Поделитесь ссылкой для загрузки - Поделитесь этим видео - Показать мою карту чаевых - Запуск камеры - Подписаться - Войдите, проведя пальцем - Проведите, чтобы оплатить - Прведите, чтобы дать чаевые - Попробовать другую учетную запись Code - Отправьте им твит - Разблокировать Code - Включить звук - Отписаться - Обновить - Посмотреть ключ доступа - Вывести Kin - Вы записали вместо этого 12 слов? - Да - Да, вывести Kin - Да, я записал(-а) их - и - Kin - Kin - Удаление всей связанной с вашей учетной записью информации на серверах Code (номер телефона, контакты, история транзакций) - Вы сможете получить доступ к своей учетной записи в других приложениях криптокошельков с помощью своей секретной фразы восстановления. Вы не сможете использовать свою учетную запись в Code - Удаление вашей учетной записи из блокчейна - Что произойдет после удаления - Что произойдет - Что не произойдет после удаления - Этот Kin уже был получен кем-то другим. - Этот Kin уже кем-то получен. - Разрешите доступ к камере в настройках, чтобы пользоваться Code. - Срок действия этих денег истек. - Введите свой номер телефона повторно и попробуйте снова. - Для отправки приглашений предоставьте доступ к контактам в настройках. - В настоящее время приложение Code недоступно в вашей стране. - В настоящее время мы не можем поддерживать ваше устройство - Поддержка eSim-карт появится, вероятно, в будущей версии Code. - Что-то пошло не так. Этот Kin не удалось собрать. - Мы не ожидали этого. Что-то пошло не так. Попробуйте создать эту учетную запись снова. - Разрешите Code доступ к фото в настройках, чтобы сохранить свой ключ доступа. - Что-то пошло не так. Убедитесь, что ваш номер вашего телефона введен правильно. - Что-то пошло не так. Попробуйте снова. - Не удалось вывести ваши средства. Что-то пошло не так, попробуйте вывести снова. - Code разработан для небольших ежедневных транзакций, не превышающих %1$s. - Code разработан для небольших ежедневных транзакций. Ваш дневной лимит увеличится завтра. - Чтобы узнать, как получить больше Kin, посетите раздел с частыми вопросами в настройках. - Введите другой пригласительный код и повторите попытку. - Введите верный номер телефона и повторите попытку. - Это недействительная карта чаевых. - Введите верный код и попробуйте снова. - Извините, у нас возникла проблема с сетью. Попробуйте пригласить друга снова. - Этот Kin был автоматически возвращен отправителю, так как не был получен в течение 24 часов. Пожалуйста, попросите их снова прислать Kin. - Введите свой номер телефона повторно и попробуйте снова. - Постарайтесь получить изображение, на котором будет более четко виден код. - Проверьте свое соединение с интернетом или попробуйте снова позже. - Code сейчас доступен только по приглашению. Мы сообщим вам, когда будет доступно больше приглашений. - Для присоединения к списку ожидания посетите %1$s - Максимальное количество, которое вы можете приобрести, — %1$s. Введите меньшее количество. - Минимальное количество, которое вы можете приобрести, — %1$s. Введите большее количество. - Ваш ключ доступа инициировал снятие блокировки. В результате вы больше не сможете использовать этот ключ доступа в Code. - Отправьте твит этому человеку, чтобы активировать его карточку чаевых. - Максимальный размер чаевых — %1$s. Введите меньшую сумму. - Минимальный размер чаевых — %1$s. Введите большую сумму. - Вы можете создать лишь столько новых учетных записей в день. - В настоящее время приложение Code ограничено одной учетной записью на устройство. Поддержка нескольких учетных записей, вероятно, появится в будущей версии Code. - В настоящее время приложение Code ограничено одной учетной записью на номер телефона. Поддержка нескольких учетных записей, вероятно, появится в будущей версии Code. - Kin уже получен - Kin уже получен - Требуется доступ к камере - Срок действия денег истек - Срок действия кода проверки истек - Необходим доступ к контактам - Страна не поддерживается - Устройство не поддерживается - В настоящее время eSim-карты не поддерживаются - Не удалось собрать - Не удалось создать учетную запись - Не удалось сохранить - Не удалось отправить - Не удалось подтвердить - Сбой транзакции - Достигнут лимит отправки - Достигнут дневной лимит - Недостаточно Kin - Пригласительный код недействителен или просрочен - Неверный номер телефона - Недействительная карта чаевых - Неверный код - Сбой приглашения - Срок действия ссылки истек - Использовано максимальное количество попыток - Код не найден - Нет соединения с интернетом - У вас нет приглашений - Вас еще не пригласили - Слишком большая сумма покупки - Слишком маленькая сумма покупки - Ключ доступа больше нельзя использовать в Code - Карточка чаевых еще не активирована - Чаевые слишком большие - Чаевые слишком маленькие - Создано слишком много учетных записей - Учетная запись уже создана - Учетная запись уже создана - Мы считаем, что платежи должны быть простыми, мощными и глобальными. Благодаря использованию передовой технологии блокчейна Code предлагает функции, недоступные традиционным платежным приложениям, такие как глобальные одноранговые переводы, микроплатежи, открывающие доступ к отдельным статьям в Интернете, а также чаевые для ваших любимых авторов с нулевой комиссией. - Kin — это криптовалюта, подобная биткойну, но она также предназначена для быстрых и недорогих платежей. - Как и в случае с биткойном, доступно только ограниченное количество Kin. Если больше людей покупают Kin, стоимость растет, а если больше людей продают Kin, стоимость падает. Эта динамика позволяет каждому, кто владеет Kin, участвовать в создании стоимости, если Kin будет становиться все более распространенной криптовалютой. - Вы можете купить Kin с помощью дебетовой карты. Покупка доступна на вкладке «Получить Kin». - Да, это возможно. Продажа Kin поддерживается на ряде криптовалютных бирж. - Вы можете помочь тремя основными способами: рассказать о своем опыте работы с Code в социальных сетях, предложить своим друзьям опробовать код самостоятельно и убедить ваши любимые веб-сайты интегрировать платежи Code, попросив их посетить [getcode.com](https //getcode.com). - Что такое Code? - Почему платежи Code деноминируются в Kin? - Почему стоимость Kin меняется? - Как мне купить больше Kin? - Можно ли продать Kin? - Чем я могу помочь? - принимаете наши - Нажимая на \"Создать учетную запись\" или \"Войти\", вы - Ваша камера используется для получения Kin. Разрешите доступ к камере, чтобы продолжить. - Мы используем push-уведомления, чтобы своевременно отправлять вам информацию о вашей учетной записи. - Выведения безвозвратны и их нельзя отменить после инициации. - Все средства на этом счету будут потеряны. Удаление своего счета является безвозвратным. Вы точно хотите удалить этот счет? - Любой Kin, который не будет получен в течение 24 часов, автоматически возвращается на ваш баланс. - Вам придется начать создание учетной записи заново и подтвердить свой номер телефона снова. - Вы можете войти в эту учетную запись снова при помощи своего ключа доступа - Вы не будете получать уведомления о новых сообщениях от %1$s. Вы можете включить звук в любое время. - На данный момент поддерживаются только учетные записи, созданные через Code. - Ваши друзья больше не смогут найти вас по этому номеру телефона. - Вы будете получать уведомления обо всех новых сообщениях от %1$s. Вы можете отключить звук в любое время. - Вы не будете получать сообщения от %1$s до тех пор, пока не заплатите ему (ей) снова. - Ваш ключ доступа предоставит доступ к вашей учетной записи Code. Храните его в безопасном месте и не разглашайте. - Эти 12 слов — единственный способ восстановить вашу учетную запись Code. Обязательно запишите их, храните в безопасном месте и не разглашайте. - Вы уверены? - Вы точно хотите удалить эту учетную запись? - Вы отправили ссылку? - Вы точно хотите выйти? - Вы точно хотите выйти? - Отключить звук %1$s? - Не является учетной записью Code - Вы уверены? - Включить звук %1$s? - Отписаться от %1$s? - Показать ваш ключ доступа? - Вы уверены? - На ваш счет зачислено %1$s Kin. - Отправленный вчера %1$s не был получен. Он был автоматически возвращен на ваш баланс. - Пригласите друга начать пользоваться Code и получите 5 долларов США - Отправляйте наличные через любой мессенджер - Вы получили %1$s Kin за отправку кому-то его/ее первого Kin. - Теперь вы можете запросить чаевые. - Депозит получен - Kin вернулся - Новое в Code - Получен реферальный бонус - Подключен аккаунт X - Срок действия вашего приглашения в Code истек. Вы можете выйти и воспользоваться другой учетной записью с действующим статусом приглашения. - Ваш ключ доступа — единственный способ получить доступ к вашим средствам. Храните его в безопасном месте и не разглашайте. - Подтвердите свою личность, чтобы посмотреть свой ключ доступа. - Нажмите на значок Google Lens, чтобы открыть QR-код и войти в Code. Вы также можете войти вручную, введя 12 слов на экране входа Code. - Внимание! Это изображение дает доступ ко всем вашим средствам в Code. Не делитесь этим изображением больше ни с кем. Храните его в безопасном и надежном месте. - Code позволяет вам получать Kin, наводя свою камеру на цифровую квитанцию на телефоне другого пользователя. - Вы должны разрешить доступ к камере, чтобы иметь возможность получать Kin - Пройдите аутентификацию для доступа к Code. - Купить Kin - Купить Kin (скоро) - Купля-продажа Kin в настоящее время представляет собой сложный процесс. Со временем эти процессы станут проще. Если вы хотите узнать, как покупать и продавать Kin сегодня, вы можете посмотреть ниже видеоинструкции. - Вы можете отправить не более %1$s - Вы можете оставить чаевые только до %1$s - Kin теперь доступны для использования в приложении Code - Вы послали кому-то его/ее первый Kin! Вот ваш реферальный бонус: - USDC конвертируются в Kin. Это должно занять примерно одну минуту - Вы успешно внесли USDC. Чтобы завершить покупку, откройте приложение Code - Приветственный бонус - Выбрать страну - Скоро выйдет - \@getcode Я хотел бы привязать свой аккаунт X, чтобы получать советы от людей со всего мира - Удалить - Убедитесь, что вы сохранили у себя свою секретную фразу восстановления, а затем введите «‎Удалить», чтобы удалить свою учетную запись Code. Это действие невозможно отменить. - Не получили код на %1$s? - Не получили код? Отправить повторно - В случае отключения Face ID вам нужно подтвердить свою личность. - У вас нет приложения Code Wallet? - У вас еще нет Kin. - Включите Face ID, чтобы повысить защиту транзакции в Code. - Введите адрес назначения - Введите до %1$s - Получите Kin в эквиваленте 5 долларов бесплатно, попросив друга зарегистрироваться в Code и отправив ему его первый Kin. - Code использует криптовалюту Kin для платежей. Вот несколько способов получить больше Kin в свой кошелек Code. - Получите бесплатно свой первый Kin в эквиваленте 1 долл. США - Подтвердите свою личность, чтобы дать kin. - Внесите Kin на свой кошелек Code, отправив Kin на свой депозитный адрес ниже. Коснитесь, чтобы скопировать. - Получите больше Kin и попробуйте заплатить еще раз - Неверный целевой счет - Пожалуйста, убедитесь, что адрес, на который вы выводите средства, был инициализирован провайдером вашего кошелька. Для этого можно сначала обменять небольшое количество SOL на Kin в кошельке, на который вы пытаетесь отправить средства. - Пригласительный код - В настоящее время Code только по приглашению. Для доступа к приложению вам понадобится пригласительный код. - %1$d приглашений - Code — это новое приложение-криптокошелек, в настоящее время доступное только по приглашениям. Чтобы загрузить Code, перейдите на %1$s - Узнать больше - Ваш номер телефона привязан к этой учетной записи Code. Друзья смогут вас по нему найти. - Я подключаю свой аккаунт X к @getcode, чтобы получать советы от людей со всего мира. - Загрузка вашего баланса и истории транзакций - Проверьте свои фото на наличие ключа доступа, который вы сохранили при создании своей учетной записи. - Вы вошли в аккаунт. Прежде чем продолжить, убедитесь, что вы сохранили ключ доступа. Хотите выйти из системы и войти под новым аккаунтом? - Номер телефона\nне привязан - У вас нет номера телефона, привязанного к этой учетной записи Code. Привяжите его, чтобы ваши друзья смогли вас найти. - Нет подключения к сети - В Code - Откройте приложение Code и наведите камеру, чтобы получить эти деньги. - Организация ваших контактов - Этого номера нет в ваших контактах. Но вы все равно можете пригласить этого человека в Code. - Введите свой номер телефона вместе с кодом страны. Убедитесь, что используете тот же номер, на который пришло приглашение. - На платформе - Теперь вы можете запросить чаевые - Реферальный бонус получен - Отправить %1$s - Вот %1$s - Запросить новый через %1$s - Отсканируйте этот QR-код с помощью камеры вашего телефона, чтобы загрузить приложение Code Wallet. - Отсканируйте, чтобы загрузить\nприложение Code Wallet - Искать валюты - Искать контакты - На ваш номер телефона было отправлено SMS-сообщение с кодом подтверждения. Введите код подтверждения выше. - Кто-то отправил вам деньги - Кто-то дал вам чаевые - Вам нужно запустить камеру для сканирования кодов - Ваша карта чаевых позволяет вам получать чаевые от пользователей Code со всего мира. Чтобы получить доступ к своей карте чаевых, подключите свою личность X. - Ваша карта чаевых позволяет вам получать чаевые от пользователей Code со всего мира. Чтобы получить доступ к карточке чаевых, опублиуйте пост в X. - Ваша карта чаевых позволяет вам получать чаевые от пользователей Code со всего мира. Чтобы получить доступ к сообщению вашей карты Чаевых @getcode на X. - Разрешите Code отправлять вам уведомления, когда вы получаете советы от других пользователей Code. - Введите «%1$s‎» - Мы кое-что изменили, чтобы улучшить ваш опыт пользования. Вам придется обновить приложение, чтобы продолжить пользоваться Code. - Действительный счет владельца - Действительный счет токенов - Стоимость Kin меняется. - вернулось вам - Куда хотите вывести свою Kin? - Подтвердите свою личность, чтобы вывести kin. - Вы внесли - Вы отдали - У вас есть %1$d приглашений - К Code сейчас можно получить доступ только по приглашению. У вас осталось %1$d. - Вы заплатили - Вы отдали - Вы получили - Вы отправили - Вы потратили - Вы дали кому-то чаевые - Вы вывели - Ваши средства успешно выведены. - Ваш аккаунт X был успешно подключен к вашему аккаунту Code. Теперь вы можете запросить чаевые. - Выведение прошло успешно - Аккаунт X успешно подключен - Время на доступ вышло - Ключ доступа - Настройки приложения - Автозапуск камеры - Баланс - Бета-флаги - Бонус - Купля и продажа Kin - Платежи наличными - Команда Code - Покупки за кины - Онлайн-платежи - Чаевые - Опции отладки - Внести Kin - Внес(-ла) - Введите слова ключа доступа - Введите номер телефона - Сбой - Частые вопросы - Дал(-а) - Получить наличные - Пригласите друга начать пользоваться Code - Получить Kin - Получить больше Kin - Дать деньги - Дал(-а) Kin - Недостаточно средств - Пригласить друга - Ограниченное по времени предложение - Привязан - Местная валюта - Моя учетная запись - Не привязан - Другие валюты - Оплачено - В ожидании - Номер телефона - Политика конфиденциальности - Куплено - Аккаунт X успешно подключен - Получать советы - Получил(-а) - Недавние валюты - Приведите друга, получите 5 долл. США - Реферальный бонус - Потребовать наличные - Запросить кин - Запросить чаевые - Запрашивать Face ID - Запросить код доступа - Запрашивать Touch ID - Результаты - Возвращено - Выберите учетную запись - Выберите страну - Выбрать валюту - Отправлено - Потрачено - Переключить учетные записи - Условия пользования - Карточка чаевых - Дать чаевые в кин - Включить уведомления от Code - Неизвестный - Требуется обновление - Подтвердить номер телефона - Приветственный бонус - Вывести Kin - Вывел(-а) - Ваш ключ доступа - Коснитесь логотипа, чтобы поделиться ссылкой для загрузки приложения - diff --git a/apps/codeApp/src/main/res/values-sk/strings-localized.xml b/apps/codeApp/src/main/res/values-sk/strings-localized.xml deleted file mode 100644 index 00913d9ea..000000000 --- a/apps/codeApp/src/main/res/values-sk/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Navýšiť hotovosť z debetnej karty - Povoliť prístup k fotoaparátu - Povoliť prístup ku kontaktom - Povoliť upozornenia push - Zostatok - Kúpiť kin - Kúpiť viac meny Kin - Zrušiť - Zrušiť odoslanie - Chat - Získajte tieto peniaze - Potvrdiť - Pripojiť sa k sieti X - Pokračovať - Skopírovaný - Kopírovať - Kopírovať adresu - Vytvoriť účet - Vytvorte si nový kódový účet - Vymazať účet - Hotovo - Stiahnite si ju už teraz - Povoliť Face ID - Povoliť Touch ID - Odísť - Dať - Dať Kin - Pozvať - Pozýva - Pridajte sa do zoznamu čakateľov - Neskôr - Naučte sa nakupovať Kiny - Naučte sa predávať Kiny - Prepojiť telefónne číslo - Prihlásiť sa - Odhlásiť sa - Pošlite správu @getcode na pripojenie - Stlmiť - Ďalší - Nie, skúsiť znova - Teraz nie - OK - Otvoriť nastavenia - Prilepiť - Prilepiť zo schránky - Uverejniť pre prepojenie účtu - Vložiť do peňaženky - Dostať - Obnoviť existujúci účet - Pripomenúť - Odstrániť telefónne číslo - Odstráňte svoje telefónne číslo - Požiadať o tip - Uložiť prístupový kľúč do mojich fotografií - Uložiť medzi fotky - Odoslať - Poslať overovací kód - Zdieľať - Zdieľať ako adresu URL - Zdieľať odkaz na stiahnutie - Zdieľajte toto video - Zobraziť moju tipovaciu kartu - Spustiť kameru - Prihlásiť sa na odber - Prihlásiť sa potiahnutím - Pre platbu potiahnite - Prejsť na možnosť Tip - Vyskúšajte iný kódový účet - Tweetujte ich - Odomknúť kód - Zrušiť stlmenie - Zrušiť odber - Aktualizovať - Zobraziť prístupový kľúč - Odvolať Kin - Namiesto toho ste napísali 12 slov? - Áno - Áno, odvolať Kin - Áno, napísal som ich - a - Kin - Kinov - Odstráňte všetky informácie spojené s vaším kontom zo serverov aplikácie Code (telefónne číslo, kontakty, história transakcií) - K svojmu kontu môžete pristupovať v iných aplikáciách kryptopeňaženky pomocou vašej tajnej frázy na obnovenie. V aplikácii Code nebudete môcť používať svoje konto - Odstráňte svoje konto z blockchainu - Čo spôsobí odstránenie - Čo sa stane - Čo odstránenie nespôsobí - Tento Kin už vyzdvihol niekto iný. - Tento Kin už niekto zhromaždil. - Ak chcete použiť kód, povoľte prístup k fotoaparátu v Nastaveniach. - Tejto hotovosti vypršala platnosť. - Znova zadajte svoje telefónne číslo a skúste to znova. - Ak chcete posielať pozvánky, povoľte prístup ku kontaktom v Nastaveniach. - Aplikácia Code je momentálne vo vašej krajine nedostupná. - Momentálne nemôžeme podporovať vaše zariadenie - Podpora pre eSims bude pravdepodobne k dispozícii v budúcej verzii aplikácie Code. - Niečo sa pokazilo. Tento Kin sa nepodarilo vyzdvihnúť. - Nečakali sme, že sa to stane. Niečo sa pokazilo. Skúste vytvoriť tento účet znova. - Ak si chcete uložiť svoj prístupový kľúč, povoľte kódový prístup k fotkám v Nastaveniach. - Niečo sa pokazilo. Uistite sa, že je vaše telefónne číslo zadané správne. - Niečo sa pokazilo. Skúste to znova. - Nepodarilo sa vybrať vaše prostriedky. Vyskytol sa problém, skúste výber zopakovať. - Aplikácia Code je navrhnutá pre malé, každodenné transakcie, ktoré sú v hodnote %1$s alebo menej. - Aplikácia Code je navrhnutá pre malé, každodenné transakcie. Váš denný limit darov sa zajtra zvýši. - Ak sa chcete dozvedieť, ako získať viac Kinov, prejdite na Často kladené otázky v Nastaveniach. - Zadajte iný pozývací kód a skúste to znova. - Zadajte platné telefónne číslo a skúste to znova. - Táto tipovacia karta je neplatná. - Zadajte platný kód a skúste to znova. - Ľutujeme, vyskytol sa problém so sieťou. Skúste znova pozvať svojho priateľa. - Tento Kin bol automaticky vrátený odosielateľovi, pretože nebol vyzdvihnutý do 24 hodín. Požiadajte ho, aby znovu poslali Kin. - Znova zadajte svoje telefónne číslo a skúste to znova. - Skúste získať obrázok, ktorý zobrazuje kód jasnejšie. - Skontrolujte svoje internetové pripojenie alebo to skúste znova neskôr. - Kód je momentálne len na pozvanie. Keď budú k dispozícii ďalšie pozvánky, budeme vás informovať. - Ak sa chcete pridať do zoznamu čakateľov, prejdite na %1$s - Maximálna hodnota nákupu je %1$s. Zadajte nižšiu sumu. - Minimálna hodnota nákupu je %1$s. Zadajte vyššiu sumu. - Váš prístupový kľúč spustil odomknutie. V dôsledku toho už nebudete môcť používať tento prístupový kľúč v kóde. - Pošlite tweet tejto osobe, aby ste aktivovali jej Tipovú kartu. - Maximálny tip je %1$s. Zadajte nižšiu sumu. - Minimálny tip je %1$s. Zadajte vyššiu sumu. - Každý deň môžete vytvoriť len toľko nových účtov. - Aplikácia Code je momentálne obmedzený na jeden účet na zariadenie. Podpora viacerých účtov pravdepodobne príde v budúcej verzii aplikácie. - Aplikácia Code je momentálne obmedzená na jeden účet na telefónne číslo. Podpora viacerých účtov pravdepodobne príde v budúcej verzii aplikácie. - Kin bol už vyzdvihnutý - Kin už bol zhromaždený - Vyžaduje sa prístup k fotoaparátu - Platnosť hotovosti vypršala - Časový limit overovacieho kódu vypršal - Vyžaduje sa prístup ku kontaktom - Krajina nie je podporovaná - Zariadenie nie je podporované - eSims nie sú momentálne podporované - Zhromažďovanie zlyhalo - Vytvorenie účtu zlyhalo - Uloženie zlyhalo - Odoslanie zlyhalo - Potvrdenie zlyhalo - Transakcia zlyhala - Uveďte dosiahnutý limit - Dosiahnutý denný limit - Nedostatočný kin - Pozývací kód je neplatný alebo vypršala jeho platnosť - Neplatné telefónne číslo - Neplatná tipovacia karta - Neplatný kód - Pozvanie zlyhalo - Platnosť odkazu vypršala - Maximálny dosiahnutý počet pokusov - Nenašiel sa žiaden kód - Žiadne internetové pripojenie - Nemáte žiadne pozvánky - Zatiaľ ste neboli pozvaný - Nákup je príliš veľký - Nákup je príliš malý - Prístupový kľúč sa už v kóde nedá použiť - Tipová karta ešte nie je aktivovaná - Príliš vysoký tip - Príliš nízky tip - Bolo vytvorených príliš veľa účtov - Účet už bol vytvorený - Účet už bol vytvorený - Veríme, že platby by mali byť jednoduché, výkonné a globálne. Vytvorením pokročilej technológie blockchain Code ponúka funkcie, ktoré tradičné platobné aplikácie nedokážu, ako napríklad globálne peer-to-peer prevody, mikroplatby, ktoré odomykajú jednotlivé články online, a tipy na nulové poplatky pre vašich obľúbených tvorcov. - Kin je kryptomena ako bitcoin, ale je tiež navrhnutá pre rýchle a lacné platby. - Rovnako ako bitcoin je k dispozícii iba obmedzené množstvo Kin. Ak viac ľudí kúpi Kin, hodnota sa zvýši a ak viac ľudí predá Kin, hodnota klesne. Táto dynamika umožňuje každému, kto vlastní Kin, podieľať sa na vytváraní hodnôt, ak získavanie Kin rastie. - Kin si môžete kúpiť debetnou kartou. Toto je dostupné na karte Získať príbuzenstvo. - Áno môžete. Predaj Kin je podporovaný na množstve kryptomenových búrz. - Existujú tri hlavné spôsoby, ako môžete pomôcť: porozprávajte sa o svojich skúsenostiach s Code na sociálnych sieťach, povzbuďte svojich priateľov, aby si Code sami vyskúšali a povzbuďte svoje obľúbené webové stránky, aby integrovali platby prostredníctvom Code tak, že ich požiadate, aby sa pozreli na [getcode.com](https ://getcode.com). - Čo je Code? - Prečo sú platby prostredníctvom Code denominované v Kin? - Prečo sa mení hodnota Kin? - Ako môžem kúpiť viac Kin? - Môžem predať Kin? - Ako môžem pomôcť? - súhlasíte s naším - Klepnutím na „Vytvoriť účet\" alebo „Prihlásiť sa\" - Váš fotoaparát sa používa, aby ste mohli prijímať Kin. Ak chcete pokračovať, povoľte prístup k fotoaparátu. - Potrebujeme push notifikácie, aby sme vám mohli včas posielať informácie o vašom účte. - Výbery sú nevratné a po inicializácii ich nemožno vrátiť späť. - Všetky prostriedky na tomto účte budú stratené. Odstránenie účtu je trvalé a nemožno ho vrátiť späť. Naozaj chcete odstrániť tento účet? - Akýkoľvek Kin, ktorý do 24 hodín nebude vyzdvihnutý, bude automaticky vrátený na vaše konto. - Budete musieť reštartovať vytváranie účtu a znova overiť svoje telefónne číslo. - Do tohto účtu sa môžete vrátiť pomocou prístupového kľúča - Nebudete dostávať upozornenia na nové správy od %1$s. Stlmenie zvuku môžete kedykoľvek vypnúť. - V súčasnosti sú podporované iba účty vytvorené prostredníctvom kódu. - Vaši priatelia vás už nebudú môcť nájsť pomocou tohto telefónneho čísla. - Budete dostávať upozornenia na všetky nové správy od %1$s. Stlmenie zvuku môžete kedykoľvek zapnúť. - Nebudete dostávať žiadne správy od %1$s, kým za ne znova nezaplatíte. - Váš prístupový kľúč umožní prístup k vášmu účtu s kódom. Udržujte to v súkromí a bezpečí. - Týchto 12 slov je jediný spôsob, ako obnoviť váš kódový účet. Uistite sa, že ste si ich zapísali a uchovávajte ich v súkromí a bezpečí. - Ste si istý? - Naozaj chcete vymazať tento účet? - Poslali ste odkaz? - Naozaj chcete odísť? - Naozaj sa chcete odhlásiť? - Stlmiť zvuk %1$s? - Nie je to kódový účet - Ste si istý? - Vypnúť stlmenie zvuku %1$s? - Zrušiť odber z %1$s? - Zobraziť váš prístupový kľúč? - Ste si istý? - %1$s Kin bol vložený na váš účet. - %1$s, ktorý ste včera poslali, nebol vyzdvihnutý. Bol automaticky vrátený na váš zostatok. - Začnite s kódom priateľa a získajte 5 $ - Posielať hotovosť prostredníctvom akejkoľvek aplikácie na odosielanie správ - Dostali ste %1$s Kinov za to, že ste niekomu poslali ich prvý Kin. - Teraz môžete požiadať o tipy. - Vklad bol prijatý - Kin bol vrátený - Novinka v kóde - Prijatý bonus za odporúčanie - Účet X je pripojený - Platnosť vašej pozvánky na prístupový kód vypršala. Môžete sa odhlásiť a použiť iný účet s platným stavom pozvánky. - Váš prístupový kľúč je jediný spôsob, ako získať prístup k vašim finančným prostriedkom. Uchovávajte ho v súkromí a bezpečí. - Ak chcete zobraziť prístupový kľúč, overte svoju totožnosť. - Klepnutím na ikonu Google Lens otvoríte QR kód na prihlásenie do Kódu. Prípadne sa môžete prihlásiť manuálne zadaním 12 slov na obrazovke prihlásenia Kódu. - Upozornenie! Tento obrázok umožňuje prístup ku všetkým prostriedkom, ktoré máte v kóde. Nezdieľajte tento obrázok s nikým iným. Udržujte ho v bezpečí. - Kód vám umožňuje získať Kin nasmerovaním fotoaparátu na digitálny účet na telefóne iného používateľa - Aby ste mohli prijímať Kin, musíte povoliť prístup k fotoaparátu - Overte ore prístup ku Code. - Kúpiť si Kin - Kúpiť Kin (Čoskoro) - Nákup a predaj Kinov je v súčasnosti zložitý proces. Tieto procesy sa časom zjednodušia. Ak sa chcete naučiť, ako nakupovať a predávať Kin dnes, môžete si pozrieť návod prostredníctvom videí nižšie. - Môžete dať maximálne %1$s - Tipovať môžete len do %1$s - Svoje mince Kin teraz môžete používať v aplikácii Code - Poslali ste niekomu ich prvý Kin! Tu je váš bonus za odporúčanie: - Prebieha konverzia USDC na Kin. Jej dokončenie by malo trvať približne minútu. - Úspešne ste vložili USDC. Dokončite nákup otvorením aplikácie Code. - Uvítací bonus - Vyberte si krajinu - Už čoskoro - \@getcode Chcel by som prepojiť svoj účet X, aby som mohol dostávať tipy od ľudí z celého sveta - Vymazať - Uistite sa, že máte uloženú svoju tajnú frázu na obnovenie, a potom zadajte „Odstrániť\", aby ste odstránili svoje konto v aplikácii Code. Táto akcia je nezvratná. - Nedostali ste kód na %1$s? - Nedostali ste kód? Znovu odoslať - Zakázanie Face ID vyžaduje, aby ste overili svoju totožnosť. - Nemáte aplikáciu Code Wallet? - Zatiaľ nemáte žiaden Kin. - Povoľte Face ID, aby ste ešte viac zvýšili bezpečnosť transakcií v rámci kódu. - Zadajte cieľovú adresu - Zadajte až %1$s - Získajte 5$ v Kinoch zadarmo, keď dostanete priateľa, aby sa zaregistroval na Kód a pošlete mu jeho prvý Kin. - Kód používa na platby kryptomenu Kin. Tu je niekoľko spôsobov, ako získať viac Kinov do vašej peňaženky s kódom. - Získajte svoj prvý 1$ v Kinoch zadarmo - Overte svoju totožnosť, ak chcete dať kin. - Vložte Kin do svojej peňaženky s kódom odoslaním Kinu na vašu depozitnú adresu nižšie. Klepnutím skopírujete. - Získajte viac Kinov a skúste platbu zopakovať - Neplatný cieľový účet - Uistite sa, že adresa, na ktorú sa sťahujete, bola inicializovaná vaším poskytovateľom peňaženky. Skratkou, ako to dosiahnuť, je najprv vymeniť malé množstvo SOL za Kin v peňaženke, do ktorej sa pokúšate niečo poslať. - Pozývací kód - Kód slúži momentálne len ako pozvánka. Na prístup do aplikácie budete potrebovať pozývací kód. - %1$d pozvaní - Kód je nová aplikácia pre krypto peňaženku, ktorá je momentálne len na pozvanie. Ak si chcete stiahnuť kód, prejdite na %1$s - Dozvedieť sa viac - Vaše telefónne číslo je prepojené s týmto kódovým účtom. Priatelia vás môžu nájsť pomocou tohto telefónneho čísla. - Prepájam svoj účet X s @getcode, aby som mohol dostávať tipy od ľudí z celého sveta. - Načítava sa váš zostatok a história transakcií - Skontrolujte svoje fotografie, či nemáte prístupový kľúč, ktorý ste si uložili pri prvom vytvorení účtu. - Momentálne ste prihlásený/-á do účtu. Pred pokračovaním sa uistite, že ste si uložili prístupový kľúč. Chcete sa odhlásiť a prihlásiť pomocou nového účtu? - Žiadne prepojené\ntelefónne číslo - S týmto kódovým účtom nemáte prepojené telefónne číslo. Prepojte jedno, aby vás vaši priatelia mohli objaviť. - Nie je dostupné internetové pripojenie - V tomto momente - Otvorte aplikáciu Code a namierte fotoaparát, aby ste získali tieto peniaze - Usporiadanie vašich kontaktov - Toto telefónne číslo nie je vo vašich kontaktoch. Stále ich môžete pozvať do aplikácie Code. - Zadajte svoje telefónne číslo vrátane kódu krajiny. Uistite sa, že používate rovnaké telefónne číslo, na ktoré ste dostali pozvánku. - Používa technológiu - Teraz môžete požiadať o tipy. - Získaný bonus za odporúčanie - Odoslať %1$s - Tu je %1$s - Požiadajte o nový v %1$s - Naskenujte tento QR kód fotoaparátom svojho telefónu a stiahnite si aplikáciu Code Wallet - Naskenovaním si stiahnete\npeňaženku aplikácie Code - Vyhľadať meny - Vyhľadať kontakty - Na vaše telefónne číslo bola odoslaná SMS správa s overovacím kódom. Zadajte overovací kód vyššie. - Niekto vám poslal peniaze - Niekto ti dal tip - Ak chcete skenovať Code-y, musíte spustiť fotoaparát - Vaša tipovacia karta vám umožňuje dostávať tipy od používateľov peňaženky Code z celého sveta. Ak chcete získať prístup k svojej tipovacej karte, pripojte sa účtom X. - Vaša Tipová karta vám umožňuje dostávať tipy od používateľov Kódu z celého sveta. Ak chcete získať prístup k príspevku Tipová karta na X. - Vaša karta Tip vám umožňuje dostávať tipy od používateľov Kódu z celého sveta. Ak chcete získať prístup k svojej karte Tip, pošlite správu @getcode na X. - Povoľte Kódu, aby vám posielal upozornenia, keď dostanete tipy od iných používateľov Kódu. - Napíšte „%1$s\" - Urobili sme niekoľko zmien na zlepšenie prostredia. Ak chcete naďalej používať kód, musíte si aplikáciu aktualizovať. - Platný účet vlastníka - Platný tokenový účet - Hodnota Kin sa mení. - bolo vám vrátené - Kam by ste chceli stiahnuť svoj Kin? - Ak chcete odobrať kin, overte svoju totožnosť. - Vložili ste - Dali ste - Máte %1$d pozvaní - Kód je momentálne len na pozvanie. Zostáva vám %1$d. - Zaplatili ste - Dali ste - Obdržali ste - Odoslali ste - Minuli ste - Niekomu ste dali tip - Vybrali ste - Vaše prostriedky boli úspešne vybraté. - Váš účet X bol úspešne prepojený s vaším účtom Code. Teraz môžete požiadať o tipy. - Výber bol úspešný - Účet X bol úspešne pripojený - Platnosť prístupu vypršala - Prístupový kľúč - Nastavenia aplikácie - Automatický štart kamery - Zostatok - Indikátory beta - Bonus - Kupiť a predať Kin - Platby v hotovosti - Tím aplikácie Code - Nákupy kinov - Internetové platby - Tipy - Možnosti ladenia - Vložiť Kin - Uložené - Zadajte prístupové kľúčové slová - Zadajte telefónne číslo - Zlyhalo - Často kladené otázky - Dal - Získať hotovosť - Začnite používať kód priateľa - Získajte Kin - Získajte viac Kinov - Dať hotovosť - Dať Kin - Nedostatok prostriedkov - Pozvať priateľa - Časovo obmedzená ponuka - Prepojené - Miestna mena - Moje konto - Neprepojené - Ostatné meny - Zaplatené - Čaká na spracovanie - Telefónne číslo - Zásady ochrany osobných údajov - Zakúpené - Účet X bol úspešne pripojený - Prijať tipy - Prijaté - Najnovšie meny - Odporučte priateľa a získajte 5 dolárov - Bonus za odporúčanie - Požiadať o hotovosť - Požiadať o kin - Požiadať o tip - Vyžaduje Face ID - Vyžadovať heslo - Vyžaduje Touch ID - Výsledky - Vrátené - Vybrať konto - Vybrať krajinu - Vyberte menu - Odoslané - Použité - Prepnúť účty - Podmienky služby - Karta tringeltov - Tipovať kin - Zapnúť oznámenia na Kód - Neznámy - Vyžaduje sa aktualizácia - Overiť telefónne číslo - Uvítací bonus - Odvolať Kin - Odvolané - Váš prístupový kľúč - Klepnutím na logo zdieľajte odkaz na stiahnutie aplikácie - diff --git a/apps/codeApp/src/main/res/values-sr/strings-localized.xml b/apps/codeApp/src/main/res/values-sr/strings-localized.xml deleted file mode 100644 index 8fefaecf9..000000000 --- a/apps/codeApp/src/main/res/values-sr/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Додај готовину помоћу дебитне картице - Dozvoli pristup kameri - Dozvoli pristup kontaktima - Dozvoli prosleđena obaveštenja - Saldo - Купи Кин - Kupite više valuta Kin - Otkaži - Откажи слање - Ћаскај - Покупите овај новац - Potvrdi - Повежи се на X - Настави - Kopirano - Kopiraj - Kopiraj adresu - Napravi nalog - Napravi novi Code nalog - Obriši nalog - Готово - Преузмите је одмах - Омогући Face ID - Омогући Touch ID - Izađi - Дај - Daj Kin - Pozovi - Pozivi - Pridruži se listi čekanja - Касније - Научите како се купује кин - Научите како се продаје кин - Poveži broj telefona - Prijavi se - Odjavi se - Пoшаљите поруку @getcode за повезивање - Искључи звук - Sledeći - Не, покушајте поново - Ne sada - U redu - Otvori postavke - Nalepi - Nalepi iz ostave - Поставите објаву да повежете налог - Stavi u novčanik - Прими - Oporavi postojeći nalog - Podseti - Ukloni broj telefona - Ukloni svoj broj telefona - Затражи напојницу - Sačuvaj ključ za pristup u Moje fotografije - Sačuvaj u Fotografije - Пошаљи - Pošalji verifikacioni kod - Подели - Дели као URL адресу - Подели везу за преузимање - Дели овај видео-запис - Покажи моју картицу за напојнице - Покрени камеру - Претплати се - Prevuci za prijavu - Брзо превуците да бисте платили - Превуци да даш напојницу - Isprobaj drugi Code nalog - Пошаљите твит - Откључај Code - Укључи звук - Откажи претплату - Ažuriraj - Prikaži ključ za pristup - Povuci Kin - Zapisali ste 12 reči umesto toga? - Да - Da, povuci Kin - Da, zapisao sam ih - i - Kin - Kin-a - Уклања са Code сервера све информације повезане са вашим налогом (број телефона, контакте, историју трансакција) - Можете да приступите свом налогу у другим апликацијама новчаницима за криптовалуту помоћу своје тајне фразе за опоравак. Нећете моћи да користите свој налог у апликацији Code - Уклања ваш налог из ланца блокова - Шта ће брисање учинити - Шта ће се догодити - Шта брисање неће учинити - Овај Кин је већ прикупио неко други. - Овај Кин је неко већ прикупио. - Dozvolite pristup kameri u podešavanjima da biste koristili Code. - Овај кеш је истекао. - Ponovo unesite broj telefona i pokušajte ponovo. - Дозволите приступ контактима у поставкама да бисте послали позиве. - Апликација Code је тренутно недоступна у вашој земљи. - Тренутно не можемо да подржимо ваш уређај - Подршка за eSims ће највероватније стићи у наредној верзији апликације Code. - Дошло је до грешке. Није могуће прикупити овај кин. - Nismo očekivali da će se to dogoditi. Došlo je do greške. Pokušajte ponovo da napravite ovaj nalog. - Dozvolite pristup aplikaciji Code fotografijama u podešavanjima da biste sačuvali svoj ključ za pristup. - Дошло је до грешке. Проверите да ли сте исправно унели број телефона. - Došlo je do greške. Pokušajte ponovo. - Povlačenje sredstava nije uspelo. Nešto nije u redu, pokušajte ponovo da povučete. - Code је дизајниран за мале, свакодневне трансакције које су за %1$s или мање. - Code је дизајниран за мале, свакодневне трансакције. Ваше дневно ограничење давања ће се повећати сутра. - Da biste saznali kako da dobijete više Kin-a, idite na Najčešća pitanja u podešavanjima. - Унесите други позивни Код и покушајте поново. - Унесите важећи број телефона и покушајте поново. - Ово је неисправна картица за напојнице. - Unesite važeći kod i pokušajte ponovo. - Žao nam je, došlo je do problema sa mrežom. Pokušajte ponovo da pozovete svog prijatelja. - Овај Кин је аутоматски враћен пошиљаоцу јер није преузет у року од 24 сата. Замолите их да поново пошаљу Кин. - Ponovo unesite broj telefona i pokušajte ponovo. - Покушајте да добијете слику на којој је код јасније приказан. - Proverite svoju internet konekciju ili pokušajte ponovo kasnije. - Code je trenutno samo za pozvane. Obavestićemo vas kada bude dostupno više poziva. - Da biste se pridružili listi čekanja idite na %1$s\n - Najviše možete da kupite %1$s. Unesite manji iznos. - Najmanje možete da kupite %1$s. Unesite veći iznos. - Ваш приступни кључ је покренуо откључавање. Као резултат тога, више нећете моћи да користите овај приступни кључ у Kоду. - Пошаљите твит овој особи да активирате њену/његову картицу за напојнице. - Максимални износ који можете дати као напојницу је %1$s. Унесите мањи износ. - Минимални износ који можете дати као напојницу је %1$s. Унесите већи износ. - Svakog dana možete kreirati samo ovoliko novih naloga. - Апликација Code је тренутно ограничена на један налог по уређају. Подршка за већи број налога ће вероватно стићи у наредној верзији апликације Code. - Апликација Code је тренутно ограничена на један налог по броју телефона. Подршка за већи број налога ће вероватно стићи у наредној верзији апликације Code. - Кин је већ прикупљен - Кин је већ прикупљен - Potreban je pristup kameri - Кеш је истекао - Isteklo je vreme za verifikacioni kod - Потребан је приступ контактима - Земља није подржана - Уређај није подржан - eSims тренутно није подржан - Прикупљање није успело - Kreiranje naloga nije uspelo - Čuvanje nije uspelo - Slanje nije uspelo - Potvrda nije uspela - Transakcija nije uspela - Достигнуто је ограничење давања - Достигнуто је дневно ограничење - Nedovoljan Kin - Позивни Код је неважећи или истекао - Неважећи број телефона - Неисправна картица за напојнице - Nevažeći kod - Poziv nije uspeo - Веза је истекла - Dostignut je maksimalan broj pokušaja - Није пронађен код - Nema internet konekcije - Nemate pozivnice - Još uvek niste pozvani - Kupovina je prevelika - Kupovina je premala - Приступни кључ се више не може користити у Kоду - Картица за напојнице још увек није активирана - Напојница је превелика - Напојница је премала - Napravljeno je previše naloga - Налог је већ креиран - Налог је већ креиран - Сматрамо да плаћања треба да буду једноставна, моћна и глобална. Уз помоћ напредне блокчејн технологије, Code нуди функције које традиционалне апликације за плаћање не могу, као што су глобални peer to peer трансфери, микроплаћања која откључавају појединачне чланке на мрежи и савети без икакве накнаде за ваше омиљене креаторе. - Кин је криптовалута попут биткоина, али је намењен и брзим, јефтиним плаћањима. - Као и код биткоина, доступна је само ограничена количина кина. Ако више људи купује кин, његова вредност расте, а ако више људи продаје кин, његова вредност пада. Ова динамика омогућава свима који имају кин да учествују у стварању вредности ако прихватање кина расте. - Можете да купите кин путем дебитне картице. Ово је доступно на картици „Набави кин\". - Да, можете. Продаја валуте кин је подржана на многим берзама криптовалута. - Постоје три главна начина како можете да помогнете: причајте о свом искуству са Code апликацијом на друштвеним мрежама, подстакните своје пријатеље да испробају Code и подстакните своје омиљене веб-сајтове да интегришу Code плаћања тако што ћете затражити да погледају [getcode.com](https://getcode.com). - Шта је Code? - Зашто се плаћања у Code новчанику деноминирају у валути кин? - Зашто се вредност кина мења? - Како да купим још кина? - Да ли могу да продам Kin? - Како могу да вам помогнем? - pristajete na naše - Dodirom na „Kreiraj nalog\" ili „Prijavi se\" vi - Vaša kamera se koristi da primite Kin. Dozvolite pristup kameri da biste nastavili. - Potrebna su nam prosleđena obaveštenja da bismo vam poslali blagovremene informacije o vašem nalogu. - Povlačenja su nepovratna i ne mogu se poništiti kada se jednom pokrenu. - Sva sredstva na ovom nalogu će biti izgubljena. Brisanje vašeg naloga je trajno i ne može se opozvati. Da li ste sigurni da želite da izbrišete ovaj nalog? - Било који Кин који се не прикупи у року од 24 сата биће аутоматски враћен на Ваше стање. - Moraćete ponovo da pokrenete kreiranje naloga i verifikujete svoj broj telefona. - Možete se vratiti na ovaj nalog koristeći svoj ključ za pristup - Нећете бити обавештени о новим порукама од %1$s. Можете да укључите звук ћаскања у било ком тренутку. - Trenutno su podržani samo nalozi kreirani preko aplikacije Code. - Vaši prijatelji više neće moći da vas pronađu sa ovim brojem telefona. - Бићете обавештени о свим новим порукама од %1$s. Можете да утишате ћаскање у било ком тренутку. - Нећете добити никакве поруке од %1$s док их поново не платите. - Vaš ključ za pristup će omogućiti pristupanje vašem Code nalogu. Čuvajte ga u privatnosti i na sigurnom. - Ovih 12 reči su jedini način da oporavite svoj Code nalog. Obavezno ih zapišite, i čuvajte ih u privatnosti i na sigurnom. - Jeste li sigurni? - Da li ste sigurni da želite da obrišete ovaj nalog? - Да ли сте послали везу? - Da li ste sigurni da želite da izađete? - Da li ste sigurni da želite da se odjavite? - Утишај %1$s? - Nije Code nalog - Jeste li sigurni? - Укључи звук ћаскања %1$s? - Откажи претплату на %1$s? - Želite li da vidite svoj ključ za pristup? - Da li ste sigurni? - %1$s Кин је депонован на Ваш рачун. - %1$s који сте послали јуче није прикупљен. Аутоматски је враћен на Ваше стање. - Нека Вам пријатељ започне са Кодом и добићете 5$ - Шаљите готовину преко било које апликације за слање порука - Примили сте %1$s Кина јер сте некоме послали први Кин. - Сада можете да затражите напојнице. - Депозит је примљен - Кин је враћен - Ново у Коду - Примљен је бонус за препоруку - X налог повезан - Vaša pozivnica za pristup aplikaciji Code je istekla. Možete da se odjavite i koristite drugi nalog sa važećim statusom pozivnice. - Vaš ključ za pristup je jedini način da pristupite svojim sredstvima. Čuvajte ga u privatnosti i na sigurnom. - Potvrdite svoj identitet da biste videli svoj ključ za pristup. - Kliknite na Google Lens ikonu kako biste otvorili QR kod kojim ćete se prijaviti u Code aplikaciju.\nMožete se prijaviti i ručno tako što ćete uneti 12 reči na ekranu za prijavu Code aplikacije. - Упозорење! Ова слика даје приступ свим средствима које имате у Code новчанику. Немојте да делите ову слику ни са ким другим. Чувајте је на сигурном и безбедном месту. - Code vam omogućava da primite Kin tako što ćete usmeriti kameru ka digitalnom računu na telefonu drugog korisnika - Morate da dozvolite pristup kameri da biste mogli da primite Kin - Потврдите идентитет да бисте приступили Code новчанику. - Купите Кин - Купи Кин (ускоро) - Тренутно је куповина и продаја криптовалуте кин сложен процес. Током времена ће ови процеси постати једноставнији. Ако желите да сазнате како се купује и продаје кин данас, у наставку можете да погледате видео-записе који ће вас провести кроз поступак. - Можете да дате највише %1$s - Можете давати напојнице само у износу до %1$s - Vaš Kin je sada dostupan za upotrebu u aplikaciji Code - Послали сте неком први Кин! Ово је ваш бонус за препоруку: - Vaš USDC se pretvara u Kin. Ovo bi trebalo da traje otprilike jedan minut da se završi - Uspešno ste deponovali USDC. Otvorite aplikaciju Code da biste dovršili kupovinu - Bonus dobrodošlice - Izaberi zemlju - Долази ускоро - \@getcode Желим да повежем свој X налог да бих добијао/ла савете од особа из целог света - Избриши - Проверите да ли је сачувана ваша тајна фраза за опоравак, а онда унесите \"Обриши\" да бисте обрисали свој Code налог. Ова радња се не може поништити. - Niste dobili kod na %1$s? - Niste dobili kod? Pošalji ponovo - Onemogućavanje opcije Face ID zahteva da potvrdite svoj identitet. - Немате Code Wallet апликацију? - Još uvek nemate nijedan Kin - Omogućite opciju Face ID da biste dodatno poboljšali bezbednost transakcije u aplikaciji Code. - Unesite adresu odredišta - Unesi do %1$s - Добијте 5$ Кина бесплатно када натерате пријатеља да се пријави за Код и пошаљете му његов први Кин. - Код користи криптовалуту Кин за плаћања. Ево неколико начина да унесете више Кин-а у свој Код новчаник. - Добијте свој први 1$ Кина бесплатно - Potvrdite svoj identitet da biste dali kin. - Uložite Kin u svoj Code novčanik tako što ćete poslati Kin na svoju adresu za ulaganje ispod. Dodirnite da biste kopirali. - Набавите још кинова, а затим покушајте поново да платите - Неважећи одредишни налог - Уверите се да је добављач новчаника покренуо адресу на коју повлачите. Да бисте ово брже постигли, прво размените мањи износ сола за кин у новчанику у који покушавате да пошаљете. - Позивни Код - Код је тренутно само позив. Биће вам потребан позивни Код да бисте приступили апликацији. - %1$d poziva - Code je nova aplikacija kripto novčanika koja trenutno radi samo po principu pozivnice. Kako biste preuzeli Code, idite na %1$s - Saznaj više - Vaš broj telefona je povezan sa ovim Code nalogom. Prijatelji vas mogu pronaći pomoću ovog broja telefona. - Повезујем свој X налог са @getcode да бих могао/ла да примам напојнице од људи из целог света. - Učitavanje vašeg stanja i istorije transakcija - Proverite svoje fotografije za ključ za pristup koji ste sačuvali kada ste prvi put otvorili nalog. - Тренутно сте пријављени на налог. Уверите се да сте сачували свој приступни кључ пре него што наставите. Да ли желите да се одјавите и пријавите са новим налогом? - Nepovezan\nBroj telefona - Nemate telefonski broj povezan sa ovim Code nalogom. Povežite jedan broj da vas prijatelji mogu pronaći. - Нема мрежне везе - Na aplikaciji Code - Отворите Code Wallet апликацију и уперите камеру да узмете овај новац - Organizovanje vaših kontakata - Овај број телефона није у вашим контактима. И даље можете да их позовете у Code. - Unesite svoj broj telefona uključujući pozivni broj zemlje. Proverite da li koristite isti broj telefona koji je primio pozivnicu. - Omogućava - Сада можете да затражите напојнице - Примљен бонус за препоруку - Пошаљи %1$s - Ево %1$s - Zatražite novi u %1$s - Скенирајте овај QR код камером свог мобилног телефона како бисте преузели Code Wallet апликацију - Скенирајте да бисте преузели\nCode Wallet апликацију - Pretražite valute - Potražite kontakte - На Ваш број телефона је послата СМС порука са верификационим кодом. Унесите верификациони код изнад. - Неко вам је послао новац - Неко вам ја дао напојницу - Морате да покренете камеру да бисте скенирали кодове - Ваша картица за напојнице вам омогућава да примате напојнице од корисника Code апликације из целог света. Да бисте приступили вашој картици за напојнице повежите се са својим X идентитетом. - Ваша картица за напојнице вам омогућава да примате напојнице од корисника Code апликације из целог света. Да бисте приступили вашој картици за напојнице поставите објаву на X платформи. - Картица „Савети\" вам омогућава да примате савете од корисника апликације Code из целог света. Да бисте приступили картици „Савети\", пошаљите поруку @getcode на X. - Дозволите апликацији Code да вам шаље обавештења кад добијете савете од других корисника апликације Code. - Откуцајте \"%1$s\" - Napravili smo neke izmene da bismo poboljšali iskustvo. Moraćete da ažurirate aplikaciju da biste nastavili da koristite Code. - Важећи налог власника - Важећи налог токена - Vrednost Kin promena. - враћено вам је - Gde biste želeli da povučete svoj Kin? - Potvrdite svoj identitet da biste povukli kin. - Ставили сте у депозит - Дали сте - Imate %1$d poziva - Code je trenutno samo za pozvane. Još vam je %1$d ostalo. - Платили сте - Дали сте - Dobili ste - Послали сте - Потрошили сте - Дали сте неком напојницу - Подигли сте - Vaša sredstva su uspešno povučena. - Ваш X налог је успешно повезан са вашим Code налогом. Сада можете затражити напојнице. - Povlačenje je uspelo - X налог успешно повезан - Pristup je istekao - Ključ za pristup - Поставке апликације - Аутоматско покретање камере - Saldo - Бета заставице - Бонус - Куповина и продаја кина - Plaćanja u gotovini - Code tim - Куповине Кина - Veb plaćanja - Напојнице - Opcije za otklanjanje grešaka - Uloži Kin - Uloženo - Unesi reči ključa za pristup - Унесите број телефона - Nije uspelo - Najčešća pitanja - Dato - Узми готовину - Нека пријатељ започне са Кодом - Прибави Кин - Набавите још кинова - Давање готовине - Daj Kin - Нема довољно средстава - Pozovi prijatelja - Ограничено време понуда - Povezan - Lokalna valuta - Мој налог - Nije povezan - Druge valute - Plaćeno - Na čekanju - Broj telefona - Pravila o privatnosti - Kupljeno - X налог успешно повезан - Добијте савете - Primljeno - Nedavne valute - Препоручите пријатеља, узмите 5$ - Бонус за препоруке - Захтевај готовину - Затражи Кин - Затражи напојницу - Захтевање Face ID-ја - Захтевање кода - Захтевање Touch ID-ја - Rezultati - Враћено - Избор налога - Избор земље - Izaberi valutu - Послато - Потрошено - Promeni naloge - Uslovi korišcenja usluge - Картица са саветом - Дај напојницу у виду Кина - Укључите обавештења за Code - Непознато - Potrebno je ažuriranje - Potvrdi broj telefona - Бонус добродошлице - Povuci Kin - Povučeno - Tvoj ključ za pristup - Додирните логотип да бисте поделили везу за преузимање апликације - diff --git a/apps/codeApp/src/main/res/values-sv/strings-localized.xml b/apps/codeApp/src/main/res/values-sv/strings-localized.xml deleted file mode 100644 index deea15ac5..000000000 --- a/apps/codeApp/src/main/res/values-sv/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Lägg till kontanter med ett betalkort - Tillåt åtkomst till kameran - Tillåt åtkomst till kontakter - Tillåt push-notiser - Balans - Köp Kin - Köp mer Kin - Avbryt - Avbryt skicka - Chatt - Ta emot pengarna - Bekräfta - Anslut till X - Fortsätt - Kopierad - Kopiera - Kopiera adress - Skapa ett konto - Skapa ett nytt kodkonto - Ta bort konto - Färdig - Ladda ner den nu - Aktivera Face ID - Aktivera Touch ID - Avsluta - Ge - Ge Kin - Bjud in - Bjuder in - Anmäl dig till väntelistan - Senare - Lär dig hur man köper Kin - Lär dig hur man säljer Kin - Länka ett telefonnummer - Logga in - Logga ut - Skriv till @getcode för att upprätta kontakt - Stäng av ljud - Nästa - Nej, försök igen - Inte nu - OK - Öppna inställningar - Klistra in - Klistra in från Urklipp - Posta för att koppla kontot - Lägg i plånboken - Ta emot - Återskapa befintligt konto - Påminn - Ta bort telefonnummer - Ta bort ditt telefonnummer - Be om dricks - Spara åtkomstnyckel till mina foton - Spara till foton - Skicka - Skicka verifieringskod - Dela - Dela som webbadress - Dela nedladdningslänk - Dela den här videon - Visa mitt drickskort - Starta kameran - Prenumerera - Svep för att logga in - Svajpa för att betala - Svep för att ge dricks - Prova ett annat kodkonto - Tweeta - Lås upp kod - Slå på ljud - Avsluta prenumeration - Uppdatera - Visa åtkomstnyckel - Återkalla Kin - Skrev du ner de 12 orden i stället? - Ja - Ja, dra tillbaka Kin - Ja, jag har skrivit ner dem - och - Kin - av Kin - Tar bort all information som är kopplad till ditt konto från Codes servrar (telefonnummer, kontakter, transaktionshistorik) - Du kan komma åt ditt konto i andra kryptovalutaappar med hjälp av din hemliga återställningsfras. Du kommer inte att kunna använda ditt konto i Code - Tar bort ditt konto från blockkedjan - Vad radering gör - Vad som kommer att hända - Vad radering inte gör - Dessa Kin har tagits emot av någon annan. - Denna Kin är redan tagen av någon. - Tillåt åtkomst till kameran i Inställningar för att kunna använda Kod. - Dessa kontanter har förfallit - Ange ditt telefonnummer på nytt och försök igen. - Tillåt åtkomst till dina kontakter i Inställningar för att skicka inbjudningar. - Code är för närvarande inte tillgängligt i ditt land. - Vi kan inte stödja din enhet just nu - Stöd för eSims kommer sannolikt i en framtida version av Code. - Något gick fel. Dessa Kin kunde inte hämtas. - Vi förväntade oss inte att det skulle hända. Något gick fel. Försök att skapa det här kontot igen. - Tillåt Kodåtkomst till foton i Inställningar för att spara din åtkomstnyckel. - Någonting gick fel. Försäkra dig om att ditt telefonnummer har skrivits in korrekt. - Något gick fel. Försök igen. - Du lyckades inte ta ut dina pengar. Något gick fel, försök att göra ett nytt uttag. - Code är avsedd för små vardagliga transaktioner på %1$s eller mindre. - Code är avsedd för små, vardagliga transaktioner. Din dagliga gräns för givande ökar i morgon. - Om du vill veta hur du får mer Kin går du till Vanliga frågor och svar i Inställningar. - Gör ett nytt försök med en annan inbjudningskod, tack. - Ange ett giltigt telefonnummer och försök igen. - Detta är ett ogiltigt drickskort. - Ange en giltig kod och försök igen. - Tyvärr hade vi ett nätverksproblem. Försök att bjuda in din vän igen. - Dessa Kin skickades automatiskt tillbaka till avsändaren för att de ej blev mottagna inom 24 timmar. Vänligen be dem skicka Kin igen. - Ange ditt telefonnummer på nytt och försök igen. - Försök att få fram en bild som visar koden tydligare. - Kontrollera din internetuppkoppling eller försök igen senare. - Koden är för närvarande endast tillgänglig för inbjudna. Vi meddelar dig när fler inbjudningar är tillgängliga. - Om du vill ansluta dig till väntelistan kan du gå till %1$s - Maxgränsen för köp är %1$s. Ange ett mindre belopp. - Minimigränsen för köp är %1$s. Ange ett större belopp. - Din åtkomstnyckel har använts för att låsa upp. Därför kan du inte längre använda nyckeln i Code. - Skicka en tweet till denna person för att aktivera hans eller hennes drickskort. - Det mesta du kan ge i dricks är %1$s. Ange ett mindre belopp, tack. - Det minsta du kan ge i dricks är %1$s. Ange ett större belopp, tack. - Du kan bara skapa ett visst antal nya konton varje dag. - Code är för närvarande begränsat till ett konto per telefonnummer. Stöd för flera konton kommer sannolikt i en framtida version av Code. - Code är för närvarande begränsat till ett konto per telefonnummer. Stöd för flera konton kommer sannolikt i en framtida version av Code. - Kin har redan tagits emot - Kin redan tagen - Tillgång till kameran krävs - Förfallna kontanter - Verifieringskoden har tagit ut sin tid - Tillgång till kontakter krävs - Landet stöds inte - Enheten stöds inte - eSims stöds inte för närvarande - Kunde inte hämta - Det gick inte att skapa ett konto - Misslyckades med att spara - Misslyckades med att skicka - Bekräftelse misslyckades - Transaktionen misslyckades - Gränsen för givande uppnådd - Daglig gräns uppnådd - Otillräcklig Kin - Inbjudningskoden är ogiltig eller har slutat gälla - Ogiltigt telefonnummer - Ogiltigt drickskort - Ogiltig kod - Inbjudan misslyckades - Länk har utgått - Maximalt antal försök som uppnåtts - Ingen kod hittades - Ingen internetanslutning - Du har inga inbjudningar - Du har inte blivit inbjuden ännu - Köpet är för stort - Köpet är för litet - Åtkomstnyckeln kan inte längre användas i Code. - Drickskort ännu inte aktiverat - Dricksbeloppet är för stort - Dricksbeloppet är för litet - För många konton har skapats - Konto redan skapat - Konto redan skapat - Vi anser att betalningar ska vara enkla, kraftfulla och globala. Genom att bygga med avancerad blockkedjeteknik erbjuder Code funktioner som traditionella betalningsappar inte kan, såsom globala peer-to-peer-överföringar, mikrobetalningar som låser upp enskilda artiklar online och avgiftsfri dricks till dina favoritkreatörer. - Kin är en kryptovaluta som Bitcoin men dessutom utformad för snabba och billiga betalningar. - Precis som med Bitcoin finns det bara en begränsad mängd Kin. Om fler personer köper Kin går värdet upp, och om fler personer säljer Kin går värdet ner. Den här dynamiken gör att alla som äger Kin kan ta del av värdet som skapas ifall allt fler anammar Kin. - Du kan köpa Kin med ditt debetkort. Du kommer åt detta under fliken \"Skaffa Kin\". - Ja, det kan du. Försäljning av Kin stöds av flera kryptovalutabörser. - Det finns huvudsakligen tre sätt som du kan hjälpa till på: prata om din Code-upplevelse på sociala medier, uppmuntra dina vänner att prova Code själva och uppmuntra dina favoritwebbplatser att integrera Code-betalningar genom att be dem titta på [getcode.com](https://getcode.com). - Vad är Code? - Varför sker Code-betalningar i Kin? - Varför varierar värdet på Kin? - Hur köper jag mer Kin? - Kan jag sälja Kin? - Hur kan jag hjälpa till? - samtycker till vår - Genom att trycka på \"Skapa ett konto\" eller \"Logga in\" gör du följande - Din kamera används för att ta emot Kin. Tillåt åtkomst till kameran för att fortsätta. - Vi behöver push-notiser för att skicka dig aktuell information om ditt konto. - Återkallelser är oåterkalleliga och kan inte göras ogjorda när de väl har påbörjats. - Alla medel på detta konto kommer att gå förlorade. Att radera ditt konto är permanent och kan inte göras ogjort. Är du säker på att du vill radera det här kontot? - All Kin som ej mottagits inom 24 timmar kommer automatiskt att returneras till ditt konto. - Du måste starta om skapandet av kontot och verifiera ditt telefonnummer igen. - Du kan komma tillbaka till det här kontot med hjälp av din Access Key. - Du kommer inte att aviseras när det kommer nya meddelanden från %1$s. Du kan aktivera aviseringar när som helst. - För närvarande stöds endast konton som skapats via Kod. - Dina vänner kommer inte längre att kunna hitta dig med det här telefonnumret. - Du kommer att aviseras när det kommer nya meddelanden från %1$s. Du kan stänga av detta när som helst. - Du kommer inte att få några meddelanden från %1$s förrän du betalar dem igen. - Din Access Key ger dig tillgång till ditt Code-konto. Håll den privat och säker. - Dessa 12 ord är det enda sättet att återställa ditt Code-konto. Se till att du skriver ner dem och förvarar dem privat och säkert. - Är du säker? - Är du säker på att du vill radera det här kontot? - Har du skickat länken? - Är du säker på att du vill avsluta? - Är du säker på att du vill logga ut? - Tysta %1$s? - Inte ett kodkonto - Är du säker? - Aktivera %1$s? - Sluta prenumerera på %1$s? - Visa din åtkomstnyckel? - Är du säker? - %1$s Kin sattes in på ditt konto. - De %1$s du skickade igår togs inte emot. Du fick tillbaka pengarna automatiskt. - Få en vän att börja med Code och få 5 USD - Skicka pengar i valfri meddelandeapp - Du fick %1$s i Kin för att du skickade en första Kin till någon. - Du kan nu be om dricks. - Insättning mottagen - Kin återsändes - Ny på Code - Värvningsbonus erhållen - X-konto kopplat - Din inbjudan till Code har gått ut. Du kan logga ut och använda ett annat konto med en giltig inbjudningsstatus. - Din Access Key är det enda sättet att få tillgång till dina pengar. Förvara den privat och säkert. - Bekräfta din identitet för att se din Access Key. - Tryck på Google Lens-ikonen för att öppna QR-koden för att logga in på Code. Alternativt kan du logga in manuellt genom att skriva in de 12 orden på skärmen Kodloggning. - Varning! Den här bilden ger åtkomst till alla medel du har i Code. Dela inte bilden med någon annan. Förvara den säkert. - Med koden kan du ta emot Kin genom att rikta kameran mot den digitala räkningen på en annan användares telefon. - Du måste tillåta kameraåtkomst för att kunna ta emot Kin - Autentisera för att få åtkomst till Code. - Köp Kin - Köp Kin (kommer snart) - Att köpa och sälja Kin är för närvarande en komplicerad process. Dessa processer blir enklare med tiden. Om du vill lära dig att köpa och sälja Kin idag kan du titta på videogenomgångarna nedan. - Du kan bara ge upp till %1$s - Du kan ge högst %1$s i dricks - Du kan nu använda dina Kin i Code-appen - Du har skickat en första Kin till någon! Här är din värvningsbonus: - Dina USDC konverteras till Kin. Detta bör ta ungefär en minut - Du har satt in USDC. Öppna Code-appen för att slutföra ditt köp - Välkomstbonus - Välj ett land - Kommer snart - \@getcode Jag skulle vilja koppla mitt X-konto så att jag kan få tips från människor över hela världen - Radera - Se till att du har sparat din hemliga återställningsfras och skriv sedan \"Radera\" för att ta bort ditt Code-konto. Denna åtgärd kan inte ångras. - Fick du ingen kod på %1$s? - Fick du inte koden? Skicka om - När du inaktiverar Face ID måste du verifiera din identitet. - Har du inte appen Code Wallet? - Du har inga Kin ännu. - Aktivera Face ID för att ytterligare förbättra säkerheten för transaktioner i Kod. - Ange destinationsadress - Ange upp till %1$s - Få 5 USD i Kin gratis genom att värva en vän till Code och skicka en första Kin till vederbörande. - Code använder kryptovalutan Kin för betalning. Här är några sätt att få mer Kin i din Code-plånbok. - Få din första 1 USD i Kin gratis - Verifiera din identitet för att kunna ge din släkt. - Sätt in Kin i din Code wallet genom att skicka Kin till din insättningsadress nedan. Tryck på för att kopiera. - Skaffa mer Kin och försök sedan att betala igen - Ogiltigt mottagarkonto - Kontrollera att adressen du gör uttag till har initialiserats av din plånboksleverantör. En genväg är att först växla en liten mängd SOL mot Kin i den plånbok du försöker skicka till. - Inbjudningskod - Code är i nuläget endast för inbjudna. Du behöver en inbjudningskod för att komma åt appen. - %1$d bjuder in - Code är en ny kryptoplånboksapp som för närvarande endast är för inbjudna. Gå till %1$s för att ladda ner Code - Läs mer - Ditt telefonnummer är kopplat till detta kodkonto. Vänner kan hitta dig med hjälp av detta telefonnummer. - Jag kopplar mitt X-konto till @getcode så att jag kan få dricks från människor över hela världen. - Läser in ditt saldo och transaktionshistorik - Kontrollera dina foton för att hitta den åtkomstnyckel som du sparade när du först skapade ditt konto. - Du är för närvarande inloggad på ett konto. Kontrollera att du har sparat din kodnyckel innan du fortsätter. Vill du logga ut och logga in med ett nytt konto? - Ingen länkad\nTelefonnummer - Du har inget telefonnummer kopplat till det här kodkontot. Koppla ett så att dina vänner kan upptäcka dig. - Ingen nätverksanslutning - På Code - Öppna appen Code och rikta kameran för att ta emot pengarna - Organisera dina kontakter - Det här telefonnumret finns inte i dina kontakter. Du kan fortfarande bjuda in dem till Code. - Ange ditt telefonnummer inklusive landskoden. Se till att du använder samma telefonnummer som du fick inbjudan till. - Drivs av - Du kan nu be om dricks - Värvningsbonus erhållen - Skicka %1$s - Här är %1$s - Begär en ny i %1$s - Skanna denna QR-kod med din telefons kamera för att ladda ner appen Code Wallet - Skanna för att ladda ned appen Code Wallet - Sök valutor - Sök efter kontakter - Ett sms med en verifieringskod skickades till ditt telefonnummer. Ange koden ovan, tack. - Någon har skickat pengar till dig - Någon gav dig dricks - Du måste starta kameran för att skanna koder - Med ditt drickskort kan du ta emot tips från Code-användare över hela världen. Koppla din profil på X för att få tillgång till ditt tipskort. - Med ditt drickskort kan du ta emot dricks från Code-användare världen över. Posta på X för att få tillgång till ditt drickskort. - Med tipskortet kan du få tips från Code-användare över hela världen. För att få tillgång till ditt tipskort skickar du ett meddelande till @getcode på X. - Tillåt Code att skicka meddelanden till dig när du får dricks från andra Code-användare. - Skriv \"%1$s\" - Vi har gjort några ändringar för att förbättra upplevelsen. Du måste uppdatera appen för att fortsätta använda Code. - Giltigt ägarkonto - Giltigt tokenkonto - Kins värde förändras. - returnerades till dig - Vart vill du ta ut din Kin till? - Verifiera din identitet för att ta ut kin. - Du har satt in - Du gav - Du har %1$d inbjudningar - Koden är för närvarande endast tillgänglig för inbjudna. Du har %1$d kvar. - Du har betalat - Du har gett - Du fick - Du har skickat - Du har spenderat - Du gav någon dricks - Du har tagit ut - Dina pengar har tagits ut framgångsrikt. - Ditt X-konto är nu kopplat till ditt Code-konto, vilket gör att du kan be om dricks. - Återkallelse framgångsrik - X-kontot är nu kopplat - Access upphörde - Tillgångsnyckel - Appinställningar - Starta kameran automatiskt - Balans - Betaflaggor - Bonus - Köp och sälj Kin - Kontantbetalningar - Code-teamet - Kin-köp - Nätbetalningar - Dricks - Alternativ för felsökning - Insättning Kin - Insatta - Ange nyckelord för åtkomst - Skriv telefonnummer - Misslyckad - VANLIGA FRÅGOR - Gav - Få kontanter - Få en vän att börja med Code - Få Kin - Skaffa mer Kin - Ge pengar - Ge Kin - Otillräckliga medel - Bjud in en vän - Tidsbegränsat erbjudande - Länkad - Lokal valuta - Mitt konto - Inte kopplad - Andra valutor - Betalt - I väntan på - Telefonnummer - Integritetspolicy - Köpt - X-kontot är nu kopplat - Få dricks - Mottagen - Senaste valutorna - Värva en vän, få 5 USD - Värvningsbonus - Begär kontanter - Begär Kin - Be om dricks - Kräv Face ID - Kräv lösenkod - Kräv Touch ID - Resultat - Returnerade - Välj ett konto - Välj ett land - Välj en valuta - Skickat - Spenderat - Byt konto - Villkor för tjänsten - Drickskort - Ge Kin i dricks - Aktivera aviseringar för Code - Okänt - Uppdatering krävs - Verifiera telefonnummer - Välkomstbonus - Återkalla Kin - Återkallad - Din åtkomstnyckel - Tryck på logotypen för att dela länken för att ladda ned appen - diff --git a/apps/codeApp/src/main/res/values-th/strings-localized.xml b/apps/codeApp/src/main/res/values-th/strings-localized.xml deleted file mode 100644 index 74d9776ce..000000000 --- a/apps/codeApp/src/main/res/values-th/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - เติมเงินสดด้วยบัตรเดบิต - อนุญาตให้เข้าถึงกล้อง - อนุญาตให้เข้าถึงรายชื่อติดต่อ - อนุญาตให้แจ้งเตือนแบบพุช - ยอดคงเหลือ - ซื้อ Kin - ซื้อ Kin เพิ่มเติม - ยกเลิก - ยกเลิกการส่ง - แชท - รับเงินสดนี้ - ยืนยัน - เชื่อมต่อกับ X\n - ดำเนินการต่อ - คัดลอกแล้ว - คัดลอก - คัดลอกที่อยู่ - สร้างบัญชี - สร้างบัญชี Code ใหม่ - ลบบัญชี - เสร็จแล้ว - ดาวน์โหลดตอนนี้เลย - เปิดใช้งาน Face ID - เปิดใช้งาน Touch ID - ออก - ให้ - ให้ Kin - เชิญ - เชิญ - เข้าร่วมบัญชีรอเรียก - ภายหลัง - เรียนรู้วิธีซื้อ Kin - เรียนรู้วิธีขาย Kin - เชื่อมโยงหมายเลขโทรศัพท์ - เข้าสู่ระบบ - ออกจากระบบ - ส่งข้อความถึง @getcode เพื่อเชื่อมต่อ - ปิดเสียง - ต่อไป - ไม่, ลองอีกครั้ง - ยังไม่ใช่ตอนนี้ - ตกลง - เปิดการตั้งค่า - วาง - วางจากคลิปบอร์ด - โพสต์เพื่อเชื่อมโยงบัญชี\n - ใส่ในกระเป๋าเงิน - รับ - กู้คืนบัญชีที่มีอยู่ - เตือน - ลบหมายเลขโทรศัพท์ - ลบหมายเลขโทรศัพท์ของคุณ - ขอรับทิป - บันทึกคีย์การเข้าถึงที่รูปภาพของฉัน - บันทึกไปยังรูปภาพ - ส่ง - ส่งรหัสยืนยัน - แชร์ - แชร์เป็น URL - แชร์ลิงก์ดาวน์โหลด - แชร์วิดีโอนี้ - แสดงบัตรทิปของฉัน - เปิดใช้งานกล้อง - สมัครสมาชิก - ปัดเพื่อเข้าสู่ระบบ - ปัดเพื่อชำระเงิน - ปัดเพื่อให้ทิป - ลองใช้บัญชี Code อื่น - ทวีตพวกเขา - ปลดล็อก Code - เปิดเสียง - ยกเลิกการสมัครสมาชิก - อัปเดต - ดูคีย์การเข้าถึง - ถอน Kin - เขียนคำ 12 คำลงไปแทนไหม - ใช่ - ใช่ ถอน Kin - ใช่ ฉันเขียนลงไป - และ - Kin - ของ Kin - ลบข้อมูลทั้งหมดที่เกี่ยวข้องกับบัญชีของคุณออกจากเซิร์ฟเวอร์ของ Code (หมายเลขโทรศัพท์ รายชื่อผู้ติดต่อ ประวัติการทำธุรกรรม) - คุณสามารถเข้าถึงบัญชีของคุณในแอปกระเป๋าเงินคริปโตเข้ารหัสอื่น ๆ โดยใช้วลีกู้คืนของคุณ คุณจะไม่สามารถใช้บัญชีของคุณใน Code - ลบบัญชีของคุณออกจากบล็อกเชน - การลบจะทำอะไร - อะไรจะเกิดขึ้น - การลบจะไม่ทำอะไร - Kin นี้ถูกเก็บโดยคนอื่นไปแล้ว - มีใครบางคนเก็บ Kin ไปแล้ว - โปรดอนุญาตให้เข้าถึงกล้องในการตั้งค่าเพื่อใช้ Code - เงินสดจำนวนนี้หมดอายุแล้ว - โปรดป้อนหมายเลขโทรศัพท์ใหม่แล้วลองอีกครั้ง - โปรดอนุญาตให้เข้าถึงรายชื่อผู้ติดต่อในการตั้งค่าเพื่อส่งคำเชิญ - ขณะนี้ Code ยังไม่พร้อมใช้งานในประเทศของคุณ - เราไม่สามารถรองรับอุปกรณ์ของคุณในขณะนี้ - การรองรับ eSim มีแนวโน้มที่จะมาในเวอร์ชันต่อไปของ Code - เกิดข้อผิดพลาดขึ้น ไม่สามารถรวบรวม Kin นี้ - เราไม่ได้คาดหวังให้เกิดสิ่งนี้ขึ้น เกิดข้อผิดพลาดขึ้น โปรดลองสร้างบัญชีนี้อีกครั้ง - โปรดอนุญาตให้ Code เข้าถึงรูปภาพในการตั้งค่าเพื่อบันทึกคีย์การเข้าถึงของคุณ - เกิดข้อผิดพลาดขึ้น โปรดตรวจสอบให้แน่ใจว่าป้อนหมายเลขโทรศัพท์ของคุณถูกต้องแล้ว - เกิดข้อผิดพลาดขึ้น โปรดลองใหม่อีกครั้ง - ถอนเงินของคุณไม่สำเร็จ มีบางอย่างผิดพลาด โปรดลองถอนเงินอีกครั้ง - Code ออกแบบมาสำหรับการทำธุรกรรมขนาดเล็กในชีวิตประจำวันที่มีจำนวนไม่เกิน %1$s - Code ออกแบบมาสำหรับการทำธุรกรรมขนาดเล็กในชีวิตประจำวัน ขีดจำกัดการให้รายวันของคุณจะเพิ่มขึ้นในวันพรุ่งนี้ - หากต้องการเรียนรู้วิธีรับ Kin เพิ่มเติมให้ไปที่คำถามที่พบบ่อยในการตั้งค่า - โปรดป้อนรหัสเชิญอื่นแล้วลองอีกครั้ง - โปรดป้อนหมายเลขโทรศัพท์ที่ถูกต้องแล้วลองอีกครั้ง - นี่คือบัตรทิปที่ไม่ถูกต้อง - โปรดป้อนรหัสที่ถูกต้องแล้วลองอีกครั้ง - ขออภัย เราประสบปัญหาเกี่ยวกับเครือข่าย โปรดลองเชิญเพื่อนของคุณอีกครั้ง - Kin นี้ได้ถูกส่งคืนให้กับผู้ส่งโดยอัตโนมัติเพราะมันไม่ถูกเก็บภายใน 24 ชั่วโมง โปรดขอให้พวกเขาส่ง Kin อีกครั้ง - โปรดป้อนหมายเลขโทรศัพท์ใหม่แล้วลองอีกครั้ง - โปรดพยายาม\nหาภาพที่แสดง code ได้ชัดเจนกว่านี้l - โปรดตรวจสอบการเชื่อมต่อทางอินเทอร์เน็ตหรือลองอีกครั้งในภายหลัง! - ปัจจุบันใช้ Code ได้เฉพาะผู้ที่ได้รับเชิญเท่านั้น เราจะแจ้งให้คุณทราบเมื่อมีคำเชิญเพิ่มเติม - หากต้องการเข้าร่วมรายการรอให้ไปที่ %1$s - คุณสามารถซื้อได้สูงสุดที่ %1$s โปรดกรอกจำนวนที่น้อยลง - คุณสามารถซื้อได้ขั้นต่ำที่ %1$s โปรดกรอกจำนวนที่มากขึ้น - คีย์การเข้าถึงของคุณได้เริ่มต้นการปลดล็อกแล้ว ด้วยเหตุนี้ คุณจะไม่สามารถใช้คีย์การเข้าถึงนี้ใน Code ได้อีกต่อไป - ส่งทวีตถึงบุคคลนี้เพื่อเปิดใช้งานบัตรทิปของพวกเขา - จำนวนเงินสูงสุดที่คุณสามารถให้ทิปได้คือ %1$s กรุณาใส่จำนวนเงินที่น้อยลง - จำนวนเงินขั้นต่ำที่คุณสามารถให้ทิปได้คือ %1$s กรุณาใส่จำนวนเงินที่มากขึ้น - คุณไม่สามารถสร้างบัญชีใหม่ได้มากมายในแต่ละวัน - ขณะนี้ Code มีการจำกัดอยู่ที่ 1 บัญชีต่ออุปกรณ์ การรองรับหลายบัญชีมีแนวโน้มที่จะมาในเวอร์ชันต่อไปของ Code - ขณะนี้ Code มีการจำกัดอยู่ที่ 1 บัญชีต่อหมายเลขโทรศัพท์ การรองรับหลายบัญชีมีแนวโน้มที่จะมาในเวอร์ชันต่อไปของ Code - Kin ถูกเก็บไปแล้ว - เก็บ Kin ไปแล้ว - จำเป็นต้องเข้าถึงกล้อง - เงินสดหมดอายุ - รหัสยืนยันหมดเวลาแล้ว - จำเป็นต้องเข้าถึงรายชื่อผู้ติดต่อ - ไม่รองรับประเทศ - อุปกรณ์ไม่รองรับ - ขณะนี้ยังไม่รองรับ eSim - ไม่สามารถรวบรวม - การสร้างบัญชีล้มเหลว - ไม่สามารถบันทึก - ไม่สามารถส่ง - ยืนยันไม่สำเร็จ - การทำธุรกรรมล้มเหลว - ถึงขีดจำกัดการให้แล้ว - ถึงขีดจำกัดรายวันแล้ว - Kin ไม่เพียงพอ - รหัสเชิญไม่ถูกต้องหรือหมดอายุ - หมายเลขโทรศัพท์ไม่ถูกต้อง - บัตรทิปไม่ถูกต้อง - รหัสไม่ถูกต้อง - การเชิญล้มเหลว - ลิงก์หมดอายุแล้ว - ถึงจำนวนการพยายามสูงสุดแล้ว - ไม่พบ Code - ไม่มีการเชื่อมต่อทางอินเทอร์เน็ต - คุณไม่มีคำเชิญ - คุณยังไม่ได้รับเชิญ - ซื้อมากเกินไป - ซื้อน้อยเกินไป - คีย์การเข้าถึงใช้ไม่ได้อีกต่อไปใน Code - ยังไม่ได้เปิดใช้งานบัตรทิป - ทิปมากเกินไป - ทิปน้อยเกินไป - มีการสร้างบัญชีมากเกินไป - บัญชีได้รับการสร้างแล้ว - บัญชีได้รับการสร้างแล้ว - เราเชื่อว่าการชำระเงินควรง่าย มีประสิทธิภาพ และครอบคลุมทั่วโลก Code สร้างด้วยรหัสเทคโนโลยีบล็อกเชนขั้นสูงจึงทำให้มีฟีเจอร์ที่แอปชำระเงินแบบดั้งเดิมไม่สามารถทำได้ เช่น การโอนเงินแบบ peer to peer ทั่วโลก การชำระเงินแบบไมโครเพย์เมนต์ที่ปลดล็อกบทความแต่ละบทความทางออนไลน์ และเคล็ดลับค่าธรรมเนียมเป็นศูนย์สำหรับครีเอเตอร์ที่คุณชื่นชอบ - Kin เป็นสกุลเงินดิจิทัลเช่นเดียวกับ Bitcoin แต่ยังได้รับการออกแบบมาสำหรับการชำระเงินที่รวดเร็วและไม่แพงอีกด้วย - Kin มีจำนวนจำกัดเช่นเดียวกับ Bitcoin หากมีคนซื้อ Kin มากขึ้นมูลค่าของ Kin ก็จะเพิ่มขึ้น และหากมีคนขาย Kin มากขึ้นมูลค่าของ Kin ก็จะลดลง การเป็นเช่นนี้ช่วยให้ทุกคนที่ถือครอง Kin สามารถมีส่วนร่วมในการสร้างมูลค่าหากมีการยอมรับ Kin เพิ่มขึ้น - คุณสามารถซื้อ Kin ด้วยบัตรเดบิต โดยสามารถเข้าถึงได้ในแท็บ Get Kin - คุณสามารถขาย Kin ได้ ตลาดแลกเปลี่ยนสกุลเงินคริปโตหลายแห่งรองรับการขาย Kin - มี 3 วิธีหลักที่คุณสามารถช่วยได้: เล่าประสบการณ์เกี่ยวกับ Code ของคุณบนโซเชียลมีเดีย สนับสนุนให้เพื่อนของคุณลองใช้ Code และสนับสนุนให้เว็บไซต์โปรดของคุณนำการชำระเงินด้วย Code ไปใช้ โดยขอให้พวกเขาลองเข้าไปตรวจสอบ [getcode.com ]( https://getcode.com) ดู - Code คืออะไร - เหตุใดการชำระเงินด้วย Code จึงมีหน่วยเป็น KIN - เหตุใดมูลค่าของ Kin จึงมีการเปลี่ยนแปลง - ฉันจะหา Kin เพิ่มได้อย่างไร - ฉันสามารถขาย Kin ได้ไหม - ฉันจะช่วยได้อย่างไร - ยอมรับใน...ของเรา - โดยการแตะที่ \"สร้างบัญชี\" หรือ \"เข้าสู่ระบบ\" คุณ - กล้องของคุณใช้ในการรับ Kin โปรดอนุญาตให้เข้าถึงกล้องเพื่อดำเนินการต่อ - เราจำเป็นต้องใช้การแจ้งเตือนแบบพุชเพื่อส่งข้อมูลที่ทันเวลาเกี่ยวกับบัญชีของคุณ - เมื่อเริ่มต้นถอนเงินแล้วจะไม่สามารถย้อนกลับได้และไม่สามารถยกเลิกได้ - เงินทั้งหมดในบัญชีนี้จะหายไป การลบบัญชีของคุณเป็นการลบแบบถาวรและไม่สามารถยกเลิกได้ แน่ใจหรือว่าต้องการลบบัญชีนี้ - Kin ใดๆ ที่ไม่ถูกเก็บภายใน 24 ชั่วโมงจะถูกส่งคืนเข้าในยอดบัญชีของคุณโดยอัตโนมัติ - คุณจะต้องเริ่มสร้างบัญชีใหม่และยืนยันหมายเลขโทรศัพท์ของคุณอีกครั้ง - คุณสามารถกลับเข้าสู่บัญชีนี้ได้โดยใช้คีย์การเข้าถึงของคุณ - คุณจะไม่ได้รับการแจ้งเตือนข้อความใหม่ใดๆ จาก %1$s คุณสามารถเปิดเสียงได้ตลอดเวลา - ขณะนี้รองรับเฉพาะบัญชีที่สร้างผ่าน Code เท่านั้น - เพื่อนของคุณจะไม่สามารถหาคุณด้วยหมายเลขโทรศัพท์นี้ได้อีกต่อไป - คุณจะได้รับการแจ้งเตือนข้อความใหม่ทั้งหมดจาก %1$s คุณสามารถปิดเสียงได้ตลอดเวลา - คุณจะไม่ได้รับข้อความใดๆ จาก %1$s จนกว่าคุณจะชำระเงินอีกครั้ง - คีย์การเข้าถึงจะให้สิทธิ์การเข้าถึงบัญชี Code ของคุณ โปรดรักษาให้เป็นส่วนตัวและปลอดภัย - 12 คำนี้เป็นวิธีเดียวที่จะกู้คืนบัญชี Code ของคุณ ตรวจสอบให้แน่ใจว่าคุณได้จดไว้แล้ว และรักษาให้เป็นส่วนตัวและปลอดภัย - แน่ใจหรือ - แน่ใจหรือว่าต้องการลบบัญชีนี้ - คุณส่งลิงก์แล้วหรือยัง? - แน่ใจหรือว่าต้องการออก - แน่ใจหรือว่าต้องการออกจากระบบ - ปิดเสียง %1$s หรือไม่ - ไม่ใช่บัญชี Code - แน่ใจหรือ - เปิดเสียง %1$s หรือไม่ - ยกเลิกการเป็นสมาชิกของ %1$s หรือไม่ - ดูคีย์การเข้าถึงของคุณหรือไม่ - แน่ใจหรือ - %1$s Kin ถูกฝากเข้าบัญชีของคุณแล้ว - %1$s ที่คุณส่งไปเมื่อวานได้รับการเก็บรวบรวมไว้แล้ว ระบบจะส่งคืนไปยังยอดคงเหลือของคุณโดยอัตโนมัติ - ชวนเพื่อนมาเริ่มใช้ Code แล้วรับ $5 - ส่งเงินสดผ่านแอเมสเซนเจอร์ใดก็ได้ - คุณได้รับ Kin %1$s จากการส่ง Kin แรกให้ใครบางคน - ตอนนี้คุณสามารถขอทิปได้แล้ว - ได้รับเงินแล้ว - Kin ที่ส่งคืน - ใหม่สำหรับ Code - โบนัสการแนะนำที่ได้รับ - เชื่อมโยงบัญชี X แล้ว - คำเชิญให้เข้าถึง Code หมดอายุแล้ว คุณสามารถออกจากระบบแล้วใช้บัญชีอื่นที่มีสถานะคำเชิญที่ถูกต้องได้ - คีย์การเข้าถึงของคุณเป็นวิธีเดียวที่จะเข้าถึงเงินของคุณ โปรดรักษาให้เป็นส่วนตัวและปลอดภัย - ยืนยันตัวตนของคุณเพื่อดูคีย์การเข้าถึงของคุณ - แตะไอคอน Google Lens เพื่อเปิดรหัส QR เพื่อเข้าสู่ระบบ Code หรืออีกทางเลือกหนึ่ง คุณสามารถเข้าสู่ระบบได้ด้วยตนเองโดยป้อนคำ 12 คำในหน้าจอเข้าสู่ระบบ Code - คำเตือน! รูปภาพนี้จะให้การเข้าถึงเงินทุนทั้งหมดที่คุณมีอยู่ใน Code อย่าได้แชร์รูปภาพนี้กับคนอื่น และโปรดเก็บรักษาให้ปลอดภัย - Code ช่วยให้คุณสามารถรับ Kin โดยการชี้กล้องของคุณไปที่ใบเรียกเก็บเงินดิจิทัลบนโทรศัพท์ของผู้ใช้อื่น - คุณต้องอนุญาตให้เข้าถึงกล้องจึงจะรับ Kin ได้ - รับรองความถูกต้องเพื่อเข้าถึง Code - ซื้อ Kin - ซื้อ Kin (เร็วๆ นี้) - ขณะนี้การซื้อและการขาย Kin เป็นกระบวนการที่ซับซ้อน กระบวนการเหล่านี้จะง่ายขึ้นเมื่อเวลาผ่านไป หากคุณต้องการเรียนรู้วิธีซื้อและขาย Kin ในวันนี้ คุณสามารถชมวิดีโอแนะนำด้านล่าง - คุณสามารถให้ได้ไม่เกิน %1$s - คุณสามารถให้ทิปได้สูงสุด %1$s เท่านั้น - Kin ของคุณพร้อมใช้งานแล้วใน Code App - คุณได้ส่ง Kin แรกให้กับใครบางคน! นี่คือโบนัสแนะนำของคุณ: - USDC กำลังแปลงเป็น Kin ขั้นตอนนี้ใช้เวลาประมาณหนึ่งนาทีจึงแล้วเสร็จ - คุณฝาก USDC สำเร็จ เปิด Code App เพื่อให้การซื้อเสร็จสมบูรณ์ - โบนัสต้อนรับ - เลือกประเทศ - จะมีให้บริการในเร็ว ๆ นี้ - \@getcode ฉันต้องการเชื่อมต่อบัญชี X ของฉันเพื่อที่จะได้รับทิปจากผู้คนทั่วโลก - ลบ - ตรวจสอบให้แน่ใจว่าคุณได้บันทึกวลีกู้คืนแล้ว จากนั้นป้อนคำว่า \"ลบ\" เพื่อลบบัญชี Code ของคุณ การดำเนินการนี้ไม่สามารถย้อนกลับได้ - ไม่ได้รับรหัสที่ %1$s หรือ - ไม่ได้รับรหัสหรือ ส่งอีกครั้ง - การปิดใช้งาน Face ID ทำให้คุณต้องยืนยันตัวตน - ไม่มีแอป Code Wallet ใช่ไหม - คุณยังไม่มี Kin เลย - เปิดใช้งาน Face ID เพื่อเพิ่มความปลอดภัยของธุรกรรมใน Code - ป้อนที่อยู่ปลายทาง - ป้อนได้สูงสุด %1$s - รับ Kin $5 ฟรีเมื่อคุณชวนเพื่อนลงทะเบียน Code แล้วส่ง Kin แรกให้พวกเขา - Code จะใช้สกุลเงินคริปโต Kin ในการชำระเงิน นี่คือวีธีบางส่วนในการรับ Kin เพิ่มเติมเข้าในกระเป๋าเงิน Code ของคุณ - รับฟรี $1 ของ Kin แรกของคุณ - ยืนยันตัวตนของคุณเพื่อให้ kin - ฝาก Kin ลงในกระเป๋าเงิน Code ของคุณโดยส่ง Kin ไปยังที่อยู่ฝากเงินของคุณด้านล่าง แตะเพื่อคัดลอก - โปรดรับ Kin เพิ่มแล้วลองชำระเงินอีกครั้ง - บัญชีปลายทางไม่ถูกต้อง - โปรดตรวจสอบให้แน่ใจว่าที่อยู่ที่คุณกำลังถอนเงินไปนั้นได้รับการตั้งค่าโดยผู้ให้บริการกระเป๋าเงินของคุณแล้ว วิธีลัดในการตรวจสอบคือการแลกเปลี่ยน SOL จำนวนเล็กน้อยเป็น Kin ในกระเป๋าเงินที่คุณกำลังจะส่งไป - รหัสเชิญ - ขณะนี้ Code ใช้ระบบเชิญเท่านั้น คุณจะต้องมีรหัสเชิญเพื่อเข้าถึงแอป - %1$d คำเชิญ - Code เป็นแอปกระเป๋าเงินคริปโตแอปใหม่ที่ขณะนี้ใช้ระบบเชิญเท่านั้น หากต้องการดาวน์โหลด Code ให้ไปที่ %1$s - เรียนรู้เพิ่มเติม - หมายเลขโทรศัพท์ของคุณเชื่อมโยงกับบัญชี Code นี้ เพื่อน ๆ สามารถหาคุณได้โดยใช้หมายเลขโทรศัพท์นี้ - ฉันกำลังเชื่อมต่อบัญชี X ของฉันกับ @getcode เพื่อให้ฉันได้รับทิปจากผู้คนทั่วโลก - กำลังโหลดยอดคงเหลือและประวัติธุรกรรมของคุณ - ตรวจสอบรูปภาพของคุณเพื่อหาคีย์การเข้าถึงที่คุณบันทึกไว้ตอนที่สร้างบัญชีครั้งแรก - ขณะนี้คุณเข้าสู่ระบบบัญชีแล้ว โปรดตรวจสอบให้แน่ใจว่าคุณได้บันทึก Access Key ของคุณแล้วก่อนดำเนินการต่อ คุณต้องการออกจากระบบและเข้าสู่ระบบด้วยบัญชีใหม่หรือไม่? - ไม่มีการเชื่อมโยง\nหมายเลขโทรศัพท์ - คุณไม่มีหมายเลขโทรศัพท์ที่เชื่อมโยงกับบัญชี Code นี้ เชื่อมโยงเพื่อให้เพื่อนของคุณสามารถค้นหาคุณพบ - ไม่มีการเชื่อมต่อเครือข่าย - บน Code - เปิดแอป Code แล้วเล็งกล้องของคุณเพื่อรับเงินสดนี้ - จัดระเบียบรายชื่อติดต่อของคุณ - หมายเลขโทรศัพท์นี้ไม่อยู่ในรายชื่อผู้ติดต่อของคุณ คุณยังสามารถเชิญพวกเขามาที่ Code ได้ - ป้อนหมายเลขโทรศัพท์ของคุณรวมถึงรหัสประเทศ ตรวจสอบให้แน่ใจว่าคุณใช้หมายเลขโทรศัพท์เดียวกันกับที่ได้รับคำเชิญ - สนับสนุนโดย - ตอนนี้คุณสามารถขอรับทิปได้แล้ว - โบนัสการแนะนำที่ได้รับ - ส่ง %1$s - นี่คือ %1$s - ขอใหม่ใน %1$s - สแกนรหัส QR นี้ด้วยกล้องโทรศัพท์ของคุณเพื่อดาวน์โหลดแอป Code Wallet - สแกนเพื่อดาวน์โหลด\nแอปกระเป๋าสตางค์ของ Code - ค้นหาสกุลเงิน - ค้นหารายชื่อติดต่อ - ข้อความ SMS ถูกส่งไปยังหมายเลขโทรศัพท์ของคุณพร้อมรหัสยืนยัน โปรดป้อนรหัสยืนยันด้านบน - มีคนส่งเงินสดให้คุณ - มีคนให้ทิปคุณ - คุณต้องเปิดใช้งานกล้องเพื่อสแกน Code - บัตรทิปของคุณช่วยให้คุณได้รับทิปจากผู้ใช้ Code ทั่วโลก ในการเข้าถึงบัตรทิปของคุณ ให้เชื่อมต่อข้อมูลประจำตัว X ของคุณ - บัตรทิปช่วยให้คุณได้รับทิปจากผู้ใช้ Code ทั่วโลก ในการเข้าถึงบัตรทิปของคุณ ให้โพสต์บน X - การ์ดทิปช่วยให้คุณได้รับทิปจากผู้ใช้ Code ทั่วโลก หากต้องการเข้าถึงการ์ดทิปของคุณ โปรดส่งข้อความถึง @getcode บน X - อนุญาตให้ Code ส่งการแจ้งเตือนถึงคุณเมื่อคุณได้รับทิปจากผู้ใช้ Code รายอื่น - พิมพ์ \"%1$s\" - เราได้ทำการเปลี่ยนแปลงบางอย่างเพื่อปรับปรุงประสบการณ์ หากต้องการใช้ Code ต่อไปคุณจะต้องอัปเดตแอป - บัญชีเจ้าของที่ถูกต้อง - บัญชีโทเค็นที่ถูกต้อง - ค่าของ Kin เปลี่ยนไป - ถูกส่งคืนให้คุณแล้ว - คุณต้องการถอน Kin ของคุณไปที่ไหน - ยืนยันตัวตนของคุณเพื่อถอน kin - คุณได้ฝาก - คุณได้ให้ - คุณมี %1$d คำเชิญ - ปัจจุบันเข้า Code ได้เฉพาะผู้ได้รับคำเชิญเท่านั้น คุณมีเหลืออยู่ %1$d - คุณได้จ่าย - คุณได้ให้ - คุณได้รับแล้ว - คุณได้ส่ง - คุณได้ใช้ไป - คุณให้ทิปบางคน - คุณได้ถอน - ถอนเงินของคุณเรียบร้อยแล้ว - บัญชี X ของคุณเชื่อมต่อกับบัญชี Code ของคุณเรียบร้อยแล้ว ตอนนี้คุณสามารถขอรับทิปได้แล้ว - ถอนสำเร็จ - เชื่อมต่อบัญชี X สำเร็จ - หมดอายุการเข้าถึงแล้ว - คีย์การเข้าถึง - การตั้งค่าแอป - เปิดใช้งานกล้องอัตโนมัติ - ยอดคงเหลือ - การตั้งค่าสถานะเบต้า - โบนัส - ซื้อและขาย Kin - การชำระเงินด้วยเงินสด - ทีม Code - การซื้อ Kin - การชำระเงินผ่านเว็บ - ทิป - ตัวเลือกการแก้จุดบกพร่อง - ฝาก Kin - ฝากแล้ว - ป้อนคำที่เป็นคีย์การเข้าถึง - ป้อนหมายเลขโทรศัพท์ - ล้มเหลว - คำถามที่พบบ่อย - ให้ - รับเงินสด - ชวนเพื่อนมาเริ่มต้นใช้งาน Code - รับ Kin - รับ Kin เพิ่มเติม - ให้เงินสด - ให้ Kin - เงินทุนไม่เพียงพอ - ชวนเพื่อน - ข้อเสนอในเวลาจำกัด - เชื่อมโยงแล้ว - สกุลเงินท้องถิ่น - บัญชีของฉัน - ไม่ได้เชื่อมโยง - สกุลเงินอื่น ๆ - ชำระแล้ว - อยู่ระหว่างดำเนินการ - หมายเลขโทรศัพท์ - นโยบายความเป็นส่วนตัว - ซื้อแล้ว - เชื่อมต่อบัญชี X สำเร็จ - รับทิป - ได้รับแล้ว - สกุลเงินล่าสุด - แนะนำเพื่อน รับ $5 - โบนัสการแนะนำ - ขอรับเงินสด - ขอรับ Kin - ขอรับทิป - ต้องใช้ Face ID - ต้องใช้รหัสผ่าน - ต้องใช้ Touch ID - ผลลัพธ์ - กลับมาแล้ว - เลือกบัญชี - เลือกประเทศ - เลือกสกุลเงิน - ส่งแล้ว - ใช้จ่าย - สลับบัญชี - เงื่อนไขการให้บริการ - บัตรทิป - ให้ทิปเป็น Kin - เปิดการแจ้งเตือนสำหรับ Code - ไม่ทราบ - จำเป็นต้องอัปเดต - ยืนยันหมายเลขโทรศัพท์ - โบนัสต้อนรับ - ถอน Kin - ถอนแล้ว - คีย์การเข้าถึงของคุณ - แตะโลโก้เพื่อแชร์ลิงก์ดาวน์โหลดแอป - diff --git a/apps/codeApp/src/main/res/values-tr/strings-localized.xml b/apps/codeApp/src/main/res/values-tr/strings-localized.xml deleted file mode 100644 index ba407c37b..000000000 --- a/apps/codeApp/src/main/res/values-tr/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Banka Kartıyla Nakit Ekle - Kamera Erişimine İzin Ver - Kişiler\'e Erişime İzin Ver - Anlık Bildirimlere İzin Ver - Bakiye - Kin Satın Al - Daha Fazla Kin Satın Al - İptal et - Göndermeyi İptal Et - Sohbet - Bu Parayı Topla - Onayla - X\'e Bağlan - Devam et - Kopyalandı - Kopyala - Adresi Kopyala - Hesap Oluştur - Yeni Code Hesabı Oluştur - Hesabı Sil - Bitti - Hemen İndir - Face ID\'yi etkinleştir - Touch ID\'yi etkinleştir - Çıkış - Ver - Kin Ver - Davet Et - Davetler - Bekleme Listesine Gir - Daha sonra - Nasıl Kin Satın Alınacağını Öğren - Nasıl Kin Satılacağını Öğren - Telefon Numarası Bağla - Giriş Yap - Çıkış Yap - Bağlamak için @getcode\'a mesaj gönder - Sesi kapat - İleri - Hayır, Tekrar Dene - Şimdi Değil - Tamam - Ayarlar\'ı Aç - Yapıştır - Panodan Yapıştır - Hesabı Bağlamak için Gönder - Cüzdana Yerleştir - Al - Mevcut Hesabı Kurtar - Hatırlat - Telefon Numarasını Kaldır - Telefon Numaranı Kaldır - Bahşiş İste - Erişim Anahtarını Fotoğraflarım\'a Kaydet - Fotoğraflar\'a Kaydet - Gönder - Doğrulama Kodu Gönder - Paylaş - URL olarak paylaş - İndirme Bağlantısı Paylaş - Bu Videoyu Paylaş - Bahşiş Kartımı Göster - Kamerayı Başlat - Abone Ol - Oturum Açmak İçin Kaydırın - Ödemek için Kaydır - Bahşiş Vermek İçin Kaydır - Farklı Code Hesabı Dene - Onları Tweet\'le - Code\'un Kilidini Aç - Sesi aç - Abonelikten çık - Güncelle - Erişim Anahtarını Görüntüle - Kin Çek - Bunun Yerine 12 Kelimeyi Yazdın mı? - Evet - Evet, Kin Çek - Evet, Yazdım - ve - Kin - Kin - Hesabınla ilişkili tüm bilgileri Code\'un sunucularından kaldır (telefon numarası, kişiler, işlem geçmişi) - Gizli Kurtarma İfadeni kullanarak diğer kripto cüzdanı uygulamalarındaki hesabına erişebilirsin. Hesabını Code\'da kullanamayacaksın - Hesabını blok zincirinden kaldır - Silme işleminin yapacakları - Olacaklar - Silme işleminin yapmayacakları - Bu Kin zaten başka biri tarafından toplandı. - Bu Kin zaten başka birisi tarafından toplandı. - Code\'u kullanmak için lütfen Ayarlar\'dan kamera erişimine izin ver. - Bu nakdin süresi doldu. - Lütfen telefon numaranı yeniden gir ve tekrar dene. - Davetler göndermek için lütfen Ayarlar\'da kişilere erişim izni ver. - Code, şu anda ülkenizde kullanılamıyor. - Şu anda cihazınızı destekleyemiyoruz - eSim desteği, muhtemelen Code\'un gelecekteki bir sürümünde gelecektir. - Bir şeyler ters gitti. Bu Kin toplanamadı. - Bunun olmasını beklemiyorduk. Bir şeyler ters gitti. Lütfen bu hesabı tekrar oluşturmayı dene. - Erişim Anahtarını kaydetmek için lütfen Ayarlar\'dan Code için Fotoğraflar\'a erişim izni ver. - Bir şeyler ters gitti. Lütfen telefon numaranızı doğru girdiğinize emin olun. - Bir şeyler ters gitti. Lütfen tekrar dene. - Paran çekilemedi. Bir şeyler ters gitti, lütfen para çekme işlemini tekrar dene. - Code, %1$s veya daha düşük tutardaki, küçük ve gündelik işlemler için tasarlanmıştır. - Code, küçük ve gündelik işlemler için tasarlanmıştır. Günlük verme limitin yarın artacak. - Daha fazla Kin\'e nasıl sahip olacağını öğrenmek için Ayarlar\'da bulunan SSS bölümüne git. - Lütfen farklı bir Davet Kodu girin ve tekrar deneyin. - Lütfen geçerli bir telefon numarası gir ve tekrar dene. - Bu, geçersiz bir Bahşiş Kartı. - Lütfen geçerli bir kod gir ve tekrar dene. - Üzgünüz, bir ağ sorunu yaşadık. Lütfen arkadaşını tekrar davet etmeyi dene. - Bu Kin, 24 saat içinde alınmadığı için otomatik olarak gönderene iade edildi. Lütfen Kin\'i tekrar göndermelerini iste. - Lütfen telefon numaranı yeniden gir ve tekrar dene. - Lütfen kodu daha net gösteren bir görsel elde etmeye çalışın. - Lütfen internet bağlantını kontrol et veya daha sonra tekrar dene. - Şu anda Code\'a yalnızca davetle katılınabilir. Daha fazla davetiye olduğunda seni haberdar edeceğiz. - Bekleme listesine girmek için %1$s adresine git - En fazla %1$s satın alabilirsiniz. Lütfen daha düşük bir miktar girin. - En az %1$s satın alabilirsiniz. Lütfen daha yüksek bir miktar girin. - Erişim Anahtarınız bir kilit açma işlemi başlattı. Sonuç olarak artık bu Erişim Anahtarını Code\'da kullanamayacaksınız. - Bahşiş Kartını etkinleştirmek için bu kişiye bir tweet gönderin. - Bahşiş verebileceğin maksimum tutar %1$s. Lütfen daha küçük bir tutar gir. - Bahşiş verebileceğin minimum tutar %1$s. Lütfen daha büyük bir tutar gir. - Her gün yalnızca belirli sayıda yeni hesap oluşturabilirsin. - Code, şu anda cihaz başına bir hesapla sınırlıdır. Birden fazla hesap desteği, muhtemelen Code\'un gelecekteki bir sürümünde gelecektir. - Code, şu anda telefon numarası başına bir hesapla sınırlıdır. Birden fazla hesap desteği, muhtemelen Code\'un gelecekteki bir sürümünde gelecektir. - Kin Zaten Toplandı - Kin Zaten Toplandı - Kamera Erişimi Gerekli - Nakit Süresi Doldu - Doğrulama Kodu Zaman Aşımı - Kişilere Erişim Gerekiyor - Desteklenmeyen Ülke - Cihaz Desteklenmiyor - eSim\'ler Şu anda Desteklenmiyor - Toplanamadı - Hesap Oluşturulamadı - Kaydedilemedi - Gönderilemedi - Onaylanamadı - İşlem Başarısız - Verme Limitine Ulaşıldı - Günlük Limite Ulaşıldı - Yetersiz Kin - Davet Kodu Geçersiz veya Süresi Dolmuş - Geçersiz Telefon Numarası - Geçersiz Bahşiş Kartı - Geçersiz Kod - Davet Başarısız - Bağlantının Süresi Doldu - Maksimum Denemeye Ulaşıldı - Kod Bulunamadı - İnternet Bağlantısı Yok - Davetin Yok - Henüz Davet Edilmedin - Alım miktarı çok yüksek - Alım miktarı çok düşük - Artık Erişim Anahtarı Kodda Kullanılamaz - Bahşiş Kartı Henüz Etkinleştirilmedi - Bahşiş Çok Fazla - Bahşiş Çok Az - Çok fazla hesap oluşturuldu - Hesap Zaten Oluşturuldu - Hesap Zaten Oluşturuldu - Ödemelerin basit, güçlü ve küresel olması gerektiğine inanıyoruz. Code, ileri blok zinciri teknolojisiyle geliştirilerek, küresel eşler arası transferler, çevrim içi bireysel makalelerin kilidini açan mikro ödemeler ve favori oluşturucularınız için sıfır ücret ipuçları gibi geleneksel ödeme uygulamalarının sunamayacağı özellikler sunar. - Kin, Bitcoin gibi bir kripto para birimidir ama aynı zamanda hızlı ve ucuz ödemeler için de tasarlanmıştır. - Bitcoin gibi yalnızca sınırlı sayıda Kin mevcuttur. Daha fazla kişi Kin satın alırsa değer artar, daha fazla kişi Kin satarsa ​​değer düşer. Bu dinamik, Kin\'in edinilmesi artarsa Kin\'i elinde bulunduran herkesin değer yaratımından pay almasını sağlar. - Kin\'i banka kartınızla satın alabilirsiniz. Buna Kin Al sekmesinden erişilebilir. - Evet, yapabilirsiniz. Kin satmak, birçok kripto para borsasında desteklenmektedir. - Yardım edebileceğiniz üç ana yol var: Sosyal medyada Code deneyiminiz hakkında konuşun, arkadaşlarınızı Code\'u kendileri denemeye teşvik edin ve favori internet sitelerinzden [getcode.com](https://getcode.com) adresine göz atmalarını isteyerek Code ödemelerini entegre etmelerini teşvik edin. - Code Nedir? - Code ödemeleri neden Kin cinsinden yapılıyor? - Kin\'in değeri neden değişiyor? - Nasıl daha fazla Kin satın alabilirim? - Kin\'i satabilir miyim? - Nasıl yardımcı olabilirim? - şunları kabul ediyorsun: - \"Hesap Oluştur\"a veya \"Giriş Yap\"a dokunarak - Kameran Kin almak için kullanılıyor. Devam etmek için lütfen kameraya erişim izni ver. - Hesabın hakkında seni zamanında bilgilendirmek için anlık bildirimlere ihtiyacımız var. - Para çekme işlemleri tek yönlüdür ve başlatıldıktan sonra geri alınamaz. - Bu hesaptaki tüm paralar kaybolacaktır. Hesabını silmek kalıcıdır ve geri alınamaz. Bu hesabı silmek istediğinden emin misin? - 24 saat içinde toplanmayan herhangi bir Kin, otomatik olarak bakiyene iade edilecektir. - Hesap oluşturmayı tekrar başlatman ve telefon numaranı tekrar doğrulaman gerekecek. - Erişim Anahtarını kullanarak bu hesaba geri dönebilirsin. - %1$s adlı sohbetten gelen yeni mesajlar sana bildirilmeyecek. İstediğin zaman sohbetin sesini açabilirsin. - Şu anda yalnızca Code aracılığıyla oluşturulan hesaplar desteklenmektedir. - Artık arkadaşların bu telefon numarasıyla seni bulamayacak. - %1$s adlı sohbetten gelen tüm mesajlar sana bildirilecek. Sohbeti istediğin zaman sessize alabilirsin. - Onlara tekrar ödeme yapana kadar %1$s adlı sohbetten herhangi bir mesaj almayacaksın. - Erişim Anahtarın, Code hesabına erişim sağlayacaktır. Kişisel ve güvenli olarak sakla. - Bu 12 kelime, Code hesabını kurtarmanın tek yoludur. Bunları not ettiğinden emin ol ve kişisel ve güvenli olarak sakla. - Emin misin? - Bu hesabı silmek istediğinden emin misin? - Bağlantıyı gönderdin mi? - Çıkmak istediğine emin misin? - Çıkış yapmak istediğinden emin misin? - %1$s sessize alınsın mı? - Bir Code Hesabı Değil - Emin misin? - %1$s sohbetinin sesi açılsın mı? - %1$s aboneliğinden çıkılsın mı? - Erişim Anahtarın Görüntülensin Mi? - Emin Misin? - Hesabınıza %1$s Kin yatırıldı. - Dün gönderdiğiniz %1$s toplanmadı. Otomatik olarak bakiyenize iade edilmiştir. - Bir arkadaşınızı Code\'a başlatın ve $5 kapın - Herhangi bir mesajlaşma uygulaması vasıtasıyla nakit gönderin - Birisine ilk Kin\'lerini gönderdiğiniz için %1$s Kin kazandınız. - Artık bahşişleri talep edebilirsiniz. - Para Yatırma Alındı - Kin İade Edildi - Code\'da Yeni - Yönlendirme Bonusu Alındı - X Hesabı Bağlandı - Code erişim davetinin süresi doldu. Oturumu kapatabilir ve geçerli bir davet durumuna sahip farklı bir hesap kullanabilirsin. - Erişim Anahtarın, paralarına erişmenin tek yoludur. Lütfen kişisel ve güvenli olarak sakla. - Erişim Anahtarını görüntülemek için kimliğini doğrula. - Code\'da oturum açmak amacıyla QR kodunu açmak için Google Lens simgesine dokunun. Alternatif olarak Code Oturum Açma ekranında 12 kelimeyi girerek manuel olarak oturum açabilirsiniz. - Uyarı! Bu görüntü, Code\'da sahip olduğunuz tüm fonlara erişim sağlar. Bu görüntüyü başka kimseyle paylaşmayın. Emniyetli bir şekilde saklayın. - Code, kameranı başka bir kullanıcının telefonundaki dijital faturaya doğrultarak Kin almanı sağlar. - Kin alabilmek için kamera erişimine izin vermen gerekiyor - Code\'a erişmek için kimlik doğrulaması yapın. - Kin Satın Al - Kin Satın Al (Çok Yakında) - Kin alıp satmak, şu anda kompleks bir süreç. Bu süreçler zamanla daha da basitleşecek. Bugün nasıl Kin alınıp satılacağını öğrenmek istiyorsanız aşağıdaki rehber videolarını izleyebilirsiniz. - Yalnızca %1$s tutarına kadar verebilirsin - En fazla %1$s tutarında bahşiş verebilirsin - Kin\'iniz artık Code uygulamasında kullanıma hazır - Birisine ilk Kin gönderen sen oldun! İşte referans bonusun: - USDC\'niz Kin\'e dönüştürülüyor. Bu işlemin tamamlanması yaklaşık bir dakika sürecektir. - USDC\'yi başarıyla yatırdınız. Satın alma işleminizi tamamlamak için Code uygulamasını açın - Hoş Geldiniz Bonusu - Ülke seç - Çok Yakında - \@getcode Dünyanın her yerindeki insanlardan ipuçları alabilmek için X hesabımı bağlamak istiyorum - Sil - Gizli Kurtarma İfadeni kaydettiğinden emin ol ve sonra Code hesabını silmek için \"Sil\"e bas. Bu eylem geri alınamaz. - %1$s\'de bir kod almadın mı? - Kodu almadın mı? Tekrar gönder - Face ID\'yi devre dışı bırakmak, kimliğini doğrulamanı gerektirir. - Code Wallet uygulamasına sahip değil misin? - Herhangi bir Kin\'in yok. - Code\'daki işlem güvenliğini daha fazla geliştirmek için Face ID\'yi etkinleştir. - Hedef adresi gir - En fazla %1$s girebilirsin - Bir arkadaşınızı Code\'a kaydettirdiğinizde ve ona, ilk Kin\'ini gönderdiğinizde, $5\'lık bedava Kin kazanırsınız. - Code, ödemeler için kripto para birimi Kin\'i kullanır. İşte Code cüzdanına daha fazla Kin almanın bazı yolları. - $1 Kin\'inizi Ücretsiz Alın - Kin vermek için kimliğini doğrula. - Aşağıda bulunan Para Yatırma Adresine göndererek Code cüzdanına Kin yatır. Kopyalamak için dokun. - Lütfen daha fazla Kin alın ve tekrar ödeme yapmayı deneyin - Geçersiz hedef hesabı - Lütfen çekim yapacağınız adresin cüzdan sağlayıcınız tarafından başlatıldığından emin olun. Bunu başarmanın bir kestirmesi, gönderim yapmayı düşündüğünüz cüzdanda küçük bir miktar SOL\'u Kin ile takas etmektir. - Davet Kodu - Şu anda Code\'a katılım sadece davetledir. Uygulamaya erişmek için bir Davet Koduna ihtiyacınız olacak. - %1$d Davet - Code, şu anda yalnızca davetle kullanılabilen yeni bir kripto cüzdan uygulamasıdır. Code\'u indirmek için %1$s adresine gidin. - Daha fazla bilgi edin - Telefon numaran bu Code hesabıyla bağlantılıdır. Arkadaşların bu telefon numarasını kullanarak seni bulabilir. - Dünyanın dört bir yanındaki kişilerden bahşiş alabilmek için X hesabımı @getcode ile bağlıyorum. - Bakiye ve işlem geçmişiniz yükleniyor - Hesabını ilk oluşturduğunda Erişim Anahtarı için kaydettiğin fotoğraflarını kontrol et. - Şu anda bir hesaba giriş yaptın. Lütfen devam etmeden önce Erişim Anahtarını kaydettiğinden emin ol. Oturumu kapatıp yeni bir hesapla oturum açmak ister misin? - Bağlantılı Olmayan\nTelefon Numarası - Bu Code hesabına bağlı bir telefon numaran bulunmuyor. Arkadaşlarının seni keşfetmesi için bağlantı kur. - Ağ bağlantısı yok - Code\'da - Code uygulamasını aç ve bu parayı almak için kameranı doğrult - Kişilerin Düzenleniyor - Bu telefon numarası, Kişilerinde değil. Onu yine de Code\'a davet edebilirsin. - Ülke koduyla birlikte telefon numaranı gir. Daveti alan telefon numarasını kullandığından emin ol. - Powered by - Artık bahşiş isteyebilirsin. - Yönlendirme bonusu alındı - %1$s Gönder - İşte %1$s - %1$s içinde yenisini iste - Code Wallet uygulamasını indirmek için bu QR kodunu telefonunun kamerasıyla tara - Code Wallet app\'i\nindirmek için tara - Para birimi ara - Kişileri ara - Telefon numaranıza doğrulama kodu içeren bir SMS mesajı gönderildi. Lütfen yukarıdaki doğrulama kodunu girin. - Birisi sana para gönderdi - Birisi sana bahşiş verdi - Code\'ları taramak için kameranızı başlatmanız gerekir - Bahşiş Kartın, dünyanın dört bir yanındaki Code kullanıcılarından bahşişler almana olanak tanır. Tip Kartına erişmek için X kimliğini bağla. - Bahşiş Kartınız, dünyanın her yerindeki Code kullanıcılarından ipuçları almanızı sağlar. Bahşiş Kartınıza erişmek için X\'e gönderin. - Tip Card\'ınız dünyanın her yerindeki Code kullanıcılarından bahşiş almanızı sağlar. Tip Card\'ınıza erişmek için X üzerinden @getcode\'a mesaj gönderin. - Diğer Code kullanıcılarından ipuçları aldığınızda Code\'un size bildirim göndermesine izin verin.\n\n\n\n\n\n\n - \"%1$s\" yaz - Deneyimi iyileştirmek için bazı değişiklikler yaptık. Code\'u kullanmaya devam etmek için uygulamayı güncellemen gerekiyor. - Geçerli sahip hesabı - Geçerli token hesabı - Kin değişim değeri. - sana geri döndü - Kin\'ini nereye çekmek istersin? - Kin çekmek için kimliğini doğrula. - Yatırdın - Verdin - %1$d Davetin Var - Şu anda Code\'a yalnızca davetle katılınabilir. %1$d süren kaldı. - Ödedin - Verdin - Aldığın - Gönderdin - Harcadın - Birisine bahşiş verdin - Çektin - Paran başarıyla çekildi. - X hesabın, Code hesabına başarıyla bağlandı. Artık Bahşiş isteyebilirsin. - Para Çekme Başarılı - X Hesabı Başarıyla Bağlandı - Erişim Sona Erdi - Erişim Anahtarı - Uygulama Ayarları - Kamerayı Otomatik Başlat - Bakiye - Beta Bayrakları - Bonus - Kin Al & Sat - Nakit Ödemeler - Code Ekibi - Kin Satın Alımları - Web Ödemeleri - Bahşişler - Hata Ayıklama Seçenekleri - Kin Yatır - Yatırdı - Erişim Anahtarı Kelimelerini Gir - Telefon Numarası Gir - Başarısız - SSS - Verdi - Nakıt Al - Bir Arkadaşınızı Code\'a başlatın - Kin Al - Daha Fazla Kin Al - Nakit Ver - Kin Ver - Yetersiz Bakiye - Arkadaşını Davet Et - Sınırlı Süre Teklifi - Bağlı - Yerel Para Birimi - Hesabım - Bağlı Değil - Diğer Para Birimleri - Ödendi - Bekliyor - Telefon Numarası - Gizlilik Politikası - Satın alındı - X Hesabı Başarıyla Bağlandı - İpuçları Alın - Aldı - Son Para Birimleri - Bir Arkadaş Yönlendir, $5\'ı Kazan - Yönlendirme Bonusu - Nakit Talep Et - Kin Talep Et - Bahşiş İste - Face ID gerekir - Şifre Gerekir - Touch ID gerekir - Sonuçlar - İade Edildi - Bir Hesap Seç - Bir Ülke Seç - Para Birimi Seç - Gönderildi - Harcanan - Hesap Değiştir - Hizmet Şartları - Bahşiş Kartı - Bahşiş Olarak Kin Ver - Code için Bildirimleri Açın - Bilinmeyen - Güncelleme Gerekli - Telefon Numarasını Doğrula - Hoş Geldin Bonusu - Kin Çek - Çekti - Erişim Anahtarın - Uygulama indirme bağlantısını paylaşmak için logoya dokun - diff --git a/apps/codeApp/src/main/res/values-uk/strings-localized.xml b/apps/codeApp/src/main/res/values-uk/strings-localized.xml deleted file mode 100644 index b26b1dd86..000000000 --- a/apps/codeApp/src/main/res/values-uk/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Внести гроші з дебетової картки - Дозволити доступ до камери - Дозволити доступ до контактів - Дозволити push-сповіщення - Баланс - Купити Kin - Купити більше Kin - Скасувати - Скасувати надсилання - Чат - Заберіть ці гроші - Підтвердити - Підключитися до X - Продовжити - Скопійовано - Копіювати - Копіювати адресу - Створити акаунт - Створити акаунт з новим гаманцем Code - Видалити акаунт - Виконано - Завантажте його зараз - Увімкнути Face ID - Увімкнути Touch ID - Вийти - Дати - Дати Кін - Запросити - Запрошення - Приєднатися до списку очікування - Пізніше - Дізнайтеся, як купити Kin - Дізнайтеся, як продати Kin - Прив\'язати номер телефону - Увійти - Вийти - Надішліть повідомлення @getcode для підключення - Вимкнути звук - Наступне - Ні, спробувати ще раз - Не зараз - ДОБРЕ - Відкрити налаштування - Вставити - Вставити з буфера обміну - Зробіть публікацію, щоб прив’язати акаунт - Покласти в гаманець - Отримати - Відновити існуючий акаунт - Нагадати - Видалити номер телефону - Видалити свій номер телефону - Запросити чайові - Зберегти ключ доступу до моїх фотографій - Зберегти до фотографій - Надіслати - Надіслати код підтвердження - Поділитися - Поділитися як URL-адресою - Поділитись посиланням на завантаження - Поділитися цим відео - Показати мою картку для чайових - Увімкнути камеру - Підписатися - Проведіть пальцем, щоб увійти - Проведіть, щоб оплатити - Проведіть пальцем, щоб дати чайові - Спробуйте акаунт з іншим гаманцем Code - Твітнути - Розблокувати Code - Увімкнути звук - Відписатись - Оновити - Переглянути ключ доступу - Вивести Kin - Натомість записав 12 слів? - Так - Так, вивести Kin - Так, я їх записав - та - Kin - Kin - Видалиться уся інформація, пов’язана з вашим обліковим записом, із серверів Code (номер телефону, контакти, історію транзакцій) - Ви можете отримати доступ до свого облікового запису в інших крипто-гаманцях за допомогою секретної фрази відновлення. Ви не зможете використовувати свій обліковий запис у Code - Усунення вашого облікового запису із блокчейну - Що станеться після видалення - Що станеться - Чого не станеться після видалення - Цей Kin вже забрав хтось інший. - Цей Kin вже хтось забрав - Щоб використовувати Code, будь ласка, надайте доступ до камери в налаштуваннях. - Термін дії цієї готівки минув. - Будь ласка, введіть номер свого телефону повторно та спробуйте ще раз. - Щоб надіслати запрошення, надайте доступ до контактів у налаштуваннях. - Code наразі недоступний у вашій країні. - Наразі ми не підтримуємо ваш пристрій - Підтримка eSims, ймовірно, з’явиться в наступній версії Code. - Щось пішло не так. Цей Kin не вдалося зібрати. - Ми не очікували, що це станеться. Щось пішло не так. Будь ласка, спробуйте створити цей акаунт знову. - Щоб зберегти ключ доступу, будь ласка, надайте Code доступ до фотографій у налаштуваннях. - Щось пішло не так. Будь ласка, переконайтеся, що ваш номер телефону введений правильно. - Щось пішло не так. Будь ласка, спробуйте ще раз. - Не вдалося зняти ваші кошти. Щось пішло не так, будь ласка, спробуйте зняти ще раз. - Code розроблено для невеликих щоденних транзакцій на суму %1$s або менше. - Code розроблено для невеликих щоденних транзакцій. Ваш щоденний ліміт віддачі збільшиться завтра. - Щоб дізнатися, як отримати більше Kin, перейдіть до розділу \"Часто задавані питання\" в налаштуваннях. - Будь ласка, введіть інший Код запрошення і спробуйте ще раз. - Будь ласка, введіть дійсний номер телефону та повторіть спробу. - Це недійсна картка для чайових. - Введіть, будь ласка, дійсний код та повторіть спробу. - На жаль, у нас виникла проблема з мережею. Будь ласка, спробуйте запросити свого друга ще раз. - Цей Kin був автоматично повернутий відправнику, оскільки його не було забрано протягом 24 годин. Будь ласка, попросіть відправника надіслати Kin знову. - Будь ласка, введіть номер свого телефону повторно та спробуйте ще раз. - Будь ласка, спробуйте отримати зображення, на якому код видно більш чітко. - Будь ласка, перевірте своє підключення до інтернету або повторіть спробу пізніше. - Наразі Code доступний лише за запрошенням. Ми повідомимо вам, коли буде більше доступних запрошень. - Щоб приєднатися до списку очікування, перейдіть до %1$s - Максимальна кількість, яку ви можете придбати, — %1$s. Введіть меншу кількість. - Мінімальна кількість, яку ви можете придбати, — %1$s. Введіть більшу кількість. - Ваш Ключ доступу розпочав розблокування. У результаті ви більше не зможете використовувати цей Ключ доступу в Code. - Надішліть твіт цій людині, щоб активувати її картку для чайових. - Максимальна сума чайових становить %1$s. Будь ласка, введіть меншу суму. - Мінімальна сума чайових становить %1$s. Будь ласка, введіть більшу суму. - Ви можете створювати лише стільки нових акаунтів щодня. - Code наразі обмежений одним обліковим записом на пристрій. Підтримка кількох облікових записів, ймовірно, з’явиться в наступній версії Code. - Code наразі обмежений одним обліковим записом на номер телефону. Підтримка кількох облікових записів, ймовірно, з’явиться в наступній версії Code. - Kin вже забрали - Kin вже забрано - Потрібний доступ до камери - Термін дії готівки минув - Термін дії коду підтвердження минув - Необхідний доступ до контактів - Країна не підтримується - Пристрій не підтримується - eSims наразі не підтримуються - Не вдалося зібрати - Не вдалося створити акаунт - Не вдалося зберегти - Не вдалося надіслати - Не вдалося підтвердити - Помилка транзакції - Досягнуто ліміту віддачі - Досягнуто щоденного ліміту - Недостатньо Kin - Код запрошення недійсний або протермінований - Недійсний номер телефону - Недійсна картка для чайових - Невірний код - Запрошення не спрацювало - Посилання прострочено - Досягнуто максимальної кількості спроб - Код не знайдено - Немає підключення до інтернету - У вас немає запрошень - Вас ще не запросили - Обсяг покупки занадто великий - Обсяг покупки занадто малий - Ключ доступу більше не може використовуватися в Code - Картку для чайових ще не активовано - Чайові занадто великі - Чайові занадто малі - Створено забагато акаунтів - Обліковий запис вже створений - Обліковий запис вже створений - Ми вважаємо, що платежі повинні бути простими, ефективними та глобальними. Завдяки використанню передової технології блокчейн, Code пропонує функції, недоступні для традиційних платіжних додатків. Серед них – міжнародні перекази між користувачами, мікроплатежі для оплати окремих онлайн-статей та безкомісійні винагороди для ваших улюблених авторів. - Kin — це криптовалюта, як і Bitcoin, але вона також призначена для швидких і недорогих платежів. - Як і з Bitcoin, загальна кількість Kin обмежена. Якщо більше людей купують Kin, його вартість зростає, а якщо більше продають – знижується. Ця динаміка дозволяє усім, хто володіє Kin, отримати вигоду з його вартості, якщо його використання зростає. - Ви можете купити Kin за допомогою дебетової картки. Це доступно на вкладці «Отримати Kin». - Так, авжеж. Продаж Kin підтримується низкою криптовалютних бірж. - Є три основні способи, як ви можете допомогти: розповідайте про свій досвід використання Code у соціальних мережах, заохочуйте своїх друзів спробувати Code та просіть улюблені вебсайти інтегрувати платежі Code, запропонувавши їм відвідати [getcode.com](https://getcode.com). - Що таке Code? - Чому платежі в Code здійснюються в Kin? - Чому змінюється вартість Kin? - Як я можу купити більше Kin? - Чи можу я продати Kin? - Чим я можу допомогти? - погоджуєтеся з нашими - Торкнувшись \"Створити акаунт\" або \"Увійти\", ви - Ваша камера використовується для отримання Kin. Щоб продовжити, надайте, будь ласка, доступ до камери. - Нам потрібні push-сповіщення, щоб надсилати вам своєчасну інформацію про ваш акаунт. - Виведення є незворотним і не може бути скасовано після початку. - Усі кошти на цьому акаунті будуть втрачені. Видалення вашого акаунту є остаточним і не може бути скасовано. Ви впевнені, що хочете видалити цей акаунт? - Будь-який Kin, який не заберуть протягом 24 годин, буде автоматично повернутий на ваш баланс. - Вам потрібно буде перезапустити створення акаунту та знову підтвердити свій номер телефону. - Ви можете повернутися до цього акаунту за допомогою свого ключа доступу - Ви не будете отримувати сповіщення про нові повідомлення від %1$s. Ви можете вимкнути чат у будь-який час. - Наразі підтримуються акаунти, створені лише за допомогою Code. - Ваші друзі більше не зможуть знайти вас за цим номером телефону. - Ви будете отримувати сповіщення про всі нові повідомлення від %1$s. Ви можете вимкнути чат у будь-який час. - Ви не будете отримувати повідомлення від %1$s, поки знову не оплатите. - Ваш ключ доступу надасть доступ до вашого акаунту Code. Зберігайте його в таємниці та безпеці. - Ці 12 слів є єдиним способом відновити ваш акаунт Code. Переконайтеся, що ви їх записали та зберігайте їх у таємниці та безпеці. - Ви впевнені? - Ви впевнені, що хочете видалити цей акаунт? - Ви надіслали посилання? - Ви впевнені, що хочете вийти? - Ви впевнені, що бажаєте вийти? - Вимкнути %1$s? - Не акаунт Code - Ви впевнені? - Увімкнути чат для %1$s? - Відписатися від %1$s? - Переглянути свій ключ доступу? - Ви впевнені? - %1$s Kin було внесено на ваш рахунок. - %1$s, який ви надіслали вчора, не був забраний. Його було автоматично повернуто на ваш рахунок. - Запросіть друга в Code та отримайте 5 дол. - Надсилайте готівку через будь-який застосунок-месенджер - Ви отримали %1$s Kin за надсилання комусь його першого Kin. - Тепер ви можете отримувати чайові. - Депозит отримано - Kin повернуто - Новинка на Code - Отримано реферальний бонус - Акаунт X прив’язано - Термін дії вашого запрошення на доступ до Code минув. Ви можете вийти з системи та використовувати інший акаунт з дійсним статусом запрошення. - Ваш ключ доступу — це єдиний спосіб отримати доступ до ваших коштів. Будь ласка, тримайте його в таємниці та безпеці. - Підтвердьте свою особу, щоб переглянути свій ключ доступу. - Торкніться іконки Google Об\'єктиву, щоб відкрити QR-код і увійти в Code. Ви також можете увійти вручну, увівши 12 слів на екрані входу у Code. - Увага! Це зображення дає доступ до всіх коштів, які Ви маєте у Code. Не діліться ні з ким цим зображенням. Зберігайте його під надійним захистом. - Code дозволяє вам отримати Kin, навівши свою камеру на цифровий рахунок на телефоні іншого користувача - Вам слід надати доступ до камери, щоб мати можливість приймати Kin - Пройдіть автентифікацію для доступу до Code. - Купити Kin - Купити Kin (Скоро буде) - Купівля та продаж Kin наразі є складним процесом. З часом ці процеси стануть простішими. Якщо ви хочете дізнатися, як купувати та продавати Kin сьогодні, ви можете переглянути навчальні відео нижче. - Ви можете віддати тільки до %1$s - Ви можете дати чайові не більше, ніж %1$s - Kin тепер доступні для використання у застосунку Code - Ви надіслали комусь їхній перший Kin! Ось ваш реферальний бонус: - USDC конвертуються в Kin. Це має зайняти приблизно одну хвилину - Ви успішно внесли USDC. Щоб завершити покупку, відкрийте застосунок Code - Вітальний бонус - Вибрати країну - Скоро буде - \@getcode Я хочу підключити свій обліковий запис X, щоб отримувати поради від людей з усього світу - Видалити - Переконайтеся, що ви зберегли секретну фразу відновлення, а потім введіть «Видалити», щоб видалити обліковий запис Code. Ця дія незворотна. - Не отримали код на %1$s? - Не отримали код? Надіслати повторно - Щоб вимкнути Face ID, вам потрібно підтвердити свою особу. - Не маєте застосунку Гаманець Code? - У вас ще немає Kin. - Увімкніть Face ID, щоб ще більше підвищити безпеку транзакцій у Code. - Введіть адресу призначення - Введіть до %1$s - Отримайте 5 дол. в Kin безкоштовно, коли ваш друг зареєструється в Code за вашим запрошенням, і ви надішлете йому його перший Kin. - Code використовує криптовалюту Kin для платежів. Ось кілька способів отримати більше Kin у ваш гаманець Code. - Отримайте свій перший 1 дол. в Kin безкоштовно - Підтвердьте свою особу, щоб надати Kin. - Внесіть Kin у свій гаманець Code, надіславши Kin на свою депозитну адресу нижче. Торкніться, щоб скопіювати. - Будь ласка, отримайте більше Kin та спробуйте оплатити знову - Недійсний рахунок призначення - Будь ласка, переконайтесь, що адреса, на яку ви виводите кошти, була ініціалізована провайдером вашого гаманця. Зробити це найпростіше можна переславши спочатку невеличку суму SOL для Kin у гаманець, в який ви намагаєтесь надіслати кошти. - Код запрошення - Приєднатись до Code зараз можна тільки за запрошенням. Вам знадобиться Код запрошення, щоб отримати доступ до застосунку. - %1$d запрошень - Code - це новий застосунок-криптогаманець, де наразі зареєструватись можна тільки за запрошенням. Щоб завантажити Code, перейдіть до %1$s - Довідатися більше - Ваш номер телефону прив’язано до акаунту цього гаманця Code. Друзі можуть знайти вас за цим номером телефону. - Я підключаю свій акаунт X до @getcode, щоб отримувати чайові від людей з усього світу. - Завантаження вашого балансу та історії транзакцій - Перевірте свої фотографії на наявність ключа доступу, який ви зберегли, коли вперше створили свій акаунт. - Ви увійшли в обліковий запис в даний момент. Будь ласка, переконайтесь, що ви зберегли свій Ключ Доступу перед продовженням. Хочете вийти та увійти до нового облікового запису? - Немає прив’язаного\nтелефонного номеру - У вас немає номера телефону, прив’язаного до цього акаунту Code. Прив’яжіть один з акаунтів, щоб ваші друзі могли знайти вас. - Немає підключення до мережі - У Code - Відкрийте застосунок Code та наведіть вашу камеру, щоб забрати ці гроші - Упорядкування ваших контактів - Цей номер телефону відсутній у ваших контактах. Ви все одно можете запросити їх до Code. - Введіть свій номер телефону разом з кодом країни. Переконайтеся, що ви використовуєте той самий номер телефону, на який ви отримали запрошення. - На платформі - Тепер ви можете запитувати чайові - Реферальний бонус отримано - Надіслати %1$s - Ось %1$s - Запросити новий через %1$s - Відскануйте цей QR-код камерою вашого телефона, щоб завантажити застосунок Гаманець Code - Відскануйте, щоб завантажити\nдодаток Code Wallet - Пошук валют - Пошук контактів - На ваш номер телефону відправлено SMS із перевірочним кодом. Будь ласка, введіть перевірочний код вище. - Хтось надіслав вам гроші - Вам хтось дав чайові - Вам потрібно увімкнути камеру, щоб сканувати коди. - Ваша картка для чайових дозволяє отримувати чайові від користувачів Code з усього світу. Щоб скористатися вашою карткою, підключіть свій ідентифікатор X. - Картка для чайових дозволяє отримувати чайові від користувачів Code зі всього світу. Щоб отримати доступ до вашої картки для чайових, зробіть публікацію в X. - Ваша Картка для чайових дозволяє вам отримувати чайові від користувачів Code зі всього світу. Для отримання доступу до своєї Картки для чайових, напишіть повідомлення @getcode на X. - Дозвольте Code надсилати вам сповіщення, коли ви отримуєте Чайові від інших користувачів Code. - Уведіть \"%1$s\" - Ми внесли деякі зміни, щоб покращити роботу. Вам слід оновити застосунок, щоб і надалі використовувати Code. - Дійсний рахунок власника - Дійсний рахунок токен - Значення Kin змінюється. - було повернуто вам - Куди б ви хотіли вивести свій Kin? - Підтвердьте свою особу, щоб вивести Kin. - Ви зробили депозит - Ви дали - Ви маєте %1$d запрошень - Наразі Code доступний лише за запрошеннями. У вас залишилося %1$d. - Ви сплатили - Ви дали - Ви отримали - Ви надіслали - Ви витратили - Ви дали комусь чайові - Ви вивели - Ваші кошти успішно виведені. - Ваш акаунт X успішно підключено до вашого акаунту Code. Тепер ви можете запитувати чайові. - Виведення успішно - Акаунт X успішно підключено - Термін дії доступу минув - Ключ доступу - Налаштування додатку - Автоувімкнення камери - Баланс - Бета прапори - Бонус - Купувати та продавати Kin - Готівкові платежі - Команда Code - Покупки Kin - Онлайн-платежі - Чайові - Параметри налагодження - Покласти Kin на депозит - Депоновано - Введіть слова ключа доступу - Введіть номер телефону - Не вдалося - Часто задавані питання - Надано - Отримати гроші. - Запросіть друга в Code - Отримати Kin - Отримати більше Kin - Дати готівку - Дати Kin - Недостатньо коштів - Запросити друга - Пропозиція обмежена в часі - Прив\'язано - Місцева валюта - Мій обліковий запис - Не прив\'язано - Інші валюти - Сплачено - В очікуванні - Телефонний номер - Політика конфіденційності - Куплено - Акаунт X успішно підключено - Отримати Чайові - Отримано - Останні валюти - Порекомендуйте другу, отримайте 5 дол. - Реферальний бонус - Запросити грошовий переказ - Запит на Kin - Запросити чайові - Вимагати Face ID - Вимагати пароль - Вимагати Touch ID - Результати - Повернуто - Обрати обліковий запис - Обрати країну - Вибрати валюту - Надіслано - Витрачено - Змінити акаунт - Умови обслуговування - Картка з підказками - Дати чайові у Kin - Увімкнути сповіщення для Code - Невідомо - Потрібне оновлення - Підтвердити номер телефону - Вітальний бонус - Вивести Kin - Знято - Ваш ключ доступу - Натисніть на логотип, щоб поділитися посиланням для завантаження додатка - diff --git a/apps/codeApp/src/main/res/values-vi/strings-localized.xml b/apps/codeApp/src/main/res/values-vi/strings-localized.xml deleted file mode 100644 index 38f3af575..000000000 --- a/apps/codeApp/src/main/res/values-vi/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - Thêm Tiền mặt Bằng Thẻ Ghi nợ - Cho phép Truy cập Camera - Cho phép Truy cập vào Danh bạ - Cho phép Thông báo Đẩy - Số dư - Mua Kin - Mua thêm Kin - Hủy - Hủy Gửi - Trò chuyện - Thu Số tiền Này - Xác nhận - Kết nối với X - Tiếp tục - Đã sao chép - Sao chép - Sao chép Địa chỉ - Tạo Tài khoản - Tạo Tài khoản Mã Mới - Xóa Tài khoản - Xong - Tải xuống Ngay - Bật Face ID - Bật Touch ID - Thoát - Cho - Tặng Kin - Mời - Lời mời - Tham gia Danh sách chờ - Để sau - Tìm hiểu Cách Mua Kin - Tìm hiểu Cách Bán Kin - Liên kết Số Điện thoại - Đăng nhập - Đăng xuất - Nhắn @getcode để kết nối - Tắt tiếng - Tiếp theo - Không, Thử Lại - Để Sau - OK - Mở Cài đặt - Dán - Dán Từ Khay nhớ tạm - Hãy đăng bài để kết nối tài khoản - Đặt trong Ví - Nhận - Khôi phục Tài khoản Hiện có - Nhắc nhở - Xóa Số Điện thoại - Xóa Số Điện thoại của Bạn - Yêu cầu Tiền boa - Lưu Khóa Truy cập vào Ảnh của Tôi - Lưu vào Ảnh - Gửi - Gửi Mã Xác minh - Chia sẻ - Chia sẻ dưới dạng URL - Chia sẻ đường liên kết tải xuống - Chia sẻ Video Này - Hiển thị Thẻ tiền boa của tôi - Bật Camera - Đăng ký - Vuốt để đăng nhập - Vuốt để Thanh toán - Vuốt để Boa - Thử Tài khoản Mã Khác - Gửi tweet tới họ - Mở khóa Code - Bật tiếng - Hủy đăng ký - Cập nhật - Xem Khóa Truy cập - Rút Kin - Thay vào đó, Bạn Đã Viết Ra 12 Từ? - - Có, Hãy Rút Kin - Có, Tôi Đã Viết Chúng Ra - - Kin - của Kin - Xóa tất cả thông tin liên quan đến tài khoản của bạn khỏi máy chủ của Code (số điện thoại, danh bạ, lịch sử giao dịch) - Bạn có thể truy cập tài khoản của mình trong các ứng dụng ví tiền mã hóa khác bằng Cụm từ Khôi phục Bí mật. Bạn sẽ không thể sử dụng tài khoản của mình trong Code - Xóa tài khoản của bạn khỏi chuỗi khối - Hành động xóa sẽ thực hiện điều gì - Điều gì sẽ xảy ra - Hành động xóa sẽ không làm điều gì - Kin này đã được người khác thu thập. - Đã có người thu thập Kin này. - Xin vui lòng cho phép truy cập vào camera trong mục Cài đặt để sử dụng Mã. - Tiền mặt này đã hết hạn. - Xin vui lòng nhập lại số điện thoại của bạn và thử lại. - Vui lòng cho phép truy cập vào danh bạ trong phần Cài đặt để gửi lời mời. - Code hiện chưa hoạt động ở quốc gia của bạn. - Chúng tôi không thể hỗ trợ thiết bị của bạn tại thời điểm này - Phiên bản Code trong tương lai có thể sẽ hỗ trợ eSim. - Đã xảy ra lỗi. Không thể thu thập Kin này. - Chúng tôi không mong đợi điều đó xảy ra. Đã xảy ra lỗi. Xin vui lòng thử tạo lại tài khoản này. - Xin vui lòng cho phép Mã truy cập vào Ảnh trong mục Cài đặt để lưu Khóa Truy cập của bạn. - Đã xảy ra lỗi. Vui lòng đảm bảo rằng bạn đã nhập đúng số điện thoại. - Đã xảy ra lỗi. Xin vui lòng thử lại. - Không thể rút tiền của bạn. Đã xảy ra sự cố, xin vui lòng thử rút tiền một lần nữa. - Code được thiết kế cho các giao dịch nhỏ hàng ngày có giá trị từ %1$s trở xuống. - Code được thiết kế cho các giao dịch nhỏ hàng ngày. Hạn mức tặng hàng ngày của bạn sẽ tăng vào ngày mai. - Để tìm hiểu cách nhận thêm Kin, hãy chuyển đến phần Hỏi Đáp trong mục Cài đặt. - Vui lòng nhập Mã Mời khác và thử lại. - Vui lòng nhập số điện thoại hợp lệ và thử lại. - Đây là Thẻ Tiền boa không hợp lệ. - Xin vui lòng nhập mã xác minh hợp lệ và thử lại. - Rất tiếc, chúng tôi đã gặp sự cố mạng. Hãy thử mời lại bạn bè của bạn. - Kin này đã tự động được trả lại cho người gửi vì đã không được thu thập trong vòng 24 giờ. Xin vui lòng yêu cầu họ gửi Kin một lần nữa. - Xin vui lòng nhập lại số điện thoại của bạn và thử lại. - Xin vui lòng cố gắng lấy một hình ảnh cho thấy mã rõ ràng hơn. - Xin vui lòng kiểm tra kết nối internet của bạn hoặc thử lại sau. - Mã hiện chỉ dành cho người được mời. Chúng tôi sẽ thông báo cho bạn khi có thêm lời mời. - Để tham gia danh sách chờ, hãy truy cập %1$s - Số tiền tối đa bạn có thể mua là %1$s. Vui lòng nhập số tiền nhỏ hơn. - Số tiền tối thiểu bạn có thể mua là %1$s. Vui lòng nhập số tiền lớn hơn. - Khóa Truy cập của bạn đã kích hoạt mở khóa. Do đó, bạn sẽ không còn có thể sử dụng Khóa Truy cập này trong Mã. - Hãy gửi một tin nhắn tweet tới người này để kích hoạt Thẻ Tip của họ. - Số tiền tối đa bạn có thể boa là %1$s. Vui lòng nhập số tiền nhỏ hơn. - Số tiền tối thiểu bạn có thể boa là %1$s. Vui lòng nhập số tiền lớn hơn. - Bạn chỉ có thể tạo rất nhiều tài khoản mới mỗi ngày. - Code hiện được giới hạn ở một tài khoản cho mỗi số điện thoại. Phiên bản Code trong tương lai có thể sẽ hỗ trợ nhiều tài khoản. - Code hiện được giới hạn ở một tài khoản cho mỗi số điện thoại. Phiên bản Code trong tương lai có thể sẽ hỗ trợ nhiều tài khoản. - Kin Đã được Thu thập - Đã có người thu thập Kin - Cần Quyền truy cập Camera - Tiền mặt Đã hết hạn - Mã Xác minh đã Hết Thời gian chờ - Yêu cầu Quyền truy cập Danh bạ - Quốc gia chưa được hỗ trợ - Thiết bị chưa được hỗ trợ - eSim hiện chưa được hỗ trợ - Không thể Thu thập - Không thể Tạo Tài khoản - Không thể Lưu - Không thể Gửi - Không thể Xác nhận - Giao dịch Không thành công - Đã đạt Hạn mức Tặng - Đã đạt Hạn mức Hàng ngày - Không đủ Kin - Mã Mời Không hợp lệ hoặc Đã hết hạn - Số Điện thoại Không hợp lệ - Thẻ Tiền boa không hợp lệ - Mã Không hợp lệ - Lời mời Không thành công - Liên kết Đã hết hạn - Đã đạt đến Số lần Thử Tối đa - Không tìm thấy mã nào - Không có Kết nối Internet - Bạn Không Có Lời mời - Bạn Vẫn Chưa Được Mời - Giao dịch mua quá lớn - Giao dịch mua quá nhỏ - Bạn Không Còn Có thể Sử dụng Khóa Truy cập trong Mã - Chưa kích hoạt Thẻ Tip - Tiền boa quá lớn - Tiền boa quá nhỏ - Quá nhiều tài khoản được tạo - Tài khoản đã được tạo - Tài khoản đã được tạo - Chúng tôi tin rằng các giao dịch thanh toán cần đơn giản, mạnh mẽ và có tính toàn cầu. Bằng cách xây dựng với công nghệ chuỗi khối tiên tiến, Code cung cấp các tính năng mà những ứng dụng thanh toán truyền thống không thể làm được, chẳng hạn như chuyển khoản ngang hàng toàn cầu, thanh toán giá trị siêu nhỏ giúp mở khóa từng bài viết trực tuyến và boa tiền cho người sáng tạo yêu thích của bạn mà không bị tính phí. - Kin là một loại tiền điện tử giống như Bitcoin nhưng cũng được thiết kế để thanh toán nhanh chóng, không tốn kém. - Giống như Bitcoin, chỉ có một lượng Kin có hạn. Nếu nhiều người mua Kin thì giá trị sẽ tăng lên và nếu nhiều người bán Kin thì giá trị sẽ giảm. Động lực này giúp tất cả những người nắm giữ Kin đóng góp vào việc tạo ra giá trị nếu ngày càng nhiều người sử dụng Kin. - Bạn có thể mua Kin bằng thẻ ghi nợ của mình. Hãy vào tab Mua Kin để thực hiện. - Có, bạn có thể. Một số sàn giao dịch tiền điện tử hỗ trợ việc bán Kin. - Có ba cách chính bạn có thể trợ giúp: nói về trải nghiệm sử dụng Code của bạn trên mạng xã hội, khuyến khích bạn bè của bạn dùng thử Code và khuyến khích các website yêu thích của bạn tích hợp thanh toán Code bằng cách đề nghị họ xem qua [getcode.com](https://getcode.com). - Code là gì? - Tại sao các khoản thanh toán bằng Code được tính bằng Kin? - Tại sao giá trị của Kin thay đổi? - Làm cách nào để mua thêm Kin? - Tôi có thể bán Kin được không? - Tôi có thể giúp gì? - đồng ý với - Bằng việc nhấn vào \"Tạo Tài khoản\" hoặc \"Đăng nhập\", bạn - Camera của bạn được sử dụng để nhận Kin. Xin vui lòng cho phép truy cập vào camera để tiếp tục. - Chúng tôi cần thông báo đẩy để gửi cho bạn thông tin kịp thời về tài khoản của bạn. - Việc rút tiền là không thể đảo ngược và không thể hoàn tác sau khi bắt đầu. - Tất cả tiền trong tài khoản này sẽ bị mất. Việc xóa tài khoản của bạn là vĩnh viễn và không thể hoàn tác. Bạn có chắc chắn muốn xóa tài khoản này không? - Bất kỳ Kin nào không được thu thập trong vòng 24 giờ sẽ tự động được trả lại vào số dư của bạn. - Bạn sẽ cần phải bắt đầu lại quá trình tạo tài khoản và xác minh lại số điện thoại của mình. - Bạn có thể truy cập lại tài khoản này bằng Khóa truy cập của mình - Bạn sẽ không nhận được thông báo về bất kỳ tin nhắn mới nào từ %1$s. Bạn có thể bật thông báo bất cứ lúc nào. - Hiện chỉ hỗ trợ các tài khoản được tạo thông qua Mã. - Bạn bè của bạn sẽ không thể tìm thấy bạn bằng số điện thoại này nữa. - Bạn sẽ nhận được thông báo về tất cả tin nhắn mới từ %1$s. Bạn có thể tắt thông báo bất cứ lúc nào. - Bạn sẽ không nhận được bất kỳ tin nhắn nào từ %1$s cho đến khi bạn thanh toán cho họ lần nưa. - Khóa Truy cập của bạn sẽ cấp quyền truy cập vào tài khoản Mã của bạn. Hãy giữ bí mật và an toàn cho Khóa Truy cập. - 12 từ này là cách duy nhất để khôi phục tài khoản Mã của bạn. Hãy chắc chắn rằng bạn đã viết ra, đồng thời giữ bí mật và an toàn cho các từ này. - Bạn có chắc không? - Bạn có chắc chắn muốn xóa tài khoản này không? - Bạn đã gửi liên kết? - Bạn có chắc chắn muốn thoát không? - Bạn có chắc chắn muốn đăng xuất không? - Tắt thông báo từ %1$s? - Không phải là Tài khoản Mã - Bạn có chắc không? - Bật thông báo từ %1$s? - Hủy đăng ký khỏi %1$s? - Xem Khóa Truy cập của Bạn? - Bạn có Chắc Không? - %1$s Kin đã được nạp vào tài khoản của bạn. - %1$s bạn gửi ngày hôm qua chưa có người thu thập. Số tiền này đã được tự động hoàn trả về số dư của bạn. - Mời một người bạn bắt đầu dùng Code để nhận được 5$ - Gửi tiền mặt qua bất kỳ ứng dụng nhắn tin nào - Bạn đã nhận được %1$s Kin vì đã gửi cho người khác Kin đầu tiên của họ. - Giờ bạn sẽ có thể yêu cầu tiền tip. - Đã nhận được tiền nạp - Đã hoàn trả Kin - Người dùng mới trên Code - Đã nhận được tiền thưởng giới thiệu - Đã kết nối tài khoản X - Lời mời truy cập Mã của bạn đã hết hạn. Bạn có thể đăng xuất và sử dụng một tài khoản khác với trạng thái lời mời hợp lệ. - Khóa Truy cập là cách duy nhất để bạn truy cập tiền của mình. Xin vui lòng giữ bí mật và an toàn cho Khóa Truy cập. - Xác minh danh tính để xem Khóa Truy cập của bạn. - Nhấn vào biểu tượng Google Ống kính để mở mã QR để đăng nhập vào ứng dụng Code. Ngoài ra, bạn có thể đăng nhập thủ công bằng cách nhập 12 từ vào màn hình Đăng nhập của Code. - Cảnh báo! Hình ảnh này cho phép truy cập vào tất cả các khoản tiền bạn có trong Code. Không chia sẻ hình ảnh này với bất kỳ ai khác. Hãy giữ an toàn và bảo mật. - Mã giúp bạn có thể nhận được Kin bằng cách hướng camera của bạn vào hóa đơn kỹ thuật số trên điện thoại của người dùng khác - Bạn cần cho phép truy cập camera để có thể nhận được Kin - Xác thực để truy cập Code. - Mua Kin - Mua Kin (Sắp Ra mắt) - Việc mua bán Kin hiện là một quá trình phức tạp. Các quá trình này sẽ dần trở nên đơn giản hơn. Nếu muốn tìm hiểu cách mua bán Kin ngay hôm nay, bạn có thể xem các video hướng dẫn ở bên dưới. - Bạn chỉ có thể tặng tối đa %1$s - Bạn chỉ có thể boa tối đa %1$s - Kin của bạn hiện có sẵn để sử dụng trong ứng dụng Code của bạn - Bạn đã gửi cho người khác Kin đầu tiên của họ! Đây là tiền thưởng giới thiệu của bạn: - USDC của bạn đang được chuyển đổi thành Kin. Quá trình này sẽ mất khoảng một phút để hoàn tất - Bạn đã nạp USDC thành công. Hãy mở ứng dụng Code để hoàn tất giao dịch mua hàng của bạn - Thưởng chào mừng - Chọn một quốc gia - Sắp ra mắt - \@getcode Tôi muốn kết nối tài khoản X của mình để có thể nhận được tiền típ từ mọi người trên khắp thế giới - Xóa - Bạn nhớ lưu Cụm từ Khôi phục Bí mật, sau đó nhập \"Xóa\" để xóa tài khoản Code của mình. Bạn không thể đảo ngược hành động này. - Bạn không nhận được mã xác minh tại %1$s? - Bạn không nhận được mã xác minh? Gửi lại - Việc tắt Face ID sẽ yêu cầu bạn xác minh danh tính của mình. - Bạn chưa có ứng dụng Ví Code? - Bạn chưa có Kin nào. - Bật Face ID để tăng cường bảo mật hơn nữa cho giao dịch trong Mã. - Nhập địa chỉ đích - Nhập tối đa %1$s - Khi bạn mời được một người bạn đăng ký dùng Code và bạn gửi Kin đầu tiên cho họ, bạn sẽ nhận được 5$ Kin miễn phí. - Code sử dụng tiền điện tử Kin để thanh toán. Dưới đây là một số cách để nhận thêm Kin vào ví Code của bạn. - Nhận 1$ Kin đầu tiên miễn phí - Xác minh danh tính của bạn để tặng Kin. - Gửi Kin đến Địa chỉ Nạp tiền của bạn ở bên dưới để nạp Kin vào ví Mã của bạn. Nhấn để sao chép. - Vui lòng nhận thêm Kin rồi thử thanh toán lại - Tài khoản đích không hợp lệ - Vui lòng đảm bảo rằng địa chỉ bạn đang rút tiền về đã được nhà cung cấp ví của bạn khởi tạo. Một cách nhanh chóng để thực hiện điều này là trước tiên hãy đổi một khoản nhỏ SOL sang Kin trong ví mà bạn đang cố gắng gửi đến. - Mã Mời - Chúng tôi hiện chỉ cung cấp Mã thông qua hình thức mời. Bạn sẽ cần Mã Mời để truy cập ứng dụng này. - %1$d Lời mời - Code là một ứng dụng ví tiền mã hóa mới, hiện chỉ có thể được tiếp cận qua lời mời. Để tải Code, vui lòng truy cập %1$s - Tìm hiểu thêm - Số điện thoại của bạn được liên kết với tài khoản Mã này. Bạn bè có thể tìm thấy bạn bằng số điện thoại này. - Tôi đang kết nối tài khoản X của mình với @getcode để có thể nhận được tiền boa từ mọi người trên khắp thế giới. - Đang tải số dư và lịch sử giao dịch của bạn - Kiểm tra ảnh của bạn để xem Khóa Truy cập mà bạn đã lưu khi tạo tài khoản lần đầu. - Bạn hiện đã đăng nhập vào một tài khoản. Xin vui lòng đảm bảo rằng bạn đã lưu Khóa Truy cập của mình trước khi tiếp tục. Bạn có muốn đăng xuất rồi đăng nhập bằng tài khoản mới không? - Chưa Liên kết\nSố Điện thoại - Bạn không có số điện thoại nào được liên kết với tài khoản Mã này. Hãy liên kết một số điện thoại để bạn bè của bạn có thể khám phá ra bạn. - Không có kết nối mạng - Trên mã - Mở ứng dụng Code và hướng điện thoại của bạn để lấy số tiền này - Đang sắp xếp Danh bạ của Bạn - Số điện thoại này không có trong Danh bạ của bạn. Bạn vẫn có thể mời họ tham gia Code. - Nhập số điện thoại của bạn bao gồm cả mã quốc gia. Đảm bảo rằng bạn sử dụng cùng một số điện thoại đã nhận được lời mời. - Được cung cấp bởi - Giờ đây, bạn có thể yêu cầu tiền boa - Đã nhận được tiền thưởng giới thiệu - Gửi %1$s - Đây là %1$s - Yêu cầu mã xác minh mới sau %1$s - Quét mã QR này bằng máy ảnh trên điện thoại của bạn để tải xuống ứng dụng Ví Code - Quét để tải xuống\nứng dụng Ví Code - Tìm kiếm đơn vị tiền tệ - Tìm kiếm liên hệ - Đã gửi tin nhắn SMS đến số điện thoại của bạn với mã xác minh. Vui lòng nhập mã xác minh vào phía trên. - Có người đã gửi tiền cho bạn - Ai đó đã boa cho bạn - Bạn cần bật camera để quét Mã - Thẻ Tiền boa của bạn giúp bạn có thể nhận tiền boa từ người dùng Code trên toàn thế giới. Để truy cập Thẻ Tiền boa, hãy kết nối danh tính X của bạn. - Thẻ Tip cho phép bạn nhận tiền tip từ người dùng Code trên toàn thế giới. Để sử dụng Thẻ Tip của mình, hãy đăng bài trên X. - Thẻ Tip cho phép bạn nhận tiền tip từ những người dùng Code trên toàn thế giới. Để sử dụng Thẻ Tip của bạn, hãy nhắn tin cho @getcode trên X. - Cho phép Code gửi thông báo cho bạn khi bạn nhận được tiền típ từ những người dùng Code khác. - Gõ \"%1$s\" - Chúng tôi đã thực hiện một số thay đổi để cải thiện trải nghiệm. Bạn sẽ cần cập nhật ứng dụng để tiếp tục sử dụng Mã. - Tài khoản chủ sở hữu hợp lệ - Tài khoản token hợp lệ - Giá trị của Kin thay đổi. - được gửi trả lại bạn - Bạn muốn rút Kin của mình về đâu? - Xác minh danh tính của bạn để rút Kin. - Bạn đã gửi tiền - Bạn đã tặng - Bạn Có %1$d Lời mời - Mã hiện chỉ dành cho người được mời. Bạn còn lại %1$d. - Bạn đã thanh toán - Bạn đã tặng - Bạn đã nhận - Bạn đã gửi đi - Bạn đã chi tiêu - Bạn đã boa cho ai đó - Bạn đã rút tiền - Bạn đã rút tiền của mình thành công. - Tài khoản X của bạn đã được kết nối thành công với tài khoản Code của bạn. Giờ đây, bạn có thể yêu cầu Tiền boa. - Rút tiền Thành công - Đã kết nối tài khoản X thành công - Quyền truy cập đã Hết hạn - Khóa Truy cập - Cài đặt Ứng dụng - Tự động Bật Camera - Số dư - Cờ thử nghiệm - Thưởng - Mua & Bán Kin - Thanh toán tiền mặt - Đội ngũ Code - Giao dịch mua Kin - Thanh toán trên web - Tiền boa - Tùy chọn Gỡ lỗi - Nạp Kin - Đã nạp - Nhập các Từ của Khóa Truy cập - Nhập Số Điện thoại - Không thành công - Hỏi Đáp - Đã tặng - Nhận Tiền mặt - Mời một người bạn bắt đầu dùng Code - Nhận Kin - Nhận Thêm Kin - Cho Tiền mặt - Tặng Kin - Không đủ Tiền - Mời Bạn bè - Ưu đãi trong thời gian có hạn - Đã liên kết - Đơn vị tiền tệ Địa phương - Tài khoản của Tôi - Chưa Liên kết - Các Đơn vị tiền tệ khác - Đã thanh toán - Đang chờ xử lý - Số Điện thoại - Chính sách Quyền riêng tư - Đã mua - Tài khoản X được kết nối thành công - Nhận tiền típ - Đã nhận - Đơn vị tiền tệ Gần đây - Giới thiệu một người bạn - Nhận 5$ - Tiền thưởng Giới thiệu - Yêu cầu Tiền mặt - Yêu cầu Kin - Yêu cầu Tiền boa - Cần có Face ID - Cần có Mật mã - Cần có Touch ID - Kết quả - Đã trả lại - Chọn Tài khoản - Chọn Quốc gia - Chọn một Đơn vị tiền tệ - Đã gửi - Đã chi tiêu - Chuyển đổi Tài khoản - Điều khoản Dịch vụ - Thẻ Boa - Boa Kin - Bật thông báo cho Code - Không xác định - Cần Cập nhật - Xác minh Số Điện thoại - Tiền thưởng Chào mừng - Rút Kin - Đã rút - Khóa Truy cập của Bạn - Nhấn vào logo để chia sẻ liên kết tải xuống ứng dụng - diff --git a/apps/codeApp/src/main/res/values-zh-rCN/strings-localized.xml b/apps/codeApp/src/main/res/values-zh-rCN/strings-localized.xml deleted file mode 100644 index e75d4ab07..000000000 --- a/apps/codeApp/src/main/res/values-zh-rCN/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - 使用借记卡添加现金 - 允许相机访问 - 允许访问通讯录 - 允许推送通知 - 余额 - 购买 Kin - 购买更多 Kin - 取消 - 取消发送 - 聊天 - 收取此现金 - 确认 - 连接 X - 继续 - 已复制 - 复制 - 复制地址 - 创建账户 - 创建一个新的 Code 账户 - 删除账户 - 完成 - 马上下载 - 启用 Face ID - 启用 Touch ID - 退出 - 给出 - 给与 Kin - 邀请 - 邀请 - 加入候补名单 - 稍后再说 - 学习如何购买 Kin 币 - 学习如何出售 Kin 币 - 关联手机号码 - 登录 - 退出登录 - 给@getcode发送消息以连接 - 静音 - 下一个 - 不,再试一次 - 暂时不要 - 确定 - 打开设置 - 粘贴 - 从剪贴板粘贴 - 发布至连接的账户 - 放入钱包 - 接收 - 恢复现有账户 - 提醒 - 移除手机号码 - 移除您的手机号码 - 请求小费 - 将访问密钥保存到我的相册 - 保存到相册 - 发送 - 发送验证码 - 分享 - 以 URL 形式分享 - 分享下载链接 - 分享此视频 - 显示我的小费卡 - 启动相机 - 订阅 - 轻扫以登录 - 滑动支付 - 滑动即可支付小费 - 尝试不同的 Code 账户 - 发推文给他们 - 解锁Code - 取消静音 - 退订 - 更新 - 查看访问密钥 - 提取 Kin - 改为记下 12 个单词 - 是的 - 是的,提取 Kin - 是的,我把它们记下来了 - - Kin - Kin - 从 Code 的服务器中删除与您的账户相关的所有信息(电话号码,联系人,交易历史记录) - 您可以使用您的恢复密语在其他加密货币钱包应用中访问您的账户。 您将无法在 Code 中使用您的账户 - 从区块链中删除您的账户 - 删除会做什么 - 会发生什么 - 删除不会做什么 - 此 Kin 币已被其他人领取。 - 这枚 Kin 已被他人收取。 - 请在设置中允许访问相机以使用 Code。 - 此现金已过期。 - 请重新输入您的手机号码,然后重试。 - 请在\"设置\"中允许访问通讯录以发送邀请。 - 目前,Code 在您所在的国家/地区不可用。 - 我们目前无法支持您的设备 - 后续版本的 Code 可能会支持 eSims。 - 出错了。该Kin无法领取。 - 我们没想到会发生这种情况。出了点问题。请尝试重新创建此账户。 - 请在设置中允许 Code 访问相册以保存您的访问密钥。 - 出错了,请确保您输入的电话号码正确无误。 - 出了点问题。请再试一次。 - 提取资金失败。出了点问题,请重新尝试提取。 - 代码专为 %1$s 或以下的小额日常交易而设计。 - Code 专为小额日常交易而设计。 您的每日付款限额明天会增加。 - 要了解如何获得更多 Kin,请访问设置中的常见问题。 - 请输入不同的邀请码并重试。 - 请输入有效的电话号码,然后重试。 - 这是一张无效的小费卡。 - 请输入有效的验证码,然后重试。 - 抱歉,我们遇到了网络问题。请尝试再次邀请您的好友。 - 此 Kin 币因未在 24 小时内领取而自动返回给发送者。请让他们重新发送 Kin 币。 - 请重新输入您的手机号码,然后重试。 - 请尝试获取能更清晰地显示代码的图像。 - 请检查您的互联网连接或稍后重试。 - Code 目前仅限邀请。当有更多可用的邀请机会时,我们会通知您。 - 要加入候补名单,请转到 %1$s - 您可以购买的最大数量是 %1$s。请输入更小的数量。 - 您可以购买的最小数量是 %1$s。请输入更大的数量。 - 您的访问密钥已启动解锁。因此,您将无法再在 Code 中使用此访问密钥。 - 向此人发送推文以激活他们的小费卡。 - 您可以给的最高小费是 %1$s。请输入一个低一点的金额。 - 您可以给的最低小费是 %1$s。请输入一个高一点的金额。 - 您每天只能创建这么多新账户。 - 目前,每个设备仅限使用一个 Code 账户。后续版本的 Code 可能会支持多个账户。 - 目前,每个电话号码仅限开设一个 Code 账户。后续版本的 Code 可能会支持多个账户。 - 该 Kin 币已被领取 - 已收集的 Kin - 需要相机访问权限 - 现金已过期 - 验证码超时 - 需要通讯录访问权限 - 不支持的国家/地区 - 不支持的设备 - 目前不支持 eSims - 领取失败 - 创建账户失败 - 保存失败 - 发送失败 - 确认失败 - 交易失败 - 达到付款限额 - 达到每日限额 - Kin 不足 - 邀请码无效或已过期 - 无效的电话号码 - 小费卡无效 - 无效的验证码 - 邀请失败 - 链接已过期 - 已达到最大尝试次数 - 未找到代码 - 没有互联网连接 - 您的邀请机会已用完 - 您还没有被邀请 - 购买量太大 - 购买量太小 - 访问密钥无法再在 Code 中使用 - 小费卡尚未激活 - 小费太多 - 小费太少 - 创建的账户过多 - 账户已创建 - 账户已创建 - 我们认为支付应该是简单,强大,全球化的。通过采用先进的区块链技术,Code提供了传统支付应用程序所无法提供的功能,如全球点对点转账,可在线解锁单篇文章的微支付,以及面向您喜爱的创作者的免手续费小贴士。 - Kin是类似于比特币的加密货币,但也为快速,廉价的支付而设计。 - 像比特币一样,Kin的数量有限。如果有更多人买入Kin,其价值就会上升;如果有更多人卖出Kin,其价值就会下降。通过这一动态变化,每个持有Kin的人都可以在Kin的应用范围扩大时分享其创造的价值。 - 您可以使用借记卡买入Kin。您可以在\"获取Kin\"标签页进行该操作。 - 当然可以。许多加密货币交易所都支持卖出Kin。 - 您可以通过3种主要方式提供帮助:在社交媒体上分享您的Code使用体验,鼓励朋友亲自试用Code,鼓励您喜爱的网站查看[getcode.com](https://getcode.com),从而在网站上集成Code支付。 - 什么是Code? - 为什么Code支付以Kin结算? - 为什么Kin的价值会变化? - 我如何买入更多Kin? - 我可以卖出Kin吗? - 我可以如何提供帮助? - 同意我们的 - 点击\"创建账户\"或\"登录\",即表示您 - 您的相机用于接收 Kin。 请允许访问相机以继续。 - 我们需要推送通知来及时向您发送有关您账户的信息。 - 提取操作不可逆转,一旦开始就无法撤消。 - 此账户中的所有资金都将丢失。删除账户的操作是永久性的,无法撤消。您确定要删除此账户吗? - 任何未在 24 小时内领取的 Kin 币都会自动退回到您的余额中。 - 您将需要重新创建账户并再次验证您的手机号码。 - 您可以使用您的访问密钥重新登录此账户 - 您将不会收到任何来自 %1$s 的新消息通知。您可以随时取消静音。 - 目前仅支持通过 Code 创建的账户。 - 您的好友将无法再使用此手机号码找到您。 - 您将收到来自 %1$s 的所有新消息的通知。您可以随时静音。 - 在您再次付款之前,您将不会收到来自 %1$s 的任何消息。 - 您的访问密钥将授予您对 Code 账户的访问权限。请将其存放在安全保密的地方。 - 这 12 个单词是恢复您的 Code 账户的唯一方法。务必将它们记下来,并存放在安全保密的地方。 - 您确定吗? - 您确定要删除此账户吗? - 您是否发送了链接? - 您确定要退出吗? - 您确定要退出登录吗? - 要将 %1$s 静音吗? - 不是 Code 账户 - 您确定吗? - 取消静音 %1$s? - 取消订阅 %1$s? - 查看您的访问密钥? - 您确定吗? - %1$s Kin 已存入您的账户。 - 您昨天发送的 %1$s 未被收取,已自动退回您的余额。 - 邀请好友使用 Code,获得 $5 - 通过任意发信应用发送现金 - 您已收到向他人发送首枚 akin 而获得的 %1$s Kin。 - 您现在可以请求小费。 - 已收到的存款 - 退回的 Kin - Code 新消息 - 已收到的推荐奖金 - X 账户已连接 - 您访问 Code 的邀请已过期。您可以退出登录并使用具有有效邀请状态的其他账户。 - 您的访问密钥是访问您资金的唯一途径。请将其存放在安全保密的地方。 - 验证您的身份以查看您的访问密钥。 - 点击 Google Lens 图标,打开 QR 码以登录 Code。或者,您可以通过在 Code 登录界面中输入 12 个单词来手动登录。 - 警告!此图像可用以访问您在 Code 中的所有资金。请勿向其他任何人分享此图像。请稳妥存放。 - Code 使您可以通过将相机对准其他用户手机上的数字账单来接收 Kin - 您需要允许相机访问才能接收 Kin - 完成身份验证以访问Code。 - 购买 Kin - 购买 Kin 币(即将推出) - 当前,购买和出售 Kin 的过程较为复杂。随着时间的推移,此过程将变得更加简单。如果您想立即了解如何买卖 Kin 币,可以观看下面的演示视频。 - 你最多只能给出 %1$s - 您最多只能给 %1$s 的小费 - 您的 Kin 现在可供在您的 Code 应用中使用。 - 您向某人发送了他/她的第一笔 Kin!这是给您的推荐奖金: - 您的 USDC 正在兑换为 Kin。这应该需要大约一分钟才能完成 - 您已成功存入 USDC。打开 Code 应用,完成您的购买 - 开户赠金 - 选择国家/地区 - 即将到来 - \@getcode我想连接我的X账号,以便收取来自世界各地的人们的提示 - 删除 - 确保您已保存您的恢复密语,然后输入\"删除\"以删除您的 Code 账户。此操作是不可逆的。 - %1$s 没有收到验证码? - 没有收到验证码?重新发送 - 禁用 Face ID 需要您验证您的身份。 - 还没有 Code Wallet 应用? - 您还没有任何 Kin。 - 启用 Face ID 以进一步增强 Code 中交易的安全性。 - 输入目的地地址 - 输入最多 %1$s - 在您成功邀请好友注册 Code 并赠送好友首枚 Kin 时,免费获得价值 $5 的 Kin。 - Code 使用加密货币 Kin 进行支付。以下是将更多 Kin 添加到您的 Code 钱包中的一些方法。 - 免费获得价值 $1 的 Kin - 验证您的身份以给与 Kin。 - 将 Kin 发送到下面的存款地址,以便将 Kin 存入您的 Code 钱包。轻点复制。 - 请获取更多 Kin 币,然后再次尝试付款 - 无效的目标账户 - 请确保您要提现到的地址已由您的钱包提供商初始化。要达成此要求有一个简便方法,即先将您要发送到的钱包中的少量 SOL 兑换为 Kin。 - 邀请码 - Code 目前仅限邀请可用。您需要邀请码才能访问该应用。 - %1$d 次邀请机会 - Code 是一款新的加密钱包应用,目前仅限受邀使用。要下载 Code,请转到 %1$s - 了解更多 - 您的手机号码已与此 Code 账户关联。好友可以通过此手机号码找到您。 - 我正在将我的 X 账户连接 @getcode ,这样我就可以接收来自世界各地的人的小费。 - 正在加载您的余额和交易历史记录 - 检查您的相册以获取您在首次创建账户时保存的访问密钥。 - 您当前已登录账户。在继续之前,请确保您已保存了访问密钥。是否要注销并使用新账户登录? - 未关联\n手机号码 - 您没有与此 Code 账户关联的手机号码。关联一个手机号码,以便您的好友可以发现您。 - 无网络连接 - 在 Code 上 - 打开 Code 应用,用相机对准二维码领取该现金 - 正在整理您的通讯录 - 此电话号码不在您的通讯录中。 您仍然可以邀请他们加入 Code。 - 输入您的手机号码,包括国家代码。确保您使用的手机号码与收到邀请的手机号码相同。 - 技术支持方: - 您现在可以请求小费了 - 已收到的推荐奖金 - 发送%1$s - 这里是%1$s - %1$s 后请求一个新的验证码 - 用您的手机相机扫描此二维码,下载 Code Wallet 应用 - 扫描以下载\nCode 钱包应用 - 搜索货币 - 搜索联系人 - 已向您的手机号码发送了一条带有验证码的短信。请在上面输入验证码。 - 有人给您发送了现金 - 有人给了您小费 - 您需要启动相机以扫描Code - 您的小费卡可让您接收来自世界各地的 Code 用户的小费。要访问您的小费卡,请连接您的 X 身份。 - 您的小费卡可让您收到来自世界各地 Code 用户的小费。要访问您的小费卡,请发帖至 X。 - 您的小费卡 (Tip Card) 可让您接收来自世界各地 Code 用户的小费。要访问您的小费卡,请在 X 上发送消息给 @getcode 。 - 允许 Code 在您收到其他 Code 用户的小费时向您发送通知。 - 输入\"%1$s\" - 我们进行了一些更改以改善体验。您需要更新应用才能继续使用 Code。 - 有效的所有者账户 - 有效的代币账户 - Kin 的价值发生变化。 - 已退还给您 - 您想将 Kin 提取到哪里? - 验证您的身份以提取 Kin。 - 您存入了 - 您给了 - 您有 %1$d 次邀请机会。 - Code 目前仅限邀请。您还剩 %1$d 次。 - 您支付了 - 您给了 - 您收到了 - 您发送了 - 您花费了 - 您给了某人小费 - 您提取了 - 您的资金已成功提取。 - 您的 X 账户已成功与您的 Code 账户连接。您现在可以请求小费了。 - 提取成功 - X 账户连接成功 - 访问已过期 - 访问密钥 - 应用程序设置 - 自动启动相机 - 余额 - 测试版标志 - 赠金 - 购买与出售 Kin 币 - 现金支付 - 代码团队 - Kin 购买 - Web 支付 - 小费 - 调试选项 - 存入 Kin - 已存入 - 输入访问密钥词 - 输入手机号码 - 失败 - 常见问题 - 已给 - 获取现金 - 让好友开始使用 Code - 获取 Kin 币 - 获取更多 Kin 币 - 给出现金 - 给与 Kin - 资金不足 - 邀请好友 - 限时优惠 - 已关联 - 当地货币 - 我的账户 - 未关联 - 其他货币 - 已支付 - 待处理 - 手机号码 - 隐私政策 - 已购买 - X 账户连接成功 - 接收小费 - 已接收 - 最近的货币 - 推荐好友,获得 $5 - 推荐奖金 - 申请现金 - 请求 Kin - 请求小费 - 需要Face ID - 需要密码 - 需要Touch ID - 结果 - 已退回 - 选择一个账户 - 选择一个国家/地区 - 选择货币 - 已发送 - 花费 - 切换账户 - 服务条款 - 小费卡 - Kin 小费 - 打开 Code 通知 - 未知 - 需要更新 - 验证手机号码 - 迎新奖励 - 提取 Kin - 已提取 - 您的访问密钥 - 点击徽标,分享应用下载链接 - diff --git a/apps/codeApp/src/main/res/values-zh-rTW/strings-localized.xml b/apps/codeApp/src/main/res/values-zh-rTW/strings-localized.xml deleted file mode 100644 index da933a176..000000000 --- a/apps/codeApp/src/main/res/values-zh-rTW/strings-localized.xml +++ /dev/null @@ -1,388 +0,0 @@ - - - 使用簽帳金融卡添加現金 - 允許相機存取權 - 允許存取聯絡人 - 允許推播通知 - 餘額 - 購買 Kin - 購買更多 Kin - 取消 - 取消發送 - 聊天 - 收取這筆現金 - 確認 - 連結至 X - 繼續 - 已複製 - 複製 - 複製位址 - 建立帳戶 - 建立新的 Code 帳戶 - 刪除帳戶 - 完成 - 現在就下載 - 啟用 Face ID - 啟用 Face ID - 退出 - 給予 - 給予 Kin - 邀請 - 邀請 - 加入候補名單 - 晚點再說 - 學習如何購買 Kin - 學習如何賣掉 Kin - 連結電話號碼 - 登入 - 登出 - 傳送 @getcode 訊息來連接 - 靜音 - 下一步 - 不,請再試 - 先不要 - - 開啟設定 - 貼上 - 從剪貼簿貼上 - 發文以連結帳戶 - 放入錢包 - 接受 - 恢復現有帳戶 - 提醒 - 移除電話號碼 - 移除您的電話號碼 - 要求小費 - 將存取金鑰儲存至我的相簿 - 儲存至相簿 - 發送 - 傳送驗證碼 - 分享 - 分享網址連結 - 分享下載連結 - 分享該影片 - 顯示我的 Tip Card - 啟用相機 - 訂閱 - 滑動以登入 - 刷卡支付 - 滑動來給小費 - 嘗試不同的 Code 帳戶 - 發推文給他們 - 解鎖 Code - 取消靜音 - 取消訂閱 - 更新 - 查看存取金鑰 - 提領 Kin - 作為替代,要把這 12 個字詞寫下來嗎? - - 是的,提領 Kin - 是的,我把它們寫下來了 - - Kin - {amount} 個 Kin - 從 Code 的伺服器中刪除與您的帳戶相關的所有資訊(包括電話號碼,聯絡人和交易記錄) - 您可以使用您的助記詞,在其他加密貨幣錢包應用程式中存取您的帳戶。您將無法在 Code 中使用您的帳戶 - 從區塊鏈中刪除您的帳戶 - 刪除將會做的 - 會發生的事 - 刪除不會做的 - 這個 Kin 已被其他人領取。 - 這筆 Kin 款項已被某人收取 - 請在設定中允許相機存取權以使用 Code。 - 該轉帳已過期。 - 請重新輸入您的電話號碼,然後再試一次。 - 請在設置中允許存取聯絡人以發送邀請。 - Code 目前無法在你的國家使用。 - 我們目前無法支援你的裝置 - 未來版本的 Code 很可能會支援 eSIM。 - 發生了點問題,無法收取此 Kin。 - 我們沒有預料到會發生這種情況。出了點問題。請嘗試重新建立此帳戶。 - 請在設定中允許 Code 存取相簿,以儲存您的存取金鑰。 - 出了點問題。請確保輸入了正確的電話號碼。 - 出了點問題。請再試一次。 - 無法提領您的資金。出了點問題,請重新嘗試提領。 - Code 旨在處理每日 %1$s 或以下的小額交易。 - Code 旨在處理每日小額交易。您的每日轉移限額將於明天增加。 - 要瞭解獲得更多 Kin 的方法,請前往設定中的常見問題集。 - 請輸入另一個邀請碼再試一次。 - 請輸入有效的電話號碼並重試。 - 這是無效的 Tip Card。 - 請輸入有效的驗證碼,然後再試一次。 - 抱歉,我們遇到了網路問題。請嘗試再次邀請您的朋友。 - 這個 Kin 自動歸還給了發送者,因為 24 小時内沒被領取。請要求他們再次發送 Kin。 - 請重新輸入您的電話號碼,然後再試一次。 - 請嘗試取得可以更清楚顯示代碼的圖像。 - 請檢查您的網路連線,或是稍後再試一次。 - Code 目前僅限邀請。我們會在有更多邀請可用時通知您。 - 要加入候補名單,請前往 %1$s - 您可購買的上限為 %1$s。請輸入較小數量。 - 您可購買的下限為 %1$s。請輸入較大數量。 - 您的存取鑰匙已經啓動解鎖。因此,您不能再在 Code 使用此存取鑰匙。 - 向此人發送推文以啟動他們的小費卡。 - 您可以給予的小費最大值為 %1$s。請輸入較小的金額。 - 您可以給予的小費最小值為 %1$s。請輸入較大的金額。 - 您每天只能建立如此數目的新帳戶。 - 目前每個裝置只能註冊一個 Code 帳戶。未來版本的 Code 可能會支援多個帳戶。 - 目前每個電話號碼只能註冊一個 Code 帳戶。未來版本的 Code 可能會支援多個帳戶。 - 已領取 Kin - Kin 已收取 - 需要相機存取權 - 轉帳過期 - 驗證碼已逾時 - 需要聯絡人存取權限 - 不支援的國家 - 不支援的裝置 - 目前不支援 eSim - 收取失敗 - 建立帳戶失敗 - 儲存失敗 - 傳送失敗 - 確認失敗 - 交易失敗 - 已達到轉移限額 - 已達到每日限額 - Kin 不足 - 邀請碼無效或過期 - 無效的電話號碼 - 無效的 Tip Card - 無效的驗證碼 - 邀請失敗 - 連結過期 - 已達到最大嘗試次數 - 找不到代碼 - 沒有網路連線 - 您沒有邀請次數了 - 您尚未受到邀請 - 購買量太大 - 購買量太小 - 存取鑰匙在 Code 已經失效 - 小費卡尚未啟用 - 小費金額過高 - 小費金額過低 - 建立太多帳戶了 - 帳戶已建立 - 帳戶已建立 - 我們認為付款應該是簡單,強大且全球化的。Code 利用先進的區塊鏈技術進行建構,提供了傳統付款應用程式無法提供的功能,例如全球點對點轉帳,解鎖網上個人文章的小額付款,以及給您最喜愛創作者零手續費小費的功能。 - Kin 是一種加密貨幣(就像比特幣一樣),同時又設計用於快速,低成本的付款。 - 與比特幣類似,Kin 的供應量是有限的。如果購買 Kin 的人越多,其價值就會上升;如果出售 Kin 的人越多,其價值就會下降。在這種動態下,如果 Kin 的使用率增加,則所有持有 Kin 的人都能共享價值的創造。 - 您可以使用簽帳金融卡購買 Kin。這功能可以在「取得 Kin」標籤中使用。 - 可以,您可以出售 Kin。許多加密貨幣交易所都支援出售 Kin。 - 您可以藉由以下三種主要方式提供幫助:在社群媒體上分享您的 Code 使用經驗,鼓勵朋友親身試用 Code,以及邀請您喜愛的網站看看 [getcode.com](https://getcode.com),並鼓勵它們整合 Code 付款功能。 - Code 是什麼? - 為什麼 Code 的付款是以 Kin 計價? - 為什麼 Kin 的價值會變動? - 如何購買更多的 Kin? - 我可以出售 Kin 嗎? - 我能提供什麼幫助嗎? - 同意我們的 - 輕觸「建立帳戶」或「登入」,即代表您 - 您的相機是用來接收 Kin 的。請允許相機存取權以繼續進行。 - 我們需要推播通知,以便及時向您傳送與帳戶有關的資訊。 - 提領是不可逆的操作,一旦開始即無法復原。 - 此帳戶中的所有資金都將遺失。您的帳戶將永久刪除且無法復原。您確定要刪除此帳戶嗎? - 24 小時内沒被領取的 Kin 將會自動歸還到您的餘額。 - 您將需要重新建立帳戶並再次驗證您的電話號碼。 - 您可以使用您的存取金鑰重新進入此帳戶 - 您將不會收到來自 %1$s 的任何新訊息通知。您可以隨時解除靜音。 - 目前僅支援透過 Code 建立的帳戶。 - 您的朋友將再也無法使用此電話號碼找到您。 - 您將會收到來自 %1$s 的所有新訊息通知。您可以隨時將其靜音。 - 您將不會收到任何來自 %1$s 的訊息,直到再次付款。 - 您的存取金鑰將授予存取 Code 帳戶的權限。請隱密並妥善地保管您的存取金鑰。 - 這 12 個字詞是恢復您 Code 帳戶的唯一辦法。請一定要將這些字詞寫下來,並隱密且妥善地保管它們。 - 您確定嗎? - 您確定要刪除此帳戶嗎? - 您是否發送了連結? - 您確定要退出嗎? - 您確定要登出嗎? - 將 %1$s 靜音? - 不是 Code 帳戶 - 您確定嗎? - 要解除靜音 %1$s 嗎? - 要取消訂閱 %1$s 嗎? - 要查看您的存取金鑰嗎? - 您確定嗎? - %1$s 的 Kin 已存入您的帳戶。 - 您昨天傳送的 %1$s 未被收取。該筆款項已自動返還到您的餘額之中。 - 邀請朋友開始使用 Code 並獲得 5 美元 - 透過任何通訊應用程式傳送現金 - 您收到了 %1$s 的Kin,因為您傳送給某人他的第一枚 Kin 。 - 現在您可以請求獲得小費了。 - 已收到存款 - Kin 已退回 - Code 新手上路 - 已收到推薦獎勵 - 已連結 X 帳戶 - 您存取 Code 的邀請已過期。您可以登出並使用具有有效邀請狀態的其他帳戶。 - 您的存取金鑰是存取自身資金的唯一辦法。請隱密並妥善地保管您的存取金鑰。 - 驗證您的身分以查看您的存取金鑰。 - 點擊 Google 智慧鏡頭圖示,開啟 QR code 以登入 Code 應用程式。或者,您可以採取手動登入方式,在 Code 登入畫面輸入 12 字元。 - 警告!此圖片可存取您在 Code 中的所有資金。請勿與他人分享此圖片,並確保其安全。 - Code 使您能夠將相機對準其他使用者手機上的數位帳單來接收 Kin - 您需要允許相機存取權才能夠接收 Kin - 請驗證以存取 Code。 - 購買 Kin - 購買 Kin(即將推出) - 目前買賣 Kin 的流程相當複雜,但這些流程以後會改進得更簡便。如果您現在想要學習如何購買和販售 Kin,可以觀看下方的操作影片。 - 您最多只能轉移 %1$s - 您最多只能給予 %1$s 的小費。 - 您的 Kin 現已可在 Code 應用程式中使用 - 您發送給某人第一筆 Kin!這裡是您的推薦獎勵: - 您的 USDC 正在轉為 Kin。這大約需要一分鐘才能完成 - 您已成功存入 USDC。請開啟 Code 應用程式完成購買 - 歡迎獎金 - 選擇國家 - 即將登場 - \@getcode 我想要連接到我的 X 帳戶,以收到全世界用戶的建議 - 刪除 - 請確認您已經妥善保存了您的助記詞,然後輸入「刪除」以刪除您的 Code 帳戶。這個操作是不可逆轉的。 - 沒有在 %1$s 收到驗證碼嗎? - 沒有收到驗證碼嗎?重新傳送 - 您需要驗證身分來停用 Face ID。 - 沒有 Code 錢包應用程式嗎? - 您尚未擁有任何 Kin。 - 啟用 Face ID 以進一步增強 Code 中的交易安全性。 - 輸入目的地位址 - 最多輸入 %1$s - 當您邀請朋友註冊 Code 並\n傳送給他們第一枚 Kin 時,即可免費獲得 5 美元的 Kin。 - Code 使用加密貨幣 Kin 作支付。這是一些方法來取得更多 Kin 放入您的 Code 錢包。 - 免費獲得您的第一筆 Kin 款項,價值 1 美元 - 驗證您的身分以給予 kin。 - 將 Kin 傳送至下方的存款位址,存入至您的 Code 錢包中。輕觸即可複製。 - 請獲得更多 Kin 之後再嘗試付款 - 無效的終端帳戶 - 請確認您要將所提領款項轉入的地址已由您的錢包提供者初始化。達成這個目的的捷徑是先在您要將款項轉入的錢包中將少量的 SOL 換成 Kin。 - 邀請碼 - Code 只可透過邀請使用。您需要一個邀請碼來存取此應用程式。 - %1$d 個邀請 - Code 是新推出的加密貨幣錢包應用程式,目前僅限受邀者使用。請前往 %1$s 下載 Code - 瞭解更多 - 您的電話號碼已與此 Code 帳戶連結。朋友可以使用這個電話號碼找到您。 - 我將把我的 X 帳戶與 @getcode 連結,讓我能夠接收來自世界各地的小費。 - 正在加載您的餘額及交易記錄 - 檢查您的相簿以取得首次建立帳戶時儲存的存取金鑰。 - 您現時已登入一個帳戶。繼續之前,請確保已儲存您的存取密鑰。您想登出並登入一個新帳戶嗎? - 未連結\n電話號碼 - 您沒有與此 Code 帳戶連結的電話號碼。請連結電話號碼,好讓朋友能夠發現您。 - 無網路連接 - 在 Code 上 - 開啟 Code 應用程式並將相機對準即可獲取這筆現金 - 整理您的聯絡人 - 此電話號碼不在您的聯絡人中,但您仍可以邀請他們加入 Code。 - 輸入包含國際區號的電話號碼。請確定您使用的電話號碼與收到邀請的電話號碼相同。 - 技術提供者: - 您現在可以提出小費要求了 - 已收到推薦獎勵 - 發送%1$s - 這是%1$s - 在 %1$s 中請求一組新的驗證碼 - 使用您手機的相機掃描此 QR Code 即可下載 Code 錢包應用程式 - 掃描以下載\nCode Wallet 應用程式 - 搜尋貨幣 - 搜尋聯絡人 - 一條帶有驗證碼的短訊已發送到您的電話號碼。請輸入上面的驗證碼。 - 有人傳送了現金給您 - 有人給您小費 - 您需要啟用相機以掃描 Codes - 您的 Tip Card 可讓您從世界各地的 Code 使用者接收小費。想使用您的 Tip Card,請連結您的 X 身分。 - 您的小費卡可讓您接收來自世界各地的 Code 使用者的小費。要存取您的小費卡,請發文至 X。 - 您的小費卡 (Tip Card) 讓您能夠接收來自世界各地 Code 使用者的小費。若要存取您的小費卡,請在 X 上傳訊息給 @getcode。 - 當您收到來自其他 Code 使用者的小費時,允許 Code 傳送通知給您。 - 輸入「%1$s」 - 我們進行了一些變更來改善體驗。您需要更新應用程式才能夠繼續使用 Code。 - 有效的所有人帳戶 - 有效的代幣帳戶 - Kin 的值發生了變化。 - 已歸還給您 - 您想要將 Kin 提領至哪裡? - 驗證您的身分以提領 kin。 - 您存入了 - 您給了 - 您擁有 %1$d 個邀請 - Code 目前僅接受邀請。您剩餘 %1$d 個。 - 您支付了 - 您給了 - 您收到的 - 您發送了 - 您花費了 - 您給了某人小費 - 您提領了 - 已成功提領您的資金。 - 您的 X 帳戶已成功與 Code 帳戶連結。您現在可以提出小費要求了。 - 提領成功 - 已成功連結 X 帳戶 - 存取權已過期 - 存取金鑰 - 應用程式設定 - 自動啟用相機 - 餘額 - 測試版旗幟 - 獎金 - 購買&販售 Kin - 現金付款 - 程式碼團隊 - Kin 購買紀錄 - 網路付款 - 小費 - 偵錯選項 - 存入 Kin - 已存入 - 輸入存取金鑰字詞 - 輸入電話號碼 - 失敗 - 常見問題集 - 已給予 - 獲取現金 - 邀請朋友開始使用 Code 錢包 - 取得 Kin - 獲得更多 Kin - 給予現金 - 給予 Kin - 餘額不足 - 邀請朋友 - 限時優惠 - 已連結 - 當地貨幣 - 我的帳戶 - 未連結 - 其他貨幣 - 已付款 - 待處理 - 電話號碼 - 隱私權政策 - 已購買 - 已成功連結 X 帳戶 - 收取小費 - 已收到 - 最近使用的貨幣 - 推薦一位朋友,可獲 5 美元獎勵 - 推薦獎勵 - 請求現金 - 要求 Kin - 要求小費 - 需要 Face ID - 需要密碼 - 需要 Touch ID - 結果 - 已歸還 - 選擇一個帳戶 - 選擇一個國家 - 選擇貨幣 - 已發送 - 花費 - 切換帳戶 - 服務條款 - 提示卡 - 以 Kin 給小費 - 開啟 Code 通知 - 不明 - 需要更新 - 驗證電話號碼 - 迎新獎勵 - 提領 Kin - 已提領 - 您的存取金鑰 - 輕觸標誌來分享應用程式下載連結 - diff --git a/apps/codeApp/src/main/res/values/colors.xml b/apps/codeApp/src/main/res/values/colors.xml deleted file mode 100644 index 1d56a948f..000000000 --- a/apps/codeApp/src/main/res/values/colors.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - #FF8383 - #FFA1A1 - #FFF383 - #FFF6A3 - #FFF6A3 - #65F57C - - - #666666 - #8D8D94 - #BABBC2 - #E6E6EB - #F0F0F5 - - - #0F0C1F - #7379A0 - #565C86 - #443091 - - #0F0C1F - #8785A9 - #66000000 - #B3000000 - #00000000 - #1D1A30 - - #FF000000 - #FFFFFFFF - #05FFFFFF - \ No newline at end of file diff --git a/apps/codeApp/src/main/res/values/strings-universal.xml b/apps/codeApp/src/main/res/values/strings-universal.xml deleted file mode 100644 index c3bf55070..000000000 --- a/apps/codeApp/src/main/res/values/strings-universal.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - https://app.getcode.com/tos - https://app.getcode.com/privacy-policy - - https://getcode.com/d - https://getcode.com/d?r=%1$s - aqr - as - https://app.getcode.com - https://cash.getcode.com - codewallet://cash.getcode.com - app.getcode.com - jump.getcode.com - cash.getcode.com - sdk.getcode.com - tipcard.getcode.com - Bucket Debugger - Request Kin - Buy Kin - Establish Relationship Account with Code - Can Unsubscribe - Vibrate on Scan - Show Connectivity Status - Tip Card - Tip Card on Home Screen - Chats - Cash Transfers in Chat - Currency Selection in Balance - Buy Kin Internally - Share Tweets to Tip - Camera Gestures - Invert Camera Drag-to-Zoom - Tap Tip Card to See Back - Photo Gallery - Show Errors - If enabled, you\'ll gain the ability to tap the balance on the Balance screen to inspect individual bucket balances. - If enabled, a \"No Connection\" badge will show on the scan screen when no internet is detected. - If enabled, the device will vibrate once to indicate that the camera has registered the code on the bill. - If enabled, the ability to change the currency displayed in the Balance screen will be available. - If enabled, Request Kin screen will replace Get Kin. - If enabled, a Buy More Kin will appear in the balance screen. - If enabled, a relationship account will be established with getcode.com if it doesn\'t yet exist. - If enabled, an option to unsubscribe from a chat will appear for supported chats. - If enabled, you\'ll gain the ability to share a Tip Card. - If enabled, your Tip Card will replace Get Cash on the home screen. - If enabled, you\'ll gain the ability to chat with with other code users. - If enabled, you\'ll gain the ability to send cash in conversations. - If enabled, the Buy Kin flow will open in an internal WebView. - If enabled, you\'ll gain the ability to share tweets directly from Twitter to Code to tip the author. - If enabled, you\'ll gain the ability to pinch-to-zoom, drag-to-zoom, and tap to auto focus on the camera screen. - If enabled, drag-to-zoom will operate in reverse (drag up to zoom instead of down). - If enabled, you\'ll gain the ability to tap your own Tip Card to see the back. - If enabled, you\'ll gain the ability to select Tip Cards from the on-device photo gallery. - - %1$s %2$s - Reset Tooltips - - - Hey @%1$s you should set up your @getcode Tip Card so I can tip you some cash. - - getcode.com/download - - - - diff --git a/apps/codeApp/src/main/res/values/strings.xml b/apps/codeApp/src/main/res/values/strings.xml deleted file mode 100644 index db4f53c10..000000000 --- a/apps/codeApp/src/main/res/values/strings.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - com.getcode.accountprovider - Nevermind - This login request could not be completed at this time. Please try again later. - Failed to create a USDC deposit account. - Payment Failed - This payment request could not be paid at this time. Please try again later. - Login Failed - Account Error - You can only request up to %1$s - Resend - No Connection - You Have %d Invite - Today - Yesterday - - Temporarily Unavailable - The ability to buy Kin is temporarily unavailable due to network congestion. Please try again later. - - Success - - 🙏 Thank - 🙏 Thanked - Message - Messaged - - Send Kin - - Require Biometrics - No biometrics currently enrolled - Biometric Authentication - Place your finger on the sensor or look at the front camera to authenticate. - Click to authenticate - Biometrics Removed - Biometrics are no longer available on your device. - - Reveal your identity? - %1$s will be able to see that you are %2$s. - - I\'m connecting my X account with @getcode - Connect Account - Connecting your X account allows you to reveal your identity to the people you tip. To connect your account post to X. - Use Kado Sandbox - - Chat - What\'s Their Username? - X Username - Username not found - This X username isn\'t on Code yet. Please try a different username. - - Don\'t have cash?\nTip me with Code! - Scan to download the app, and then scan the code on the other side to tip - - Send %1$s to Start Chatting - Loading your balance and transaction history - Loading your chats - Start a New Chat - You don\'t have any chats yet. - - Connect Your X - Identity in %1$s is based on your X identity. - %1$s I’d like to connect my X - Message %1$s to Connect - Cash - Chats - Settings - - Code - Code - \@getcode - CodeAccount - diff --git a/apps/codeApp/src/main/res/values/themes.xml b/apps/codeApp/src/main/res/values/themes.xml deleted file mode 100644 index aec3a4945..000000000 --- a/apps/codeApp/src/main/res/values/themes.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - diff --git a/apps/codeApp/src/main/res/xml/authenticator.xml b/apps/codeApp/src/main/res/xml/authenticator.xml deleted file mode 100644 index 415269cbc..000000000 --- a/apps/codeApp/src/main/res/xml/authenticator.xml +++ /dev/null @@ -1,7 +0,0 @@ - - \ No newline at end of file diff --git a/apps/codeApp/src/release/AndroidManifest.xml b/apps/codeApp/src/release/AndroidManifest.xml deleted file mode 100644 index 16c292d86..000000000 --- a/apps/codeApp/src/release/AndroidManifest.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/apps/codeApp/src/test/kotlin/com/getcode/DeeplinkTests.kt b/apps/codeApp/src/test/kotlin/com/getcode/DeeplinkTests.kt deleted file mode 100644 index 7ab1b6227..000000000 --- a/apps/codeApp/src/test/kotlin/com/getcode/DeeplinkTests.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.getcode - -import org.junit.Test - -class DeeplinkTests { - - private val regex = Regex("^(login|payment|tip)?-?request-(modal|page)-(mobile|desktop)\$") - - private val loginPathRoutes = listOf( - "codewallet://getcode.com/v1/elements/login-request-modal-mobile", - "codewallet://getcode.com/v1/elements/login-request-page-mobile", - "codewallet://getcode.com/v1/elements/login-request-modal-desktop", - "codewallet://getcode.com/v1/elements/login-request-page-desktop" - ) - - private val paymentPathRoutes = listOf( - "codewallet://getcode.com/v1/elements/payment-request-modal-mobile", - "codewallet://getcode.com/v1/elements/payment-request-page-mobile", - "codewallet://getcode.com/v1/elements/payment-request-modal-desktop", - "codewallet://getcode.com/v1/elements/payment-request-page-desktop" - ) - - private val tipsPathRoutes = listOf( - "codewallet://getcode.com/v1/elements/tip-request-modal-mobile", - "codewallet://getcode.com/v1/elements/tip-request-page-mobile", - "codewallet://getcode.com/v1/elements/tip-request-modal-desktop", - "codewallet://getcode.com/v1/elements/tip-request-page-desktop" - ) - - @Test - fun testSdkTriggers() { - loginPathRoutes.map { it.substringAfterLast("/") } - .onEach { assert(regex.matches(it)) } - - paymentPathRoutes.map { it.substringAfterLast("/") } - .onEach { assert(regex.matches(it)) } - - tipsPathRoutes.map { it.substringAfterLast("/") } - .onEach { assert(regex.matches(it)) } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/.gitignore b/apps/flipchatApp/.gitignore deleted file mode 100644 index 80efa742e..000000000 --- a/apps/flipchatApp/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -build/ -.gradle/ -google-services.json diff --git a/apps/flipchatApp/build.gradle.kts b/apps/flipchatApp/build.gradle.kts deleted file mode 100644 index e0c544bed..000000000 --- a/apps/flipchatApp/build.gradle.kts +++ /dev/null @@ -1,252 +0,0 @@ -import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - id(Plugins.android_application) - id(Plugins.kotlin_android) - id(Plugins.kotlin_parcelize) - id(Plugins.kotlin_kapt) - id(Plugins.kotlin_serialization) - id(Plugins.androidx_navigation_safeargs) - id(Plugins.hilt) - id(Plugins.google_services) - id(Plugins.firebase_crashlytics) - id(Plugins.firebase_perf) - id(Plugins.bugsnag_android_gradle) - id(Plugins.secrets_gradle_plugin) - id(Plugins.versioning_gradle_plugin) - id(Plugins.jetbrains_compose_compiler) -} - -val contributorsSigningConfig = ContributorsSignatory(rootProject) -val appNamespace = "${Gradle.flipchatNamespace}.app" - -android { - // static namespace - namespace = appNamespace - compileSdk = Android.compileSdkVersion - - defaultConfig { - versionCode = versioning.getVersionCode() - versionName = Packaging.Flipchat.versionName - applicationId = appNamespace - minSdk = Android.minSdkVersion - targetSdk = Android.targetSdkVersion - testInstrumentationRunner = Android.testInstrumentationRunner - - buildConfigField("String", "MIXPANEL_API_KEY", "\"${tryReadProperty(rootProject.rootDir, "MIXPANEL_API_KEY")}\"") - buildConfigField("String", "KADO_API_KEY", "\"${tryReadProperty(rootProject.rootDir, "KADO_API_KEY")}\"") - buildConfigField("Boolean", "NOTIFY_ERRORS", "false") - buildConfigField("String", "X_CLIENT_ID", "\"${tryReadProperty(rootProject.rootDir, "X_CLIENT_ID")}\"") - } - - signingConfigs { - create("contributors") { - storeFile = contributorsSigningConfig.keystore - storePassword = contributorsSigningConfig.keystorePassword - keyAlias = contributorsSigningConfig.keyAlias - keyPassword = contributorsSigningConfig.keyPassword - } - } - - buildFeatures { - buildConfig = true - compose = true - } - - buildTypes { - getByName("release") { - resValue("string", "applicationId", appNamespace) - - isMinifyEnabled = true - isShrinkResources = true - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") - } - getByName("debug") { - applicationIdSuffix = ".dev" - resValue("string", "applicationId", "${appNamespace}.dev") - signingConfig = signingConfigs.getByName("contributors") - - val debugMinifyEnabled = tryReadProperty(rootProject.rootDir, "DEBUG_MINIFY", "false").toBooleanStrictOrNull() ?: false - isMinifyEnabled = debugMinifyEnabled - isShrinkResources = debugMinifyEnabled - - if (debugMinifyEnabled) { - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - - configure { - mappingFileUploadEnabled = tryReadProperty(rootProject.rootDir, "DEBUG_CRASHLYTICS_UPLOAD", "false").toBooleanStrictOrNull() ?: false - } - } - } - - compileOptions { - sourceCompatibility(Versions.java) - targetCompatibility(Versions.java) - isCoreLibraryDesugaringEnabled = true - } - - packaging { - resources.excludes.add("**/*.proto") - resources.excludes.add("META-INF/LICENSE.md") - resources.excludes.add("META-INF/LICENSE-notice.md") - } -} - -kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(Versions.java)) - } - - compilerOptions { - jvmTarget.set(JvmTarget.fromTarget(Versions.java)) - optIn.addAll( - "kotlin.time.ExperimentalTime", - "kotlin.ExperimentalUnsignedTypes", - "kotlin.RequiresOptIn" - ) - } -} - -dependencies { - implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) - implementation(project(":services:flipchat:sdk")) - - implementation(project(":libs:datetime")) - implementation(project(":libs:locale:bindings")) - implementation(project(":libs:vibrator:bindings")) - implementation(project(":libs:encryption:ed25519")) - implementation(project(":libs:encryption:keys")) - implementation(project(":libs:encryption:mnemonic")) - implementation(project(":libs:encryption:utils")) - implementation(project(":libs:crypto:kin")) - implementation(project(":libs:currency")) - implementation(project(":libs:logging")) - implementation(project(":libs:messaging")) - implementation(project(":libs:network:exchange")) - implementation(project(":libs:network:connectivity:bindings")) - implementation(project(":libs:opengraph")) - implementation(project(":libs:permissions:public")) - implementation(project(":libs:quickresponse")) - implementation(project(":libs:requests")) - implementation(project(":ui:components")) - implementation(project(":ui:emojis")) - implementation(project(":ui:navigation")) - implementation(project(":ui:resources")) - implementation(project(":ui:theme")) - implementation(project(":vendor:tipkit:tipkit-m2")) - - coreLibraryDesugaring(Libs.android_desugaring) - - //standard libraries - implementation(Libs.kotlinx_collections_immutable) - implementation(Libs.kotlinx_serialization_json) - implementation(Libs.kotlinx_datetime) - implementation(Libs.androidx_core) - implementation(Libs.androidx_constraint_layout) - implementation(Libs.androidx_lifecycle_runtime) - implementation(Libs.androidx_lifecycle_viewmodel) - implementation(Libs.androidx_navigation_fragment) - implementation(Libs.androidx_navigation_ui) - - //hilt dependency injection - implementation(Libs.hilt) - kapt(Libs.hilt_android_compiler) - kapt(Libs.hilt_compiler) - implementation(Libs.hilt_worker) - - implementation("androidx.webkit:webkit:1.12.1") - - androidTestImplementation(Libs.hilt) - androidTestImplementation(Libs.hilt_android_test) - kaptAndroidTest(Libs.hilt_android_compiler) - testImplementation(Libs.hilt_android_test) - kaptTest(Libs.hilt_android_compiler) - - androidTestImplementation("io.mockk:mockk:1.13.12") - - //Jetpack compose - implementation(platform(Libs.compose_bom)) - implementation(Libs.compose_ui) - debugImplementation(Libs.compose_ui_tools) - implementation(Libs.compose_accompanist) - implementation(Libs.compose_foundation) - implementation(Libs.compose_material) - implementation(Libs.compose_materialIconsExtended) - implementation(Libs.compose_activities) - implementation(Libs.compose_view_models) - implementation(Libs.compose_livedata) - implementation(Libs.compose_navigation) - implementation(Libs.compose_paging) - implementation(Libs.compose_webview) - - implementation(Libs.androidx_biometrics) - - implementation(Libs.androidx_activity) - - // cameraX - implementation(Libs.androidx_camerax_core) - implementation(Libs.androidx_camerax_camera2) - implementation(Libs.androidx_camerax_lifecycle) - implementation(Libs.androidx_camerax_view) - - implementation(Libs.coil3) - implementation(Libs.coil3_network) - - implementation(Libs.androidx_browser) - implementation(Libs.androidx_constraint_layout_compose) - - implementation(Libs.rxjava) - implementation(Libs.rxandroid) - - implementation(Libs.slf4j) - implementation(Libs.grpc_android) - - implementation(platform(Libs.firebase_bom)) - implementation(Libs.firebase_analytics) - implementation(Libs.firebase_crashlytics) - implementation(Libs.firebase_messaging) - - implementation(Libs.hilt_nav_compose) - implementation(Libs.lib_phone_number_port) - implementation(Libs.mp_android_chart) - implementation(Libs.mixpanel) - - implementation(Libs.retrofit) - implementation(Libs.retrofit_converter_gson) - implementation(Libs.okhttp_logging_interceptor) - - androidTestImplementation(Libs.androidx_test_runner) - androidTestImplementation(Libs.androidx_junit) - androidTestImplementation(Libs.junit) - androidTestImplementation(Libs.espresso_core) - androidTestImplementation(Libs.espresso_contrib) { - exclude(module = "protobuf-lite") - } - androidTestImplementation(Libs.espresso_intents) - implementation(Libs.androidx_room_runtime) - implementation(Libs.androidx_room_ktx) - implementation(Libs.androidx_room_rxjava3) - implementation(Libs.androidx_room_paging) - kapt(Libs.androidx_room_compiler) - - implementation(Libs.androidx_datastore) - - implementation(Libs.markwon_core) - implementation(Libs.markwon_linkify) - implementation(Libs.markwon_ext_strikethrough) - - implementation(Libs.play_service_auth) - implementation(Libs.play_service_auth_phone) - - implementation(Libs.timber) - implementation(Libs.bugsnag) - - implementation(Libs.haze) - - implementation(Libs.rinku_compose) -} diff --git a/apps/flipchatApp/proguard-rules.pro b/apps/flipchatApp/proguard-rules.pro deleted file mode 100644 index c355f26c9..000000000 --- a/apps/flipchatApp/proguard-rules.pro +++ /dev/null @@ -1,69 +0,0 @@ --optimizationpasses 5 --dontusemixedcaseclassnames --verbose -#-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*,!code/allocation/variable --obfuscationdictionary shuffled-dictionary.txt --classobfuscationdictionary shuffled-dictionary.txt - --keepclasseswithmembernames class * { - native ; -} - --keepclassmembers class **.R$* { - public static ; -} - -# Room --keep class * extends androidx.room.RoomDatabase --keep @androidx.room.Entity class * --keep class net.sqlcipher.** { *; } - -## Code API --keep class com.codeinc.gen.** {*;} --keep class com.google.protobuf.** { *; } - -# Keep our scan classes that interact with native --keep class com.kik.scan.** { *; } - -# BouncyCastle --keep public class org.bouncycastle.** # Refine this further! --keepclassmembers class org.bouncycastle.crypto.** { - ; -} - --assumenosideeffects class android.util.Log { - public static int v(...); - public static int i(...); - public static int w(...); - public static int d(...); - public static int e(...); -} - --keepattributes SourceFile,LineNumberTable # Keep file names and line numbers. --keep public class * extends java.lang.Exception --keep public class * extends com.getcode.network.repository.ErrorSubmitIntent --keep public class * extends com.getcode.network.repository.ErrorSubmitIntentException --keep public class * extends com.getcode.network.repository.WithdrawException --keep public class * extends com.getcode.network.repository.FetchUpgradeableIntentsException --keep public class * extends com.getcode.network.repository.AirdropException - -# https://github.com/firebase/firebase-android-sdk/issues/3688 --keep class org.json.** { *; } --keepclassmembers class org.json.** { *; } - -# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items). - -keep,allowobfuscation,allowshrinking interface retrofit2.Call - -keep,allowobfuscation,allowshrinking class retrofit2.Response - - # With R8 full mode generic signatures are stripped for classes that are not - # kept. Suspend functions are wrapped in continuations where the type argument - # is used. - -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation - -# libsodium --keep class com.ionspin.kotlin.crypto.** { *; } --keep class com.sun.jna.** { *; } --dontwarn java.awt.Component --dontwarn java.awt.GraphicsEnvironment --dontwarn java.awt.HeadlessException --dontwarn java.awt.Window \ No newline at end of file diff --git a/apps/flipchatApp/shuffled-dictionary.txt b/apps/flipchatApp/shuffled-dictionary.txt deleted file mode 100644 index 1c04f878b..000000000 --- a/apps/flipchatApp/shuffled-dictionary.txt +++ /dev/null @@ -1,811 +0,0 @@ -Isht -BQUQ -lYNE -YWWv -zhIz -dRko -ihUe -duhS -oRft -jWAX -KLsZ -xbYj -NxZs -ercX -Zikf -jygi -xRWw -BBdO -hOvC -HEqd -QlDS -zchV -mrtd -tAMc -beae -NzVI -WGdk -mlen -mTqY -zOWI -yDek -WkdC -EvHb -fjdG -tXQX -lysK -tJrI -nTyR -QoId -teja -tBns -WdjJ -rzBH -CsrS -rGDS -mgig -lKwS -dsew -PEIO -Iagi -FzdP -jyBf -dkuR -gwNv -GoKt -bHKA -YPEH -sRnT -JfEF -ifmJ -zyoT -dgLf -XeSR -MbvH -EviK -etia -MrSO -sXtv -Bgfm -lRiu -RwJl -tUXp -ZwyR -daJE -Duhs -qXtR -QXRy -NKZp -EVoJ -APSm -YgnC -Xsxv -VlNx -fYSo -NWos -JpCK -bOOW -PpQM -xMex -kZmG -fgZr -Jxmu -BVUp -LBel -WrBi -JFtE -jMcJ -IiRY -rmmT -LyLw -GxwL -xwEO -MDtQ -tXXA -yJqI -XWEk -IchN -snux -ycvY -vrwB -juTi -wISO -OIcx -enkB -ELNl -ymTW -ZAhU -NZjR -bYGt -EHts -lzjc -FUZJ -oGFN -GRql -kKUH -iRfG -kKBN -gavV -CtCD -Jlcd -rkLy -RGmg -sacF -UsUf -rJfG -mLPF -IsTH -gYXw -XNZg -QeWt -zONE -osyG -cLMf -RhpR -oaoi -vaPm -peYL -DGbn -IWRQ -FOMD -zTcd -Hvbw -vqPu -lWCZ -ZwrR -IPnJ -BDfW -ZLPb -mdFn -ZKGS -dOQK -vOTL -nGQY -aoOn -sJwn -DrgD -bIwW -zJIB -Fdra -COnA -EULK -WlWi -bAif -LgwE -yMjR -HdjF -vCmv -LyGj -lGkZ -FIHF -MTiY -YHSv -bsEG -qluJ -PZRG -fXfC -nscO -UIxm -rIyJ -YUDD -ffjW -GXpA -bQji -IAyQ -FZXu -kclk -ryYv -DZOO -rNcr -xLYb -BZJL -RfqD -DOnu -Skiq -VFQs -fuzj -cYxv -FGTC -VWfA -GcgM -EffG -YsaB -Kvpz -uMqE -iBWV -RNlt -NSDn -VpKD -IOhC -KGja -fznE -LldB -KTWJ -SGiI -tGCd -mKFS -kpjE -inNw -YFhv -IoDg -xPxa -ZSGd -VQAo -WaFi -FJOL -bEPM -OraL -CosS -Qpuw -ugST -yNMr -Esbm -FYIB -nXLw -IAJC -wvIu -rEdl -rAIt -RbZu -BGAB -JvuF -ChCH -ywvf -GwoF -lmGe -oGcJ -tGtZ -wkSy -uXTa -OmAD -FPNM -yOYL -eiJD -GGzr -rFrR -Wtyr -smhO -Qusu -MHBp -gYfm -AQMx -JVnn -dcdI -cVrJ -tTwl -QRJf -oyRp -ZaOh -Dmjk -wDaF -vVWP -twIb -FajC -orbD -UTlY -yJOD -FTaL -tiNW -xSNV -lOiN -KrqV -oHaN -JCWM -EKhf -rbdj -dipY -mkjj -WELS -FwtI -zMLd -PyQf -yEtV -xClf -hrPJ -Yftz -HlbL -cyha -oynP -ncsf -atCi -TZCr -iwXC -WuLG -GdVK -OLYf -FgLC -AKUb -nKYb -BWnv -nkhv -Omrp -gsSt -wJnx -HZHh -dLna -PmVN -juAs -MsNP -kjBm -WoRy -ujpz -WskB -ZxUN -DjCe -fyOr -JQIG -utyB -ifvv -uDEA -usVw -OCSt -RCtd -LYDI -HejM -SwhM -gKcH -Bahm -hUTr -SzWQ -fxar -PAdX -icPy -eCYN -SucB -Yjnf -sdxY -sogd -wQCZ -KbLO -wSbt -kLsz -OkpF -MPmo -YcUL -TPJF -SgMm -ekQU -FKYn -qZQR -Fdtt -XVNw -GwFc -XauS -vQrD -jmoJ -UzaQ -zsKx -ndeh -FeuU -teGY -Trkd -pRLL -zeHL -SGPZ -nUti -qQws -BLbX -yqHy -lmFr -TMYU -NlbJ -bRAt -bCFJ -pAuF -fcrw -SVWh -sSde -DAGR -AduC -hVYM -fUSZ -BbRe -RrFF -ZKFL -UtSD -BVqP -UBfb -EgjY -hpxM -cyQy -MLSV -fpdI -eBuR -Kyua -tYDV -CNdO -ZQUw -yDMT -RHAZ -KSms -lgaB -bxzl -YlIs -MVGF -ENts -dgRD -SybV -Mpne -DLah -anUP -nbFc -KZDx -VhDU -UnCj -mFPU -QpML -VKRz -sMQB -JzdA -ZtHp -AkzT -LdLP -CBrv -CtHu -vTfo -uwce -ihuY -xqIU -rvyG -YIeY -nhXV -PvbA -gahw -QTPg -Tcxn -WDuH -HbXF -AJpF -kvzs -VhWQ -dvhm -CGnG -ppzZ -Azky -sWsN -UrPx -Znvg -ngaV -Frhs -lpAR -RpIm -eHlK -UvCX -jHQK -bRCz -Ntid -vVcM -wdim -mttW -qtHO -hwTH -DxKD -shKW -vgBc -oWSA -LeTC -aweS -tsjN -BXOK -vgrh -CSvq -rwUV -cnhU -VFuP -qzBz -Nbay -mKib -TQOI -IxTd -ufEE -uHls -pSTp -JDNU -qHMb -wyBD -Uxof -hPWy -wyZc -arfA -AoZs -qpkG -duyB -teoC -BHLw -rEUj -MMmn -tbUY -jcMO -RbjW -ofCJ -CKNe -YxsR -ozgG -UgOi -JBJO -PZkK -kCIL -JtNn -TVAI -zQsm -VZgL -rukZ -UIEr -Gaoa -tFRW -YPAz -FoKp -hxOK -Zrbo -PbMj -mkaj -Jfvu -RVRh -keIJ -lXOG -HkcV -sbFt -wFcX -iaHo -ncGe -vLSU -PvJz -XlId -Iqbc -FNHp -OauE -Cnsp -cYdn -dTjy -ARVt -GjAP -NeOb -QBRs -teHM -NNFq -pQtt -Ydde -nRZl -jVjd -LGOI -NYIF -PwqS -Twdw -zLme -xomi -PEYK -OjFT -JnFx -vMSR -MtCN -Vmbv -CHXq -hVyA -gzBT -PmnH -cAqk -Fcie -llOf -uVTH -goDD -IEJJ -kGDH -xJPI -LIce -ZtnA -NatG -fXys -hikO -jeXD -nHEB -uVNP -OAJq -gdhL -yPaj -MyJn -wvXg -WuvS -FMXw -ZtSa -cTtl -ioBO -fvdC -ZTbi -ZSon -CYoZ -pODY -HhcT -ABlS -sWns -iDwj -cPNL -DMIa -Hexg -nYen -lPYb -nSND -QDYO -gGIC -Sgvg -yIpe -SiFd -WXRO -IxHN -bkSq -dlVA -LKYw -muTP -HRnO -KiMy -uAzD -alTU -oEID -EmRn -BAzw -LFgd -qaUi -crwT -OIbV -nnET -pZpN -GXiC -aPKz -pwdU -ijjR -Hohs -DbdX -FyRB -fmkK -GFkw -EUuW -LAFO -TbwB -VEmz -Uvdu -ypFl -IBzE -bzWY -KUOp -JFIw -VOTC -nIJJ -thMd -zCuy -ZkaW -vNvX -mUfe -iCoZ -ZpvJ -fbsS -Cfzj -orEO -KpTn -lzzD -JBcp -ISWa -DAfh -HlJM -BdsO -aytI -LSvL -BarO -Wnth -cQyc -qwUp -Fwgx -QXaW -ZiVt -epbZ -VVVC -xtVZ -lPKP -YpCp -wSqt -ssNm -wFLC -NulV -rsqe -Rgde -XKRR -Ludw -mYXR -STdD -vQPY -OXnG -uvkJ -GoHg -nROB -duCc -Qjrw -WZUp -vvVZ -Rvtc -VyFa -LLdY -qlPD -ueda -ClMc -bMVB -zmXD -asSs -wyaY -bJRq -fIsz -yhWi -fZXO -qfLK -HBIx -ReOh -RbVD -Zfeo -LNOU -oHYO -xOjl -XnVx -SwJK -foCJ -PxwF -JmoH -rqKo -rBjl -sNJV -GcFn -Moky -WzQl -WTzB -AIjG -SDfe -dZXz -VIDB -Zlww -UzuA -nXUD -bUrp -QNdb -FkSO -imLC -WqLj -qSbN -mfvq -bPog -uYVI -CKbj -BNcy -RLng -GHjM -FFwD -qSfw -ZvlI -JITU -rHGC -Wigr -zdHB -Orfj -QKgP -oVhJ -SOml -kEuj -GrKj -lVMG -xTxC -HCUN -ZWTO -vclb -wWcc -dckr -vevq -pHjd -zBFk -oneP -ZhKt -ABmP -oJxu -QPfa -yjEq -oPvN -ZsAI -waDy -VKNw -QzHV -KPNY -rhwm diff --git a/apps/flipchatApp/src/debug/AndroidManifest.xml b/apps/flipchatApp/src/debug/AndroidManifest.xml deleted file mode 100644 index 0775aa4c4..000000000 --- a/apps/flipchatApp/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/apps/flipchatApp/src/debug/res/values/ic_launcher_background.xml b/apps/flipchatApp/src/debug/res/values/ic_launcher_background.xml deleted file mode 100644 index fe71b618f..000000000 --- a/apps/flipchatApp/src/debug/res/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #C372FF - diff --git a/apps/flipchatApp/src/debug/res/values/strings.xml b/apps/flipchatApp/src/debug/res/values/strings.xml deleted file mode 100644 index 5587c526f..000000000 --- a/apps/flipchatApp/src/debug/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - Flipchat Dev - \ No newline at end of file diff --git a/apps/flipchatApp/src/debug/res/xml/authenticator.xml b/apps/flipchatApp/src/debug/res/xml/authenticator.xml deleted file mode 100644 index 494bd7566..000000000 --- a/apps/flipchatApp/src/debug/res/xml/authenticator.xml +++ /dev/null @@ -1,7 +0,0 @@ - - \ No newline at end of file diff --git a/apps/flipchatApp/src/main/AndroidManifest.xml b/apps/flipchatApp/src/main/AndroidManifest.xml deleted file mode 100644 index 501e70b8c..000000000 --- a/apps/flipchatApp/src/main/AndroidManifest.xml +++ /dev/null @@ -1,246 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/flipchatApp/src/main/ic_launcher-playstore.png b/apps/flipchatApp/src/main/ic_launcher-playstore.png deleted file mode 100644 index cc79f6603..000000000 Binary files a/apps/flipchatApp/src/main/ic_launcher-playstore.png and /dev/null differ diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/App.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/App.kt deleted file mode 100644 index 5b115187e..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/App.kt +++ /dev/null @@ -1,198 +0,0 @@ -package xyz.flipchat.app - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.lifecycle.Lifecycle -import cafe.adriel.voyager.core.registry.ScreenRegistry -import cafe.adriel.voyager.navigator.Navigator -import cafe.adriel.voyager.navigator.currentOrThrow -import cafe.adriel.voyager.transitions.SlideTransition -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.core.BottomSheetNavigator -import com.getcode.navigation.core.CombinedNavigator -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.extensions.getActivityScopedViewModel -import com.getcode.navigation.transitions.SheetSlideTransition -import com.getcode.theme.LocalCodeColors -import com.getcode.ui.components.OnLifecycleEvent -import com.getcode.ui.components.bars.BottomBarContainer -import com.getcode.ui.components.bars.TopBarContainer -import com.getcode.ui.components.bars.rememberBarManager -import com.getcode.ui.core.RestrictionType -import com.getcode.ui.theme.CodeScaffold -import com.getcode.ui.utils.getActivity -import dev.bmcreations.tipkit.TipScaffold -import dev.bmcreations.tipkit.engines.TipsEngine -import dev.theolm.rinku.DeepLink -import dev.theolm.rinku.compose.ext.DeepLinkListener -import xyz.flipchat.app.features.home.HomeViewModel -import xyz.flipchat.app.features.payments.PaymentScaffold -import xyz.flipchat.app.theme.FlipchatTheme -import xyz.flipchat.app.ui.LocalUserManager -import xyz.flipchat.app.ui.navigation.AppScreenContent -import xyz.flipchat.app.ui.navigation.MainRoot -import xyz.flipchat.app.util.DeeplinkType - -@Composable -fun App( - tipsEngine: TipsEngine, -) { - val homeViewModel = getActivityScopedViewModel() - val router = homeViewModel.router - val context = LocalContext.current - - //We are obtaining deep link here, in case we want to allow for some amount of deep linking when not - //authenticated. Currently we will require authentication to see anything, but can be changed in future. - var deepLink by remember { mutableStateOf(null) } - var loginRequest by remember { mutableStateOf(null) } - - DeepLinkListener { - val type = router.processType(it) - if (type is DeeplinkType.Login) { - loginRequest = type.entropy - return@DeepLinkListener - } - deepLink = it - } - - val userManager = LocalUserManager.currentOrThrow - val userState by userManager.state.collectAsState() - - FlipchatTheme { - val barManager = rememberBarManager() - AppScreenContent { - PaymentScaffold { - TipScaffold(tipsEngine = tipsEngine) { - AppNavHost { - val codeNavigator = LocalCodeNavigator.current - CodeScaffold { innerPaddingModifier -> - Navigator( - screen = MainRoot { deepLink }, - ) { navigator -> - LaunchedEffect(navigator.lastItem) { - // update global navigator for platform access to support push/pop from a single - // navigator current - codeNavigator.screensNavigator = navigator - } - - Box( - modifier = Modifier - .padding(innerPaddingModifier) - ) { - SlideTransition(navigator) - } - - LaunchedEffect(deepLink) { - if (codeNavigator.lastItem !is MainRoot) { - if (deepLink != null) { - val screenSet = router.processDestination(deepLink) - if (screenSet.isNotEmpty()) { - codeNavigator.replaceAll(screenSet) - } - } - } - } - - LaunchedEffect(loginRequest) { - loginRequest?.let { entropy -> - homeViewModel.handleLoginEntropy( - entropy, - onSwitchAccounts = { - loginRequest = null - context.getActivity()?.let { - homeViewModel.logout(it) { - codeNavigator.replaceAll( - ScreenRegistry.get( - NavScreenProvider.Login.Home( - entropy - ) - ) - ) - } - } - }, - onCancel = { - loginRequest = null - } - ) - } - } - - LaunchedEffect(userState.isTimelockUnlocked) { - if (userState.isTimelockUnlocked) { - codeNavigator.replaceAll( - ScreenRegistry.get( - NavScreenProvider.AppRestricted(RestrictionType.TIMELOCK_UNLOCKED) - ) - ) - } - } - - OnLifecycleEvent { _, event -> - when (event) { - Lifecycle.Event.ON_RESUME -> { - homeViewModel.onAppOpen() - } - - Lifecycle.Event.ON_STOP, - Lifecycle.Event.ON_DESTROY -> { - homeViewModel.closeStream() - } - - else -> Unit - } - } - } - } - } - } - } - } - TopBarContainer(barManager.barMessages) - BottomBarContainer(barManager.barMessages) - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun AppNavHost(content: @Composable () -> Unit) { - var combinedNavigator by remember { - mutableStateOf(null) - } - BottomSheetNavigator( - modifier = Modifier.fillMaxSize(), - sheetBackgroundColor = LocalCodeColors.current.background, - sheetContentColor = LocalCodeColors.current.onBackground, - sheetContent = { sheetNav -> - combinedNavigator = combinedNavigator?.apply { sheetNavigator = sheetNav } - ?: CombinedNavigator(sheetNav) - combinedNavigator?.let { - CompositionLocalProvider(LocalCodeNavigator provides it) { - SheetSlideTransition(navigator = it) - } - } - - }, - onHide = com.getcode.services.manager.ModalManager::clear - ) { sheetNav -> - combinedNavigator = - combinedNavigator?.apply { sheetNavigator = sheetNav } ?: CombinedNavigator(sheetNav) - combinedNavigator?.let { - CompositionLocalProvider(LocalCodeNavigator provides it) { - content() - } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/FlipchatApp.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/FlipchatApp.kt deleted file mode 100644 index 8f4e4e0fd..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/FlipchatApp.kt +++ /dev/null @@ -1,100 +0,0 @@ -package xyz.flipchat.app - -import android.app.Application -import androidx.appcompat.app.AppCompatDelegate -import androidx.hilt.work.HiltWorkerFactory -import androidx.work.Configuration -import coil3.ImageLoader -import coil3.PlatformContext -import coil3.SingletonImageLoader -import coil3.disk.DiskCache -import coil3.disk.directory -import coil3.request.CachePolicy -import coil3.request.crossfade -import com.bugsnag.android.Bugsnag -import com.getcode.crypt.MnemonicCache -import com.getcode.ui.emojis.EmojiCompatController -import com.getcode.utils.ErrorUtils -import com.getcode.utils.trace -import com.google.firebase.Firebase -import com.google.firebase.crashlytics.crashlytics -import com.google.firebase.initialize -import dagger.hilt.android.HiltAndroidApp -import io.reactivex.rxjava3.plugins.RxJavaPlugins -import timber.log.Timber -import xyz.flipchat.app.auth.AuthManager -import javax.inject.Inject - -@HiltAndroidApp -class FlipchatApp : Application(), Configuration.Provider, SingletonImageLoader.Factory { - - @Inject - lateinit var authManager: AuthManager - - @Inject - lateinit var workerFactory: HiltWorkerFactory - - override val workManagerConfiguration: Configuration - get() = Configuration.Builder() - .setWorkerFactory(workerFactory) - .build() - - override fun onCreate() { - super.onCreate() - - if (BuildConfig.DEBUG) { - Timber.plant(object : Timber.DebugTree() { - override fun createStackElementTag(element: StackTraceElement): String { - val elementTag = super.createStackElementTag(element) - .orEmpty() - .split("$") - .filter { it.isNotEmpty() } - .take(2) - .joinToString(" ") - .replace("_", " ") - - val methodName = element.methodName - .split("$") - .firstOrNull() - .orEmpty() - - return String.format( - "%s | %s ", - elementTag, - methodName - ) - } - }) - } else { - Bugsnag.start(this) - } - - RxJavaPlugins.setErrorHandler { - ErrorUtils.handleError(it) - } - - EmojiCompatController.init(this) - - Firebase.initialize(this) - Firebase.crashlytics.setCrashlyticsCollectionEnabled(BuildConfig.NOTIFY_ERRORS || !BuildConfig.DEBUG) - MnemonicCache.init(this) - authManager.init { trace("NaCl init") } - - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) - trace("app onCreate end") - } - - override fun newImageLoader(context: PlatformContext): ImageLoader { - return ImageLoader.Builder(context) - .crossfade(true) - .memoryCachePolicy(CachePolicy.ENABLED) - .diskCachePolicy(CachePolicy.ENABLED) - .diskCache { - DiskCache.Builder() - .directory(context.cacheDir.resolve("image_cache")) - .maxSizePercent(0.2) - .build() - } - .build() - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/MainActivity.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/MainActivity.kt deleted file mode 100644 index c7cfef740..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/MainActivity.kt +++ /dev/null @@ -1,149 +0,0 @@ -package xyz.flipchat.app - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import android.os.Process.killProcess -import android.os.Process.myPid -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.compose.runtime.CompositionLocalProvider -import androidx.fragment.app.FragmentActivity -import com.getcode.libs.emojis.EmojiUsageController -import com.getcode.libs.emojis.EmojiUsageTracker -import com.getcode.libs.opengraph.LocalOpenGraphParser -import com.getcode.libs.opengraph.OpenGraphParser -import com.getcode.network.BalanceController -import com.getcode.network.LocalBalanceController -import com.getcode.network.client.Client -import com.getcode.network.exchange.ExchangeNull -import com.getcode.network.exchange.LocalExchange -import com.getcode.ui.emojis.LocalEmojiUsageController -import xyz.flipchat.app.ui.LocalUserManager -import com.getcode.util.resources.LocalResources -import com.getcode.util.resources.LocalSystemSettings -import com.getcode.util.resources.ResourceHelper -import com.getcode.util.resources.SettingsHelper -import com.getcode.util.vibration.LocalVibrator -import com.getcode.util.vibration.Vibrator -import com.getcode.utils.CurrencyUtils -import com.getcode.utils.LocalCurrencyUtils -import com.getcode.utils.network.LocalNetworkObserver -import com.getcode.utils.network.NetworkConnectivityListener -import com.google.firebase.crashlytics.FirebaseCrashlytics -import dagger.hilt.android.AndroidEntryPoint -import dev.bmcreations.tipkit.engines.TipsEngine -import dev.theolm.rinku.compose.ext.Rinku -import xyz.flipchat.app.beta.Labs -import xyz.flipchat.app.ui.LocalLabs -import xyz.flipchat.services.LocalPaymentController -import xyz.flipchat.services.PaymentController -import xyz.flipchat.services.billing.BillingClient -import xyz.flipchat.services.billing.LocalBillingClient -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject -import kotlin.system.exitProcess - -@AndroidEntryPoint -class MainActivity : FragmentActivity() { - - @Inject - lateinit var resources: ResourceHelper - - @Inject - lateinit var settingsHelper: SettingsHelper - - @Inject - lateinit var tipsEngine: TipsEngine - - @Inject - lateinit var networkObserver: NetworkConnectivityListener - - @Inject - lateinit var currencyUtils: CurrencyUtils - - @Inject - lateinit var vibrator: Vibrator - - @Inject - lateinit var userManager: UserManager - - @Inject - lateinit var client: Client - - @Inject - lateinit var paymentController: PaymentController - - @Inject - lateinit var betaFeatures: Labs - - @Inject - lateinit var iapController: BillingClient - - @Inject - lateinit var openGraphParser: OpenGraphParser - - @Inject - lateinit var balanceController: BalanceController - - @Inject - lateinit var emojiUsageController: EmojiUsageTracker - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - handleUncaughtException() - enableEdgeToEdge() - - setContent { - CompositionLocalProvider( - LocalResources provides resources, - LocalSystemSettings provides settingsHelper, - LocalNetworkObserver provides networkObserver, - LocalExchange provides ExchangeNull(), - LocalCurrencyUtils provides currencyUtils, - LocalVibrator provides vibrator, - LocalUserManager provides userManager, - LocalPaymentController provides paymentController, - LocalLabs provides betaFeatures, - LocalBillingClient provides iapController, - LocalOpenGraphParser provides openGraphParser, - LocalBalanceController provides balanceController, - LocalEmojiUsageController provides emojiUsageController - ) { - Rinku { - App(tipsEngine = tipsEngine) - } - } - } - } - - override fun onResume() { - super.onResume() - client.startTimer() - iapController.connect() - } - - override fun onStop() { - super.onStop() - client.stopTimer() - } -} - -private fun Activity.handleUncaughtException() { - val crashedKey = "isCrashed" - if (intent.getBooleanExtra(crashedKey, false)) return - Thread.setDefaultUncaughtExceptionHandler { _, throwable -> - if (BuildConfig.DEBUG) throw throwable - - FirebaseCrashlytics.getInstance().recordException(throwable) - - val intent = Intent(this, MainActivity::class.java).apply { - putExtra(crashedKey, true) - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) - } - startActivity(intent) - finish() - killProcess(myPid()) - exitProcess(2) - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/auth/AuthManager.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/auth/AuthManager.kt deleted file mode 100644 index feb48cdcc..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/auth/AuthManager.kt +++ /dev/null @@ -1,305 +0,0 @@ -package xyz.flipchat.app.auth - -import android.annotation.SuppressLint -import android.content.Context -import androidx.core.app.NotificationManagerCompat -import com.bugsnag.android.Bugsnag -import com.getcode.ed25519.Ed25519 -import com.getcode.model.ID -import com.getcode.network.BalanceController -import xyz.flipchat.app.util.AccountUtils -import com.getcode.services.db.Database -import com.getcode.services.utils.token -import com.getcode.utils.ErrorUtils -import com.getcode.utils.TraceType -import com.getcode.utils.base58 -import com.getcode.utils.encodeBase64 -import com.getcode.utils.trace -import com.getcode.vendor.Base58 -import com.google.firebase.Firebase -import com.google.firebase.messaging.FirebaseMessaging -import com.google.firebase.messaging.messaging -import com.ionspin.kotlin.crypto.LibsodiumInitializer -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import xyz.flipchat.FlipchatServices -import xyz.flipchat.app.BuildConfig -import xyz.flipchat.app.beta.Labs -import xyz.flipchat.app.util.UserIdResult -import xyz.flipchat.controllers.AuthController -import xyz.flipchat.controllers.ProfileController -import xyz.flipchat.controllers.PushController -import xyz.flipchat.services.user.AuthState -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class AuthManager @Inject constructor( - @ApplicationContext private val context: Context, - private val authController: AuthController, - private val profileController: ProfileController, - private val userManager: UserManager, - private val pushController: PushController, - private val betaFlags: Labs, - private val notificationManager: NotificationManagerCompat, - private val balanceController: BalanceController, -// private val analytics: AnalyticsService, -// private val mixpanelAPI: MixpanelAPI -) : CoroutineScope by CoroutineScope(Dispatchers.IO) { - private var softLoginDisabled: Boolean = false - - companion object { - private const val TAG = "AuthManager" - internal fun taggedTrace(message: String, type: TraceType = TraceType.Log, cause: Throwable? = null) { - trace(message = message, type = type, tag = TAG, error = cause) - } - } - - @SuppressLint("CheckResult") - fun init(onInitialized: () -> Unit = { }) { - launch { - val token = AccountUtils.getToken(context)?.token - softLogin(token.orEmpty()) - .onSuccess { LibsodiumInitializer.initializeWithCallback(onInitialized) } - .onFailure(ErrorUtils::handleError) - } - } - - private suspend fun softLogin(entropyB64: String): Result { - if (softLoginDisabled) return Result.failure(Throwable("Disabled")) - return login(entropyB64, isSoftLogin = true) - } - - private fun setupAsNew(): String { - val entropyB64 = userManager.entropy - return if (entropyB64 == null) { - val seedB64 = Ed25519.createSeed16().encodeBase64() - userManager.establish(seedB64) - return seedB64 - } else { - entropyB64 - } - } - - suspend fun createAccount(): Result { - val entropy = setupAsNew() - FlipchatServices.openDatabase(context, entropy) - return authController.createAccount() - .onSuccess { userId -> - AccountUtils.addAccount( - context = context, - name = "Flipchat User", - password = userId.base58, - token = entropy, - isUnregistered = true - ) - userManager.set(userId) - userManager.set(AuthState.Unregistered) - profileController.getUserFlags() - }.onFailure { - it.printStackTrace() - clearToken() - } - } - - - suspend fun register(displayName: String): Result { - val entropyB64 = userManager.entropy ?: setupAsNew() - if (entropyB64.isEmpty()) { - userManager.clear() - return Result.failure(Throwable("Provided entropy was empty")) - } - - softLoginDisabled = true - - FlipchatServices.openDatabase(context, entropyB64) - - // if we are in an unregistered state attempting to register - // it means the user account was setup on device and a public key registered on server. - // in this case, we simply need to set the display name on server to flip `is_registered`. - if (userManager.authState is AuthState.Unregistered) { - userManager.set(displayName = displayName) - - return profileController.setDisplayName(displayName) - .onSuccess { - AccountUtils.updateAccount( - context = context, - name = displayName, - ) - userManager.set(displayName = displayName) - userManager.set(AuthState.LoggedIn) - profileController.getUserFlags() - savePrefs() - } - .map { userManager.userId!! } - } else { - return authController.register(displayName) - .onSuccess { userId -> - if (userManager.authState is AuthState.Unregistered) { - AccountUtils.updateAccount( - context = context, - name = displayName, - ) - } else { - AccountUtils.addAccount( - context = context, - name = displayName, - password = userId.base58, - token = entropyB64, - isUnregistered = false, - ) - userManager.set(userId = userId) - } - userManager.set(displayName = displayName) - userManager.set(AuthState.LoggedIn) - profileController.getUserFlags() - savePrefs() - } - .onFailure { - it.printStackTrace() - softLoginDisabled = false - clearToken() - } - } - } - - suspend fun login( - entropyB64: String, - isSoftLogin: Boolean = false, - rollbackOnError: Boolean = false - ): Result { - taggedTrace("Login: isSoftLogin: $isSoftLogin, rollbackOnError: $rollbackOnError") - - if (entropyB64.isEmpty()) { - userManager.clear() - return Result.failure(Throwable("Provided entropy was empty")) - } - - FlipchatServices.openDatabase(context, entropyB64) - - val originalEntropy = userManager.entropy - userManager.establish(entropy = entropyB64) - userManager.set(AuthState.LoggedInAwaitingUser) - - if (!isSoftLogin) { - loginAnalytics() - } - - if (!isSoftLogin) softLoginDisabled = true - - val lookup = AccountUtils.getUserId(context) - - val ret = if (isSoftLogin) { - when (lookup) { - is UserIdResult.Registered -> Result.success(Base58.decode(lookup.userId).toList()) - is UserIdResult.Unregistered -> Result.success(Base58.decode(lookup.userId).toList()) - null -> Result.failure(Throwable("No user Id found")) - } - } else { - authController.login() - } - - return ret - .map { it to profileController.getProfile(it) } - .map { (id, profileResult) -> - id to profileResult.getOrNull() - } - .onSuccess { (userId, profile) -> - if (!isSoftLogin) { - AccountUtils.addAccount( - context = context, - name = profile?.displayName ?: "Flipchat User", - password = userId.base58, - token = entropyB64, - isUnregistered = false, - ) - } - - userManager.set(userId = userId) - if (profile?.displayName != null) { - userManager.set(displayName = profile.displayName) - } - - userManager.setSocialProfiles(profile?.socialProfiles.orEmpty()) - - profileController.getUserFlags() - .onSuccess { flags -> - userManager.set(flags) - }.onFailure { - taggedTrace("Failed to get user flags", type = TraceType.Error, cause = it) - userManager.set(authState = AuthState.Unregistered) - } - - FlipchatServices.scheduleChatSync(context) - savePrefs() - } - .onFailure { - it.printStackTrace() - if (rollbackOnError) { - login( - originalEntropy.orEmpty(), - isSoftLogin, - rollbackOnError = false - ) - } else { - logout(context) - clearToken() - } - }.map { it.first } - } - - suspend fun deleteAndLogout(context: Context): Result { - //todo: add account deletion - return logout(context) - } - - suspend fun logout(context: Context): Result { - return AccountUtils.removeAccounts(context).toFlowable() - .to { runCatching { it.firstOrError().blockingGet() } } - .map { clearToken() } - .map { Result.success(Unit) } - } - - private fun loginAnalytics() { - taggedTrace("analytics login event") -// analytics.login( -// ownerPublicKey = owner.getPublicKeyBase58(), -// autoCompleteCount = 0, -// inputChangeCount = 0 -// ) - } - - private suspend fun clearToken() { - FirebaseMessaging.getInstance().deleteToken() - pushController.deleteTokens() - notificationManager.cancelAll() - Database.close() - userManager.clear() - Database.delete(context) - betaFlags.reset() - balanceController.reset() - if (!BuildConfig.DEBUG) Bugsnag.setUser(null, null, null) - } - - private suspend fun savePrefs() { - updateFcmToken() - } - - @SuppressLint("CheckResult") - private suspend fun updateFcmToken() { - val pushToken = Firebase.messaging.token() ?: return - pushController.addToken(pushToken) - .onSuccess { - trace("push token updated", type = TraceType.Silent) - }.onFailure { - trace(message = "Failure updating push token", error = it) - } - } - - sealed class AuthManagerException : Exception() { - class TimelockUnlockedException : AuthManagerException() - } -} diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/beta/Labs.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/beta/Labs.kt deleted file mode 100644 index 77a12767e..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/beta/Labs.kt +++ /dev/null @@ -1,28 +0,0 @@ -package xyz.flipchat.app.beta - -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow - -interface Labs { - fun set(flag: Lab, value: Boolean) - suspend fun get(flag: Lab): Boolean - fun observe(flag: Lab): StateFlow - fun observe(): StateFlow> - fun reset(flag: Lab) - fun reset() -} - -object NoOpLabs: Labs { - override fun set(flag: Lab, value: Boolean) = Unit - - override suspend fun get(flag: Lab): Boolean = false - - override fun observe(flag: Lab): StateFlow = MutableStateFlow(false) - - override fun observe(): StateFlow> = - MutableStateFlow(Lab.entries.map { BetaFeature(it, it.default) }) - - override fun reset(flag: Lab) = Unit - override fun reset() = Unit - -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/beta/LabsController.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/beta/LabsController.kt deleted file mode 100644 index c5db6f584..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/beta/LabsController.kt +++ /dev/null @@ -1,204 +0,0 @@ -package xyz.flipchat.app.beta - -import android.content.Context -import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler -import androidx.datastore.preferences.core.PreferenceDataStoreFactory -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.emptyPreferences -import androidx.datastore.preferences.preferencesDataStoreFile -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import javax.inject.Inject - -sealed interface Lab { - val key: String - val default: Boolean - val launched: Boolean - - data object ReplyToMessage : Lab { - override val key = "pref_reply_enabled" - override val default: Boolean = true - override val launched: Boolean = true - } - - data object FollowerMode : Lab { - override val key: String = "pref_follower_mode_enabled" - override val default: Boolean = true - override val launched: Boolean = true - } - - data object StartChatAtUnread : Lab { - override val key: String = "pref_start_at_unread_enabled" - override val default: Boolean = true - override val launched: Boolean = true - } - - data object RoomNameChanges : Lab { - override val key: String = "pref_room_name_changes_enabled" - override val default: Boolean = true - override val launched: Boolean = true - } - - data object DeleteMessage : Lab { - override val key: String = "delete_message_enabled" - override val default: Boolean = true - override val launched: Boolean = true - } - - data object OpenCloseRoom : Lab { - override val key: String = "open_close_room_enabled" - override val default: Boolean = true - override val launched: Boolean = true - } - - data object Tipping : Lab { - override val key: String = "tipping_enabled" - override val default: Boolean = true - override val launched: Boolean = true - } - - data object ShowConnectedSocials: Lab { - override val key: String = "show_connected_socials_enabled" - override val default: Boolean = true - override val launched: Boolean = true - } - - data object ConnectX: Lab { - override val key: String = "connect_x_enabled" - override val default: Boolean = true - override val launched: Boolean = true - } - - data object LinkImages : Lab { - override val key: String = "link_image_preview_enabled" - override val default: Boolean = false - override val launched: Boolean = false - } - - data object TypingInChat: Lab { - override val key: String = "typing_indicator_enabled" - override val default: Boolean = true - override val launched: Boolean = true - } - - data object EmojiReactions: Lab { - override val key: String = "emojis_enabled" - override val default: Boolean = true - override val launched: Boolean = true - } - - data object RoomDescriptions: Lab { - override val key: String = "room_descriptions_enabled" - override val default: Boolean = false - override val launched: Boolean = false - } - - companion object { - val entries = listOf( - ReplyToMessage, - FollowerMode, - StartChatAtUnread, - RoomNameChanges, - DeleteMessage, - OpenCloseRoom, - Tipping, - ConnectX, - TypingInChat, - ShowConnectedSocials, - EmojiReactions, - RoomDescriptions - ) - - internal fun byKey(key: Preferences.Key<*>): Lab? { - return entries.firstOrNull { it.key == key.name } - } - } -} - -data class BetaFeature( - val flag: Lab, - val enabled: Boolean, -) - -private val Lab.preferenceKey - get() = booleanPreferencesKey(key) - -class LabsController @Inject constructor( - @ApplicationContext context: Context, -) : Labs { - private val dataScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - - private val betaFlags = PreferenceDataStoreFactory.create( - corruptionHandler = ReplaceFileCorruptionHandler( - produceNewData = { emptyPreferences() } - ), - migrations = listOf(), - scope = dataScope, - produceFile = { context.preferencesDataStoreFile("beta-flags") } - ) - - init { - // reset launched flags - Lab.entries - .filter { it.launched } - .onEach { reset(it) } - } - - override fun set(flag: Lab, value: Boolean) { - dataScope.launch(Dispatchers.IO) { - betaFlags.edit { prefs -> - prefs[flag.preferenceKey] = value - } - } - } - - override suspend fun get(flag: Lab): Boolean { - return betaFlags.data.map { prefs -> - if (flag.launched) return@map flag.default - prefs[flag.preferenceKey] ?: flag.default - }.firstOrNull() ?: flag.default - } - - override fun observe(flag: Lab): StateFlow = betaFlags.data.map { prefs -> - if (flag.launched) return@map flag.default - prefs[flag.preferenceKey] ?: flag.default - }.stateIn(dataScope, started = SharingStarted.Eagerly, flag.default) - - override fun observe(): StateFlow> = betaFlags.data.map { prefs -> - Lab.entries.filterNot { it.launched }.map { - val value = if (it.launched) { - it.default - } else { - prefs[it.preferenceKey] ?: it.default - } - - BetaFeature(it, value) - } - }.stateIn( - dataScope, - started = SharingStarted.Eagerly, - Lab.entries.map { BetaFeature(it, it.default) } - ) - - override fun reset(flag: Lab) { - dataScope.launch { - betaFlags.edit { it.remove(flag.preferenceKey) } - } - } - - override fun reset() { - dataScope.launch { - betaFlags.edit { it.clear() } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/data/Account.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/data/Account.kt deleted file mode 100644 index d7219da3b..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/data/Account.kt +++ /dev/null @@ -1,7 +0,0 @@ -package xyz.flipchat.app.data - - -data class Account( - val entropy: String, - val userIdBase58: String -) diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/data/RoomInfo.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/data/RoomInfo.kt deleted file mode 100644 index d122f8254..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/data/RoomInfo.kt +++ /dev/null @@ -1,34 +0,0 @@ -package xyz.flipchat.app.data - -import androidx.compose.ui.graphics.Color -import com.getcode.model.ID -import com.getcode.model.Kin -import com.getcode.ui.utils.generateComplementaryColorPalette - -data class RoomInfo( - val id: ID? = null, - val number: Long = 0, - val title: String = "", - val description: String = "", - val imageUrl: String? = null, - val memberCount: Int = 0, - val hostId: ID? = null, - val hostName: String? = null, - val roomNumber: Long = 0, - val messagingFee: Kin = Kin.fromQuarks(0), -) { - val customTitle: String = runCatching { Regex("^#\\d+:\\s*(.*)").find(title)?.groupValues?.get(1).orEmpty() }.getOrDefault("") - - companion object { - val DEFAULT_GRADIENT_SAMPLE = Triple( - Color(0xFFFFBB00), - Color(0xFF7306B7), - Color(0xFF3E32C4), - ) - } - - val gradientColors: Triple - get() { - return id?.let { generateComplementaryColorPalette(it) } ?: DEFAULT_GRADIENT_SAMPLE - } -} diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/accesskey/BaseAccessKeyViewModel.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/accesskey/BaseAccessKeyViewModel.kt deleted file mode 100644 index 0513a8450..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/accesskey/BaseAccessKeyViewModel.kt +++ /dev/null @@ -1,288 +0,0 @@ -package xyz.flipchat.app.features.accesskey - -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.Typeface -import android.os.Environment -import androidx.core.graphics.applyCanvas -import androidx.core.graphics.drawable.toBitmap -import androidx.lifecycle.viewModelScope -import com.getcode.libs.qr.QRCodeGenerator -import com.getcode.manager.TopBarManager -import com.getcode.services.manager.MnemonicManager -import com.getcode.theme.Alert -import com.getcode.theme.White -import com.getcode.ui.utils.toAGColor -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.decodeBase64 -import com.getcode.view.BaseViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.distinctUntilChangedBy -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.kin.sdk.base.tools.Base58 -import timber.log.Timber -import xyz.flipchat.app.R -import xyz.flipchat.app.theme.FC_Primary -import xyz.flipchat.app.util.media.MediaScanner -import xyz.flipchat.app.util.save -import xyz.flipchat.services.user.UserManager -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import kotlin.math.roundToInt -import com.getcode.theme.R as themeR - - -data class AccessKeyUiModel( - val entropyB64: String? = null, - val isLoading: Boolean = false, - val isSuccess: Boolean = false, - val isEnabled: Boolean = true, - val words: List = listOf(), - val wordsFormatted: String = "", - val accessKeyBitmap: Bitmap? = null, - val accessKeyCroppedBitmap: Bitmap? = null, -) - -abstract class BaseAccessKeyViewModel( - private val resources: ResourceHelper, - private val mnemonicManager: MnemonicManager, - private val mediaScanner: MediaScanner, - userManager: UserManager, - private val qrCodeGenerator: QRCodeGenerator -) : BaseViewModel(resources) { - val uiFlow = MutableStateFlow(AccessKeyUiModel()) - - init { - userManager.state - .distinctUntilChangedBy { it.entropy } - .map { it.entropy } - .filterNotNull() - .take(1) - .onEach { initWithEntropy(it) } - .launchIn(viewModelScope) - } - - fun initWithEntropy(entropyB64: String) { - if (uiFlow.value.entropyB64 == entropyB64) return - Timber.d("entropy=$entropyB64") - val words = mnemonicManager.fromEntropyBase64(entropyB64).words - val wordsFormatted = getAccessKeyText(words).joinToString("\n") - - uiFlow.value = uiFlow.value.copy( - entropyB64 = entropyB64, - words = words, - wordsFormatted = wordsFormatted - ) - - CoroutineScope(Dispatchers.IO).launch { - val accessKeyBitmap = createBitmapForExport(words = words, entropyB64 = entropyB64) - val accessKeyBitmapDisplay = - createBitmapForExport(drawBackground = false, words, entropyB64) - val accessKeyCroppedBitmap = - Bitmap.createBitmap(accessKeyBitmapDisplay, 0, 500, 1200, 1450) - - uiFlow.value = uiFlow.value.copy( - accessKeyBitmap = accessKeyBitmap, - accessKeyCroppedBitmap = accessKeyCroppedBitmap - ) - } - } - - private fun getAccessKeyText(words: List): List { - return listOf( - words.subList(0, 6).joinToString(" "), - words.subList(6, 12).joinToString(" ") - ) - } - - private val targetWidth = 1200 - private val targetHeight = 2500 - - private val logoWidth = 92.4f - private val logoHeight = 132 - private val qrCodeSize = 360 - - private val bgTopOffset = 550 - private val logoTopOffset = 770 - private val qrTopOffset = 980 - private val keyTextTopOffset = 1600 - private val topTextTopOffset = 200 - private val bottomTextTopOffset = 2000 - - internal suspend fun saveBitmapToFile(): Result { - uiFlow.update { it.copy(isLoading = true) } - val bitmap = uiFlow.value.accessKeyBitmap - ?: return Result.failure(IllegalStateException("No access key?")) - val destination = - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) - - return withContext(Dispatchers.IO) { - runCatching { - val result = bitmap.save( - destination = destination, - name = { - val date: DateFormat = SimpleDateFormat("yyy-MM-dd-h-mm", Locale.CANADA) - "Flipchat-Recovery-${date.format(Date())}.png" - } - ) - if (result) { - mediaScanner.scan(destination) - } - result - } - }.onFailure { - getAccessKeySaveError() - uiFlow.update { it.copy(isLoading = false, isSuccess = false) } - }.onSuccess { - uiFlow.update { it.copy(isLoading = false, isSuccess = true) } - } - } - - private fun createBitmapForExport( - drawBackground: Boolean = true, - words: List, - entropyB64: String - ): Bitmap { - val accessKeyText = getAccessKeyText(words) - - val accessKeyBg = resources.getDrawable(R.drawable.ic_access_key_bg) - ?.toBitmap(812, 1353)!! - - val imageLogo = - resources.getDrawable(R.drawable.ic_flipchat_logo_access_key) - ?.toBitmap(logoWidth.roundToInt(), logoHeight)!! - - val imageOut = Bitmap.createBitmap( - targetWidth, targetHeight, - Bitmap.Config.ARGB_8888 - ).applyCanvas { - val accessBgActualWidth = - accessKeyBg.getScaledWidth(resources.displayMetrics) - - if (drawBackground) { - val paintBackground = Paint() - paintBackground.color = FC_Primary.toAGColor() - paintBackground.style = Paint.Style.FILL - drawPaint(paintBackground) - } - - val topTextChunks = getString(R.string.subtitle_accessKeySnapshotWarning) - .split(" ", "\n") - .chunked(7) - .map { it.joinToString(" ") } - - topTextChunks.forEachIndexed { index, text -> - drawText( - canvas = this, - y = topTextTopOffset + (60 * (index + 1)), - sizePx = 40, - color = Alert.toAGColor(), - text = text - ) - } - - - drawBitmap( - accessKeyBg, - (((targetWidth - accessBgActualWidth) / 2)).toFloat(), - bgTopOffset.toFloat(), - null - ) - - drawBitmap( - imageLogo, - ((targetWidth - logoWidth) / 2).toFloat(), - logoTopOffset.toFloat(), - null - ) - - getQrCode(entropyB64)?.let { bitmap -> - drawBitmap( - bitmap, - ((targetWidth - qrCodeSize) / 2).toFloat(), - qrTopOffset.toFloat(), - null - ) - } - - drawText( - canvas = this, - y = keyTextTopOffset, - sizePx = 32, - color = White.toAGColor(), - text = accessKeyText[0] - ) - - drawText( - canvas = this, - y = keyTextTopOffset + 40, - sizePx = 32, - color = White.toAGColor(), - text = accessKeyText[1] - ) - - val bottomTextChunks = getString(R.string.subtitle_accessKeySnapshotDescription) - .split(" ") - .chunked(8) - .map { it.joinToString(" ") } - - bottomTextChunks.forEachIndexed { index, text -> - drawText( - canvas = this, - y = bottomTextTopOffset + (60 * (index + 1)), - sizePx = 40, - color = White.toAGColor(), - text = text - ) - } - - } - return imageOut - } - - private fun getQrCode(entropyB64: String): Bitmap? { - val base58 = Base58.encode(entropyB64.decodeBase64()) - val url = "${resources.getString(R.string.app_root_url)}/login?data=$base58" - - return qrCodeGenerator.generate(url, qrCodeSize) - } - - private fun drawText( - canvas: Canvas, - y: Int, - x: Int? = null, - sizePx: Int, - color: Int, - text: String - ) { - val textPaint = Paint(Paint.ANTI_ALIAS_FLAG) - textPaint.color = color - textPaint.textSize = sizePx.toFloat() - textPaint.typeface = Typeface.create( - resources.getFont(themeR.font.avenir_next_demi), - Typeface.BOLD - ) - - val bounds1 = android.graphics.Rect() - textPaint.getTextBounds(text, 0, text.length, bounds1) - val xV: Int = x ?: ((targetWidth - bounds1.width()) / 2) - canvas.drawText(text, xV.toFloat(), y.toFloat(), textPaint) - } - - private fun getAccessKeySaveError() = TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToSave), - resources.getString(R.string.error_description_failedToSave), - ) -} diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/balance/BalanceScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/balance/BalanceScreen.kt deleted file mode 100644 index 8f98f1fbe..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/balance/BalanceScreen.kt +++ /dev/null @@ -1,341 +0,0 @@ -package xyz.flipchat.app.features.balance - -import android.os.Parcelable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.text.ClickableText -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Alignment.Companion.CenterHorizontally -import androidx.compose.ui.Alignment.Companion.CenterVertically -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import xyz.flipchat.app.R -import com.getcode.model.Currency -import com.getcode.model.CurrencyCode -import com.getcode.model.Rate -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.extensions.getActivityScopedViewModel -import com.getcode.theme.CodeTheme -import com.getcode.theme.DesignSystem -import com.getcode.ui.components.text.AmountArea -import com.getcode.ui.theme.CodeCircularProgressIndicator -import com.getcode.utils.Kin -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import xyz.flipchat.services.user.AuthState - -@Parcelize -class BalanceScreen : Screen, Parcelable { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @Composable - override fun Content() { - val viewModel = getActivityScopedViewModel() - val state by viewModel.stateFlow.collectAsState() - BalanceScreenContent(state, viewModel::dispatchEvent) - } - -} - -@Composable -fun BalanceScreenContent( - state: BalanceSheetViewModel.State, - dispatch: (BalanceSheetViewModel.Event) -> Unit, -) { - val navigator = LocalCodeNavigator.current - - BalanceContent( - state = state, - dispatch = dispatch, - faqOpen = { }, -// openChat = { }, - buyMoreKin = { } - ) -} - -@Composable -fun BalanceContent( - state: BalanceSheetViewModel.State, - dispatch: (BalanceSheetViewModel.Event) -> Unit, - faqOpen: () -> Unit, -// openChat: (Chat) -> Unit, - buyMoreKin: () -> Unit, -) { - val lazyListState = rememberLazyListState() - val navigator = LocalCodeNavigator.current - - val chatsEmpty = false -// val chatsEmpty by remember(state.chats) { -// derivedStateOf { state.chats.isEmpty() } -// } - - val canClickBalance = false - - LazyColumn( - modifier = Modifier - .fillMaxWidth(), - state = lazyListState - ) { - item { - Column( - modifier = Modifier - .fillParentMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset,) - .padding(top = CodeTheme.dimens.inset) - ) { - BalanceTop( - state, - canClickBalance, - ) - } - } - -// item { -// Column( -// modifier = Modifier -// .fillParentMaxWidth() -// .padding(horizontal = CodeTheme.dimens.inset) -// ) { -// if (!chatsEmpty && !state.chatsLoading && !state.isKinSelected) { -// KinValueHint(faqOpen) -// } -// } -// } - -// itemsIndexed( -// state.chats, -// key = { _, item -> item.id }, -// contentType = { _, item -> item } -// ) { index, chat -> -// ChatNode(chat = chat, onClick = { openChat(chat) }) -// Divider( -// modifier = Modifier.padding(start = CodeTheme.dimens.inset), -// color = White10, -// ) -// } - - when { - state.chatsLoading -> { - item { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = CenterHorizontally, - verticalArrangement = Arrangement.spacedBy( - CodeTheme.dimens.grid.x2, - CenterVertically - ), - ) { - CodeCircularProgressIndicator() - Text( - modifier = Modifier.fillMaxWidth(0.6f), - text = stringResource(R.string.subtitle_loadingBalanceAndTransactions), - textAlign = TextAlign.Center - ) - } - } - } - -// chatsEmpty -> { -// item { -// EmptyTransactionsHint(faqOpen) -// } -// } - } - } -} - -@Composable -fun BalanceTop( - state: BalanceSheetViewModel.State, - isClickable: Boolean, - onClick: () -> Unit = {} -) { - if (state.amountText.isEmpty() && state.authState is AuthState.LoggedIn) { - - Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { - CodeCircularProgressIndicator() - } - } else { - AmountArea( - amountText = state.amountText, - isAltCaption = false, - isAltCaptionKinIcon = false, - isLoading = state.chatsLoading, - currencyResId = state.currencyFlag, - isClickable = false, - onClick = onClick, - textStyle = CodeTheme.typography.displayLarge, - ) - } -} - -@Composable -private fun ColumnScope.KinValueHint(onClick: () -> Unit) { - val context = LocalContext.current - Row( - modifier = Modifier - .align(CenterHorizontally) - ) { - val annotatedBalanceString = buildAnnotatedString { - val infoString = stringResource(R.string.subtitle_valueKinChanges) - val actionString = stringResource(R.string.subtitle_learnMore) - val textString = "$infoString $actionString" - - val startIndex = textString.indexOf(actionString) - val endIndex = textString.length - append(textString) - - addStyle( - style = SpanStyle( - textDecoration = TextDecoration.Underline - ), start = startIndex, end = endIndex - ) - addStyle( - style = SpanStyle(color = CodeTheme.colors.textSecondary), - start = 0, - end = textString.length - ) - addStringAnnotation( - tag = stringResource(R.string.subtitle_learnMore), - annotation = "", - start = startIndex, - end = endIndex - ) - } - - ClickableText( - modifier = Modifier - .padding(horizontal = CodeTheme.dimens.grid.x1), - text = annotatedBalanceString, - style = CodeTheme.typography.textMedium, - onClick = { - annotatedBalanceString - .getStringAnnotations( - context.getString(R.string.subtitle_learnMore), - it, - it - ) - .firstOrNull()?.let { onClick() } - } - ) - } -} - -@Composable -private fun EmptyTransactionsHint(faqOpen: () -> Unit) { - val context = LocalContext.current - Column( - modifier = Modifier - .height(200.dp) - .padding(horizontal = CodeTheme.dimens.grid.x6), - verticalArrangement = Arrangement.Bottom, - ) { - Row( - modifier = Modifier - .align(CenterHorizontally) - ) { - Text( - modifier = Modifier.padding(vertical = CodeTheme.dimens.grid.x1), - text = stringResource(R.string.subtitle_dontHaveKin), - color = CodeTheme.colors.textSecondary, - style = CodeTheme.typography.textMedium - ) - } - - val annotatedLinkString: AnnotatedString = buildAnnotatedString { - val linkString = "Check out the FAQ" - val remainderString = " to find out how to get some." - val textString = linkString + remainderString - - val startIndex = textString.indexOf(linkString) - val endIndex = linkString.length - - append(textString) - addStyle( - style = SpanStyle( - color = CodeTheme.colors.textSecondary, - ), start = 0, end = textString.length - ) - addStyle( - style = SpanStyle( - textDecoration = TextDecoration.Underline - ), start = startIndex, end = endIndex - ) - - addStringAnnotation( - tag = context.getString(R.string.title_faq), - annotation = "", - start = startIndex, - end = endIndex - ) - } - - Row( - modifier = Modifier - .align(CenterHorizontally) - ) { - ClickableText( - text = annotatedLinkString, - style = CodeTheme.typography.textMedium.copy(textAlign = TextAlign.Center), - onClick = { - annotatedLinkString - .getStringAnnotations( - context.getString(R.string.title_faq), - it, - it - ) - .firstOrNull()?.let { _ -> faqOpen() } - } - ) - } - } -} - - -@Preview -@Composable -private fun TopPreview() { - DesignSystem { - val model = BalanceSheetViewModel.State( - amountText = "$12.34 of Kin", - marketValue = 2_225_100.0, - selectedRate = Rate(Currency.Kin.rate, CurrencyCode.KIN), - chatsLoading = false, - currencyFlag = R.drawable.ic_currency_kin, -// chats = emptyList(), - isBucketDebuggerEnabled = false, - isBucketDebuggerVisible = false, - ) - - BalanceTop( - state = model, - isClickable = true - ) - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/balance/BalanceSheetViewModel.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/balance/BalanceSheetViewModel.kt deleted file mode 100644 index 45fe88b32..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/balance/BalanceSheetViewModel.kt +++ /dev/null @@ -1,119 +0,0 @@ -package xyz.flipchat.app.features.balance - -import androidx.lifecycle.viewModelScope -import com.getcode.model.Currency -import com.getcode.model.Rate -import com.getcode.network.BalanceController -import com.getcode.utils.Kin -import com.getcode.utils.network.NetworkConnectivityListener -import com.getcode.view.BaseViewModel2 -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import xyz.flipchat.services.user.AuthState -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject - -@HiltViewModel -class BalanceSheetViewModel @Inject constructor( - userManager: UserManager, - balanceController: BalanceController, - networkObserver: NetworkConnectivityListener, -) : BaseViewModel2( - initialState = State(), - updateStateForEvent = updateStateForEvent -) { - data class State( - val authState: AuthState = AuthState.Unknown, - val amountText: String = "", - val marketValue: Double = 0.0, - val selectedRate: Rate? = null, - val isKinSelected: Boolean = false, - val currencyFlag: Int? = null, - val chatsLoading: Boolean = false, - val isBucketDebuggerEnabled: Boolean = false, - val isBucketDebuggerVisible: Boolean = false, - ) - - sealed interface Event { - data class OnAuthStateChanged(val authState: AuthState): Event - data class OnDebugBucketsEnabled(val enabled: Boolean) : Event - data class OnDebugBucketsVisible(val show: Boolean) : Event - data class OnLatestRateChanged(val rate: Rate) : Event - - data class OnBalanceChanged( - val flagResId: Int?, - val marketValue: Double, - val display: String, - val isKin: Boolean, - ) : Event - - data class OnChatsLoading(val loading: Boolean) : Event -// data class OnChatsUpdated(val chats: List) : Event - data object OnOpened: Event - } - - init { - userManager.state - .map { it.authState } - .onEach { dispatchEvent(Event.OnAuthStateChanged(it)) } - .launchIn(viewModelScope) - - balanceController.formattedBalance - .filterNotNull() - .distinctUntilChanged() - .onEach { - dispatchEvent( - Dispatchers.Main, - Event.OnBalanceChanged( - flagResId = it.currency?.resId, - marketValue = it.marketValue, - display = it.formattedValue, - isKin = it.currency == Currency.Kin - ) - ) - } - .launchIn(viewModelScope) - } - - companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - is Event.OnDebugBucketsEnabled -> { state -> - state.copy(isBucketDebuggerEnabled = event.enabled) - } - - is Event.OnDebugBucketsVisible -> { state -> - state.copy(isBucketDebuggerVisible = event.show) - } - - is Event.OnLatestRateChanged -> { state -> - state.copy(selectedRate = event.rate) - } - - is Event.OnBalanceChanged -> { state -> - state.copy( - currencyFlag = event.flagResId, - marketValue = event.marketValue, - amountText = event.display, - isKinSelected = event.isKin - ) - } - is Event.OnChatsLoading -> { state -> - state.copy(chatsLoading = event.loading) - } -// is Event.OnChatsUpdated -> { state -> -// state.copy(chats = event.chats) -// } - - Event.OnOpened -> { state -> state } - is Event.OnAuthStateChanged -> { state -> state.copy(authState = event.authState) } - } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/beta/BetaFlagsScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/beta/BetaFlagsScreen.kt deleted file mode 100644 index 94ac9cd8f..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/beta/BetaFlagsScreen.kt +++ /dev/null @@ -1,132 +0,0 @@ -package xyz.flipchat.app.features.beta - -import android.os.Parcelable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.components.SettingsSwitchRow -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import xyz.flipchat.app.R -import xyz.flipchat.app.beta.Lab -import xyz.flipchat.app.ui.LocalLabs - -@Parcelize -class BetaFlagsScreen : Screen, Parcelable { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AppBarWithTitle( - title = stringResource(R.string.title_betaFlags), - backButton = true, - onBackIconClicked = navigator::pop - ) - BetaFlagsScreenContent() - } - } -} - -@Composable -private fun BetaFlagsScreenContent() { - val betaFlagsController = LocalLabs.current - val betaFlags by betaFlagsController.observe().collectAsState() - - LazyColumn( - modifier = Modifier.fillMaxSize(), - ) { - items(betaFlags) { feature -> - SettingsSwitchRow( - title = feature.flag.title, - subtitle = feature.flag.message, - checked = feature.enabled - ) { - betaFlagsController.set(feature.flag, !feature.enabled) - } - } - if (betaFlags.isEmpty()) { - item { - Box(modifier = Modifier.fillParentMaxSize()) { - Column( - modifier = Modifier.align(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "\uD83D\uDE2D", - style = CodeTheme.typography.displayMedium - ) - Text( - text = "Nothing Cooking in the Lab Right Now", - style = CodeTheme.typography.textLarge, - color = CodeTheme.colors.textMain - ) - - Text( - text = "Check back in the next app update.", - style = CodeTheme.typography.textSmall, - color = CodeTheme.colors.textSecondary - ) - - } - } - } - } - } -} - -private val Lab.title: String - get() = when (this) { - Lab.FollowerMode -> "Follower Mode" - Lab.ReplyToMessage -> "Swipe To Reply" - Lab.StartChatAtUnread -> "Open Conversation @ Last Unread" - Lab.RoomNameChanges -> "Room Name Changes For Hosts" - Lab.DeleteMessage -> "Delete Message Support" - Lab.OpenCloseRoom -> "Open/Close Rooms" - Lab.Tipping -> "Tipping" - Lab.LinkImages -> "Show Previews for Links" - Lab.ConnectX -> "Connect X Account" - Lab.TypingInChat -> "Typing Indicators" - Lab.ShowConnectedSocials -> "Show User Social Names and Images" - Lab.EmojiReactions -> "Emoji Reactions" - Lab.RoomDescriptions -> "Room Descriptions" - } - -private val Lab.message: String - get() = when (this) { - Lab.FollowerMode -> "When enabled, you will gain the ability to watch rooms without joining first" - Lab.ReplyToMessage -> "When enabled, you will gain the ability to swipe to reply to messages in chat" - Lab.StartChatAtUnread -> "When enabled, conversations will resume at the last message you read" - Lab.RoomNameChanges -> "When enabled, hosts will gain the ability to set a desired name for their room" - Lab.DeleteMessage -> "When enabled, hosts will gain the ability to delete messages" - Lab.OpenCloseRoom -> "When enabled, hosts will gain the ability to temporarily close (and reopen) their rooms" - Lab.Tipping -> "When enabled, you'll gain the ability to double tap messages to tip the author" - Lab.LinkImages -> "When enabled, links shared in chat will show a preview image for the link" - Lab.ConnectX -> "When enabled, you'll gain the ability to connect your X account" - Lab.TypingInChat -> "When enabled, you'll gain the ability to see when other uses are typing and let them know when you are" - Lab.ShowConnectedSocials -> "When enabled, users connected social accounts will show in chat as their name and avatar. You additionally will be able to click avatars to view user profiles" - Lab.EmojiReactions -> "When enabled, you'll gain the ability to send emoji reactions to messages in chat" - Lab.RoomDescriptions -> "When enabled, hosts will gain the ability to set a description for their room" - } \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/ChatDirectiveBottomModal.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/ChatDirectiveBottomModal.kt deleted file mode 100644 index febfa2b1f..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/ChatDirectiveBottomModal.kt +++ /dev/null @@ -1,46 +0,0 @@ -package xyz.flipchat.app.features.chat - -import cafe.adriel.voyager.core.registry.ScreenRegistry -import com.getcode.manager.BottomBarManager -import com.getcode.model.Currency -import com.getcode.model.Kin -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.core.CodeNavigator -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.Kin -import com.getcode.utils.formatAmountString -import xyz.flipchat.app.R -import xyz.flipchat.app.features.chat.list.ChatListViewModel - - -fun openChatDirectiveBottomModal( - resources: ResourceHelper, - createCost: Kin, - viewModel: ChatListViewModel, - navigator: CodeNavigator, -) { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - positiveText = resources.getString(R.string.action_enterRoomNumber), - negativeText = resources.getString( - R.string.action_createNewRoomWithCost, - formatAmountString( - resources = resources, - currency = Currency.Kin, - amount = createCost.quarks.toDouble(), - suffix = resources.getKinSuffix() - ) - ), - negativeStyle = BottomBarManager.BottomBarButtonStyle.Filled, - tertiaryText = resources.getString(R.string.action_cancel), - onPositive = { - navigator.push(ScreenRegistry.get(NavScreenProvider.Room.Lookup.Entry)) - }, - onNegative = { - viewModel.dispatchEvent(ChatListViewModel.Event.CreateRoomSelected) - }, - type = BottomBarManager.BottomBarMessageType.THEMED, - showScrim = true, - ) - ) -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ChattableState.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ChattableState.kt deleted file mode 100644 index 5a2b3c58b..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ChattableState.kt +++ /dev/null @@ -1,20 +0,0 @@ -package xyz.flipchat.app.features.chat.conversation - -import com.getcode.model.Kin -import xyz.flipchat.app.features.chat.conversation.ChattableState.Enabled -import xyz.flipchat.app.features.chat.conversation.ChattableState.Spectator -import xyz.flipchat.app.features.chat.conversation.ChattableState.TemporarilyEnabled - - -sealed interface ChattableState { - interface Active - data object DisabledByMute: ChattableState, Active - data class Spectator(val messageFee: Kin): ChattableState - data object TemporarilyEnabled: ChattableState - data object Enabled: ChattableState - data object DisabledByClosedRoom: ChattableState - - fun isActiveMember() = this is Active -} - -fun ChattableState?.canTriggerInput() = this == null || this is Spectator || this is Enabled || this is TemporarilyEnabled \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationChatInput.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationChatInput.kt deleted file mode 100644 index f55f44cc1..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationChatInput.kt +++ /dev/null @@ -1,273 +0,0 @@ -package xyz.flipchat.app.features.chat.conversation - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.getcode.model.Currency -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.chat.ChatInput -import com.getcode.ui.core.rememberAnimationScale -import com.getcode.ui.core.scaled -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.core.addIf -import com.getcode.ui.utils.keyboardAsState -import com.getcode.ui.core.withTopBorder -import com.getcode.util.resources.LocalResources -import com.getcode.utils.Kin -import com.getcode.utils.formatAmountString -import kotlinx.coroutines.delay -import xyz.flipchat.app.R - -@Composable -fun ConversationChatInput( - state: ConversationViewModel.State, - focusRequester: FocusRequester, - dispatchEvent: (ConversationViewModel.Event) -> Unit, -) { - var previousState by remember { mutableStateOf(null) } - val ime = LocalSoftwareKeyboardController.current - val keyboardVisible by keyboardAsState() - - AnimatedContent( - targetState = state.chattableState, - transitionSpec = { - if (previousState == null) { - // Skip animation when coming from null - EnterTransition.None.togetherWith(ExitTransition.None) - } else { - (slideInVertically { it }).togetherWith(ExitTransition.None) - } - }, - label = "chat input area" - ) { chattableState -> - when (chattableState) { - null -> { - Spacer( - modifier = Modifier - .fillMaxWidth() - .height(100.dp) - ) - } - - ChattableState.DisabledByMute -> { - Text( - modifier = Modifier - .fillMaxWidth() - .background(CodeTheme.colors.secondary) - .padding( - top = CodeTheme.dimens.grid.x1, - bottom = CodeTheme.dimens.grid.x3 - ) - .navigationBarsPadding(), - textAlign = TextAlign.Center, - text = stringResource(R.string.title_youHaveBeenMuted), - style = CodeTheme.typography.textSmall, - color = CodeTheme.colors.textSecondary - ) - } - - is ChattableState.TemporarilyEnabled, - is ChattableState.Enabled -> { - Column { - var didHitSend by remember(state.textFieldState.text) { mutableStateOf(false) } - - ChatInput( - modifier = Modifier.navigationBarsPadding(), - state = state.textFieldState, - sendCashEnabled = false, - focusRequester = focusRequester, - onSendMessage = { - didHitSend = true - if (chattableState is ChattableState.TemporarilyEnabled) { - ime?.hide() - } - dispatchEvent(ConversationViewModel.Event.OnSendMessage) - }, - onSendCash = { dispatchEvent(ConversationViewModel.Event.OnSendCash) } - ) - - LaunchedEffect(chattableState) { - if (chattableState is ChattableState.TemporarilyEnabled) { - focusRequester.requestFocus() - } - } - - var wasKeyboardVisible by remember { mutableStateOf(false) } - - val animationScale by rememberAnimationScale() - LaunchedEffect(keyboardVisible) { - if (wasKeyboardVisible && !keyboardVisible) { - delay(150.scaled(animationScale)) // Short delay to prevent accidental blips - if ( - !keyboardVisible && - chattableState is ChattableState.TemporarilyEnabled && - !didHitSend - ) { - dispatchEvent(ConversationViewModel.Event.ResetToSpectator) - dispatchEvent(ConversationViewModel.Event.CancelReply) - } - } - wasKeyboardVisible = keyboardVisible - } - - LaunchedEffect(state.replyMessage) { - if (state.replyMessage != null) { - focusRequester.requestFocus() - } - } - - } - } - - is ChattableState.Spectator -> { - Column( - modifier = Modifier - .addIf( - !state.isRoomOpen && state.isOpenCloseEnabled - ) { - Modifier.background(CodeTheme.colors.secondary) - } - .navigationBarsPadding(), - ) { - if (!state.isRoomOpen && state.isOpenCloseEnabled) { - Text( - modifier = Modifier - .fillMaxWidth() - .padding( - top = CodeTheme.dimens.grid.x1, - bottom = CodeTheme.dimens.grid.x2, - start = CodeTheme.dimens.inset, - end = CodeTheme.dimens.inset, - ), - textAlign = TextAlign.Center, - text = stringResource(R.string.title_roomIsClosed), - style = CodeTheme.typography.textSmall, - color = CodeTheme.colors.textSecondary - ) - } - - if (state.isRoomOpen) { - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding( - start = CodeTheme.dimens.inset, - end = CodeTheme.dimens.inset - ), - buttonState = ButtonState.Filled, - text = stringResource( - R.string.action_sendMessageInRoom, - formatAmountString( - resources = LocalResources.current!!, - currency = Currency.Kin, - amount = chattableState.messageFee.quarks.toDouble(), - suffix = stringResource(R.string.core_kin) - ) - ), - ) { - dispatchEvent(ConversationViewModel.Event.OnSendMessageForFee) - } - } - } - } - - ChattableState.DisabledByClosedRoom -> { - if (state.isHost && !state.isRoomOpen) { - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding( - start = CodeTheme.dimens.inset, - end = CodeTheme.dimens.inset - ).navigationBarsPadding(), - buttonState = ButtonState.Filled, - text = stringResource(R.string.action_reopenRoom), - ) { - dispatchEvent(ConversationViewModel.Event.OnOpenStateChangedRequested) - } - } else { - Text( - modifier = Modifier - .fillMaxWidth() - .background(CodeTheme.colors.secondary) - .padding( - top = CodeTheme.dimens.grid.x1, - bottom = CodeTheme.dimens.grid.x3, - start = CodeTheme.dimens.inset, - end = CodeTheme.dimens.inset, - ) - .navigationBarsPadding(), - textAlign = TextAlign.Center, - text = stringResource(R.string.title_roomIsClosed), - style = CodeTheme.typography.textSmall, - color = CodeTheme.colors.textSecondary - ) - } - } - } - } - - // Update the previous state - LaunchedEffect(state.chattableState) { - previousState = state.chattableState - } -} - -@Composable -private fun RoomOpenControlBar( - modifier: Modifier = Modifier, - onChangeRequest: () -> Unit, -) { - Row( - modifier = modifier - .withTopBorder(color = CodeTheme.colors.dividerVariant) - .padding(CodeTheme.dimens.grid.x2), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = stringResource(R.string.subtitle_roomIsClosed), - style = CodeTheme.typography.textSmall, - color = CodeTheme.colors.textMain - ) - - CodeButton( - text = stringResource(R.string.action_reopen), - shape = CircleShape, - buttonState = ButtonState.Filled, - overrideContentPadding = true, - contentPadding = PaddingValues(horizontal = CodeTheme.dimens.grid.x2), - style = CodeTheme.typography.textSmall - ) { - onChangeRequest() - } - } -} diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationMessages.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationMessages.kt deleted file mode 100644 index 0152e151e..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationMessages.kt +++ /dev/null @@ -1,368 +0,0 @@ -package xyz.flipchat.app.features.chat.conversation - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.spring -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Surface -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.KeyboardArrowDown -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.platform.LocalUriHandler -import androidx.paging.compose.LazyPagingItems -import cafe.adriel.voyager.core.registry.ScreenRegistry -import com.getcode.manager.TopBarManager -import com.getcode.model.chat.AnnouncementAction -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.ContextSheet -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.chat.MessageList -import com.getcode.ui.components.chat.MessageListEvent -import com.getcode.ui.components.chat.MessageListPointerResult -import com.getcode.ui.components.chat.TypingIndicator -import com.getcode.ui.components.chat.messagecontents.LocalAnnouncementActionResolver -import com.getcode.ui.components.chat.messagecontents.MessageContextAction -import com.getcode.ui.components.chat.messagecontents.ResolvedAction -import com.getcode.ui.components.chat.utils.ChatItem -import com.getcode.ui.components.chat.utils.HandleMessageChanges -import com.getcode.ui.components.emojis.EmojiModal -import com.getcode.ui.components.text.markup.Markup -import com.getcode.ui.core.rememberAnimationScale -import com.getcode.ui.core.scaled -import com.getcode.ui.emojis.FrequentlyUsedEmojis -import com.getcode.ui.utils.animateScrollToItemWithFullVisibility -import com.getcode.ui.utils.keyboardAsState -import com.getcode.ui.utils.scrollToItemWithFullVisibility -import com.getcode.ui.core.verticalScrollStateGradient -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import xyz.flipchat.app.R -import xyz.flipchat.app.util.dialNumber -import kotlin.math.abs - -@OptIn(ExperimentalMaterialApi::class) -@Composable -internal fun ConversationMessages( - modifier: Modifier = Modifier, - state: ConversationViewModel.State, - messages: LazyPagingItems, - focusRequester: FocusRequester, - dispatchEvent: (ConversationViewModel.Event) -> Unit, -) { - val navigator = LocalCodeNavigator.current - val lazyListState = rememberLazyListState() - val keyboardVisible by keyboardAsState() - val ime = LocalSoftwareKeyboardController.current - val composeScope = rememberCoroutineScope() - val uriHandler = LocalUriHandler.current - val context = LocalContext.current - val animationScale by rememberAnimationScale() - - fun resolveAnnouncementAction(action: AnnouncementAction): ResolvedAction? { - return when (action) { - AnnouncementAction.Unknown -> null - AnnouncementAction.Share -> ResolvedAction( - text = if (state.isHost) { - context.getString(R.string.action_shareRoomLinkAsHost) - } else { - context.getString(R.string.action_shareRoomLinkAsMember) - }, - onClick = { dispatchEvent(ConversationViewModel.Event.OnShareRoomLink) } - ) - } - } - - Box( - modifier = modifier, - ) { - CompositionLocalProvider( - LocalAnnouncementActionResolver provides { resolveAnnouncementAction(it) } - ) { - MessageList( - modifier = Modifier - .fillMaxSize() - .verticalScrollStateGradient( - scrollState = lazyListState, - color = CodeTheme.colors.background, - showAtStartAlways = true, - showAtEnd = false - ), - messages = messages, - listState = lazyListState, - handleMessagePointers = { (current, previous, next) -> - MessageListPointerResult( - current.sender.id == previous?.sender?.id, - current.sender.id == next?.sender?.id - ) - }, - footer = { - AnimatedContent( - targetState = state.otherUsersTyping.isNotEmpty(), - transitionSpec = { - slideInVertically( - animationSpec = spring( - dampingRatio = Spring.DampingRatioLowBouncy, - stiffness = Spring.StiffnessLow - ) - ) { it } + scaleIn() + fadeIn() togetherWith - fadeOut() + slideOutVertically { it } - } - ) { show -> - if (show) { - TypingIndicator( - modifier = Modifier - .padding(horizontal = CodeTheme.dimens.inset), - userImages = state.otherUsersTyping - ) - } - } - }, - dispatch = { event -> - when (event) { - is MessageListEvent.AdvancePointer -> { - dispatchEvent(ConversationViewModel.Event.MarkRead(event.messageId)) - } - - is MessageListEvent.OpenMessageActions -> { - composeScope.launch { - if (keyboardVisible) { - ime?.hide() - } - - val actions = event.actions.map { - if (it is MessageContextAction.Emojis) { - it.copy( - Content = { - FrequentlyUsedEmojis( - modifier = Modifier.fillMaxWidth(), - onSelect = { emoji -> - navigator.hide() - dispatchEvent( - ConversationViewModel.Event.SendReaction( - event.messageId, - emoji - ) - ) - }, - onViewAll = { - composeScope.launch { - navigator.hide() - delay(250.scaled(animationScale)) - navigator.show( - EmojiModal { emoji -> - navigator.hide() - dispatchEvent( - ConversationViewModel.Event.SendReaction( - event.messageId, - emoji - ) - ) - } - ) - } - } - ) - } - ) - } else { - it - } - } - navigator.show(ContextSheet(actions)) - } - } - - is MessageListEvent.OnMarkupEvent -> { - when (val markup = event.markup) { - is Markup.RoomNumber -> { - dispatchEvent(ConversationViewModel.Event.LookupRoom(markup.number)) - } - - is Markup.Url -> { - runCatching { - uriHandler.openUri(markup.link) - }.onFailure { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = context.getString(R.string.error_title_failedToOpenLink), - message = context.getString(R.string.error_description_failedToOpenLink) - ) - ) - } - } - - is Markup.Phone -> { - context.dialNumber(markup.phoneNumber) - } - } - } - - is MessageListEvent.ReplyToMessage -> { - dispatchEvent(ConversationViewModel.Event.ReplyTo(event.message)) - } - - is MessageListEvent.ViewOriginalMessage -> { - composeScope.launch { - val itemIndex = messages.itemSnapshotList - .filterIsInstance() - .indexOfFirst { it.chatMessageId == event.originalMessageId } - - val currentItemIndex = messages.itemSnapshotList - .filterIsInstance() - .indexOfFirst { it.chatMessageId == event.messageId } - - if (itemIndex >= 0) { - val distance = abs(itemIndex - currentItemIndex) - - println("distance from current ($currentItemIndex) is $distance") - if (distance <= 100) { - // Animate smoothly if within 100 items - lazyListState.animateScrollToItemWithFullVisibility( - to = itemIndex, - ) - } else { - // Jump directly if too far - lazyListState.scrollToItemWithFullVisibility( - to = itemIndex, - ) - } - } - } - } - - is MessageListEvent.TipMessage -> { - composeScope.launch { - if (keyboardVisible) { - ime?.hide() - delay(500.scaled(animationScale)) - } - dispatchEvent( - ConversationViewModel.Event.TipUser( - event.message.chatMessageId, - event.message.sender.id.orEmpty() - ) - ) - } - } - - is MessageListEvent.ShowMessageReactions -> { - composeScope.launch { - if (keyboardVisible) { - ime?.hide() - delay(500.scaled(animationScale)) - } - navigator.show(MessageReactionsSheet( - tips = event.tips, - reactions = event.reactions, - startingWith = event.startingWith - )) - } - } - - is MessageListEvent.UnreadStateHandled -> { - dispatchEvent(ConversationViewModel.Event.OnUnreadStateHandled) - } - - is MessageListEvent.ViewUserProfile -> { - composeScope.launch { - if (keyboardVisible) { - ime?.hide() - delay(500.scaled(animationScale)) - } - navigator.push( - ScreenRegistry.get( - NavScreenProvider.UserProfile( - event.userId - ) - ) - ) - } - } - - is MessageListEvent.AddReaction -> { - dispatchEvent( - ConversationViewModel.Event.SendReaction( - event.messageId, - event.emoji - ) - ) - } - is MessageListEvent.RemoveReaction -> { - dispatchEvent( - ConversationViewModel.Event.RemoveReaction( - event.originalMessageId, - ) - ) - } - } - } - ) - } - - val animatedAlpha by animateFloatAsState( - targetValue = if (lazyListState.canScrollBackward) 1f else 0f, - animationSpec = tween(durationMillis = 300), - label = "alpha of jump-to-bottom" - ) - - Surface( - modifier = Modifier - .graphicsLayer { - alpha = animatedAlpha - } - .padding( - end = CodeTheme.dimens.inset, - bottom = CodeTheme.dimens.inset - ) - .align(Alignment.BottomEnd), - shape = CircleShape, - color = CodeTheme.colors.tertiary, - onClick = { - composeScope.launch { - if (lazyListState.firstVisibleItemIndex > 100) { - lazyListState.scrollToItem(0) - } else { - lazyListState.animateScrollToItem(0) - } - } - } - ) { - Image( - modifier = Modifier.padding(CodeTheme.dimens.grid.x2), - imageVector = Icons.Outlined.KeyboardArrowDown, - contentDescription = null, - colorFilter = ColorFilter.tint(CodeTheme.colors.onSurface) - ) - } - } - - HandleMessageChanges(listState = lazyListState, items = messages) { message -> - dispatchEvent(ConversationViewModel.Event.MarkDelivered(message.chatMessageId)) - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationScreen.kt deleted file mode 100644 index de3b4049c..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationScreen.kt +++ /dev/null @@ -1,321 +0,0 @@ -package xyz.flipchat.app.features.chat.conversation - -import android.os.Parcelable -import android.widget.Toast -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Clear -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.lifecycle.Lifecycle -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems -import cafe.adriel.voyager.core.registry.ScreenRegistry -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.hilt.getViewModel -import com.getcode.model.ID -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.AppScreen -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.AppBarDefaults -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.components.OnLifecycleEvent -import com.getcode.ui.components.chat.messagecontents.MessageReplyPreview -import com.getcode.ui.components.chat.utils.ChatItem -import com.getcode.ui.components.chat.utils.ReplyMessageAnchor -import com.getcode.ui.core.rememberAnimationScale -import com.getcode.ui.core.scaled -import com.getcode.ui.theme.CodeScaffold -import com.getcode.ui.core.addIf -import com.getcode.ui.utils.keyboardAsState -import com.getcode.ui.core.noRippleClickable -import com.getcode.ui.core.unboundedClickable -import com.getcode.ui.core.withTopBorder -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import xyz.flipchat.app.features.home.TabbedHomeScreen - -@Parcelize -data class ConversationScreen( - val chatId: ID? = null, - val roomNumber: Long? = null -) : AppScreen(), Parcelable { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - val vm = getViewModel() - - val keyboardVisible by keyboardAsState() - val keyboard = LocalSoftwareKeyboardController.current - val composeScope = rememberCoroutineScope() - val animationScale by rememberAnimationScale() - - LaunchedEffect(chatId) { - if (chatId != null) { - vm.dispatchEvent( - ConversationViewModel.Event.OnChatIdChanged(chatId) - ) - } - } - - LaunchedEffect(roomNumber) { - if (roomNumber != null) { - vm.dispatchEvent( - ConversationViewModel.Event.OnRoomNumberChanged(roomNumber) - ) - } - } - - LaunchedEffect(vm) { - vm.eventFlow - .filterIsInstance() - .onEach { - navigator.show(ScreenRegistry.get(NavScreenProvider.CreateAccount.Start)) - }.launchIn(this) - } - - val context = LocalContext.current - LaunchedEffect(vm) { - vm.eventFlow - .filterIsInstance() - .onEach { - if (it.show) { - Toast.makeText(context, it.message, Toast.LENGTH_SHORT).show() - } - if (it.fatal) { - navigator.popAll() - } - }.launchIn(this) - } - - LaunchedEffect(vm) { - vm.eventFlow - .filterIsInstance() - .map { it.roomId } - .onEach { - navigator.push(ScreenRegistry.get(NavScreenProvider.Room.Messages(it))) - }.launchIn(this) - } - - LaunchedEffect(vm) { - vm.eventFlow - .filterIsInstance() - .map { it.roomInfoArgs } - .onEach { - navigator.push( - ScreenRegistry.get( - NavScreenProvider.Room.Preview(args = it, returnToSender = true) - ) - ) - }.launchIn(this) - } - - LaunchedEffect(vm) { - vm.eventFlow - .filterIsInstance() - .onEach { - context.startActivity(it.intent) - }.launchIn(this) - } - - LaunchedEffect(result) { - result - .filter { it == true } - .onEach { vm.dispatchEvent(ConversationViewModel.Event.OnAccountCreated) } - .launchIn(this) - } - - val state by vm.stateFlow.collectAsState() - - val goBack = { - composeScope.launch { - vm.dispatchEvent(ConversationViewModel.Event.Stopped) - if (keyboardVisible) { - keyboard?.hide() - delay(500.scaled(animationScale)) - } - navigator.popUntil { it is TabbedHomeScreen } - } - } - - BackHandler { goBack() } - - OnLifecycleEvent { _, event -> - when (event) { - Lifecycle.Event.ON_RESUME -> { - vm.dispatchEvent(ConversationViewModel.Event.Resumed) - } - - Lifecycle.Event.ON_STOP, - Lifecycle.Event.ON_DESTROY -> { - vm.dispatchEvent(ConversationViewModel.Event.Stopped) - } - - else -> Unit - } - } - - val openRoomDetails = { - composeScope.launch { - if (keyboardVisible) { - keyboard?.hide() - delay(500.scaled(animationScale)) - } - navigator.push( - ScreenRegistry.get( - NavScreenProvider.Room.Info( - state.roomInfoArgs - ) - ) - ) - } - } - - Column { - AppBarWithTitle( - title = { - ConversationTitle( - modifier = Modifier - .noRippleClickable { openRoomDetails() }, - state = state - ) - }, - leftIcon = { AppBarDefaults.UpNavigation { goBack() } }, - rightContents = { - AppBarDefaults.Overflow { openRoomDetails() } - } - ) - - val messages = vm.messages.collectAsLazyPagingItems() - - ConversationScreenContent( - state = state, - messages = messages, - dispatchEvent = vm::dispatchEvent - ) - } - } -} - -@Composable -private fun ConversationScreenContent( - state: ConversationViewModel.State, - messages: LazyPagingItems, - dispatchEvent: (ConversationViewModel.Event) -> Unit, -) { - val navigator = LocalCodeNavigator.current - val focusRequester = remember { FocusRequester() } - - CodeScaffold( - bottomBar = { - Column( - modifier = Modifier - .addIf(navigator.lastItem is ConversationScreen) { - Modifier.imePadding() - }, - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x3), - ) { - Column( - modifier = Modifier - .addIf(state.chattableState?.isActiveMember() == true) { - Modifier.withTopBorder(color = CodeTheme.colors.dividerVariant) - } - ) { - AnimatedContent( - targetState = state.replyMessage, - transitionSpec = { - (slideInVertically { it }).togetherWith(slideOutVertically { it }) - }, - label = "replying to message visibility", - ) { replyingTo -> - if (replyingTo != null) { - Row( - modifier = Modifier - .fillMaxWidth() - .background(CodeTheme.colors.background) - .height(IntrinsicSize.Min) - ) { - MessageReplyPreview( - modifier = Modifier.weight(1f), - originalMessage = ReplyMessageAnchor( - id = replyingTo.id, - sender = replyingTo.sender, - message = replyingTo.message, - isDeleted = false, - deletedBy = null, - ) - ) - Image( - modifier = Modifier - .align(Alignment.CenterVertically) - .padding(vertical = CodeTheme.dimens.grid.x1) - .padding(end = CodeTheme.dimens.grid.x1) - .unboundedClickable { - dispatchEvent(ConversationViewModel.Event.CancelReply) - }, - imageVector = Icons.Outlined.Clear, - colorFilter = ColorFilter.tint(Color.White), - contentDescription = null - ) - } - } - } - - ConversationChatInput( - state = state, - focusRequester = focusRequester, - dispatchEvent = dispatchEvent - ) - } - } - } - ) { padding -> - ConversationMessages( - modifier = Modifier - .fillMaxSize() - .padding(padding), - state = state, - messages = messages, - focusRequester = focusRequester, - dispatchEvent = dispatchEvent - ) - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationTitle.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationTitle.kt deleted file mode 100644 index 449e05925..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationTitle.kt +++ /dev/null @@ -1,96 +0,0 @@ -package xyz.flipchat.app.features.chat.conversation - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.chat.UserAvatar -import xyz.flipchat.app.R - -@Composable -internal fun ConversationTitle( - modifier: Modifier = Modifier, - state: ConversationViewModel.State, -) { - val listenerCount = remember(state.members) { - state.listeners - } - - ConversationTitle( - modifier = modifier, - imageUri = state.imageUri ?: state.conversationId, - title = state.title, - listenerCount = listenerCount, - ) -} - -@Composable -internal fun ConversationTitle( - modifier: Modifier = Modifier, - imageUri: Any?, - title: String, - listenerCount: Int?, -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2) - ) { - UserAvatar( - modifier = Modifier - .padding(start = CodeTheme.dimens.grid.x2) - .size(CodeTheme.dimens.staticGrid.x6) - .clip(CircleShape), - data = imageUri, - overlay = { - Image( - modifier = Modifier.padding(5.dp), - painter = painterResource(R.drawable.ic_fc_chats), - colorFilter = ColorFilter.tint(Color.White), - contentDescription = null, - ) - } - ) - Column { - Text( - modifier = Modifier.fillMaxWidth(), - text = title, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = CodeTheme.typography.screenTitle.copy(fontSize = 18.sp) - ) - - Text( - text = if (listenerCount != null) { - pluralStringResource( - R.plurals.title_roomInfoListenerCount, - listenerCount, - listenerCount - ) - } else { - "" - }, - style = CodeTheme.typography.caption, - color = CodeTheme.colors.textSecondary, - ) - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationViewModel.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationViewModel.kt deleted file mode 100644 index 66ec193d4..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/ConversationViewModel.kt +++ /dev/null @@ -1,1853 +0,0 @@ -package xyz.flipchat.app.features.chat.conversation - -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Intent -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.foundation.text.input.clearText -import androidx.compose.runtime.snapshotFlow -import androidx.lifecycle.viewModelScope -import androidx.paging.PagingData -import androidx.paging.cachedIn -import androidx.paging.insertSeparators -import androidx.paging.map -import com.getcode.manager.BottomBarManager -import com.getcode.manager.TopBarManager -import com.getcode.model.ID -import com.getcode.model.Kin -import com.getcode.model.KinAmount -import com.getcode.model.chat.Deleter -import com.getcode.model.chat.MessageContent -import com.getcode.model.chat.MessageStatus -import com.getcode.model.chat.Sender -import com.getcode.model.uuid -import xyz.flipchat.app.navigation.RoomInfoArgs -import com.getcode.services.model.ExtendedMetadata -import com.getcode.ui.components.chat.messagecontents.MessageContextAction -import com.getcode.ui.components.chat.messagecontents.MessageControls -import com.getcode.ui.components.chat.utils.ChatItem -import com.getcode.ui.components.chat.utils.MessageReaction -import com.getcode.ui.components.chat.utils.MessageTip -import com.getcode.ui.components.chat.utils.ReplyMessageAnchor -import com.getcode.ui.components.chat.utils.localizedText -import com.getcode.util.resources.ResourceHelper -import com.getcode.util.toInstantFromMillis -import com.getcode.utils.CurrencyUtils -import com.getcode.utils.ErrorUtils -import com.getcode.utils.SuppressibleException -import com.getcode.utils.TraceType -import com.getcode.utils.base58 -import com.getcode.utils.network.retryable -import com.getcode.utils.timestamp -import com.getcode.utils.trace -import com.flipcash.libs.coroutines.DispatcherProvider -import com.getcode.view.BaseViewModel2 -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.datetime.Instant -import timber.log.Timber -import xyz.flipchat.app.R -import xyz.flipchat.app.beta.Lab -import xyz.flipchat.app.beta.Labs -import xyz.flipchat.app.features.login.register.onError -import xyz.flipchat.app.features.login.register.onResult -import xyz.flipchat.app.util.IntentUtils -import xyz.flipchat.chat.RoomController -import xyz.flipchat.controllers.ChatsController -import xyz.flipchat.controllers.ProfileController -import xyz.flipchat.services.PaymentController -import xyz.flipchat.services.PaymentEvent -import xyz.flipchat.services.data.metadata.JoinChatPaymentMetadata -import xyz.flipchat.services.data.metadata.SendMessageAsListenerPaymentMetadata -import xyz.flipchat.services.data.metadata.SendTipMessagePaymentMetadata -import xyz.flipchat.services.data.metadata.erased -import xyz.flipchat.services.data.metadata.typeUrl -import xyz.flipchat.services.domain.model.chat.ConversationMessage -import xyz.flipchat.services.domain.model.chat.ConversationWithMembersAndLastPointers -import xyz.flipchat.services.domain.model.people.FlipchatUserWithSocialProfiles -import xyz.flipchat.services.domain.model.profile.toLinked -import xyz.flipchat.services.extensions.titleOrFallback -import xyz.flipchat.services.internal.data.mapper.nullIfEmpty -import xyz.flipchat.services.internal.network.service.PromoteUserError -import xyz.flipchat.services.user.AuthState -import xyz.flipchat.services.user.TypingNotificationsConstraints -import xyz.flipchat.services.user.UserManager -import java.util.UUID -import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds - -sealed interface CreateAccountRequest { - data object JoinRoom : CreateAccountRequest - data object SendMessage : CreateAccountRequest -} - -data class TypingConstraints( - val enabled: Boolean = false, - val canSendAsListener: Boolean = false, - val interval: Long = 3_000, - val timeout: Long = 5_000 -) - -@HiltViewModel -class ConversationViewModel @Inject constructor( - private val userManager: UserManager, - private val roomController: RoomController, - private val chatsController: ChatsController, - private val paymentController: PaymentController, - private val profileController: ProfileController, - private val resources: ResourceHelper, - private val currencyUtils: CurrencyUtils, - clipboardManager: ClipboardManager, - betaFeatures: Labs, - dispatchers: DispatcherProvider, -) : BaseViewModel2( - initialState = State.Default, - updateStateForEvent = updateStateForEvent, - dispatchers = dispatchers, -) { - private var stopTypingJob: Job? = null - private var typingStillJob: Job? = null - - data class State( - val selfId: ID?, - val selfName: String?, - val hostId: ID?, - val conversationId: ID?, - val unreadCount: Int?, - val chattableState: ChattableState?, - val textFieldState: TextFieldState, - val replyEnabled: Boolean, - val replyMessage: MessageReplyAnchor?, - val startAtUnread: Boolean, - val unreadStateHandled: Boolean, - val title: String, - val imageUri: String?, - val lastSeen: Instant?, - val members: Int?, - val listeners: Int?, - val pointers: Map, - val pointerRefs: Map, - val otherUsersTyping: List, - val isSelfTyping: Boolean, - val typingConstraints: TypingConstraints, - val isRoomOpen: Boolean, - val isOpenCloseEnabled: Boolean, - val isTippingEnabled: Boolean, - val isLinkImagePreviewsEnabled: Boolean, - val canClickUserAvatars: Boolean, - val showConnectedSocials: Boolean, - val canReactToMessages: Boolean, - val roomInfoArgs: RoomInfoArgs, - val lastReadMessage: UUID?, - val createAccountRequest: CreateAccountRequest?, - ) { - val isHost: Boolean - get() = selfId != null && hostId != null && selfId == hostId - - companion object { - val Default = State( - selfId = null, - selfName = null, - hostId = null, - imageUri = null, - conversationId = null, - unreadCount = null, - unreadStateHandled = false, - chattableState = null, - lastReadMessage = null, - textFieldState = TextFieldState(), - replyEnabled = false, - startAtUnread = false, - replyMessage = null, - title = "", - lastSeen = null, - pointers = emptyMap(), - pointerRefs = emptyMap(), - members = null, - listeners = null, - otherUsersTyping = emptyList(), - isSelfTyping = false, - typingConstraints = TypingConstraints(), - isRoomOpen = true, - isOpenCloseEnabled = false, - isTippingEnabled = false, - isLinkImagePreviewsEnabled = false, - canClickUserAvatars = false, - showConnectedSocials = false, - canReactToMessages = false, - roomInfoArgs = RoomInfoArgs(), - createAccountRequest = null - ) - } - } - - sealed interface Event { - data class OnSelfChanged(val id: ID?, val displayName: String?) : Event - data class OnChatIdChanged(val chatId: ID?) : Event - data class OnRoomNumberChanged(val roomNumber: Long) : Event - data class OnConversationChanged(val conversationWithPointers: ConversationWithMembersAndLastPointers) : - Event - - data class OnInitialUnreadCountDetermined(val count: Int) : Event - data object OnUnreadStateHandled : Event - data class OnTitlesChanged(val title: String, val roomCardTitle: String) : Event - data class OnUserActivity(val activity: Instant) : Event - data object OnSendCash : Event - data object OnSendMessage : Event - data class SendMessage(val paymentId: ID? = null) : Event - data object SendMessageWithFee : Event - data object RevealIdentity : Event - - data class OnAbilityToChatChanged(val state: ChattableState) : Event - data object ResetToSpectator : Event - data class OnPointersUpdated(val pointers: Map) : Event - data class MarkRead(val messageId: ID) : Event - data class MarkDelivered(val messageId: ID) : Event - - data class OnReplyEnabled(val enabled: Boolean) : Event - data class OnOpenCloseEnabled(val enabled: Boolean) : Event - data class OnTippingEnabled(val enabled: Boolean) : Event - data class OnLinkImagePreviewsEnabled(val enabled: Boolean) : Event - data class OnUserProfilesEnabled(val enabled: Boolean) : Event - data class ShowConnectedSocials(val enabled: Boolean) : Event - data class EnableEmojiReactions(val enabled: Boolean) : Event - data class ReplyTo(val anchor: MessageReplyAnchor) : Event { - constructor(chatItem: ChatItem.Message) : this( - MessageReplyAnchor( - chatItem.chatMessageId, - chatItem.sender, - chatItem.message - ) - ) - } - - data object CancelReply : Event - - data class SendReaction(val messageId: ID, val emoji: String) : Event - data class RemoveReaction(val reactionId: ID) : Event - - data class OnStartAtUnread(val enabled: Boolean) : Event - - data object OnJoinRequestedFromSpectating : Event - data object OnSendMessageForFee : Event - data class NeedsAccountCreated(val createFor: CreateAccountRequest) : Event - data object OnAccountCreated : Event - data object OnJoinRoom : Event - data object ResetCreateAccountRequest : Event - - data object Resumed : Event - data object Stopped : Event - - data class OnTypingConstraintsChanged( - val typingFlags: TypingNotificationsConstraints - ) : Event - - data class OnOtherUsersTyping(val users: List) : Event - - data object OnOpenStateChangedRequested : Event - data class OnOpenRoom(val conversationId: ID) : Event - data class OnCloseRoom(val conversationId: ID) : Event - - data class CopyMessage(val text: String) : Event - data class DeleteMessage(val conversationId: ID, val messageId: ID) : Event - data class RemoveUser(val conversationId: ID, val userId: ID) : Event - data class ReportUser(val userId: ID, val messageId: ID) : Event - data class MuteUser(val conversationId: ID, val userId: ID) : Event - data class BlockUser(val userId: ID) : Event - data class UnblockUser(val userId: ID) : Event - data class TipUser(val messageId: ID, val userId: ID) : Event - data class PromoteUser(val conversationId: ID, val userId: ID) : Event - data class DemoteUser(val conversationId: ID, val userId: ID) : Event - - data object OnShareRoomLink : Event - data class ShareRoom(val intent: Intent) : Event - - data object OnUserTypingStarted : Event - data object OnUserTypingStill : Event - data object OnUserTypingStopped : Event - - data class LookupRoom(val number: Long) : Event - data class OpenRoomPreview(val roomInfoArgs: RoomInfoArgs) : Event - data class OpenRoom(val roomId: ID) : Event - - data class Error( - val fatal: Boolean, - val message: String = "", - val show: Boolean = true - ) : Event - } - - init { - userManager.state - .map { it.userId to it.displayName } - .distinctUntilChanged() - .onEach { (id, displayName) -> - dispatchEvent(Event.OnSelfChanged(id, displayName)) - }.launchIn(viewModelScope) - - combine( - betaFeatures.observe(Lab.TypingInChat), - userManager.state - .mapNotNull { it.flags } - .map { it.typingNotifications } - .distinctUntilChanged() - ) { flag, typing -> - typing.copy(canSendAtAll = typing.canSendAtAll && flag) - }.onEach { dispatchEvent(Event.OnTypingConstraintsChanged(it)) - }.launchIn(viewModelScope) - - betaFeatures.observe(Lab.ReplyToMessage) - .onEach { dispatchEvent(Event.OnReplyEnabled(it)) } - .launchIn(viewModelScope) - - betaFeatures.observe(Lab.OpenCloseRoom) - .onEach { dispatchEvent(Event.OnOpenCloseEnabled(it)) } - .launchIn(viewModelScope) - - betaFeatures.observe(Lab.StartChatAtUnread) - .onEach { dispatchEvent(Event.OnStartAtUnread(it)) } - .launchIn(viewModelScope) - - betaFeatures.observe(Lab.Tipping) - .onEach { dispatchEvent(Event.OnTippingEnabled(it)) } - .launchIn(viewModelScope) - - betaFeatures.observe(Lab.LinkImages) - .onEach { dispatchEvent(Event.OnLinkImagePreviewsEnabled(it)) } - .launchIn(viewModelScope) - - betaFeatures.observe(Lab.ShowConnectedSocials) - .onEach { - dispatchEvent(Event.ShowConnectedSocials(it)) - dispatchEvent(Event.OnUserProfilesEnabled(it)) - } - .launchIn(viewModelScope) - - betaFeatures.observe(Lab.EmojiReactions) - .onEach { dispatchEvent(Event.EnableEmojiReactions(it)) } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.chatId } - .filterNotNull() - .map { roomController.getUnreadCount(it) } - .onEach { dispatchEvent(Event.OnInitialUnreadCountDetermined(it)) } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.chatId } - .filterNotNull() - .mapNotNull { - retryable( - maxRetries = 5, - delayDuration = 3.seconds, - ) { roomController.getConversation(it) } - }.onEach { dispatchEvent(Event.OnConversationChanged(it)) } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.chatId } - .filterNotNull() - .onEach { - runCatching { - roomController.openMessageStream(viewModelScope, it) - }.onFailure { - it.printStackTrace() - ErrorUtils.handleError(it) - } - }.launchIn(viewModelScope) - - stateFlow - .mapNotNull { it.conversationId } - .distinctUntilChanged() - .flatMapLatest { - roomController.observeConversation(it) - }.filterNotNull() - .distinctUntilChanged() - .onEach { - val (conversation, members, _) = it - val selfMember = members.firstOrNull { userManager.isSelf(it.id) } - val chattableState = if (selfMember != null) { - val isMuted = selfMember.isMuted - val isSpectator = !selfMember.isFullMember - val isRoomClosed = !conversation.isOpen - - when { - isRoomClosed -> ChattableState.DisabledByClosedRoom - // remain temp enabled - stateFlow.value.chattableState is ChattableState.TemporarilyEnabled -> ChattableState.TemporarilyEnabled - isMuted -> ChattableState.DisabledByMute - isSpectator -> ChattableState.Spectator( - Kin.fromQuarks(it.conversation.messagingFee ?: 0) - ) - - else -> ChattableState.Enabled - } - } else { - ChattableState.Enabled - } - - if (stateFlow.value.chattableState != chattableState) { - dispatchEvent(Event.OnAbilityToChatChanged(chattableState)) - } - - dispatchEvent(Event.OnConversationChanged(it)) - } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.conversationWithPointers.conversation } - .distinctUntilChanged() - .map { - val title = it.titleOrFallback(resources) - val roomCardTitle = it.titleOrFallback(resources) - title to roomCardTitle - }.distinctUntilChanged() - .onEach { dispatchEvent(Event.OnTitlesChanged(it.first, it.second)) } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .onEach { delay(300) } - .map { it.messageId } - .filter { stateFlow.value.conversationId != null } - .map { it to stateFlow.value.conversationId!! } - .onEach { (messageId, conversationId) -> - roomController.advancePointer( - conversationId, - messageId, - MessageStatus.Read - ) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .onEach { delay(300) } - .map { it.messageId } - .filter { stateFlow.value.conversationId != null } - .map { it to stateFlow.value.conversationId!! } - .onEach { (messageId, conversationId) -> - roomController.advancePointer( - conversationId, - messageId, - MessageStatus.Delivered - ) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .onEach { - if (stateFlow.value.chattableState is ChattableState.Enabled) { - dispatchEvent(Event.SendMessage()) - } else { - if (userManager.authState is AuthState.LoggedIn) { - dispatchEvent(Event.SendMessageWithFee) - } else { - dispatchEvent(Event.NeedsAccountCreated(CreateAccountRequest.SendMessage)) - } - } - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { stateFlow.value.roomInfoArgs } - .filter { it.ownerId != null } - .map { profileController.getPaymentDestinationForUser(it.ownerId!!) } - .mapNotNull { - if (it.isSuccess) { - val paymentDestination = it.getOrNull() ?: return@mapNotNull null - val sendMessageMetadata = SendMessageAsListenerPaymentMetadata( - userId = userManager.userId!!, - chatId = stateFlow.value.conversationId!! - ) - - val metadata = ExtendedMetadata.Any( - data = sendMessageMetadata.erased(), - typeUrl = sendMessageMetadata.typeUrl - ) - - val amount = - KinAmount.fromQuarks(stateFlow.value.roomInfoArgs.messagingFeeQuarks) - - paymentController.presentPublicPaymentConfirmation( - amount = amount, - destination = paymentDestination, - metadata = metadata, - ) - } else { - return@mapNotNull null - } - }.flatMapLatest { - paymentController.eventFlow.take(1) - }.onEach { event -> - when (event) { - PaymentEvent.OnPaymentCancelled -> Unit - is PaymentEvent.OnPaymentError -> Unit - - is PaymentEvent.OnPaymentSuccess -> { - event.acknowledge(true) { - dispatchEvent(Event.SendMessage(event.intentId)) - } - } - } - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .mapNotNull { - val paymentId = it.paymentId - val state = stateFlow.value - val textFieldState = state.textFieldState - val text = textFieldState.text.toString().trim() - if (text.isEmpty()) return@mapNotNull null - - textFieldState.clearText() - - val replyingTo = state.replyMessage - - dispatchEvent(Event.CancelReply) - - if (replyingTo != null) { - roomController.sendReply( - state.conversationId!!, - replyingTo.id, - text, - paymentId - ) - } else { - roomController.sendMessage(state.conversationId!!, text, paymentId) - } - }.onResult( - onError = { - trace( - tag = "Conversation", - message = "message failed to send", - type = TraceType.Error, - error = it - ) - }, - onSuccess = { - // if we are temporarily allowed to speak, reset back to spectator - if (stateFlow.value.chattableState is ChattableState.TemporarilyEnabled) { - val fee = Kin.fromQuarks(stateFlow.value.roomInfoArgs.messagingFeeQuarks) - dispatchEvent(Event.OnAbilityToChatChanged(ChattableState.Spectator(fee))) - } - - trace( - tag = "Conversation", - message = "message sent successfully", - type = TraceType.Silent - ) - } - ) - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { (messageId, emoji) -> - val sentEmojis = roomController.getEmojiReactionsForMessage(messageId) - .filter { userManager.isSelf(it.sender?.id) } - .map { it.reaction.emoji } - - if (!sentEmojis.contains(emoji)) { - roomController.sendReaction( - conversationId = stateFlow.value.conversationId.orEmpty(), - messageId = messageId, - emoji = emoji - ) - } else { - trace( - tag = "Conversation", - message = "User already sent this emoji; not resending.", - type = TraceType.Silent - ) - Result.success(Unit) - } - }.onResult( - onError = { - trace( - tag = "Conversation", - message = "reaction failed to send", - type = TraceType.Error, - error = it - ) - }, - onSuccess = { - trace( - tag = "Conversation", - message = "reaction sent successfully", - type = TraceType.Silent - ) - } - ).launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { (reactionId) -> - roomController.removeReaction( - conversationId = stateFlow.value.conversationId.orEmpty(), - reactionId = reactionId, - ) - }.onResult( - onError = { - trace( - tag = "Conversation", - message = "reaction failed to be removed", - type = TraceType.Error, - error = it - ) - }, - onSuccess = { - trace( - tag = "Conversation", - message = "reaction removed successfully", - type = TraceType.Silent - ) - } - ).launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .mapNotNull { stateFlow.value.conversationId } - .distinctUntilChanged() - .onEach { - runCatching { - roomController.openMessageStream(viewModelScope, it) - }.onFailure { - ErrorUtils.handleError(it) - } - } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .mapNotNull { stateFlow.value.createAccountRequest } - .onEach { - when (it) { - CreateAccountRequest.JoinRoom -> { - delay(400) - dispatchEvent(Event.OnJoinRoom) - } - - CreateAccountRequest.SendMessage -> { - dispatchEvent(Event.OnSendMessageForFee) - } - } - } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .onEach { - roomController.closeMessageStream() - userManager.roomClosed() - roomController.onUserStoppedTypingIn(stateFlow.value.conversationId.orEmpty()) - }.launchIn(viewModelScope) - - stateFlow - .map { it.conversationId } - .filterNotNull() - .distinctUntilChanged() - .filter { stateFlow.value.typingConstraints.enabled } - .flatMapLatest { roomController.observeTyping(it) } - .distinctUntilChanged() - .onEach { users -> dispatchEvent(Event.OnOtherUsersTyping(users)) } - .launchIn(viewModelScope) - - stateFlow - .filter { - when (it.chattableState) { - ChattableState.Enabled -> it.typingConstraints.enabled - ChattableState.TemporarilyEnabled -> it.typingConstraints.enabled && it.typingConstraints.canSendAsListener - else -> false - } - } - .map { it.textFieldState } - .flatMapLatest { ts -> snapshotFlow { ts.text } } - .distinctUntilChanged() - .onEach { text -> - if (text.isEmpty()) { - // Stop everything when text is cleared - stopTypingJob?.cancel() - typingStillJob?.cancel() - dispatchEvent(Event.OnUserTypingStopped) - } else { - if (!stateFlow.value.isSelfTyping) { - dispatchEvent(Event.OnUserTypingStarted) - } - - if (typingStillJob?.isActive != true) { - typingStillJob = viewModelScope.launch { - while (isActive) { - delay(stateFlow.value.typingConstraints.interval) - dispatchEvent(Event.OnUserTypingStill) - } - } - } - - stopTypingJob?.cancel() - stopTypingJob = viewModelScope.launch { - delay(stateFlow.value.typingConstraints.timeout) - dispatchEvent(Event.OnUserTypingStopped) - typingStillJob?.cancel() - } - } - }.launchIn(viewModelScope) - - - eventFlow - .filterIsInstance() - .mapNotNull { stateFlow.value.conversationId } - .onEach { roomController.onUserStartedTypingIn(it) } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .mapNotNull { stateFlow.value.conversationId } - .onEach { roomController.onUserStillTypingIn(it) } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .mapNotNull { stateFlow.value.conversationId } - .onEach { roomController.onUserStoppedTypingIn(it) } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .mapNotNull { stateFlow.value.conversationId } - .map { it to stateFlow.value.isRoomOpen } - .onEach { (conversationId, isOpen) -> - confirmOpenStateChange(conversationId, isOpen) - } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.conversationId } - .map { roomController.enableChat(it) } - .onResult( - onError = { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToReopenRoom), - resources.getString(R.string.error_description_failedToReopenRoom) - ) - ) - }, - ).launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.conversationId } - .map { roomController.disableChat(it) } - .onResult( - onError = { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToCloseRoom), - resources.getString(R.string.error_description_failedToCloseRoom) - ) - ) - }, - ).launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { (conversationId, messageId) -> - roomController.deleteMessage(conversationId, messageId) - }.onError { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = resources.getString(R.string.error_title_failedToDeleteMessage), - message = resources.getString(R.string.error_description_failedToDeleteMessage) - ) - ) - } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { (conversationId, userId) -> - roomController.removeUser(conversationId, userId) - }.onError { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = resources.getString(R.string.error_title_failedToRemoveUser), - message = resources.getString(R.string.error_description_failedToRemoveUser) - ) - ) - } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { (userId, messageId) -> - roomController.reportUserForMessage(userId, messageId) - }.onResult( - onError = { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = resources.getString(R.string.error_title_failedToReportUserForMessage), - message = resources.getString(R.string.error_description_failedToReportUserForMessage) - ) - ) - }, - onSuccess = { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = resources.getString(R.string.success_title_reportUser), - message = resources.getString(R.string.success_description_reportUser), - type = TopBarManager.TopBarMessageType.SUCCESS - ) - ) - } - ) - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { (chatId, userId) -> - roomController.muteUser(chatId, userId) - }.onResult( - onError = { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = resources.getString(R.string.error_title_failedToMuteUser), - message = resources.getString(R.string.error_description_failedToMuteUser) - ) - ) - } - ) - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.userId } - .map { roomController.blockUser(it) } - .onResult( - onError = { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = resources.getString(R.string.error_title_failedToBlockUser), - message = resources.getString(R.string.error_description_failedToBlockUser) - ) - ) - } - ) - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.userId } - .map { roomController.unblockUser(it) } - .onResult( - onError = { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = resources.getString(R.string.error_title_failedToUnblockUser), - message = resources.getString(R.string.error_description_failedToUnblockUser) - ) - ) - } - ) - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.text } - .onEach { - clipboardManager.setPrimaryClip(ClipData.newPlainText("", it)) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { data -> - val result = profileController.getPaymentDestinationForUser(data.userId) - if (result.isSuccess) { - result.getOrNull()?.let { Result.success(it to data.messageId) } - ?: Result.failure(Throwable()) - } else { - Result.failure(result.exceptionOrNull() ?: Throwable()) - } - } - .mapNotNull { - if (it.isSuccess) { - val (paymentDestination, messageId) = it.getOrNull() ?: return@mapNotNull null - val tipPaymentMetadata = SendTipMessagePaymentMetadata( - tipperId = userManager.userId!!, - chatId = stateFlow.value.conversationId!!, - messageId = messageId - ) - - val metadata = ExtendedMetadata.Any( - data = tipPaymentMetadata.erased(), - typeUrl = tipPaymentMetadata.typeUrl - ) - - paymentController.presentMessageTipConfirmation( - destination = paymentDestination, - metadata = metadata - ) - } else { - ErrorUtils.handleError( - it.exceptionOrNull() - ?: SuppressibleException("Failed retrieving destination address") - ) - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToSendTip), - resources.getString( - R.string.error_description_failedToSendTip, - ) - ) - ) - return@mapNotNull null - } - }.flatMapLatest { - paymentController.eventFlow.take(1) - } - .onEach { event -> - when (event) { - PaymentEvent.OnPaymentCancelled -> Unit - is PaymentEvent.OnPaymentError -> Unit - - is PaymentEvent.OnPaymentSuccess -> { - val metadata = (event.metadata as ExtendedMetadata.Any).data.let { - SendTipMessagePaymentMetadata.unerase(it) - } - - roomController.sendTip( - conversationId = metadata.chatId, - messageId = metadata.messageId, - amount = event.amount, - paymentIntentId = event.intentId - ).onFailure { - event.acknowledge(false) { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToSendTip), - resources.getString( - R.string.error_description_failedToSendTip, - ) - ) - ) - } - }.onSuccess { - event.acknowledge(true) { - - } - } - } - } - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.number } - .map { roomNumber -> - chatsController.lookupRoom(roomNumber) - .onFailure { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToGetRoom), - resources.getString( - R.string.error_description_failedToGetRoom, - roomNumber - ) - ) - ) - }.onSuccess { (room, members) -> - val moderator = members.firstOrNull { it.isModerator } - val isMember = members.any { it.isSelf } - if (isMember) { - dispatchEvent(Event.OpenRoom(room.id)) - } else { - val roomInfo = RoomInfoArgs( - roomId = room.id, - roomNumber = room.roomNumber, - roomTitle = room.titleOrFallback(resources), - roomDescription = room.description, - memberCount = members.count(), - ownerId = room.ownerId, - hostName = moderator?.identity?.displayName, - messagingFeeQuarks = room.messagingFee.quarks, - ) - dispatchEvent(Event.OpenRoomPreview(roomInfo)) - } - } - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { userManager.authState } - .onEach { - if (it is AuthState.LoggedIn) { - dispatchEvent(Event.OnJoinRoom) - } else { - dispatchEvent(Event.NeedsAccountCreated(CreateAccountRequest.JoinRoom)) - } - } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .filter { stateFlow.value.chattableState is ChattableState.Spectator } - .map { stateFlow.value.roomInfoArgs } - .filter { it.ownerId != null } - .map { profileController.getPaymentDestinationForUser(it.ownerId!!) } - .mapNotNull { - if (it.isSuccess) { - val paymentDestination = it.getOrNull() ?: return@mapNotNull null - val joinChatMetadata = JoinChatPaymentMetadata( - userId = userManager.userId!!, - chatId = stateFlow.value.conversationId!! - ) - - val metadata = ExtendedMetadata.Any( - data = joinChatMetadata.erased(), - typeUrl = joinChatMetadata.typeUrl - ) - - val amount = - KinAmount.fromQuarks(stateFlow.value.roomInfoArgs.messagingFeeQuarks) - - paymentController.presentPublicPaymentConfirmation( - amount = amount, - destination = paymentDestination, - metadata = metadata, - ) - } else { - return@mapNotNull null - } - }.flatMapLatest { - paymentController.eventFlow.take(1) - }.onEach { event -> - when (event) { - PaymentEvent.OnPaymentCancelled -> Unit - is PaymentEvent.OnPaymentError -> Unit - - is PaymentEvent.OnPaymentSuccess -> { - val roomId = stateFlow.value.conversationId.orEmpty() - chatsController.joinRoomAsFullMember(roomId, event.intentId) - .onFailure { - event.acknowledge(false) { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToJoinRoom), - resources.getString( - R.string.error_description_failedToJoinRoom, - stateFlow.value.roomInfoArgs.roomTitle.orEmpty() - ) - ) - ) - } - } - .onSuccess { - event.acknowledge(true) { - - } - } - } - } - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { IntentUtils.shareRoom(stateFlow.value.roomInfoArgs.roomNumber) } - .onEach { dispatchEvent(Event.ShareRoom(it)) } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { roomController.promoteUser(it.conversationId, it.userId) } - .onResult( - onError = { error -> - if (error is PromoteUserError.NotRegistered) { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToPromoteUserNotRegistered), - resources.getString(R.string.error_description_failedToPromoteUserNotRegistered) - ) - ) - } else { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToPromoteUser), - resources.getString(R.string.error_description_failedToPromoteUser) - ) - ) - } - } - ) - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { roomController.demoteUser(it.conversationId, it.userId) } - .onResult( - onError = { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToDemoteUser), - resources.getString(R.string.error_description_failedToDemoteUser) - ) - ) - } - ).launchIn(viewModelScope) - } - - @OptIn(ExperimentalCoroutinesApi::class) - val messages: Flow> = stateFlow - .map { it.conversationId to it.chattableState } - .filter { it.first != null } // assuming conversationId can be null - .distinctUntilChanged() - .flatMapLatest { (conversationId, _) -> roomController.messages(conversationId!!).flow } - .distinctUntilChanged() - .map { page -> - val currentState = stateFlow.value // Cache state upfront - val pointerRefs = currentState.pointerRefs // cache expensive pointer ref map upfront - val enableReply = - currentState.replyEnabled && currentState.chattableState.canTriggerInput() - - val showConnectedSocials = currentState.showConnectedSocials - val enableLinkImages = currentState.isLinkImagePreviewsEnabled - val canClickUserAvatars = currentState.canClickUserAvatars - - page.map { indice -> - val (_, message, member, contents, reply, tipInfo, reactionInfo) = indice - - val status = findClosestMessageStatus( - timestamp = message.id.uuid?.timestamp, - statusMap = pointerRefs, - fallback = if (contents.isFromSelf) MessageStatus.Sent else MessageStatus.Unknown - ) - - val anchor = if (reply != null) { - ReplyMessageAnchor( - id = reply.message.id, - message = reply.contentEntity, - isDeleted = reply.message.isDeleted, - deletedBy = reply.message.deletedBy?.let { id -> - Deleter( - id = id, - isSelf = userManager.isSelf(id), - isHost = currentState.hostId == message.deletedBy - ) - }, - sender = Sender( - id = reply.message.senderId, - profileImageUrl = reply.personalInfo?.imageUri.takeIf { - it.orEmpty().isNotEmpty() - }, - isFullMember = reply.member?.isFullMember == true, - name = reply.personalInfo?.memberName.orEmpty().ifEmpty { "Member" }, - isSelf = reply.contentEntity.isFromSelf, - isBlocked = reply.personalInfo?.isBlocked == true, - isHost = reply.message.senderId == currentState.hostId && !contents.isFromSelf, - socialProfiles = if (showConnectedSocials) { - reply.socialProfiles.mapNotNull { it.toLinked() } - } else { - emptyList() - } - ) - ) - } else { - null - } - - val tippingEnabled = - currentState.isTippingEnabled && !userManager.isSelf(message.senderId) - - val emojisEnabled = currentState.canReactToMessages - - val reactions = if (currentState.canReactToMessages && reactionInfo.isNotEmpty()) { - reactionInfo.map { (reaction, member, user, socials) -> - MessageReaction( - messageId = reaction.id, - emoji = reaction.emoji, - sentAt = reaction.sentAt, - sender = Sender( - id = member?.id, - profileImageUrl = user?.imageUri.nullIfEmpty(), - name = user?.memberName, - isHost = member?.isHost ?: false, - isSelf = userManager.isSelf(member?.id), - isBlocked = user?.isBlocked ?: false, - socialProfiles = if (showConnectedSocials) { - socials.mapNotNull { it.toLinked() } - } else { - emptyList() - } - ) - ) - } - } else { - emptyList() - } - - val tips = if (currentState.isTippingEnabled && tipInfo.isNotEmpty()) { - tipInfo.map { (tip, member, user, socials) -> - MessageTip( - amount = tip.kin, - tipper = Sender( - id = member?.id, - profileImageUrl = user?.imageUri.nullIfEmpty(), - name = user?.memberName, - isHost = member?.isHost ?: false, - isSelf = userManager.isSelf(member?.id), - isBlocked = user?.isBlocked ?: false, - socialProfiles = if (showConnectedSocials) { - socials.mapNotNull { it.toLinked() } - } else { - emptyList() - } - ) - ) - } - } else { - emptyList() - } - - val sender = Sender( - id = message.senderId, - profileImageUrl = member?.imageUri.takeIf { it.orEmpty().isNotEmpty() }, - name = member?.displayName.orEmpty().ifEmpty { "Member" }, - isSelf = contents.isFromSelf, - isFullMember = member?.isFullMember == true, - isHost = message.senderId == currentState.hostId, - isBlocked = member?.isBlocked == true, - socialProfiles = if (showConnectedSocials) { - member?.profiles?.mapNotNull { it.toLinked() }.orEmpty() - } else { - emptyList() - }, - ) - - ChatItem.Message( - chatMessageId = message.id, - message = contents, - date = message.dateMillis.toInstantFromMillis(), - status = status, - isDeleted = message.isDeleted, - deletedBy = if (message.isDeleted) { - Deleter( - id = message.deletedBy, - isSelf = userManager.isSelf(message.deletedBy), - isHost = currentState.hostId == message.deletedBy - ) - } else { - null - }, - wasSentAsFullMember = !message.sentOffStage, - enableMarkup = true, - enableReply = enableReply && !message.isDeleted, - showTimestamp = false, - enableTipping = tippingEnabled, - enableLinkImagePreview = enableLinkImages, - enableAvatarClicks = canClickUserAvatars, - sender = sender, - originalMessage = anchor, - messageControls = MessageControls( - actions = buildMessageActions( - message = message, - sender = sender, - contents = contents, - enableReply = enableReply, - enableTip = tippingEnabled, - enableReactions = emojisEnabled - ), - ), - tips = tips, - reactions = reactions - ) - } - } - .flowOn(Dispatchers.Default) - .mapLatest { data -> - var unreadSeparatorInserted = false - - data.insertSeparators { before: ChatItem.Message?, after: ChatItem.Message? -> - val separators = mutableListOf() - - if ( - stateFlow.value.startAtUnread && - !unreadSeparatorInserted && - after?.chatMessageId?.uuid == stateFlow.value.lastReadMessage && - before?.sender?.isSelf == false - ) { - unreadSeparatorInserted = true - val unreadCount = stateFlow.value.unreadCount ?: return@insertSeparators null - separators.add(ChatItem.UnreadSeparator(unreadCount)) - } - - val beforeDate = before?.relativeDate - val afterDate = after?.relativeDate - - // if the date changes between two items, add a date separator - if (beforeDate != afterDate) { - beforeDate?.let { separators.add(ChatItem.Date(before.date)) } - } - - if (separators.isNotEmpty()) { - ChatItem.Separators(separators) - } else { - null - } - } - }.cachedIn(viewModelScope) - - private fun buildMessageActions( - message: ConversationMessage, - sender: Sender, - contents: MessageContent, - enableReply: Boolean, - enableTip: Boolean, - enableReactions: Boolean, - ): List { - return mutableListOf().apply { - if (enableReactions) { - add( - MessageContextAction.Emojis( - onSelect = { emoji -> - - } - ) - ) - } - if (stateFlow.value.isHost) { - if (sender.displayName?.isNotEmpty() == true && !contents.isFromSelf) { - if (sender.isFullMember) { - add( - MessageContextAction.DemoteUser { - confirmUserDemote( - conversationId = message.conversationId, - user = sender.displayName, - userId = message.senderId - ) - } - ) - } else { - add( - MessageContextAction.PromoteUser { - confirmUserPromote( - conversationId = message.conversationId, - user = sender.displayName, - userId = message.senderId - ) - } - ) - } - } - } - - if (enableReply) { - add( - MessageContextAction.Reply { - val anchor = MessageReplyAnchor(message.id, sender, contents) - dispatchEvent(Event.ReplyTo(anchor)) - } - ) - } - - if (enableTip) { - add( - MessageContextAction.Tip { - dispatchEvent(Event.TipUser(message.id, message.senderId)) - } - ) - } - - add( - MessageContextAction.Copy { - dispatchEvent( - Event.CopyMessage( - contents.localizedText( - resources = resources, - currencyUtils = currencyUtils - ) - ) - ) - } - ) - } + buildSelfDefenseControls(message, sender, contents) - } - - private fun buildSelfDefenseControls( - message: ConversationMessage, - sender: Sender, - contents: MessageContent - ): List { - return mutableListOf().apply { - // delete message - if (stateFlow.value.isHost || contents.isFromSelf) { - add( - MessageContextAction.Delete { - confirmMessageDelete( - conversationId = message.conversationId, - messageId = message.id - ) - } - ) - } - - - if (stateFlow.value.isHost) { - if (sender.displayName?.isNotEmpty() == true && !contents.isFromSelf) { -// add( -// MessageControlAction.RemoveUser(member.memberName.orEmpty()) { -// confirmUserRemoval( -// conversationId = message.conversationId, -// user = member.memberName, -// userId = message.senderId, -// ) -// } -// ) - add( - MessageContextAction.MuteUser { - confirmUserMute( - conversationId = message.conversationId, - user = sender.displayName, - userId = message.senderId, - ) - } - ) - } - } - - if (!contents.isFromSelf) { - if (sender.isBlocked) { - add( - MessageContextAction.UnblockUser { - dispatchEvent(Event.UnblockUser(message.senderId)) - } - ) - } else { - add( - MessageContextAction.BlockUser { - confirmUserBlock( - user = sender.displayName, - userId = message.senderId, - ) - } - ) - } - - add( - MessageContextAction.ReportUserForMessage(sender.displayName.orEmpty()) { - confirmUserReport( - user = sender.displayName, - userId = message.senderId, - messageId = message.id - ) - } - ) - } - }.toList() - } - - private fun confirmMessageDelete(conversationId: ID, messageId: ID) { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = resources.getString(R.string.title_deleteMessage), - subtitle = resources.getString(R.string.subtitle_deleteMessage), - positiveText = resources.getString(R.string.action_delete), - tertiaryText = resources.getString(R.string.action_cancel), - onPositive = { dispatchEvent(Event.DeleteMessage(conversationId, messageId)) }, - type = BottomBarManager.BottomBarMessageType.DESTRUCTIVE, - showScrim = true, - ) - ) - } - - private fun confirmUserRemoval(conversationId: ID, user: String?, userId: ID) { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = resources.getString(R.string.title_removeUserFromRoom, user.orEmpty()), - subtitle = resources.getString(R.string.subtitle_removeUserFromRoom), - positiveText = resources.getString(R.string.action_remove), - negativeText = "", - tertiaryText = resources.getString(R.string.action_cancel), - onPositive = { dispatchEvent(Event.RemoveUser(conversationId, userId)) }, - onNegative = { }, - type = BottomBarManager.BottomBarMessageType.DESTRUCTIVE, - showScrim = true, - ) - ) - } - - private fun confirmUserMute(conversationId: ID, user: String?, userId: ID) { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = resources.getString( - R.string.title_muteUserInRoom, - user.orEmpty().ifEmpty { "User" }), - subtitle = resources.getString(R.string.subtitle_muteUserInRoom), - positiveText = resources.getString(R.string.action_mute), - negativeText = "", - tertiaryText = resources.getString(R.string.action_cancel), - onPositive = { dispatchEvent(Event.MuteUser(conversationId, userId)) }, - onNegative = { }, - type = BottomBarManager.BottomBarMessageType.DESTRUCTIVE, - showScrim = true, - ) - ) - } - - private fun confirmUserPromote(conversationId: ID, user: String?, userId: ID) { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = resources.getString( - R.string.title_promoteUserInRoom, - user.orEmpty().ifEmpty { "User" }), - subtitle = resources.getString(R.string.subtitle_promoteUserInRoom), - positiveText = resources.getString(R.string.action_promote), - negativeText = "", - tertiaryText = resources.getString(R.string.action_cancel), - onPositive = { dispatchEvent(Event.PromoteUser(conversationId, userId)) }, - onNegative = { }, - type = BottomBarManager.BottomBarMessageType.THEMED, - showScrim = true, - ) - ) - } - - private fun confirmUserDemote(conversationId: ID, user: String?, userId: ID) { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = resources.getString( - R.string.title_demoteUserInRoom, - user.orEmpty().ifEmpty { "User" }), - subtitle = resources.getString(R.string.subtitle_demoteUserInRoom), - positiveText = resources.getString(R.string.action_demote), - negativeText = "", - tertiaryText = resources.getString(R.string.action_cancel), - onPositive = { dispatchEvent(Event.DemoteUser(conversationId, userId)) }, - onNegative = { }, - type = BottomBarManager.BottomBarMessageType.DESTRUCTIVE, - showScrim = true, - ) - ) - } - - private fun confirmUserBlock(user: String?, userId: ID) { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = resources.getString( - R.string.title_blockUserInRoom, - user.orEmpty().ifEmpty { "User" }, - ), - subtitle = resources.getString(R.string.subtitle_blockUserInRoom, user.orEmpty()), - positiveText = resources.getString(R.string.action_blockUser, user.orEmpty()), - negativeText = "", - tertiaryText = resources.getString(R.string.action_cancel), - onPositive = { dispatchEvent(Event.BlockUser(userId)) }, - onNegative = { }, - type = BottomBarManager.BottomBarMessageType.DESTRUCTIVE, - showScrim = true, - ) - ) - } - - private fun confirmUserReport(user: String?, userId: ID, messageId: ID) { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = resources.getString(R.string.title_reportUserForMessage, user ?: "User"), - subtitle = resources.getString(R.string.subtitle_reportUserForMessage), - positiveText = resources.getString(R.string.action_report), - negativeText = "", - tertiaryText = resources.getString(R.string.action_cancel), - onPositive = { dispatchEvent(Event.ReportUser(userId, messageId)) }, - onNegative = { }, - type = BottomBarManager.BottomBarMessageType.DESTRUCTIVE, - showScrim = true, - ) - ) - } - - private fun confirmOpenStateChange(conversationId: ID, isRoomOpen: Boolean) { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = if (isRoomOpen) resources.getString(R.string.prompt_title_closeRoom) else resources.getString( - R.string.prompt_title_reopenRoom - ), - subtitle = if (isRoomOpen) resources.getString(R.string.prompt_description_closeRoom) else resources.getString( - R.string.prompt_description_reopenRoom - ), - positiveText = if (isRoomOpen) resources.getString(R.string.action_closeTemporarily) else resources.getString( - R.string.action_reopenRoom - ), - tertiaryText = resources.getString(R.string.action_cancel), - onPositive = { - if (isRoomOpen) { - dispatchEvent(Event.OnCloseRoom(conversationId)) - } else { - dispatchEvent(Event.OnOpenRoom(conversationId)) - } - }, - type = BottomBarManager.BottomBarMessageType.THEMED, - showScrim = true, - ) - ) - } - - override fun onCleared() { - super.onCleared() - roomController.closeMessageStream() - viewModelScope.launch { - stateFlow.value.conversationId?.let { - roomController.onUserStoppedTypingIn(it) - } - } - } - - internal companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - is Event.OnChatIdChanged -> Timber.d("onChatID changed ${event.chatId?.base58}") - is Event.OnSelfChanged -> Timber.d("onSelf changed ${event.id?.base58}") - is Event.OnConversationChanged -> { - val members = event.conversationWithPointers.members.count() - Timber.d( - "conversation changed={id:${event.conversationWithPointers.conversation.id.base58}, " + - "unreadCount:${event.conversationWithPointers.conversation.unreadCount}, " + - "members:$members, " + - "pointers:${event.conversationWithPointers.pointers.count()}" - ) - } - - is Event.DeleteMessage -> { - Timber.d("Delete Message => ${event.messageId.uuid.toString()}") - } - - is Event.OnOtherUsersTyping -> { - Timber.d("${event.users.count()} other user(s) typing") - } - - else -> Timber.d("event=${event}") - } - - when (event) { - is Event.OnChatIdChanged -> { state -> - state.copy( - conversationId = event.chatId, - textFieldState = TextFieldState() - ) - } - - is Event.OnInitialUnreadCountDetermined -> { state -> state.copy(unreadCount = event.count) } - - is Event.OnTitlesChanged -> { state -> - state.copy( - title = event.title, - roomInfoArgs = state.roomInfoArgs.copy( - roomTitle = event.roomCardTitle - ) - ) - } - - is Event.OnConversationChanged -> { state -> - val (conversation, _, _) = event.conversationWithPointers - val members = event.conversationWithPointers.members - val host = members.firstOrNull { it.isHost } - - state.copy( - conversationId = conversation.id, - imageUri = conversation.imageUri.orEmpty().takeIf { it.isNotEmpty() }, - pointers = event.conversationWithPointers.pointers, - lastReadMessage = state.lastReadMessage - ?: findLastReadMessage(event.conversationWithPointers.pointers), - pointerRefs = event.conversationWithPointers.pointers - .asSequence() - .mapNotNull { (key, value) -> - key.timestamp?.let { it to (value ?: MessageStatus.Unknown) } - } - .toMap(), - members = members.count(), - listeners = members.count { !it.isFullMember }, - isRoomOpen = conversation.isOpen, - hostId = host?.id, - roomInfoArgs = RoomInfoArgs( - roomId = conversation.id, - roomDescription = conversation.description, - roomNumber = conversation.roomNumber, - ownerId = conversation.ownerId, - hostName = host?.displayName, - memberCount = members.count(), - messagingFeeQuarks = conversation.coverCharge.quarks - ) - ) - } - - Event.OnSendMessageForFee -> { state -> - state.copy(chattableState = ChattableState.TemporarilyEnabled) - } - - is Event.OnPointersUpdated -> { state -> - state.copy(pointers = event.pointers) - } - - is Event.OnOtherUsersTyping -> { state -> - state.copy(otherUsersTyping = event.users.map { it.imageData }) - } - - is Event.OnUserTypingStarted -> { state -> - state.copy(isSelfTyping = true) - } - - is Event.OnUserTypingStill -> { state -> state } - - is Event.OnUserTypingStopped -> { state -> - state.copy(isSelfTyping = false) - } - - is Event.OnShareRoomLink, - is Event.ShareRoom, - is Event.OnOpenStateChangedRequested, - is Event.OnOpenRoom, - is Event.OnCloseRoom, - is Event.OnRoomNumberChanged, - is Event.OnJoinRoom, - is Event.OnAccountCreated, - is Event.NeedsAccountCreated, - is Event.OnJoinRequestedFromSpectating, - is Event.Error, - Event.RevealIdentity, - Event.OnSendCash, - is Event.MarkRead, - is Event.MarkDelivered, - is Event.DeleteMessage, - is Event.CopyMessage, - is Event.RemoveUser, - is Event.ReportUser, - is Event.MuteUser, - is Event.BlockUser, - is Event.UnblockUser, - is Event.TipUser, - is Event.PromoteUser, - is Event.DemoteUser, - is Event.Resumed, - is Event.Stopped, - is Event.LookupRoom, - is Event.OpenRoomPreview, - is Event.OpenRoom, - is Event.OnSendMessage, - is Event.SendMessage, - is Event.SendMessageWithFee, - is Event.SendReaction, - is Event.RemoveReaction, -> { state -> state } - - is Event.ResetCreateAccountRequest -> { state -> state.copy(createAccountRequest = null) } - - is Event.OnUserActivity -> { state -> - state.copy(lastSeen = event.activity) - } - - is Event.OnSelfChanged -> { state -> - state.copy( - selfId = event.id, - selfName = event.displayName - ) - } - - is Event.ResetToSpectator -> { state -> - val fee = Kin.fromQuarks(state.roomInfoArgs.messagingFeeQuarks) - state.copy(chattableState = ChattableState.Spectator(fee)) - } - - is Event.OnAbilityToChatChanged -> { state -> state.copy(chattableState = event.state) } - is Event.OnReplyEnabled -> { state -> state.copy(replyEnabled = event.enabled) } - is Event.OnStartAtUnread -> { state -> state.copy(startAtUnread = event.enabled) } - is Event.ReplyTo -> { state -> - val isFullMember = state.chattableState is ChattableState.Enabled - state.copy( - replyMessage = event.anchor, - chattableState = if (isFullMember) ChattableState.Enabled else ChattableState.TemporarilyEnabled - ) - } - - is Event.CancelReply -> { state -> state.copy(replyMessage = null) } - is Event.OnOpenCloseEnabled -> { state -> state.copy(isOpenCloseEnabled = event.enabled) } - is Event.OnTippingEnabled -> { state -> state.copy(isTippingEnabled = event.enabled) } - is Event.OnLinkImagePreviewsEnabled -> { state -> - state.copy( - isLinkImagePreviewsEnabled = event.enabled - ) - } - - is Event.OnUserProfilesEnabled -> { state -> - state.copy(canClickUserAvatars = event.enabled) - } - - is Event.ShowConnectedSocials -> { state -> - state.copy(showConnectedSocials = event.enabled) - } - - is Event.EnableEmojiReactions -> { state -> - state.copy(canReactToMessages = event.enabled) - } - - Event.OnUnreadStateHandled -> { state -> state.copy(unreadStateHandled = true) } - is Event.OnTypingConstraintsChanged -> { state -> - state.copy( - typingConstraints = TypingConstraints( - enabled = event.typingFlags.canSendAtAll, - canSendAsListener = event.typingFlags.canSendAsListener, - interval = event.typingFlags.interval, - timeout = event.typingFlags.timeout, - ) - ) - } - } - } - } -} - -private fun findLastReadMessage( - statusMap: Map, -): UUID? { - return statusMap - .filter { it.value == MessageStatus.Read } - .maxByOrNull { it.key.timestamp ?: 0L } - ?.key -} - -private fun findClosestMessageStatus( - timestamp: Long?, - statusMap: Map, - fallback: MessageStatus -): MessageStatus { - timestamp ?: return fallback - var closestKey: Long? = null - - for (key in statusMap.keys) { - if (timestamp <= key && (closestKey == null || key <= closestKey)) { - closestKey = key - } - } - - return closestKey?.let { statusMap[it] } ?: fallback -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/MessageReactionsSheet.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/MessageReactionsSheet.kt deleted file mode 100644 index a37fb998d..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/MessageReactionsSheet.kt +++ /dev/null @@ -1,368 +0,0 @@ -package xyz.flipchat.app.features.chat.conversation - -import android.annotation.SuppressLint -import android.os.Parcelable -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.PagerState -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Tab -import androidx.compose.material.TabRowDefaults -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Person -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import com.getcode.extensions.formattedRaw -import com.getcode.model.KinAmount -import com.getcode.model.chat.Sender -import com.getcode.model.sum -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.FullWidthScrollableTabRow -import com.getcode.ui.components.R -import com.getcode.ui.components.chat.UserAvatar -import com.getcode.ui.components.chat.messagecontents.SelectedReaction -import com.getcode.ui.components.chat.utils.MessageReaction -import com.getcode.ui.components.chat.utils.MessageTip -import com.getcode.ui.components.tabIndicatorOffset -import com.getcode.ui.components.user.social.SenderNameDisplay -import com.getcode.ui.emojis.processEmoji -import kotlinx.coroutines.launch -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import xyz.flipchat.services.internal.data.mapper.nullIfEmpty - -private sealed interface Feedback { - val data: FeedbackData - data class Tips(override val data: FeedbackData.Tips) : Feedback - data class Emoji(val emoji: String, override val data: FeedbackData.Senders) : Feedback - data class All(override val data: FeedbackData.All) : Feedback -} - -private typealias GroupedTips = List> -private sealed interface FeedbackData { - data class Tips(val tips: GroupedTips) : FeedbackData - data class Senders(val senders: List) : FeedbackData - data class All(val tips: List>, val reactions: List) : FeedbackData { - val totalCount: Int - get() = reactions.count() + tips.count() - } -} - -@Parcelize -internal data class MessageReactionsSheet( - val tips: List, - val reactions: List, - val startingWith: SelectedReaction, -) : Screen, Parcelable { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @Composable - override fun Content() { - val feedback = remember(tips, reactions, startingWith) { - val items = mutableListOf() - var groupedTips: GroupedTips = emptyList() - if (tips.isNotEmpty()) { - groupedTips = tips.groupBy { it.tipper } - .mapValues { it.value.map { it.amount }.sum() } - .toList().sortedByDescending { it.second.fiat } - items.add(Feedback.Tips(FeedbackData.Tips(groupedTips))) - } - - reactions.groupBy { it.emoji } - .mapValues { it.value.map { e -> e.sender } } - .onEach { - items.add(Feedback.Emoji(it.key, FeedbackData.Senders(it.value))) - } - - if ((groupedTips.isNotEmpty() || reactions.isNotEmpty()) && items.count() > 1) { - items.add(0, Feedback.All(FeedbackData.All(groupedTips, reactions))) - } - - return@remember items - } - - fun getInitialPage(): Int { - return feedback.indexOfFirst { - when (it) { - is Feedback.All -> false - is Feedback.Emoji -> { - if (startingWith !is SelectedReaction.Emoji) return@indexOfFirst false - startingWith.unicode == it.emoji - } - is Feedback.Tips -> startingWith is SelectedReaction.Tips - } - }.takeIf { it >= 0 } ?: 0 - } - - val pagerState = rememberPagerState(initialPage = getInitialPage()) { feedback.count() } - val composeScope = rememberCoroutineScope() - - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(0.5f) - ) { - if (pagerState.pageCount > 1) { - TabIndicator( - pagerState = pagerState, - feedback = feedback, - ) { - composeScope.launch { - pagerState.animateScrollToPage(it) - } - } - } - HorizontalPager( - modifier = Modifier.weight(1f), - state = pagerState - ) { page -> - val item = feedback[page] - FeedbackContent(item) - } - } - } -} - -@Composable -private fun TabIndicator( - pagerState: PagerState, - feedback: List, - onClick: (Int) -> Unit, -) { - FullWidthScrollableTabRow( - modifier = Modifier.fillMaxWidth(), - selectedTabIndex = pagerState.currentPage, - backgroundColor = Color.Transparent, - edgePadding = CodeTheme.dimens.grid.x2, - indicator = { tabPositions -> - TabRowDefaults.Indicator( - color = CodeTheme.colors.onSurface, - modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]) - ) - }, - divider = { - TabRowDefaults.Divider(color = CodeTheme.colors.divider) - } - ) { - feedback.onEachIndexed { index, feedback -> - Tab( - modifier = Modifier.padding( -// horizontal = CodeTheme.dimens.grid.x5, - vertical = CodeTheme.dimens.grid.x3 - ), - selected = index == pagerState.currentPage, - selectedContentColor = Color.Transparent, - onClick = { - onClick(index) - } - ) { - val text = when (feedback) { - is Feedback.Emoji -> { - if (LocalInspectionMode.current) { - feedback.emoji - } else { - processEmoji(feedback.emoji).toString() - } - } - is Feedback.Tips -> "⬢" - is Feedback.All -> "All" - } - - Text( - text = text, - color = CodeTheme.colors.onSurface, - style = CodeTheme.typography.textSmall.copy(fontWeight = FontWeight.W700), - ) - } - } - } -} - -@Composable -private fun FeedbackContent( - item: Feedback -) { - val imageModifier = Modifier - .size(CodeTheme.dimens.staticGrid.x8) - .clip(CircleShape) - - LazyColumn( - modifier = Modifier - .fillMaxSize() - .navigationBarsPadding(), - contentPadding = PaddingValues(CodeTheme.dimens.inset), - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.inset), - ) { - val count = when (val data = item.data) { - is FeedbackData.Senders -> data.senders.count() - is FeedbackData.Tips -> data.tips.count() - is FeedbackData.All -> data.totalCount - } - items(count) { index -> - when (val data = item.data) { - is FeedbackData.Senders -> { - val sender = data.senders[index] - EmojiReactionRow( - sender = sender, - imageModifier = imageModifier, - modifier = Modifier.fillParentMaxWidth(), - ) - } - is FeedbackData.Tips -> { - val tip = data.tips[index] - val tipper = tip.first - val tipAmount = tip.second - - TipReactionRow( - tipper = tipper, - tip = tipAmount, - imageModifier = imageModifier, - modifier = Modifier.fillParentMaxWidth(), - ) - } - - is FeedbackData.All -> { - val items = data.tips + data.reactions - val i = items[index] - when (i) { - is MessageReaction -> { - EmojiReactionRow( - sender = i.sender, - imageModifier = imageModifier, - modifier = Modifier.fillParentMaxWidth(), - emoji = i.emoji - ) - } - is Pair<*, *> -> { - val row = i as Pair - TipReactionRow( - tipper = row.first, - tip = i.second, - imageModifier = imageModifier, - modifier = Modifier.fillParentMaxWidth(), - ) - } - } - } - } - } - } -} - -@Composable -private fun EmojiReactionRow( - sender: Sender, - emoji: String? = null, - @SuppressLint("ModifierParameter") - imageModifier: Modifier, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), - verticalAlignment = Alignment.CenterVertically, - ) { - UserAvatar( - modifier = imageModifier, - data = sender.profileImage ?: sender.id - ) { - Image( - modifier = Modifier.padding(5.dp), - imageVector = Icons.Default.Person, - colorFilter = ColorFilter.tint(Color.White), - contentDescription = null, - ) - } - SenderNameDisplay( - sender = sender, - textStyle = CodeTheme.typography.textMedium, - textColor = CodeTheme.colors.onSurface - ) - - Spacer(Modifier.weight(1f)) - - if (emoji != null) { - Text( - text = if (LocalInspectionMode.current) { - emoji - } else { - processEmoji(emoji).toString() - }, - color = CodeTheme.colors.onSurface, - style = CodeTheme.typography.textMedium.copy(fontWeight = FontWeight.W700), - ) - } - } -} - -@Composable -private fun TipReactionRow( - tipper: Sender, - tip: KinAmount, - @SuppressLint("ModifierParameter") - imageModifier: Modifier, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), - verticalAlignment = Alignment.CenterVertically, - ) { - UserAvatar( - modifier = imageModifier, - data = tipper.profileImage.nullIfEmpty() ?: tipper.id - ) { - Image( - modifier = Modifier.padding(5.dp), - imageVector = Icons.Default.Person, - colorFilter = ColorFilter.tint(Color.White), - contentDescription = null, - ) - } - SenderNameDisplay( - sender = tipper, - textStyle = CodeTheme.typography.textMedium, - textColor = CodeTheme.colors.onSurface - ) - - Spacer(modifier = Modifier.weight(1f)) - - Text( - text = stringResource( - R.string.title_kinAmountWithLogo, - tip.formattedRaw() - ), - color = CodeTheme.colors.onSurface, - style = CodeTheme.typography.textMedium, - ) - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/MessageReplyAnchor.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/MessageReplyAnchor.kt deleted file mode 100644 index 2122dd73a..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/MessageReplyAnchor.kt +++ /dev/null @@ -1,11 +0,0 @@ -package xyz.flipchat.app.features.chat.conversation - -import com.getcode.model.ID -import com.getcode.model.chat.MessageContent -import com.getcode.model.chat.Sender - -data class MessageReplyAnchor( - val id: ID, - val sender: Sender, - val message: MessageContent -) \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/MessageTipsSheet.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/MessageTipsSheet.kt deleted file mode 100644 index 01f45d950..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/conversation/MessageTipsSheet.kt +++ /dev/null @@ -1,108 +0,0 @@ -package xyz.flipchat.app.features.chat.conversation - -import androidx.compose.foundation.Image -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Person -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.screen.Screen -import com.getcode.extensions.formattedRaw -import com.getcode.model.KinAmount -import com.getcode.model.chat.Sender -import com.getcode.model.sum -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.R -import com.getcode.ui.components.chat.UserAvatar -import com.getcode.ui.components.chat.utils.MessageTip -import com.getcode.ui.components.user.social.SenderNameDisplay -import xyz.flipchat.services.internal.data.mapper.nullIfEmpty - -internal data class MessageTipsSheet(val tips: List) : Screen { - - @Composable - override fun Content() { - val userTips: List> = remember { - tips.groupBy { it.tipper } - .mapValues { it.value.map { it.amount }.sum() } - .toList().sortedByDescending { it.second.fiat } - } - - val imageModifier = Modifier - .size(CodeTheme.dimens.staticGrid.x8) - .clip(CircleShape) - - - Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(0.5f)) { - Text( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(top = CodeTheme.dimens.inset), - text = "Tips", - style = CodeTheme.typography.screenTitle - ) - LazyColumn( - modifier = Modifier.navigationBarsPadding(), - contentPadding = PaddingValues(CodeTheme.dimens.inset), - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.inset), - ) { - items(userTips) { (tipper, amount) -> - Row( - modifier = Modifier.fillParentMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), - verticalAlignment = Alignment.CenterVertically, - ) { - UserAvatar( - modifier = imageModifier, - data = tipper.profileImage.nullIfEmpty() ?: tipper.id - ) { - Image( - modifier = Modifier.padding(5.dp), - imageVector = Icons.Default.Person, - colorFilter = ColorFilter.tint(Color.White), - contentDescription = null, - ) - } - SenderNameDisplay( - sender = tipper, - textStyle = CodeTheme.typography.textMedium, - textColor = CodeTheme.colors.onSurface - ) - - Spacer(modifier = Modifier.weight(1f)) - - Text( - text = stringResource( - R.string.title_kinAmountWithLogo, - amount.formattedRaw() - ), - color = CodeTheme.colors.onSurface, - style = CodeTheme.typography.textMedium, - ) - } - } - } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/cover/CoverChargeScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/cover/CoverChargeScreen.kt deleted file mode 100644 index 7a9645814..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/cover/CoverChargeScreen.kt +++ /dev/null @@ -1,113 +0,0 @@ -package xyz.flipchat.app.features.chat.cover - -import android.os.Parcelable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.hilt.getViewModel -import com.getcode.model.ID -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.NamedScreen -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import xyz.flipchat.app.R -import xyz.flipchat.app.ui.AmountWithKeypad - -@Parcelize -data class CoverChargeScreen(val roomId: ID) : Screen, NamedScreen, Parcelable { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(R.string.title_changeMessageFee) - - - @Composable - override fun Content() { - val viewModel = getViewModel() - val navigator = LocalCodeNavigator.current - - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AppBarWithTitle( - title = name, - backButton = true, - onBackIconClicked = navigator::pop - ) - ChangeCoverScreenContent(viewModel) - } - - LaunchedEffect(viewModel) { - viewModel.dispatchEvent(CoverChargeViewModel.Event.OnRoomIdChanged(roomId)) - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .onEach { - navigator.pop() - }.launchIn(this) - } - } -} - -@Composable -private fun ChangeCoverScreenContent( - viewModel: CoverChargeViewModel, -) { - val state by viewModel.stateFlow.collectAsState() - Column( - modifier = Modifier.fillMaxSize(), - ) { - AmountWithKeypad( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - amountAnimatedModel = state.amountAnimatedModel, - isKin = true, - placeholder = "0", - onNumberPressed = { viewModel.dispatchEvent(CoverChargeViewModel.Event.OnNumberPressed(it)) }, - onBackspace = { viewModel.dispatchEvent(CoverChargeViewModel.Event.OnBackspace) }, - ) - - Box(modifier = Modifier.fillMaxWidth()) { - CodeButton( - enabled = state.canChange, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset) - .padding(bottom = CodeTheme.dimens.grid.x2) - .navigationBarsPadding(), - buttonState = ButtonState.Filled, - text = stringResource(R.string.action_saveChanges), - isLoading = state.submitting, - isSuccess = state.success, - ) { - viewModel.dispatchEvent(CoverChargeViewModel.Event.OnChangeFee) - } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/cover/CoverChargeViewModel.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/cover/CoverChargeViewModel.kt deleted file mode 100644 index 2dec10f25..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/cover/CoverChargeViewModel.kt +++ /dev/null @@ -1,134 +0,0 @@ -package xyz.flipchat.app.features.chat.cover - -import androidx.lifecycle.viewModelScope -import com.getcode.manager.TopBarManager -import com.getcode.model.ID -import com.getcode.model.KinAmount -import com.getcode.ui.components.text.AmountAnimatedInputUiModel -import com.getcode.ui.components.text.NumberInputHelper -import com.getcode.util.resources.ResourceHelper -import com.getcode.view.BaseViewModel2 -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import xyz.flipchat.app.R -import xyz.flipchat.app.features.login.register.onResult -import xyz.flipchat.chat.RoomController -import javax.inject.Inject - -@HiltViewModel -class CoverChargeViewModel @Inject constructor( - roomController: RoomController, - resources: ResourceHelper, -) : BaseViewModel2( - initialState = State(), - updateStateForEvent = updateStateForEvent -) { - private val numberInputHelper = NumberInputHelper() - - data class State( - val roomId: ID? = null, - val submitting: Boolean = false, - val success: Boolean = false, - val amountAnimatedModel: AmountAnimatedInputUiModel = AmountAnimatedInputUiModel(), - val canChange: Boolean = false, - ) - - sealed interface Event { - data class OnRoomIdChanged(val roomId: ID) : Event - data class OnNumberPressed(val number: Int) : Event - data object OnBackspace : Event - data class OnEnteredNumberChanged(val backspace: Boolean = false) : Event - data class OnCoverChanged(val amountAnimatedModel: AmountAnimatedInputUiModel) : Event - data object OnChangeFee : Event - data class OnChangingFee(val changing: Boolean): Event - data object OnFeeChangedSuccessfully : Event - } - - init { - numberInputHelper.reset() - - eventFlow - .filterIsInstance() - .map { it.number } - .onEach { number -> - numberInputHelper.maxLength = 10 // 1 billion Kin - numberInputHelper.onNumber(number) - dispatchEvent(Event.OnEnteredNumberChanged()) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .onEach { - numberInputHelper.onBackspace() - dispatchEvent(Event.OnEnteredNumberChanged(true)) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.backspace } - .onEach { backspace -> - val current = stateFlow.value.amountAnimatedModel - val model = stateFlow.value.amountAnimatedModel - val amount = numberInputHelper.getFormattedStringForAnimation(includeCommas = true) - - val updated = model.copy( - amountDataLast = current.amountData, - amountData = amount, - lastPressedBackspace = backspace - ) - - dispatchEvent(Event.OnCoverChanged(updated)) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .onEach { dispatchEvent(Event.OnChangingFee(true)) } - .mapNotNull { - stateFlow.value.roomId ?: return@mapNotNull null - stateFlow.value.roomId!! to stateFlow.value.amountAnimatedModel.amountData.amount.toLong() - }.map { (roomId, value) -> - roomController.setMessagingFee(roomId, KinAmount.fromQuarks(value)) - }.onResult( - onError = { - dispatchEvent(Event.OnChangingFee(false)) - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToChangeMessageFee), - resources.getString(R.string.error_description_failedToChangeMessageFee) - ) - ) - }, - onSuccess = { - dispatchEvent(Event.OnChangingFee(false)) - dispatchEvent(Event.OnFeeChangedSuccessfully) - } - ).launchIn(viewModelScope) - } - - companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - is Event.OnChangingFee -> { state -> state.copy(submitting = event.changing) } - Event.OnFeeChangedSuccessfully -> { state -> state.copy(success = true) } - Event.OnBackspace, - is Event.OnEnteredNumberChanged, - is Event.OnChangeFee, - is Event.OnNumberPressed -> { state -> state } - - is Event.OnCoverChanged -> { state -> - val cover = event.amountAnimatedModel.amountData.amount.toLongOrNull() - state.copy( - amountAnimatedModel = event.amountAnimatedModel, - canChange = (cover ?: 0) > 0 - ) - } - - is Event.OnRoomIdChanged -> { state -> state.copy(roomId = event.roomId) } - } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/create/CreateRoomScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/create/CreateRoomScreen.kt deleted file mode 100644 index 18182602c..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/create/CreateRoomScreen.kt +++ /dev/null @@ -1,111 +0,0 @@ -package xyz.flipchat.app.features.chat.create - -import android.os.Parcelable -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.hilt.getViewModel -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.modal.ModalScreen -import com.getcode.ui.components.AppBarDefaults -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.utils.keyboardAsState -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import xyz.flipchat.app.features.chat.name.RoomNameScreenContent -import xyz.flipchat.app.features.chat.name.RoomNameScreenViewModel -import kotlin.time.Duration.Companion.seconds - -@Parcelize -class CreateRoomScreen : ModalScreen, Parcelable { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - - @Composable - override fun ModalContent() { - val navigator = LocalCodeNavigator.current - val keyboardIsVisible by keyboardAsState() - val keyboard = LocalSoftwareKeyboardController.current - val scope = rememberCoroutineScope() - - val goBack = { - scope.launch { - if (keyboardIsVisible) { - keyboard?.hide() - delay(500) - } - navigator.hide() - } - } - BackHandler { - if (keyboardIsVisible) { - keyboard?.hide() - return@BackHandler - } - - navigator.hide() - } - - Column( - modifier = Modifier - .fillMaxSize() - ) { - AppBarWithTitle( - backButton = false, - isInModal = true, - title = "", - endContent = { - AppBarDefaults.Close { goBack() } - }, - ) - - val viewModel = getViewModel() - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .onEach { - delay(2.seconds) - navigator.pop() - } - .launchIn(this) - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .map { it.roomId } - .onEach { - navigator.hideWithResult(it) - } - .launchIn(this) - } - - - - val state by viewModel.stateFlow.collectAsState() - RoomNameScreenContent( - isUpdate = false, - state = state, - dispatch = viewModel::dispatchEvent - ) - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/description/RoomDescriptionScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/description/RoomDescriptionScreen.kt deleted file mode 100644 index 4f968e367..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/description/RoomDescriptionScreen.kt +++ /dev/null @@ -1,252 +0,0 @@ -package xyz.flipchat.app.features.chat.description - -import android.os.Parcelable -import androidx.activity.compose.BackHandler -import androidx.compose.animation.Crossfade -import androidx.compose.animation.animateColorAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.shape.CornerSize -import androidx.compose.foundation.shape.ZeroCornerSize -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.hilt.getViewModel -import com.getcode.model.ID -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.theme.CodeTheme -import com.getcode.theme.inputColors -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.components.TextInput -import com.getcode.ui.core.rememberAnimationScale -import com.getcode.ui.core.scaled -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.theme.CodeScaffold -import com.getcode.ui.utils.keyboardAsState -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import xyz.flipchat.app.R -import kotlin.time.Duration.Companion.seconds - -@Parcelize -data class RoomDescriptionScreen(val roomId: ID, val existingDescription: String) : Screen, - Parcelable { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - val keyboardIsVisible by keyboardAsState() - val keyboard = LocalSoftwareKeyboardController.current - val scope = rememberCoroutineScope() - val animationScale by rememberAnimationScale() - val goBack = { - scope.launch { - if (keyboardIsVisible) { - keyboard?.hide() - delay(500.scaled(animationScale)) - } - navigator.pop() - } - } - BackHandler { - goBack() - } - - Column( - modifier = Modifier - .fillMaxSize() - ) { - AppBarWithTitle( - backButton = true, - title = stringResource(R.string.action_changeRoomDescription), - onBackIconClicked = { goBack() }, - ) - - val viewModel = getViewModel() - - LaunchedEffect(viewModel, roomId, existingDescription) { - viewModel.dispatchEvent( - RoomDescriptionScreenViewModel.Event.OnNewRequest( - roomId, - existingDescription - ) - ) - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .onEach { - delay(2.seconds) - navigator.pop() - } - .launchIn(this) - } - - val state by viewModel.stateFlow.collectAsState() - RoomDescriptionScreenContent( - state = state, - dispatch = viewModel::dispatchEvent - ) - } - } -} - -@Composable -private fun RoomDescriptionScreenContent( - state: RoomDescriptionScreenViewModel.State, - dispatch: (RoomDescriptionScreenViewModel.Event) -> Unit, -) { - val keyboard = LocalSoftwareKeyboardController.current - val focusRequester = remember { FocusRequester() } - - CodeScaffold( - modifier = Modifier - .fillMaxSize() - .imePadding(), - bottomBar = { - Box( - modifier = Modifier - .fillMaxWidth() - .navigationBarsPadding() - .padding(bottom = CodeTheme.dimens.grid.x3), - ) { - CodeButton( - enabled = state.canCheck || (state.previousRoomDescription.isNotEmpty() && state.isLimitValid), // allow resets - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset), - buttonState = ButtonState.Filled, - text = stringResource(R.string.action_save), - isLoading = state.update.loading, - isSuccess = state.update.success - ) { - keyboard?.hide() - dispatch(RoomDescriptionScreenViewModel.Event.UpdateDescription) - focusRequester.freeFocus() - } - } - } - ) { padding -> - Box( - modifier = Modifier - .fillMaxSize() - .padding(padding) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = CodeTheme.dimens.inset) - .align(Alignment.TopStart) - .padding(horizontal = CodeTheme.dimens.inset), - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.inset) - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - ) { - TextInput( - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester) - .align(Alignment.Center), - state = state.textFieldState, - colors = inputColors( - backgroundColor = Color.Transparent, - errorBorderColor = CodeTheme.colors.errorText, - ), - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - capitalization = KeyboardCapitalization.Sentences - ), - contentPadding = PaddingValues(CodeTheme.dimens.grid.x3), - textFieldAlignment = Alignment.TopStart, - minHeight = 120.dp, - maxLines = 6, - isError = state.textFieldState.text.length > state.maxLimit, - placeholder = stringResource(R.string.subtitle_roomDescription), - ) - - Crossfade( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding( - bottom = CodeTheme.dimens.border, - end = CodeTheme.dimens.border, - ), - targetState = state.isApproachingLengthOrOver - ) { show -> - if (show) { - val textColor by animateColorAsState( - if (state.textFieldState.text.length > state.maxLimit) { - CodeTheme.colors.errorText - } else { - CodeTheme.colors.textSecondary - } - ) - Text( - modifier = Modifier - .background( - color = CodeTheme.colors.brandDark.copy(0.54f), - shape = CodeTheme.shapes.small.copy( - topEnd = ZeroCornerSize, - bottomStart = ZeroCornerSize, - ) - ) - .padding(5.dp), - textAlign = TextAlign.Center, - text = "${state.textFieldState.text.length}/${state.maxLimit}", - style = CodeTheme.typography.caption, - color = textColor, - ) - } else { - Text("") - } - } - } - } - } - - LaunchedEffect(focusRequester) { - focusRequester.requestFocus() - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/description/RoomDescriptionScreenViewModel.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/description/RoomDescriptionScreenViewModel.kt deleted file mode 100644 index d6f05c26a..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/description/RoomDescriptionScreenViewModel.kt +++ /dev/null @@ -1,137 +0,0 @@ -package xyz.flipchat.app.features.chat.description - -import androidx.compose.foundation.text.input.TextFieldState -import androidx.lifecycle.viewModelScope -import com.getcode.manager.TopBarManager -import com.getcode.model.ID -import com.getcode.model.KinAmount -import com.getcode.model.NoId -import com.getcode.services.model.ExtendedMetadata -import com.getcode.services.utils.onSuccessWithDelay -import com.getcode.util.resources.ResourceHelper -import com.getcode.view.BaseViewModel2 -import com.getcode.view.LoadingSuccessState -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.take -import xyz.flipchat.app.R -import xyz.flipchat.chat.RoomController -import xyz.flipchat.controllers.ChatsController -import xyz.flipchat.services.PaymentEvent -import xyz.flipchat.services.data.metadata.StartGroupChatPaymentMetadata -import xyz.flipchat.services.data.metadata.erased -import xyz.flipchat.services.data.metadata.typeUrl -import xyz.flipchat.services.internal.network.service.CheckDisplayNameError -import xyz.flipchat.services.internal.network.service.SetRoomDisplayNameError -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds - -@HiltViewModel -internal class RoomDescriptionScreenViewModel @Inject constructor( - roomController: RoomController, - resources: ResourceHelper, -) : BaseViewModel2( - initialState = State(), - updateStateForEvent = updateStateForEvent -) { - data class State( - val roomId: ID = NoId, - val previousRoomDescription: String = "", - val maxLimit: Int = 160, - val update: LoadingSuccessState = LoadingSuccessState(), - val textFieldState: TextFieldState = TextFieldState(""), - ) { - val isApproachingLengthOrOver: Boolean - get() = textFieldState.text.length >= 150 - - val isLimitValid: Boolean - get() = textFieldState.text.length <= 160 - - val canCheck: Boolean - get() = textFieldState.text.isNotEmpty() && isLimitValid - } - - sealed interface Event { - data class OnNewRequest(val id: ID, val description: String) : Event - data object UpdateDescription : Event - data object OnError : Event - data object OnSuccess : Event - } - - init { - eventFlow - .filterIsInstance() - .map { stateFlow.value } - .onEach { - val textFieldState = it.textFieldState - val text = textFieldState.text.toString().trim() - - roomController.setDescription(it.roomId, text) - .onFailure { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = resources.getString(R.string.error_title_failedToChangeRoomDescription), - message = resources.getString(R.string.error_description_failedToChangeRoomDescription) - ) - ) - - dispatchEvent(Event.OnError) - } - .onSuccessWithDelay(2.seconds) { - dispatchEvent(Event.OnSuccess) - } - }.launchIn(viewModelScope) - } - - internal companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - is Event.UpdateDescription -> { state -> - state.copy( - update = LoadingSuccessState( - loading = true, - success = false - ) - ) - } - - is Event.OnError -> { state -> - state.copy( - update = LoadingSuccessState( - loading = false, - success = false - ) - ) - } - - Event.OnSuccess -> { state -> - state.copy( - update = LoadingSuccessState( - loading = false, - success = true - ) - ) - } - - is Event.OnNewRequest -> { state -> - if (state.roomId != event.id) { - state.copy( - update = LoadingSuccessState(), - roomId = event.id, - textFieldState = TextFieldState(initialText = event.description), - previousRoomDescription = event.description, - ) - } else { - state - } - } - } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/info/ChatInfoViewModel.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/info/ChatInfoViewModel.kt deleted file mode 100644 index 785bc7db6..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/info/ChatInfoViewModel.kt +++ /dev/null @@ -1,698 +0,0 @@ -package xyz.flipchat.app.features.chat.info - -import android.Manifest -import android.app.NotificationManager -import android.content.Intent -import android.os.Build -import androidx.core.app.NotificationManagerCompat -import androidx.lifecycle.viewModelScope -import com.getcode.manager.BottomBarManager -import com.getcode.manager.TopBarManager -import com.getcode.model.ID -import com.getcode.model.Kin -import com.getcode.model.chat.MinimalMember -import xyz.flipchat.app.navigation.RoomInfoArgs -import com.getcode.solana.keys.PublicKey -import com.getcode.ui.components.text.markup.Markup -import com.getcode.util.permissions.PermissionChecker -import com.getcode.util.resources.ResourceHelper -import com.flipcash.libs.coroutines.DispatcherProvider -import com.getcode.view.BaseViewModel2 -import com.getcode.view.LoadingSuccessState -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import xyz.flipchat.app.R -import xyz.flipchat.app.beta.Lab -import xyz.flipchat.app.beta.Labs -import xyz.flipchat.app.data.RoomInfo -import xyz.flipchat.app.features.login.register.onResult -import xyz.flipchat.app.util.IntentUtils -import xyz.flipchat.chat.RoomController -import xyz.flipchat.controllers.ChatsController -import xyz.flipchat.notifications.FcNotificationType -import xyz.flipchat.services.domain.model.profile.toLinked -import xyz.flipchat.services.extensions.titleOrFallback -import xyz.flipchat.services.internal.data.mapper.nullIfEmpty -import xyz.flipchat.services.internal.network.service.PromoteUserError -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject -import kotlin.reflect.KClass - -sealed interface MemberType { - data object Speaker : MemberType - data object Listener : MemberType -} - -@HiltViewModel -class ChatInfoViewModel @Inject constructor( - private val labs: Labs, - private val roomController: RoomController, - private val chatsController: ChatsController, - private val resources: ResourceHelper, - private val userManager: UserManager, - private val permissionChecker: PermissionChecker, - private val notificationManager: NotificationManagerCompat, - dispatchers: DispatcherProvider, -) : BaseViewModel2( - initialState = State(), - updateStateForEvent = updateStateForEvent, - dispatchers = dispatchers, -) { - - data class State( - val isPreview: Boolean = false, - val isHost: Boolean = false, - val isMember: Boolean = false, - val paymentDestination: PublicKey? = null, - val roomNameChangesEnabled: Boolean = false, - val roomDescriptionChangesEnabled: Boolean = false, - val canViewUserProfile: Boolean = false, - val isOpen: Boolean = true, - val roomInfo: RoomInfo = RoomInfo(), - val descriptionMarkups: List> = listOf( - Markup.RoomNumber::class, - Markup.Url::class, - Markup.Phone::class, - ), - val joining: LoadingSuccessState = LoadingSuccessState(), - val leaving: LoadingSuccessState = LoadingSuccessState(), - val members: Map> = emptyMap() - ) - - sealed interface Event { - // region state updates - data class OnRoomNameChangesEnabled(val enabled: Boolean) : Event - data class OnRoomDescriptionChangesEnabled(val enabled: Boolean) : Event - data class OnUserProfilesEnabled(val enabled: Boolean) : Event - data class OnHostStatusChanged(val isHost: Boolean) : Event - data class OnRoomOpenStateChanged(val isOpen: Boolean) : Event - data class OnDestinationChanged(val destination: PublicKey) : Event - data class OnInfoChanged(val args: RoomInfoArgs, val isPreview: Boolean) : Event - data class OnMembersUpdated(val members: List) : Event - // endregion state updates - - // region action/reaction - data class OnChangeMessageFee(val roomId: ID) : Event - data class OnFeeChanged(val cover: Kin) : Event - - data class OnChangeName(val id: ID, val title: String) : Event - data class OnChangeDescription(val id: ID, val description: String) : Event - data class OnNameChanged(val name: String) : Event - data class OnDescriptionChanged(val description: String) : Event - - data object OnShareRoomClicked : Event - data class ShareRoom(val intent: Intent) : Event - - data object OnListenToClicked : Event - data object OnStartListening : Event - data object RequestNotificationPermissions : Event - data class OnJoiningStateChanged(val joining: Boolean, val joined: Boolean = false) : Event - data class OnBecameMember(val roomId: ID) : Event - - data object OnOpenStateChangedRequested : Event - data class OnOpenRoom(val conversationId: ID) : Event - data class OnCloseRoom(val conversationId: ID) : Event - - data class PromoteRequested(val member: MinimalMember) : Event - data class PromoteUser(val conversationId: ID, val userId: ID) : Event - data class OnUserPromoted(val id: ID) : Event - data class DemoteRequested(val member: MinimalMember) : Event - data class DemoteUser(val conversationId: ID, val userId: ID) : Event - data class OnUserDemoted(val id: ID) : Event - - data object LeaveRoom : Event - data class OnLeavingStateChanged(val leaving: Boolean, val left: Boolean = false) : Event - data object OnLeaveRoomConfirmed : Event - // endregion action/reaction - - data class LookupRoom(val number: Long) : Event - data class OpenRoomPreview(val roomInfoArgs: RoomInfoArgs) : Event - data class OpenRoom(val roomId: ID) : Event - - data object OnLeftRoom : Event - } - - init { - labs.observe(Lab.ShowConnectedSocials) - .map { dispatchEvent(Event.OnUserProfilesEnabled(it)) } - .launchIn(viewModelScope) - - labs.observe(Lab.RoomDescriptions) - .map { dispatchEvent(Event.OnRoomDescriptionChangesEnabled(it)) } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.args.ownerId } - .map { hostId -> userManager.userId == hostId } - .onEach { isHost -> - dispatchEvent(Event.OnHostStatusChanged(isHost)) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .mapNotNull { it.args } - .onEach { args -> - val showConnectedSocials = stateFlow.value.canViewUserProfile - val exists = roomController.getConversation(args.roomId.orEmpty()) != null - if (!exists) { - chatsController.lookupRoom(args.roomNumber) - .onSuccess { (room, members) -> - dispatchEvent(Event.OnRoomOpenStateChanged(room.isOpen)) - dispatchEvent(Event.OnNameChanged(room.titleOrFallback(resources))) - dispatchEvent(Event.OnDescriptionChanged(room.description.orEmpty())) - dispatchEvent( - Event.OnMembersUpdated( - members.map { m -> - MinimalMember( - id = m.id, - name = m.identity?.displayName.nullIfEmpty(), - profileImageUrl = m.identity?.imageUrl.nullIfEmpty(), - canSpeak = m.isModerator || !m.isSpectator, - isHost = m.isModerator, - isSelf = userManager.isSelf(m.id), - socialProfiles = if (showConnectedSocials) { - m.identity?.socialProfiles?.mapNotNull { - it.toLinked() - }.orEmpty() - } else { - emptyList() - } - ) - } - ) - ) - dispatchEvent(Event.OnFeeChanged(room.messagingFee)) - } - } else { - roomController.observeConversation(args.roomId.orEmpty()) - .filterNotNull() - .map { Triple(it.conversation, it.members, it.conversation.coverCharge) } - .onEach { (conversation, members, cover) -> - dispatchEvent(Event.OnRoomOpenStateChanged(conversation.isOpen)) - dispatchEvent(Event.OnNameChanged(conversation.titleOrFallback(resources))) - dispatchEvent(Event.OnDescriptionChanged(conversation.description.orEmpty())) - dispatchEvent( - Event.OnMembersUpdated( - members.map { m -> - MinimalMember( - id = m.id, - name = m.displayName.nullIfEmpty(), - profileImageUrl = m.imageUri, - isHost = m.isHost, - canSpeak = m.isFullMember, - isSelf = userManager.isSelf(m.id), - socialProfiles = if (showConnectedSocials) { - m.profiles.mapNotNull { it.toLinked() } - } else { - emptyList() - } - ) - } - ) - ) - dispatchEvent(Event.OnFeeChanged(cover)) - }.launchIn(viewModelScope) - } - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { stateFlow.value.roomInfo.number } - .onEach { roomNumber -> - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = resources.getString(R.string.title_leaveRoom), - subtitle = resources.getString(R.string.subtitle_leaveRoom), - positiveText = resources.getString( - R.string.action_leaveRoomByName, - resources.getString(R.string.title_implicitRoomTitle, roomNumber) - ), - negativeText = "", - tertiaryText = resources.getString(R.string.action_cancel), - onPositive = { dispatchEvent(Event.OnLeaveRoomConfirmed) }, - onNegative = { }, - type = BottomBarManager.BottomBarMessageType.DESTRUCTIVE, - showScrim = true, - ) - ) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .onEach { - if (requestNotificationPermissionsIfNeeded()) { - dispatchEvent(Event.RequestNotificationPermissions) - return@onEach - } else { - dispatchEvent(Event.OnStartListening) - } - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { stateFlow.value.roomInfo } - .onEach { roomInfo -> - dispatchEvent(Event.OnJoiningStateChanged(true)) - chatsController.joinRoomAsSpectator(roomInfo.id.orEmpty()) - .onFailure { - dispatchEvent(Event.OnJoiningStateChanged(false)) - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToFollowRoom), - resources.getString( - R.string.error_description_failedToFollowRoom, - stateFlow.value.roomInfo.title - ) - ) - ) - }.onSuccess { - dispatchEvent(Event.OnBecameMember(it.room.id)) - } - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { stateFlow.value.roomInfo.id } - .mapNotNull { - if (it == null) { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = resources.getString(R.string.error_title_failedToLeaveRoom), - message = resources.getString(R.string.error_description_failedToLeaveRoom) - ) - ) - return@mapNotNull null - } - dispatchEvent(Event.OnLeavingStateChanged(true)) - roomController.leaveRoom(it) - }.onResult( - onError = { - dispatchEvent(Event.OnLeavingStateChanged(false)) - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = resources.getString(R.string.error_title_failedToLeaveRoom), - message = resources.getString(R.string.error_description_failedToLeaveRoom) - ) - ) - }, - onSuccess = { - dispatchEvent(Event.OnLeavingStateChanged(leaving = false, left = true)) - dispatchEvent(Event.OnLeftRoom) - } - ).launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .mapNotNull { stateFlow.value.roomInfo.id } - .map { it to stateFlow.value.isOpen } - .onEach { (conversationId, isOpen) -> - confirmOpenStateChange(conversationId, isOpen) - } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.conversationId } - .map { roomController.enableChat(it) } - .onResult( - onError = { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToReopenRoom), - resources.getString(R.string.error_description_failedToReopenRoom) - ) - ) - }, - ).launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.conversationId } - .map { roomController.disableChat(it) } - .onResult( - onError = { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToCloseRoom), - resources.getString(R.string.error_description_failedToCloseRoom) - ) - ) - }, - ).launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { IntentUtils.shareRoom(stateFlow.value.roomInfo.roomNumber) } - .onEach { dispatchEvent(Event.ShareRoom(it)) } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.member } - .onEach { - confirmUserPromote( - conversationId = stateFlow.value.roomInfo.id.orEmpty(), - userId = it.id.orEmpty(), - user = it.displayName - ) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.member } - .onEach { - confirmUserDemote( - conversationId = stateFlow.value.roomInfo.id.orEmpty(), - userId = it.id.orEmpty(), - user = it.displayName - ) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .onEach { member -> - roomController.promoteUser(member.conversationId, member.userId) - .onFailure { error -> - if (error is PromoteUserError.NotRegistered) { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToPromoteUserNotRegistered), - resources.getString(R.string.error_description_failedToPromoteUserNotRegistered) - ) - ) - } else { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToPromoteUser), - resources.getString(R.string.error_description_failedToPromoteUser) - ) - ) - } - }.onSuccess { - dispatchEvent(Event.OnUserPromoted(member.userId)) - } - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .onEach { member -> - roomController.demoteUser(member.conversationId, member.userId) - .onFailure { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToDemoteUser), - resources.getString(R.string.error_description_failedToDemoteUser) - ) - ) - }.onSuccess { - dispatchEvent(Event.OnUserDemoted(member.userId)) - } - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.number } - .map { roomNumber -> - chatsController.lookupRoom(roomNumber) - .onFailure { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToGetRoom), - resources.getString( - R.string.error_description_failedToGetRoom, - roomNumber - ) - ) - ) - }.onSuccess { (room, members) -> - val moderator = members.firstOrNull { it.isModerator } - val isMember = members.any { it.isSelf } - if (isMember) { - dispatchEvent(Event.OpenRoom(room.id)) - } else { - val roomInfo = RoomInfoArgs( - roomId = room.id, - roomNumber = room.roomNumber, - roomTitle = room.titleOrFallback(resources), - roomDescription = room.description, - memberCount = members.count(), - ownerId = room.ownerId, - hostName = moderator?.identity?.displayName, - messagingFeeQuarks = room.messagingFee.quarks, - ) - dispatchEvent(Event.OpenRoomPreview(roomInfo)) - } - } - }.launchIn(viewModelScope) - } - - private fun confirmOpenStateChange(conversationId: ID, isRoomOpen: Boolean) { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = if (isRoomOpen) resources.getString(R.string.prompt_title_closeRoom) else resources.getString( - R.string.prompt_title_reopenRoom - ), - subtitle = if (isRoomOpen) resources.getString(R.string.prompt_description_closeRoom) else resources.getString( - R.string.prompt_description_reopenRoom - ), - positiveText = if (isRoomOpen) resources.getString(R.string.action_closeTemporarily) else resources.getString( - R.string.action_reopenRoom - ), - tertiaryText = resources.getString(R.string.action_cancel), - onPositive = { - if (isRoomOpen) { - dispatchEvent(Event.OnCloseRoom(conversationId)) - } else { - dispatchEvent(Event.OnOpenRoom(conversationId)) - } - }, - type = BottomBarManager.BottomBarMessageType.THEMED, - showScrim = true, - ) - ) - } - - private fun confirmUserPromote(conversationId: ID, user: String?, userId: ID) { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = resources.getString( - R.string.title_promoteUserInRoom, - user.orEmpty().ifEmpty { "User" }), - subtitle = resources.getString(R.string.subtitle_promoteUserInRoom), - positiveText = resources.getString(R.string.action_promote), - negativeText = "", - tertiaryText = resources.getString(R.string.action_cancel), - onPositive = { dispatchEvent(Event.PromoteUser(conversationId, userId)) }, - onNegative = { }, - type = BottomBarManager.BottomBarMessageType.THEMED, - showScrim = true, - ) - ) - } - - private fun confirmUserDemote(conversationId: ID, user: String?, userId: ID) { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = resources.getString( - R.string.title_demoteUserInRoom, - user.orEmpty().ifEmpty { "User" }), - subtitle = resources.getString(R.string.subtitle_demoteUserInRoom), - positiveText = resources.getString(R.string.action_demote), - negativeText = "", - tertiaryText = resources.getString(R.string.action_cancel), - onPositive = { dispatchEvent(Event.DemoteUser(conversationId, userId)) }, - onNegative = { }, - type = BottomBarManager.BottomBarMessageType.DESTRUCTIVE, - showScrim = true, - ) - ) - } - - private fun requestNotificationPermissionsIfNeeded(): Boolean { - val isDenied = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - permissionChecker.isDenied(Manifest.permission.POST_NOTIFICATIONS) - } else { - false - } - - val channel = - notificationManager.getNotificationChannel(FcNotificationType.ChatMessage().name) - val isChannelOff = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - channel?.importance == NotificationManager.IMPORTANCE_NONE - } else { - false - } - - val show = isDenied || isChannelOff - - return show - } - - companion object { - private fun sortMembers(members: List): Map> { - return members - .groupBy { it.canSpeak } - .mapKeys { - if (it.key) { - MemberType.Speaker - } else { - MemberType.Listener - } - } - .mapValues { (_, members) -> - members.sortedWith( - compareByDescending { it.isHost } - .thenByDescending { it.isSelf } - .thenByDescending { it.socialProfiles.any { it.isVerifiedOnPlatform } } - .thenByDescending { it.socialProfiles.isNotEmpty() } - ) - } - } - - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - (when (event) { - Event.LeaveRoom -> { state -> state } - is Event.OnInfoChanged -> { state -> - val args = event.args - state.copy( - isPreview = event.isPreview, - roomInfo = RoomInfo( - id = args.roomId, - number = args.roomNumber, - title = args.roomTitle.orEmpty(), - description = args.roomDescription.orEmpty(), - memberCount = args.memberCount, - hostId = args.ownerId, - hostName = args.hostName, - roomNumber = args.roomNumber, - messagingFee = Kin.fromQuarks(args.messagingFeeQuarks) - ) - ) - } - - is Event.PromoteRequested, - is Event.PromoteUser, - is Event.DemoteRequested, - is Event.DemoteUser, - is Event.OnChangeMessageFee, - Event.OnLeaveRoomConfirmed, - is Event.OnChangeName, - is Event.OnChangeDescription, - is Event.OnShareRoomClicked, - is Event.ShareRoom, - is Event.OnListenToClicked, - is Event.OnStartListening, - is Event.OnBecameMember, - is Event.OnOpenStateChangedRequested, - is Event.OnCloseRoom, - is Event.OnOpenRoom, - is Event.LookupRoom, - is Event.OpenRoom, - is Event.OpenRoomPreview, - is Event.RequestNotificationPermissions, - Event.OnLeftRoom -> { state -> state } - - is Event.OnUserPromoted -> { state -> - val members = state.members.flatMap { it.value } - val updatedMembers = members.map { - if (it.id == event.id) { - it.copy(canSpeak = true) - } else { - it - } - } - - state.copy(members = sortMembers(updatedMembers)) - } - - is Event.OnUserDemoted -> { state -> - val members = state.members.flatMap { it.value } - val updatedMembers = members.map { - if (it.id == event.id) { - it.copy(canSpeak = false) - } else { - it - } - } - - state.copy(members = sortMembers(updatedMembers)) - } - - is Event.OnUserProfilesEnabled -> { state -> state.copy(canViewUserProfile = event.enabled) } - - is Event.OnHostStatusChanged -> { state -> state.copy(isHost = event.isHost) } - is Event.OnFeeChanged -> { state -> - state.copy( - roomInfo = state.roomInfo.copy( - messagingFee = event.cover, - ) - ) - } - - is Event.OnNameChanged -> { state -> - state.copy( - roomInfo = state.roomInfo.copy( - title = event.name, - ) - ) - } - - is Event.OnMembersUpdated -> { state -> - val groupedMembers = sortMembers(event.members) - - state.copy( - roomInfo = state.roomInfo.copy( - memberCount = event.members.count(), - ), - isMember = event.members.any { it.isSelf }, - members = groupedMembers, - ) - } - - is Event.OnRoomNameChangesEnabled -> { state -> - state.copy( - roomNameChangesEnabled = event.enabled - ) - } - - is Event.OnDestinationChanged -> { state -> state.copy(paymentDestination = event.destination) } - is Event.OnJoiningStateChanged -> { state -> - state.copy( - joining = state.joining.copy( - loading = event.joining, - success = event.joined - ) - ) - } - - is Event.OnLeavingStateChanged -> { state -> - state.copy( - leaving = state.joining.copy( - loading = event.leaving, - success = event.left - ) - ) - } - - is Event.OnRoomOpenStateChanged -> { state -> state.copy(isOpen = event.isOpen) } - is Event.OnDescriptionChanged -> { state -> - state.copy( - roomInfo = state.roomInfo.copy( - description = event.description, - ) - ) - } - - is Event.OnRoomDescriptionChangesEnabled -> { state -> - state.copy(roomDescriptionChangesEnabled = event.enabled) - } - }) - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/info/RoomControlAction.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/info/RoomControlAction.kt deleted file mode 100644 index c89023127..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/info/RoomControlAction.kt +++ /dev/null @@ -1,72 +0,0 @@ -package xyz.flipchat.app.features.chat.info - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.Logout -import androidx.compose.material.icons.outlined.Bedtime -import androidx.compose.material.icons.outlined.Description -import androidx.compose.material.icons.outlined.LightMode -import androidx.compose.material.icons.outlined.Title -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import com.getcode.ui.core.ContextMenuAction -import xyz.flipchat.app.R - - -sealed interface RoomControlAction : ContextMenuAction { - data class MessageFee(override val onSelect: () -> Unit) : RoomControlAction, ContextMenuAction.Single { - override val isDestructive: Boolean = false - override val delayUponSelection: Boolean = true - override val title: String - @Composable get() = stringResource(R.string.action_changeMessagingFee) - override val painter: Painter - @Composable get() = painterResource(R.drawable.ic_kin_white_small) - } - - data class CloseRoom(override val onSelect: () -> Unit) : RoomControlAction, ContextMenuAction.Single { - override val isDestructive: Boolean = false - override val delayUponSelection: Boolean = true - override val title: String - @Composable get() = stringResource(R.string.action_closeFlipchatTemporarily) - override val painter: Painter - @Composable get() = rememberVectorPainter(Icons.Outlined.Bedtime) - } - - data class OpenRoom(override val onSelect: () -> Unit) : RoomControlAction, ContextMenuAction.Single { - override val isDestructive: Boolean = false - override val delayUponSelection: Boolean = true - override val title: String - @Composable get() = stringResource(R.string.action_reopenFlipchat) - override val painter: Painter - @Composable get() = rememberVectorPainter(Icons.Outlined.LightMode) - } - - data class LeaveRoom(override val onSelect: () -> Unit) : RoomControlAction, ContextMenuAction.Single { - override val isDestructive: Boolean = false - override val delayUponSelection: Boolean = true - override val title: String - @Composable get() = stringResource(R.string.action_leaveRoom) - override val painter: Painter - @Composable get() = rememberVectorPainter(Icons.AutoMirrored.Outlined.Logout) - } - - data class ChangeName(override val onSelect: () -> Unit) : RoomControlAction, ContextMenuAction.Single { - override val isDestructive: Boolean = false - override val delayUponSelection: Boolean = true - override val title: String - @Composable get() = stringResource(R.string.action_changeRoomName) - override val painter: Painter - @Composable get() = rememberVectorPainter(Icons.Outlined.Title) - } - - data class ChangeDescription(override val onSelect: () -> Unit) : RoomControlAction, ContextMenuAction.Single { - override val isDestructive: Boolean = false - override val delayUponSelection: Boolean = true - override val title: String - @Composable get() = stringResource(R.string.action_changeRoomDescription) - override val painter: Painter - @Composable get() = rememberVectorPainter(Icons.Outlined.Description) - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/info/RoomInfoScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/info/RoomInfoScreen.kt deleted file mode 100644 index 730b34951..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/info/RoomInfoScreen.kt +++ /dev/null @@ -1,628 +0,0 @@ -package xyz.flipchat.app.features.chat.info - -import android.os.Parcelable -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.Crossfade -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.Image -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.GridItemSpan -import androidx.compose.foundation.lazy.grid.LazyGridState -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.lazy.grid.rememberLazyGridState -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Person -import androidx.compose.material.icons.outlined.BorderColor -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight.Companion.W500 -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.sp -import cafe.adriel.voyager.core.registry.ScreenRegistry -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.hilt.getViewModel -import com.getcode.manager.TopBarManager -import com.getcode.model.chat.MinimalMember -import xyz.flipchat.app.navigation.NavScreenProvider -import xyz.flipchat.app.navigation.RoomInfoArgs -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.ContextSheet -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.AppBarDefaults -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.components.chat.AvatarEndAction -import com.getcode.ui.components.chat.HostableAvatar -import com.getcode.ui.components.chat.messagecontents.LocalTextLayoutResult -import com.getcode.ui.components.chat.messagecontents.MarkupTouchHandler -import com.getcode.ui.components.text.markup.Markup -import com.getcode.ui.components.text.markup.MarkupTextHelper -import com.getcode.ui.components.user.social.MemberNameDisplay -import com.getcode.ui.core.ContextMenuAction -import com.getcode.ui.core.noRippleClickable -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.theme.CodeScaffold -import com.getcode.ui.core.verticalScrollStateGradient -import com.getcode.util.permissions.notificationPermissionCheck -import com.getcode.utils.base58 -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import xyz.flipchat.app.R -import xyz.flipchat.app.features.home.TabbedHomeScreen -import xyz.flipchat.app.util.dialNumber - -@Parcelize -class RoomInfoScreen( - private val info: RoomInfoArgs, - private val isPreview: Boolean, - private val returnToSender: Boolean -) : Screen, Parcelable { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @Composable - override fun Content() { - val viewModel = getViewModel() - val navigator = LocalCodeNavigator.current - val context = LocalContext.current - - var didRequest by remember { mutableStateOf(false) } - val notificationPermissionChecker = notificationPermissionCheck { - if (didRequest) { - viewModel.dispatchEvent(ChatInfoViewModel.Event.OnStartListening) - } - } - - LaunchedEffect(info) { - viewModel.dispatchEvent(ChatInfoViewModel.Event.OnInfoChanged(info, isPreview)) - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .onEach { - didRequest = true - notificationPermissionChecker(true) - }.launchIn(this) - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .onEach { - navigator.popUntil { it is TabbedHomeScreen } - }.launchIn(this) - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .map { it.roomId } - .onEach { - navigator.push(ScreenRegistry.get(NavScreenProvider.Room.Messages(it))) - }.launchIn(this) - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .map { it.roomId } - .onEach { - navigator.push( - ScreenRegistry.get(NavScreenProvider.Room.ChangeCover(it)), - delay = 100 - ) - }.launchIn(this) - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .onEach { - context.startActivity(it.intent) - }.launchIn(this) - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .onEach { delay(200) } - .onEach { - navigator.push( - ScreenRegistry.get( - NavScreenProvider.Room.ChangeName( - it.id, - it.title - ) - ) - ) - }.launchIn(this) - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .onEach { delay(200) } - .onEach { - navigator.push( - ScreenRegistry.get( - NavScreenProvider.Room.ChangeDescription( - it.id, - it.description - ) - ) - ) - }.launchIn(this) - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .map { it.roomId } - .onEach { - navigator.push(ScreenRegistry.get(NavScreenProvider.Room.Messages(it))) - }.launchIn(this) - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .map { it.roomInfoArgs } - .onEach { - navigator.push( - ScreenRegistry.get( - NavScreenProvider.Room.Preview(args = it, returnToSender = true) - ) - ) - }.launchIn(this) - } - - val state by viewModel.stateFlow.collectAsState() - - val goBack = { - if (!returnToSender) { - navigator.pop() - } else { - navigator.popUntil { it is TabbedHomeScreen } - } - } - - BackHandler { - goBack() - } - - val listState = rememberLazyGridState() - - val showTitle by remember(listState) { - derivedStateOf { listState.firstVisibleItemIndex >= 1 } - } - - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AppBarWithTitle( - title = { - AnimatedVisibility( - visible = showTitle, - enter = slideInVertically { it } + fadeIn(), - exit = fadeOut() + slideOutVertically { it } - ) { - AppBarDefaults.Title( - text = state.roomInfo.customTitle.ifEmpty { state.roomInfo.title }, - style = CodeTheme.typography.screenTitle.copy(fontSize = 18.sp) - ) - } - }, - leftIcon = { - AppBarDefaults.UpNavigation { goBack() } - }, - rightContents = { - if (state.isMember) { - AppBarDefaults.Settings { - navigator.show( - ContextSheet( - buildActions( - state, - viewModel::dispatchEvent - ) - ) - ) - } - } - } - ) - - RoomInfoScreenContent(listState, state, viewModel::dispatchEvent) - } - } -} - -@Composable -private fun RoomInfoScreenContent( - listState: LazyGridState, - state: ChatInfoViewModel.State, - dispatch: (ChatInfoViewModel.Event) -> Unit -) { - val navigator = LocalCodeNavigator.current - val context = LocalContext.current - val uriHandler = LocalUriHandler.current - - CodeScaffold( - bottomBar = { - if (!state.isMember) { - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset) - .navigationBarsPadding(), - buttonState = ButtonState.Filled, - text = stringResource(R.string.action_startListening), - ) { - dispatch(ChatInfoViewModel.Event.OnListenToClicked) - } - } - } - ) { padding -> - val speakers = remember(state.members) { - state.members.getOrDefault(MemberType.Speaker, emptyList()) - } - - val listeners = remember(state.members) { - state.members.getOrDefault(MemberType.Listener, emptyList()) - } - - val viewUserProfile = { member: MinimalMember -> - navigator.push(ScreenRegistry.get(NavScreenProvider.UserProfile(member.id!!))) - } - - LazyVerticalGrid( - modifier = Modifier - .padding(padding) - .navigationBarsPadding() - .verticalScrollStateGradient(listState, color = CodeTheme.colors.background), - state = listState, - columns = GridCells.Adaptive(CodeTheme.dimens.grid.x16), - contentPadding = PaddingValues( - top = CodeTheme.dimens.grid.x6, - start = CodeTheme.dimens.inset, - end = CodeTheme.dimens.inset, - bottom = CodeTheme.dimens.inset, - ), - horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), - ) { - item(span = { GridItemSpan(maxLineSpan) }) { - // header - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2) - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .noRippleClickable( - enabled = state.isHost - ) { - if (state.roomDescriptionChangesEnabled) { - navigator.show(ContextSheet(buildTextActions(state, dispatch))) - } else { - dispatch( - ChatInfoViewModel.Event.OnChangeName( - state.roomInfo.id!!, - state.roomInfo.customTitle - ) - ) - } - }, - contentAlignment = Alignment.Center - ) { - HostableAvatar( - size = CodeTheme.dimens.grid.x20, - imageData = state.roomInfo.imageUrl ?: state.roomInfo.id, - overlay = { - Image( - modifier = Modifier.size(CodeTheme.dimens.grid.x12), - painter = painterResource(R.drawable.ic_fc_chats), - colorFilter = ColorFilter.tint(Color.White), - contentDescription = null, - ) - }, - endAction = AvatarEndAction.Icon( - icon = rememberVectorPainter(Icons.Outlined.BorderColor), - contentColor = Color.White, - backgroundColor = CodeTheme.colors.indicator - ).takeIf { state.isHost }, - ) - } - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), - ) { - Text( - text = state.roomInfo.customTitle.ifEmpty { state.roomInfo.title }, - style = CodeTheme.typography.screenTitle, - color = CodeTheme.colors.textMain, - textAlign = TextAlign.Center - ) - if (!state.roomDescriptionChangesEnabled) { - Crossfade(state.isOpen) { open -> - Text( - text = if (open) "" else stringResource(R.string.subtitle_roomInfoRoomIsClosed), - style = CodeTheme.typography.caption, - color = CodeTheme.colors.textSecondary.copy(0.54f), - ) - } - } else { - InteractiveDescription(state, dispatch) - } - } - } - } - - if (state.isMember) { - item(span = { GridItemSpan(maxLineSpan) }) { - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(top = CodeTheme.dimens.grid.x4), - buttonState = ButtonState.Filled, - text = if (state.isHost) { - stringResource(R.string.action_shareRoomLinkAsHost) - } else { - stringResource(R.string.action_shareRoomLinkAsMember) - }, - ) { - dispatch(ChatInfoViewModel.Event.OnShareRoomClicked) - } - } - } - - item(span = { GridItemSpan(maxLineSpan) }) { - Text( - modifier = Modifier.padding(top = CodeTheme.dimens.grid.x3), - text = pluralStringResource( - R.plurals.title_roomInfoSpeakerCount, - speakers.count(), - speakers.count() - ), - style = CodeTheme.typography.screenTitle, - color = CodeTheme.colors.textMain - ) - } - - items(speakers, key = { it.id?.base58.orEmpty() }) { member -> - Column( - modifier = Modifier - .pointerInput(Unit) { - detectTapGestures( - onLongPress = if (state.isHost && !member.isSelf) { _ -> - dispatch(ChatInfoViewModel.Event.DemoteRequested(member)) - } else null, - onTap = if (state.canViewUserProfile) { _ -> - viewUserProfile(member) - } else { - null - } - ) - }.animateItem(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2) - ) { - HostableAvatar( - size = CodeTheme.dimens.grid.x15, - isHost = member.isHost, - imageData = member.imageData, - ) { - Image( - modifier = Modifier.size(CodeTheme.dimens.grid.x8), - imageVector = Icons.Default.Person, - colorFilter = ColorFilter.tint(Color.White), - contentDescription = null, - ) - } - MemberNameDisplay(member) - } - } - - item(span = { GridItemSpan(maxLineSpan) }) { - Text( - modifier = Modifier.padding(top = CodeTheme.dimens.grid.x4), - text = pluralStringResource( - R.plurals.title_roomInfoListenerCount, - listeners.count(), - listeners.count() - ), - style = CodeTheme.typography.screenTitle, - color = CodeTheme.colors.textMain - ) - } - - items(listeners, key = { it.id?.base58.orEmpty() }) { member -> - Column( - modifier = Modifier - .pointerInput(Unit) { - detectTapGestures( - onLongPress = if (state.isHost && !member.isSelf) { _ -> - dispatch(ChatInfoViewModel.Event.PromoteRequested(member)) - } else null, - onTap = if (state.canViewUserProfile) { _ -> - viewUserProfile(member) - } else { - null - } - ) - }.animateItem(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2) - ) { - HostableAvatar( - size = CodeTheme.dimens.grid.x15, - isHost = member.isHost, - imageData = member.imageData - ) { - Image( - modifier = Modifier.size(CodeTheme.dimens.grid.x8), - imageVector = Icons.Default.Person, - colorFilter = ColorFilter.tint(Color.White), - contentDescription = null, - ) - } - MemberNameDisplay(member) - } - } - } - } -} - -@Composable -private fun InteractiveDescription( - state: ChatInfoViewModel.State, - dispatch: (ChatInfoViewModel.Event) -> Unit -) { - val uriHandler = LocalUriHandler.current - val context = LocalContext.current - Crossfade(state.roomInfo.description) { text -> - MarkupTouchHandler( - onMarkupClicked = { markup -> - when (markup) { - is Markup.Phone -> context.dialNumber(markup.phoneNumber) - is Markup.RoomNumber -> dispatch(ChatInfoViewModel.Event.LookupRoom(markup.number)) - is Markup.Url -> { - runCatching { - uriHandler.openUri(markup.link) - }.onFailure { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = context.getString(R.string.error_title_failedToOpenLink), - message = context.getString(R.string.error_description_failedToOpenLink) - ) - ) - } - } - } - - } - ) { onTap -> - val markupTextHelper = remember { MarkupTextHelper() } - val markups = state.descriptionMarkups.map { Markup.create(it) } - - val annotatedString = markupTextHelper.annotate(text, markups) - val markupHoist = LocalTextLayoutResult.current - - Text( - modifier = Modifier - .padding(horizontal = CodeTheme.dimens.inset) - .pointerInput(Unit) { - detectTapGestures( - onTap = { offset -> onTap(PaddingValues(), offset) }, - ) - }, - text = annotatedString, - style = CodeTheme.typography.textSmall.copy(fontWeight = W500), - textAlign = TextAlign.Center, - color = CodeTheme.colors.textSecondary.copy(0.54f), - onTextLayout = { markupHoist(it) } - ) - } - } -} - -private fun buildTextActions( - state: ChatInfoViewModel.State, - dispatch: (ChatInfoViewModel.Event) -> Unit, -): List { - return buildList { - if (state.isHost) { - add( - RoomControlAction.ChangeName { - dispatch(ChatInfoViewModel.Event.OnChangeName(state.roomInfo.id!!, state.roomInfo.customTitle)) - } - ) - - if (state.roomDescriptionChangesEnabled) { - add( - RoomControlAction.ChangeDescription { - dispatch(ChatInfoViewModel.Event.OnChangeDescription(state.roomInfo.id!!, state.roomInfo.description)) - } - ) - } - } - } -} - -private fun buildActions( - state: ChatInfoViewModel.State, - dispatch: (ChatInfoViewModel.Event) -> Unit, -): List { - return buildList { - if (state.isHost) { - add( - RoomControlAction.MessageFee { - dispatch(ChatInfoViewModel.Event.OnChangeMessageFee(state.roomInfo.id!!)) - } - ) - if (state.isOpen) { - add( - RoomControlAction.CloseRoom { - dispatch(ChatInfoViewModel.Event.OnOpenStateChangedRequested) - } - ) - } else { - add( - RoomControlAction.OpenRoom { - dispatch(ChatInfoViewModel.Event.OnOpenStateChangedRequested) - } - ) - } - } - - add( - RoomControlAction.LeaveRoom { - dispatch(ChatInfoViewModel.Event.LeaveRoom) - } - ) - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/list/ChatListViewModel.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/list/ChatListViewModel.kt deleted file mode 100644 index cb11314de..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/list/ChatListViewModel.kt +++ /dev/null @@ -1,224 +0,0 @@ -package xyz.flipchat.app.features.chat.list - -import androidx.lifecycle.viewModelScope -import androidx.paging.PagingData -import androidx.paging.cachedIn -import androidx.paging.map -import com.getcode.manager.TopBarManager -import com.getcode.model.ID -import com.getcode.model.Kin -import com.getcode.model.kin -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.network.NetworkConnectivityListener -import com.getcode.view.BaseViewModel2 -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.filterNot -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import xyz.flipchat.app.R -import xyz.flipchat.app.features.login.register.onError -import xyz.flipchat.controllers.ChatsController -import xyz.flipchat.services.domain.model.chat.ConversationWithMembersAndLastMessage -import xyz.flipchat.services.extensions.titleOrFallback -import xyz.flipchat.services.user.AuthState -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject - -@HiltViewModel -class ChatListViewModel @Inject constructor( - userManager: UserManager, - private val chatsController: ChatsController, - networkObserver: NetworkConnectivityListener, - resources: ResourceHelper, -) : BaseViewModel2( - initialState = State(userManager.userId), - updateStateForEvent = updateStateForEvent -) { - data class State( - val selfId: ID? = null, - val showScrim: Boolean = false, - val showFullscreenSpinner: Boolean = false, - val networkConnected: Boolean = true, - val chatTapCount: Int = 0, - val isLoggedIn: Boolean = false, - val isLogOutEnabled: Boolean = false, - val createRoomCost: Kin = 0.kin, - ) - - sealed interface Event { - data class OnSelfIdChanged(val id: ID?) : Event - data class OnLoggedInStateChanged(val loggedIn: Boolean) : Event - data class ShowFullScreenSpinner( - val showScrim: Boolean = true, - val showSpinner: Boolean = true - ) : - Event - - data class OnCreateCostChanged(val cost: Kin): Event - data class OnNetworkChanged(val connected: Boolean) : Event - data object OnOpen : Event - data object CreateRoomSelected : Event - data object CreateRoom : Event - data object NeedsAccountCreated : Event - data object OnAccountCreated : Event - data class OpenRoom(val roomId: ID) : Event - data object OnChatsTapped : Event - data class MuteRoom(val roomId: ID) : Event - data class UnmuteRoom(val roomId: ID) : Event - data object OnLogOutUnlocked : Event - } - - init { - userManager.state - .map { it.userId } - .distinctUntilChanged() - .onEach { dispatchEvent(Event.OnSelfIdChanged(it)) } - .launchIn(viewModelScope) - - userManager.state - .mapNotNull { it.flags } - .map { it.createCost } - .onEach { dispatchEvent(Event.OnCreateCostChanged(it)) } - .launchIn(viewModelScope) - - userManager.state - .map { it.authState } - .distinctUntilChanged() - .onEach { dispatchEvent(Event.OnLoggedInStateChanged(it is AuthState.LoggedIn)) } - .launchIn(viewModelScope) - - networkObserver.state - .map { it.connected } - .distinctUntilChanged() - .onEach { dispatchEvent(Event.OnNetworkChanged(it)) } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { stateFlow.value.chatTapCount } - .filter { it >= TAP_THRESHOLD } - .filterNot { stateFlow.value.isLogOutEnabled } - .onEach { dispatchEvent(Event.OnLogOutUnlocked) } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.roomId } - .map { chatsController.muteRoom(it) } - .onError { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToMuteChat), - resources.getString(R.string.error_description_failedToMuteChat) - ) - ) - } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.roomId } - .map { chatsController.unmuteRoom(it) } - .onError { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToUnmuteChat), - resources.getString(R.string.error_description_failedToUnmuteChat) - ) - ) - } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { userManager.authState } - .onEach { - if (it is AuthState.LoggedIn) { - dispatchEvent(Event.CreateRoom) - } else { - dispatchEvent(Event.NeedsAccountCreated) - } - } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .onEach { delay(400) } - .onEach { dispatchEvent(Event.CreateRoom) } - .launchIn(viewModelScope) - } - - val chats: Flow> = - userManager.state - .map { it.authState } - .map { it.canOpenChatStream() } - .distinctUntilChanged() - .flatMapLatest { canOpen -> - if (canOpen) { - chatsController.chats.flow - } else { - flowOf(PagingData.empty()) - } - }.map { page -> - page.map { - it.copy( - conversation = it.conversation.copy( - title = it.conversation.titleOrFallback( - resources = resources, - ) - ) - ) - } - } - .cachedIn(viewModelScope) - - companion object { - private const val TAP_THRESHOLD = 6 - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - is Event.OnNetworkChanged -> { state -> - state.copy(networkConnected = event.connected) - } - - is Event.OnCreateCostChanged -> { state -> - state.copy(createRoomCost = event.cost) - } - - is Event.OnChatsTapped -> { state -> - if (state.chatTapCount >= TAP_THRESHOLD) state - else state.copy(chatTapCount = state.chatTapCount + 1) - } - - is Event.OnLogOutUnlocked -> { state -> state.copy(isLogOutEnabled = true) } - is Event.OpenRoom -> { state -> state } - is Event.ShowFullScreenSpinner -> { state -> - state.copy( - showFullscreenSpinner = event.showSpinner, - showScrim = event.showScrim - ) - } - - is Event.OnSelfIdChanged -> { state -> state.copy(selfId = event.id) } - - is Event.OnLoggedInStateChanged -> { state -> state.copy(isLoggedIn = event.loggedIn) } - - is Event.NeedsAccountCreated, - is Event.OnAccountCreated, - is Event.OnOpen, - is Event.CreateRoomSelected, - is Event.CreateRoom, - is Event.MuteRoom, - is Event.UnmuteRoom -> { state -> state } - } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/list/ChatNode.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/list/ChatNode.kt deleted file mode 100644 index 408b10a3e..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/list/ChatNode.kt +++ /dev/null @@ -1,189 +0,0 @@ -package xyz.flipchat.app.features.chat.list - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.InlineTextContent -import androidx.compose.material.DismissDirection -import androidx.compose.material.DismissState -import androidx.compose.material.DismissValue -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.FixedThreshold -import androidx.compose.material.SwipeToDismiss -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.VolumeOff -import androidx.compose.material.icons.automirrored.filled.VolumeUp -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.navigator.currentOrThrow -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.chat.utils.localizedText -import com.getcode.util.vibration.LocalVibrator -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filter -import xyz.flipchat.app.R -import xyz.flipchat.app.ui.LocalUserManager -import xyz.flipchat.services.domain.model.chat.ConversationWithMembersAndLastMessage - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun ChatNode( - chat: ConversationWithMembersAndLastMessage, - modifier: Modifier = Modifier, - onToggleMute: (mute: Boolean) -> Unit = { }, - onClick: () -> Unit, -) { - val userManager = LocalUserManager.currentOrThrow - - val dismissState = rememberChatDismissState({ chat.isMuted }, onToggleMute) - - LaunchedEffect(dismissState) { - snapshotFlow { dismissState.currentValue } - .filter { it == DismissValue.DismissedToStart } - .collect { - dismissState.animateTo(DismissValue.Default) - } - } - - var muteContentState by remember { mutableStateOf(chat.isMuted) } - - LaunchedEffect(chat.id, chat.isMuted) { - delay(400) - muteContentState = chat.isMuted - } - - SwipeToDismiss( - state = dismissState, - dismissThresholds = { FixedThreshold(150.dp) }, - directions = if (chat.canChangeMuteState) setOf(DismissDirection.EndToStart) else emptySet(), - background = { - if (chat.canChangeMuteState) { - DismissBackground(dismissState, muteContentState) - } - } - ) { - com.getcode.ui.components.chat.ChatNode( - modifier = modifier.background(CodeTheme.colors.background), - title = chat.title, - messagePreview = chat.messagePreview, - messageTextStyle = CodeTheme.typography.textSmall, - messageMinLines = 2, - avatar = chat.imageUri ?: chat.id, - avatarIconWhenFallback = { - Image( - modifier = Modifier.padding(5.dp), - painter = painterResource(R.drawable.ic_fc_chats), - colorFilter = ColorFilter.tint(Color.White), - contentDescription = null, - ) - }, - timestamp = chat.lastMessage?.dateMillis, - isMuted = muteContentState, - isHost = chat.ownerId == userManager.userId, - unreadCount = chat.unreadCount, - showMoreUnread = chat.hasMoreUnread, - onClick = onClick - ) - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun DismissBackground(dismissState: DismissState, isMuted: Boolean) { - val color = when (dismissState.dismissDirection) { - DismissDirection.EndToStart -> Color(0xFF251B4C) - else -> CodeTheme.colors.background - } - val direction = dismissState.dismissDirection - - Row( - modifier = Modifier - .fillMaxSize() - .background(color) - .padding(end = CodeTheme.dimens.inset), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End - ) { - if (direction == DismissDirection.EndToStart) { - Image( - imageVector = if (isMuted) Icons.AutoMirrored.Filled.VolumeUp else Icons.AutoMirrored.Filled.VolumeOff, - colorFilter = ColorFilter.tint(Color.White), - contentDescription = null - ) - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun rememberChatDismissState( - isChatMuted: () -> Boolean, - onToggleMute: (mute: Boolean) -> Unit -): DismissState { - val mutedState by rememberUpdatedState(isChatMuted()) - val vibrator = LocalVibrator.current - return remember { - DismissState( - initialValue = DismissValue.Default, - confirmStateChange = { - if (it == DismissValue.DismissedToStart) { - onToggleMute(!mutedState) - vibrator.tick() - true - } else false - } - ) - } -} - -private val ConversationWithMembersAndLastMessage.messagePreview: Pair> - @Composable get() { - val user = LocalUserManager.currentOrThrow - val contents = messageContentPreview ?: return AnnotatedString("No content") to emptyMap() - - val messageBody = buildAnnotatedString { - if (lastMessage?.isDeleted == true) { - when { - user.isSelf(lastMessage?.deletedBy) -> { - pushStyle(SpanStyle(fontStyle = FontStyle.Italic)) - append(stringResource(R.string.title_messageDeletedByYou)) - pop() - } - ownerId == lastMessage?.deletedBy -> { - pushStyle(SpanStyle(fontStyle = FontStyle.Italic)) - append(stringResource(R.string.title_messageDeletedByHost)) - pop() - } - else -> { - pushStyle(SpanStyle(fontStyle = FontStyle.Italic)) - append(stringResource(R.string.title_messageWasDeleted)) - pop() - } - } - } else { - append(contents.localizedText) - } - } - - return messageBody to emptyMap() - } \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/list/RoomListScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/list/RoomListScreen.kt deleted file mode 100644 index bd3f0c32b..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/list/RoomListScreen.kt +++ /dev/null @@ -1,220 +0,0 @@ -package xyz.flipchat.app.features.chat.list - -import android.os.Parcelable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.Snapshot -import androidx.compose.ui.Alignment.Companion.CenterHorizontally -import androidx.compose.ui.Alignment.Companion.CenterVertically -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.paging.LoadState -import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.compose.itemContentType -import cafe.adriel.voyager.core.registry.ScreenRegistry -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.navigator.currentOrThrow -import com.getcode.model.ID -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.extensions.getActivityScopedViewModel -import com.getcode.navigation.screens.AppScreen -import com.getcode.navigation.screens.NamedScreen -import com.getcode.navigation.screens.OnScreenResult -import com.getcode.theme.CodeTheme -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.theme.CodeCircularProgressIndicator -import com.getcode.ui.theme.CodeScaffold -import com.getcode.ui.core.addIf -import com.getcode.util.resources.LocalResources -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import xyz.flipchat.app.R -import xyz.flipchat.app.features.chat.openChatDirectiveBottomModal - -@Parcelize -class RoomListScreen : AppScreen(), NamedScreen, Parcelable { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(id = R.string.title_chat) - - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - val viewModel = getActivityScopedViewModel() - ChatListScreenContent( - viewModel = viewModel, - openChat = { - navigator.push(ScreenRegistry.get(NavScreenProvider.Room.Messages(chatId = it))) - } - ) - - OnScreenResult { - if (it is List<*>) { - val roomId = runCatching { it as? ID }.getOrNull() - if (roomId != null) { - delay(400) - navigator.push(ScreenRegistry.get(NavScreenProvider.Room.Messages(chatId = roomId))) - } - } else if (it is Boolean) { - if (it) { - viewModel.dispatchEvent(ChatListViewModel.Event.OnAccountCreated) - } - } - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .onEach { - navigator.show(ScreenRegistry.get(NavScreenProvider.CreateAccount.Start)) - }.launchIn(this) - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .onEach { - navigator.show(ScreenRegistry.get(NavScreenProvider.Room.Create)) - }.launchIn(this) - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .onEach { - navigator.push(ScreenRegistry.get(NavScreenProvider.Room.Messages(it.roomId))) - }.launchIn(this) - } - } -} - -@Composable -private fun ChatListScreenContent( - viewModel: ChatListViewModel, - openChat: (ID) -> Unit, -) { - val navigator = LocalCodeNavigator.current - val resources = LocalResources.currentOrThrow - val state by viewModel.stateFlow.collectAsState() - val chats = viewModel.chats.collectAsLazyPagingItems() - val isLoading = chats.loadState.refresh is LoadState.Loading - var isInitialLoad by rememberSaveable { mutableStateOf(true) } - val listState = rememberLazyListState() - - CodeScaffold { padding -> - LazyColumn( - modifier = Modifier.fillMaxSize().padding(padding), - contentPadding = PaddingValues(bottom = CodeTheme.dimens.inset), - state = listState - ) { - items( - count = chats.itemCount, -// key = chats.itemKey { it.id }, - contentType = chats.itemContentType { "chat" } - ) { index -> - chats[index]?.let { - Column { - ChatNode( - chat = it, - onToggleMute = { mute -> - if (mute) { - viewModel.dispatchEvent(ChatListViewModel.Event.MuteRoom(it.id)) - } else { - viewModel.dispatchEvent(ChatListViewModel.Event.UnmuteRoom(it.id)) - } - }, - ) { openChat(it.conversation.id) } - } - } - } - - when { - isLoading && isInitialLoad -> { - item { - Column( - modifier = Modifier.fillParentMaxSize(), - horizontalAlignment = CenterHorizontally, - verticalArrangement = Arrangement.spacedBy( - CodeTheme.dimens.grid.x2, - CenterVertically - ), - ) { - CodeCircularProgressIndicator() - Text( - modifier = Modifier.fillMaxWidth(0.6f), - text = stringResource(R.string.subtitle_loadingChats), - textAlign = TextAlign.Center - ) - } - } - } - - else -> { - item { - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(top = CodeTheme.dimens.grid.x6) - .padding(horizontal = CodeTheme.dimens.inset) - .addIf(!state.isLoggedIn) { Modifier.navigationBarsPadding() }, - buttonState = ButtonState.Filled, - text = stringResource(R.string.action_findRoom) - ) { - openChatDirectiveBottomModal( - resources = resources, - createCost = state.createRoomCost, - viewModel = viewModel, - navigator = navigator - ) - } - } - } - } - - // opts out of the list maintaining - // scroll position when adding elements before the first item - // we are checking first visible item index to ensure - // the list doesn't shift when scrolled - Snapshot.withoutReadObservation { - if (listState.firstVisibleItemIndex == 0) { - listState.requestScrollToItem( - index = listState.firstVisibleItemIndex, - scrollOffset = listState.firstVisibleItemScrollOffset - ) - } - } - } - } - - LaunchedEffect(chats.loadState.refresh) { - if (chats.loadState.refresh !is LoadState.Loading || chats.itemCount == 0) { - isInitialLoad = false - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/lookup/LookupRoomScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/lookup/LookupRoomScreen.kt deleted file mode 100644 index 054285b19..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/lookup/LookupRoomScreen.kt +++ /dev/null @@ -1,119 +0,0 @@ -package xyz.flipchat.app.features.chat.lookup - -import android.os.Parcelable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import cafe.adriel.voyager.core.registry.ScreenRegistry -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.hilt.getViewModel -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.NamedScreen -import xyz.flipchat.app.R -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import xyz.flipchat.app.ui.AmountWithKeypad - -@Parcelize -class LookupRoomScreen : Screen, NamedScreen, Parcelable { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(R.string.title_lookupRoom) - - @Composable - override fun Content() { - val viewModel = getViewModel() - val navigator = LocalCodeNavigator.current - - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AppBarWithTitle( - title = name, - backButton = true, - onBackIconClicked = navigator::pop - ) - LookupRoomScreenContent(viewModel) - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .map { it.roomId } - .onEach { - navigator.push(ScreenRegistry.get(NavScreenProvider.Room.Messages(it))) - }.launchIn(this) - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .map { it.args } - .onEach { - navigator.push(ScreenRegistry.get(NavScreenProvider.Room.Info(it, returnToSender = true))) - }.launchIn(this) - } - } - - @Composable - private fun LookupRoomScreenContent( - viewModel: LookupRoomViewModel, - ) { - val state by viewModel.stateFlow.collectAsState() - - Column( - modifier = Modifier.fillMaxSize(), - ) { - AmountWithKeypad( - modifier = Modifier.weight(1f), - state.amountAnimatedModel, - prefix = "#", - hint = stringResource(R.string.subtitle_enterRoomNumber), - onNumberPressed = { viewModel.dispatchEvent(LookupRoomViewModel.Event.OnNumberPressed(it)) }, - onBackspace = { viewModel.dispatchEvent(LookupRoomViewModel.Event.OnBackspace) }, - ) - - Box(modifier = Modifier.fillMaxWidth()) { - CodeButton( - enabled = state.canLookup, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset) - .padding(bottom = CodeTheme.dimens.grid.x2) - .navigationBarsPadding(), - buttonState = ButtonState.Filled, - text = stringResource(R.string.action_next), - isLoading = state.lookingUp, - isSuccess = state.success, - ) { - viewModel.dispatchEvent(LookupRoomViewModel.Event.OnLookupRoom) - } - } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/lookup/LookupRoomViewModel.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/lookup/LookupRoomViewModel.kt deleted file mode 100644 index b38cf2605..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/lookup/LookupRoomViewModel.kt +++ /dev/null @@ -1,163 +0,0 @@ -package xyz.flipchat.app.features.chat.lookup - -import androidx.lifecycle.viewModelScope -import com.getcode.manager.TopBarManager -import com.getcode.model.ID -import xyz.flipchat.app.navigation.RoomInfoArgs -import xyz.flipchat.app.R -import xyz.flipchat.app.features.login.register.onResult -import com.getcode.ui.components.text.AmountAnimatedInputUiModel -import com.getcode.ui.components.text.NumberInputHelper -import com.getcode.util.resources.ResourceHelper -import com.flipcash.libs.coroutines.DispatcherProvider -import com.getcode.view.BaseViewModel2 -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import xyz.flipchat.controllers.ChatsController -import xyz.flipchat.services.extensions.titleOrFallback -import javax.inject.Inject - -@HiltViewModel -class LookupRoomViewModel @Inject constructor( - chatsController: ChatsController, - resources: ResourceHelper, - dispatchers: DispatcherProvider, -) : BaseViewModel2( - initialState = State(), - updateStateForEvent = updateStateForEvent, - dispatchers = dispatchers, -) { - private val numberInputHelper = NumberInputHelper() - - data class State( - val lookingUp: Boolean = false, - val success: Boolean = false, - val amountAnimatedModel: AmountAnimatedInputUiModel = AmountAnimatedInputUiModel( - amountData = NumberInputHelper.AmountAnimatedData("") - ), - val canLookup: Boolean = false, - ) - - sealed interface Event { - data class OnLookingUpRoom(val requesting: Boolean) : Event - data class OnNumberPressed(val number: Int) : Event - data object OnBackspace : Event - data class OnEnteredNumberChanged(val backspace: Boolean = false) : Event - data class OnRoomNumberChanged(val animatedInputUiModel: AmountAnimatedInputUiModel) : Event - data object OnLookupRoom : Event - data object OnRoomFound : Event - data class OnOpenConfirmation(val args: RoomInfoArgs) : Event - data class OpenExistingRoom(val roomId: ID) : Event - } - - init { - numberInputHelper.reset() - - eventFlow - .filterIsInstance() - .map { it.number } - .onEach { number -> - numberInputHelper.maxLength = 9 - numberInputHelper.onNumber(number) - dispatchEvent(Event.OnEnteredNumberChanged()) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .onEach { - numberInputHelper.onBackspace() - dispatchEvent(Event.OnEnteredNumberChanged(true)) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.backspace } - .onEach { backspace -> - val current = stateFlow.value.amountAnimatedModel - val model = stateFlow.value.amountAnimatedModel - val amount = numberInputHelper.getFormattedStringForAnimation(includeCommas = false) - - val updated = model.copy( - amountDataLast = current.amountData, - amountData = amount, - lastPressedBackspace = backspace - ) - - dispatchEvent(Event.OnRoomNumberChanged(updated)) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .onEach { dispatchEvent(Event.OnLookingUpRoom(true)) } - .map { stateFlow.value.amountAnimatedModel.amountData.amount.toLong() } - .map { chatsController.lookupRoom(it) } - .onResult( - onError = { - dispatchEvent(Event.OnLookingUpRoom(false)) - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToGetRoom), - resources.getString( - R.string.error_description_failedToGetRoom, - stateFlow.value.amountAnimatedModel.amountData.amount - ) - ) - ) - }, - onSuccess = { - val isExistingMember = it.members.any { m -> m.isSelf } - if (isExistingMember) { - dispatchEvent(Event.OnLookingUpRoom(false)) - dispatchEvent(Event.OnRoomFound) - viewModelScope.launch { - delay(400) - dispatchEvent(Event.OpenExistingRoom(it.room.id)) - } - } else { - val host = it.members.firstOrNull { m -> m.isModerator } - - val confirmJoinArgs = RoomInfoArgs( - roomId = it.room.id, - roomTitle = it.room.titleOrFallback(resources), - roomNumber = it.room.roomNumber, - memberCount = it.members.count(), - ownerId = it.room.ownerId, - hostName = host?.identity?.displayName, - messagingFeeQuarks = it.room.messagingFee.quarks - ) - dispatchEvent(Event.OnOpenConfirmation(confirmJoinArgs)) - } - } - ).launchIn(viewModelScope) - } - - - companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - is Event.OnRoomNumberChanged -> { state -> - val room = event.animatedInputUiModel.amountData.amount.toIntOrNull() - state.copy( - amountAnimatedModel = event.animatedInputUiModel, - canLookup = (room ?: 0) > 0 - ) - } - - Event.OnBackspace, - is Event.OnEnteredNumberChanged, - Event.OnLookupRoom, - is Event.OnOpenConfirmation, - is Event.OpenExistingRoom, - is Event.OnNumberPressed -> { state -> state } - - is Event.OnRoomFound -> { state -> state.copy(success = true) } - is Event.OnLookingUpRoom -> { state -> state.copy(lookingUp = event.requesting) } - } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/name/RoomNameScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/name/RoomNameScreen.kt deleted file mode 100644 index 4943302c0..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/name/RoomNameScreen.kt +++ /dev/null @@ -1,210 +0,0 @@ -package xyz.flipchat.app.features.chat.name - -import android.os.Parcelable -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.KeyboardType -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.hilt.getViewModel -import com.getcode.model.ID -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.theme.CodeTheme -import com.getcode.theme.inputColors -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.components.TextInput -import com.getcode.ui.core.rememberAnimationScale -import com.getcode.ui.core.scaled -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.theme.CodeScaffold -import com.getcode.ui.utils.ConstraintMode -import com.getcode.ui.utils.keyboardAsState -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import xyz.flipchat.app.R -import kotlin.time.Duration.Companion.seconds - -@Parcelize -data class RoomNameScreen(val roomId: ID, val customTitle: String) : Screen, Parcelable { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - val keyboardIsVisible by keyboardAsState() - val keyboard = LocalSoftwareKeyboardController.current - val scope = rememberCoroutineScope() - val animationScale by rememberAnimationScale() - val goBack = { - scope.launch { - if (keyboardIsVisible) { - keyboard?.hide() - delay(500.scaled(animationScale)) - } - navigator.pop() - } - } - BackHandler { - goBack() - } - - Column( - modifier = Modifier - .fillMaxSize() - ) { - AppBarWithTitle( - backButton = true, - title = stringResource(R.string.action_changeRoomName), - onBackIconClicked = { goBack() }, - ) - - val viewModel = getViewModel() - - LaunchedEffect(viewModel, roomId, customTitle) { - viewModel.dispatchEvent( - RoomNameScreenViewModel.Event.OnNewRequest( - roomId, - customTitle - ) - ) - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .onEach { - delay(2.seconds) - navigator.pop() - } - .launchIn(this) - } - - val state by viewModel.stateFlow.collectAsState() - RoomNameScreenContent( - isUpdate = true, - state = state, - dispatch = viewModel::dispatchEvent - ) - } - } -} - -@Composable -internal fun RoomNameScreenContent( - isUpdate: Boolean, - state: RoomNameScreenViewModel.State, - dispatch: (RoomNameScreenViewModel.Event) -> Unit, -) { - val keyboard = LocalSoftwareKeyboardController.current - val focusRequester = remember { FocusRequester() } - - CodeScaffold( - modifier = Modifier - .fillMaxSize() - .imePadding(), - bottomBar = { - Box( - modifier = Modifier - .fillMaxWidth() - .navigationBarsPadding() - .padding(bottom = CodeTheme.dimens.grid.x3), - ) { - CodeButton( - enabled = state.canCheck || state.previousRoomName.isNotEmpty(), // allow resets - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset), - buttonState = ButtonState.Filled, - text = if (isUpdate) stringResource(R.string.action_save) else stringResource(R.string.action_next), - isLoading = state.update.loading, - isSuccess = state.update.success - ) { - keyboard?.hide() - if (isUpdate) { - dispatch(RoomNameScreenViewModel.Event.UpdateName) - } else { - dispatch(RoomNameScreenViewModel.Event.CreateRoom) - } - focusRequester.freeFocus() - } - } - } - ) { padding -> - Box( - modifier = Modifier - .fillMaxSize() - .padding(padding) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterStart) - .padding(horizontal = CodeTheme.dimens.inset), - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.inset) - ) { - TextInput( - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - state = state.textFieldState, - colors = inputColors( - backgroundColor = Color.Transparent, - borderColor = Color.Transparent - ), - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Password, - capitalization = KeyboardCapitalization.Sentences - ), - maxLines = 1, - constraintMode = ConstraintMode.AutoSize(minimum = CodeTheme.typography.displaySmall), - style = CodeTheme.typography.displayMedium, - placeholderStyle = CodeTheme.typography.displayMedium, - placeholder = stringResource(R.string.subtitle_roomName), - ) - - Text( - text = stringResource(R.string.subtitle_roomNameHint), - style = CodeTheme.typography.textMedium, - color = Color.White.copy(0.4f) - ) - } - } - - LaunchedEffect(focusRequester) { - focusRequester.requestFocus() - } - } -} diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/name/RoomNameScreenViewModel.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/name/RoomNameScreenViewModel.kt deleted file mode 100644 index 55b3606f7..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/chat/name/RoomNameScreenViewModel.kt +++ /dev/null @@ -1,266 +0,0 @@ -package xyz.flipchat.app.features.chat.name - -import androidx.compose.foundation.text.input.TextFieldState -import androidx.lifecycle.viewModelScope -import com.getcode.manager.TopBarManager -import com.getcode.model.ID -import com.getcode.model.KinAmount -import com.getcode.model.NoId -import com.getcode.services.model.ExtendedMetadata -import com.getcode.services.utils.onSuccessWithDelay -import com.getcode.util.resources.ResourceHelper -import com.flipcash.libs.coroutines.DispatcherProvider -import com.getcode.view.BaseViewModel2 -import com.getcode.view.LoadingSuccessState -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.take -import xyz.flipchat.app.R -import xyz.flipchat.chat.RoomController -import xyz.flipchat.controllers.ChatsController -import xyz.flipchat.controllers.ProfileController -import xyz.flipchat.services.PaymentController -import xyz.flipchat.services.PaymentEvent -import xyz.flipchat.services.data.metadata.StartGroupChatPaymentMetadata -import xyz.flipchat.services.data.metadata.erased -import xyz.flipchat.services.data.metadata.typeUrl -import xyz.flipchat.services.internal.network.service.CheckDisplayNameError -import xyz.flipchat.services.internal.network.service.SetRoomDisplayNameError -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds - -@HiltViewModel -internal class RoomNameScreenViewModel @Inject constructor( - userManager: UserManager, - roomController: RoomController, - chatsController: ChatsController, - paymentController: PaymentController, - profileController: ProfileController, - resources: ResourceHelper, - dispatchers: DispatcherProvider, -) : BaseViewModel2( - initialState = State(), - updateStateForEvent = updateStateForEvent, - dispatchers = dispatchers, -) { - data class State( - val roomId: ID = NoId, - val previousRoomName: String = "", - val update: LoadingSuccessState = LoadingSuccessState(), - val textFieldState: TextFieldState = TextFieldState(""), - ) { - val canCheck: Boolean - get() = textFieldState.text.isNotEmpty() - } - - sealed interface Event { - data class OnNewRequest(val id: ID, val title: String) : Event - data object UpdateName : Event - data object CreateRoom: Event - data object PromptForPayment : Event - data object OnSuccess : Event - data class OpenRoom(val roomId: ID) : Event - data object OnError : Event - } - - init { - eventFlow - .filterIsInstance() - .map { stateFlow.value } - .onEach { - val textFieldState = it.textFieldState - val text = textFieldState.text.toString().trim() - - roomController.setDisplayName(it.roomId, text) - .onFailure { error -> - if (error is SetRoomDisplayNameError.CantSet) { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = resources.getString(R.string.error_title_failedToChangeRoomNameBecauseInappropriate), - message = resources.getString(R.string.error_description_failedToChangeRoomNameBecauseInappropriate) - ) - ) - } else { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = resources.getString(R.string.error_title_failedToChangeRoomNameOtherReason), - message = resources.getString(R.string.error_description_failedToChangeRoomNameOtherReason) - ) - ) - } - - dispatchEvent(Event.OnError) - } - .onSuccessWithDelay(2.seconds) { - dispatchEvent(Event.OnSuccess) - } - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { stateFlow.value } - .onEach { - val textFieldState = it.textFieldState - val text = textFieldState.text.toString().trim() - - chatsController.checkDisplayNameForRoom(text) - .onFailure { error -> - if (error is CheckDisplayNameError.CantSet) { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = resources.getString(R.string.error_title_failedToChangeRoomNameBecauseInappropriate), - message = resources.getString(R.string.error_description_failedToChangeRoomNameBecauseInappropriate) - ) - ) - } else { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = resources.getString(R.string.error_title_failedToCreateRoom), - message = resources.getString(R.string.error_description_failedToCreateRoom) - ) - ) - } - dispatchEvent(Event.OnError) - } - .onSuccess { - dispatchEvent(Event.PromptForPayment) - } - }.launchIn(viewModelScope) - - eventFlow.filterIsInstance() - .map { profileController.getUserFlags() } - .mapNotNull { - it.exceptionOrNull()?.let { error -> - error.printStackTrace() - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = resources.getString(R.string.error_title_failedToCreateRoom), - message = resources.getString(R.string.error_description_failedToCreateRoom) - ) - ) - dispatchEvent(Event.OnError) - return@mapNotNull null - } - - it.getOrNull()?.let { flags -> - val startGroupMetadata = StartGroupChatPaymentMetadata( - userId = userManager.userId!! - ) - - val metadata = ExtendedMetadata.Any( - data = startGroupMetadata.erased(), - typeUrl = startGroupMetadata.typeUrl - ) - - val amount = - KinAmount.fromQuarks(flags.createCost.quarks) - - paymentController.presentPublicPaymentConfirmation( - amount = amount, - destination = flags.feeDestination, - metadata = metadata - ) - } - }.flatMapLatest { - paymentController.eventFlow.take(1) - }.onEach { event -> - when (event) { - PaymentEvent.OnPaymentCancelled -> Unit - is PaymentEvent.OnPaymentError -> Unit - - is PaymentEvent.OnPaymentSuccess -> { - chatsController.createGroup( - title = stateFlow.value.textFieldState.text.toString().trim(), - participants = emptyList(), - paymentId = event.intentId - ).onFailure { - event.acknowledge(false) { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToCreateRoom), - resources.getString(R.string.error_description_failedToCreateRoom) - ) - ) - dispatchEvent(Event.OnError) - } - }.onSuccess { - event.acknowledge(true) { - dispatchEvent(Event.OpenRoom(it.room.id)) - } - } - } - } - }.launchIn(viewModelScope) - } - - internal companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - is Event.CreateRoom -> { state -> - state.copy( - update = LoadingSuccessState( - loading = true, - success = false - ) - ) - } - - is Event.UpdateName -> { state -> - state.copy( - update = LoadingSuccessState( - loading = true, - success = false - ) - ) - } - - is Event.OnError -> { state -> - state.copy( - update = LoadingSuccessState( - loading = false, - success = false - ) - ) - } - - Event.OnSuccess -> { state -> - state.copy( - update = LoadingSuccessState( - loading = false, - success = true - ) - ) - } - - is Event.OpenRoom -> { state -> state } - is Event.PromptForPayment -> { state -> - state.copy( - update = LoadingSuccessState( - loading = false, - success = false - ) - ) - } - - is Event.OnNewRequest -> { state -> - if (state.roomId != event.id) { - state.copy( - update = LoadingSuccessState(), - roomId = event.id, - textFieldState = TextFieldState(initialText = event.title), - previousRoomName = event.title, - ) - } else { - state - } - } - } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/home/HomeViewModel.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/home/HomeViewModel.kt deleted file mode 100644 index 0b2a77124..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/home/HomeViewModel.kt +++ /dev/null @@ -1,130 +0,0 @@ -package xyz.flipchat.app.features.home - -import android.app.Activity -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.getcode.manager.BottomBarManager -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.TraceType -import com.getcode.utils.trace -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import xyz.flipchat.app.R -import xyz.flipchat.app.auth.AuthManager -import xyz.flipchat.app.util.Router -import xyz.flipchat.controllers.ChatsController -import xyz.flipchat.controllers.CodeController -import xyz.flipchat.controllers.ProfileController -import xyz.flipchat.services.user.AuthState -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject - -@HiltViewModel -class HomeViewModel @Inject constructor( - private val authManager: AuthManager, - private val codeController: CodeController, - private val chatsController: ChatsController, - private val profileController: ProfileController, - private val userManager: UserManager, - val router: Router, - val resources: ResourceHelper, -) : ViewModel() { - - val isLoggedIn = userManager.state.map { it.authState }.filterIsInstance() - .map { true } - .stateIn(viewModelScope, started = SharingStarted.Eagerly, false) - - init { - userManager.state - .mapNotNull { it.authState } - .filterIsInstance() - .distinctUntilChanged() - .onEach { onAppOpen() } - .launchIn(viewModelScope) - } - - fun onAppOpen() { - getUpdatedUserFlags() - openStream() - requestAirdrop() - updateChats() - } - - private fun getUpdatedUserFlags() { - viewModelScope.launch { - profileController.getUserFlags() - } - } - - private fun requestAirdrop() { - if (userManager.authState is AuthState.LoggedIn) { - viewModelScope.launch { - codeController.fetchBalance() - .onFailure { it.printStackTrace() } - .onSuccess { codeController.requestAirdrop() } - } - } - } - - private fun updateChats() { - viewModelScope.launch { - if (userManager.authState.canOpenChatStream()) { - chatsController.updateRooms() - } - } - } - - fun openStream() { - if (userManager.authState.canOpenChatStream()) { - chatsController.openEventStream(viewModelScope) - } - } - - fun closeStream() { - chatsController.closeEventStream() - } - - fun handleLoginEntropy(entropy: String, onSwitchAccounts: () -> Unit, onCancel: () -> Unit) { - if (entropy != userManager.entropy) { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = resources.getString(R.string.subtitle_logoutAndLoginConfirmation), - positiveText = resources.getString(R.string.action_logIn), - tertiaryText = resources.getString(R.string.action_cancel), - isDismissible = false, - onPositive = onSwitchAccounts, - onTertiary = onCancel, - ) - ) - } - } - - fun logout(activity: Activity, onComplete: () -> Unit) = viewModelScope.launch { - authManager.logout(activity) - .onSuccess { - chatsController.closeEventStream() - onComplete() - } - } - - fun deleteAccount(activity: Activity, onComplete: () -> Unit) = viewModelScope.launch { - authManager.deleteAndLogout(activity) - .onSuccess { - chatsController.closeEventStream() - onComplete() - } - } - - override fun onCleared() { - super.onCleared() - closeStream() - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/home/TabbedHomeScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/home/TabbedHomeScreen.kt deleted file mode 100644 index cacdbd377..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/home/TabbedHomeScreen.kt +++ /dev/null @@ -1,92 +0,0 @@ -package xyz.flipchat.app.features.home - -import android.os.Parcelable -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.lifecycle.Lifecycle -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.navigator.tab.CurrentTab -import cafe.adriel.voyager.navigator.tab.TabDisposable -import cafe.adriel.voyager.navigator.tab.TabNavigator -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.extensions.getActivityScopedViewModel -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.OnLifecycleEvent -import dev.theolm.rinku.DeepLink -import kotlinx.coroutines.launch -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import kotlinx.parcelize.RawValue -import xyz.flipchat.app.features.home.components.BottomBar - -@Parcelize -class TabbedHomeScreen(private val deepLink: @RawValue DeepLink?) : Screen, Parcelable { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @Composable - override fun Content() { - val viewModel = getActivityScopedViewModel() - val router = viewModel.router - val isLoggedIn by viewModel.isLoggedIn.collectAsState() - - val initialTab = remember(deepLink) { router.getInitialTabIndex(deepLink) } - - val navigator = LocalCodeNavigator.current - val composeScope = rememberCoroutineScope() - OnLifecycleEvent { _, event -> - when (event) { - Lifecycle.Event.ON_RESUME -> { - composeScope.launch { - router.checkTabs() - } - } - else -> Unit - } - } - - TabNavigator( - tab = viewModel.router.rootTabs[initialTab], - tabDisposable = { - TabDisposable( - navigator = it, - tabs = router.rootTabs, - ) - } - ) { tabNavigator -> - DisposableEffect(tabNavigator) { - navigator.tabsNavigator = tabNavigator - - onDispose { - navigator.tabsNavigator = null - } - } - - Column( - modifier = Modifier - .statusBarsPadding() - .background(CodeTheme.colors.background) - ) { - Box( - modifier = Modifier.weight(1f) - ) { - CurrentTab() - } - if (isLoggedIn) { - BottomBar(tabNavigator, router.rootTabs) - } - } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/home/components/BottomBar.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/home/components/BottomBar.kt deleted file mode 100644 index 998fa0008..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/home/components/BottomBar.kt +++ /dev/null @@ -1,109 +0,0 @@ -package xyz.flipchat.app.features.home.components - -import androidx.compose.animation.animateColorAsState -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEach -import cafe.adriel.voyager.navigator.tab.TabNavigator -import com.getcode.navigation.screens.ChildNavTab -import com.getcode.theme.CodeTheme -import com.getcode.ui.utils.SystemNavigationMode -import com.getcode.ui.core.addIf -import com.getcode.ui.utils.rememberSystemNavigationMode -import com.getcode.ui.core.withTopBorder - -@Composable -internal fun BottomBar( - tabNavigator: TabNavigator, - tabs: List -) { - val systemNavigationMode by rememberSystemNavigationMode() - - Row( - modifier = Modifier - .fillMaxWidth() - .withTopBorder() - .addIf(systemNavigationMode != SystemNavigationMode.Gesture) { - Modifier - .background(CodeTheme.colors.background) - .navigationBarsPadding() - } - ) { - tabs.fastForEach { tab -> - BottomBarTab(tabNavigator, tab, systemNavigationMode) - } - } -} - -@Composable -private fun RowScope.BottomBarTab( - tabNavigator: TabNavigator, - tab: ChildNavTab, - systemNavigationMode: SystemNavigationMode, -) { - val backgroundColor by animateColorAsState( - targetValue = if (tabNavigator.current.options.index == tab.options.index) { - CodeTheme.colors.brandSubtle - } else { - CodeTheme.colors.surface - }, - label = "selected tab color" - ) - Box( - modifier = Modifier - .background(backgroundColor) - .weight(1f) - .clickable { tabNavigator.current = tab } - .addIf(systemNavigationMode == SystemNavigationMode.Gesture) { - Modifier.navigationBarsPadding() - }, - contentAlignment = Alignment.Center - ) { - Column( - modifier = Modifier - .padding( - top = CodeTheme.dimens.grid.x2, - bottom = if (systemNavigationMode != SystemNavigationMode.Gesture) { - CodeTheme.dimens.grid.x2 - } else { - 0.dp - } - ), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy( - CodeTheme.dimens.grid.x1, - Alignment.CenterVertically - ) - ) { - Image( - modifier = Modifier.size(CodeTheme.dimens.staticGrid.x6), - painter = tab.options.icon!!, - colorFilter = ColorFilter.tint(CodeTheme.colors.onBackground), - contentDescription = null, - ) - - Text( - text = tab.options.title, - style = CodeTheme.typography.textSmall, - color = CodeTheme.colors.textMain, - ) - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/home/tabs/CashTab.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/home/tabs/CashTab.kt deleted file mode 100644 index 02d577e68..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/home/tabs/CashTab.kt +++ /dev/null @@ -1,50 +0,0 @@ -package xyz.flipchat.app.features.home.tabs - -import android.os.Parcelable -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import cafe.adriel.voyager.core.registry.ScreenRegistry -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.navigator.Navigator -import cafe.adriel.voyager.navigator.tab.TabOptions -import cafe.adriel.voyager.transitions.SlideTransition -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.core.CodeNavigatorStub -import com.getcode.navigation.core.NavigationLocator -import com.getcode.navigation.core.NavigatorWrapper -import com.getcode.navigation.screens.ChildNavTab -import com.getcode.ui.components.AppBarWithTitle -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import xyz.flipchat.app.R - -@Parcelize -internal class CashTab(override val ordinal: Int) : ChildNavTab, Parcelable { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @IgnoredOnParcel - override var childNav: NavigationLocator = CodeNavigatorStub - - override val options: TabOptions - @Composable get() = TabOptions( - index = ordinal.toUShort(), - title = stringResource(R.string.title_cashTab), - icon = painterResource(R.drawable.ic_fc_balance) - ) - - @Composable - override fun Content() { - Column { - AppBarWithTitle(title = options.title) - Navigator(ScreenRegistry.get(NavScreenProvider.Balance)) { navigator -> - childNav = NavigatorWrapper(navigator) - SlideTransition(navigator) - } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/home/tabs/ChatTab.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/home/tabs/ChatTab.kt deleted file mode 100644 index c01ca6d78..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/home/tabs/ChatTab.kt +++ /dev/null @@ -1,188 +0,0 @@ -package xyz.flipchat.app.features.home.tabs - -import android.os.Parcelable -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.core.registry.ScreenRegistry -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.hilt.getViewModel -import cafe.adriel.voyager.navigator.Navigator -import cafe.adriel.voyager.navigator.currentOrThrow -import cafe.adriel.voyager.navigator.tab.TabOptions -import cafe.adriel.voyager.transitions.SlideTransition -import com.getcode.manager.BottomBarManager -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.core.NavigationLocator -import com.getcode.navigation.core.NavigatorStub -import com.getcode.navigation.core.NavigatorWrapper -import com.getcode.navigation.extensions.getActivityScopedViewModel -import com.getcode.navigation.screens.ChildNavTab -import com.getcode.theme.Black40 -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.AppBarDefaults -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.theme.CodeCircularProgressIndicator -import com.getcode.ui.core.addIf -import com.getcode.ui.utils.getActivity -import com.getcode.ui.core.noRippleClickable -import com.getcode.ui.core.rememberedClickable -import com.getcode.ui.core.unboundedClickable -import com.getcode.util.resources.LocalResources -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import xyz.flipchat.app.R -import xyz.flipchat.app.features.chat.list.ChatListViewModel -import xyz.flipchat.app.features.chat.openChatDirectiveBottomModal -import xyz.flipchat.app.features.settings.SettingsViewModel - -@Parcelize -internal class ChatTab(override val ordinal: Int) : ChildNavTab, Parcelable { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @IgnoredOnParcel - override var childNav: NavigationLocator = NavigatorStub - - override val options: TabOptions - @Composable get() = TabOptions( - index = ordinal.toUShort(), - title = stringResource(R.string.title_chatsTab), - icon = painterResource(R.drawable.ic_fc_chats) - ) - - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - val context = LocalContext.current - val resources = LocalResources.currentOrThrow - val viewModel = getActivityScopedViewModel() - val settingsVm = getViewModel() - val state by viewModel.stateFlow.collectAsState() - - Box { - Column { - AppBarWithTitle( - title = { - LogOutTitle( - state = state, - onTitleClicked = { - viewModel.dispatchEvent(ChatListViewModel.Event.OnChatsTapped) - }, - onLogout = { - context.getActivity()?.let { - settingsVm.logout(it) { - navigator.replaceAll(ScreenRegistry.get(NavScreenProvider.Login.Home())) - } - } - } - ) - }, - rightContents = { - Image( - modifier = Modifier - .background(color = CodeTheme.colors.tertiary, shape = CircleShape) - .padding(CodeTheme.dimens.grid.x1) - .unboundedClickable { - openChatDirectiveBottomModal( - resources = resources, - createCost = state.createRoomCost, - viewModel = viewModel, - navigator = navigator - ) - }, - imageVector = Icons.Default.Add, - contentDescription = null, - colorFilter = ColorFilter.tint(CodeTheme.colors.onBackground) - ) - } - ) - Navigator(ScreenRegistry.get(NavScreenProvider.Room.List)) { navigator -> - childNav = NavigatorWrapper(navigator) - SlideTransition(navigator) - } - } - - val scrimAlpha by animateFloatAsState( - if (state.showScrim) 1f else 0f, - label = "scrim visibility" - ) - - if (state.showScrim) { - Box( - modifier = Modifier - .fillMaxSize() - .alpha(scrimAlpha) - .background(Black40) - .rememberedClickable(indication = null, - interactionSource = remember { MutableInteractionSource() }) {} - ) { - if (state.showFullscreenSpinner) { - CodeCircularProgressIndicator( - modifier = Modifier - .size(50.dp) - .align(Alignment.Center) - ) - } - } - } - } - } - - @Composable - private fun LogOutTitle( - modifier: Modifier = Modifier, - state: ChatListViewModel.State, - onTitleClicked: () -> Unit, - onLogout: () -> Unit - ) { - val context = LocalContext.current - AppBarDefaults.Title( - modifier = modifier - .addIf(!state.isLogOutEnabled) { - Modifier.noRippleClickable { - onTitleClicked() - } - } - .addIf(state.isLogOutEnabled) { - Modifier.unboundedClickable { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = context.getString(R.string.prompt_title_logout), - subtitle = context - .getString(R.string.prompt_description_logout), - positiveText = context.getString(R.string.action_logout), - tertiaryText = context.getString(R.string.action_cancel), - onPositive = onLogout - ) - ) - } - }, - text = options.title - ) - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/home/tabs/ProfileTab.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/home/tabs/ProfileTab.kt deleted file mode 100644 index ce2bdd927..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/home/tabs/ProfileTab.kt +++ /dev/null @@ -1,47 +0,0 @@ -package xyz.flipchat.app.features.home.tabs - -import android.os.Parcelable -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Person -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.res.stringResource -import cafe.adriel.voyager.core.registry.ScreenRegistry -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.navigator.Navigator -import cafe.adriel.voyager.navigator.tab.TabOptions -import cafe.adriel.voyager.transitions.SlideTransition -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.core.NavigationLocator -import com.getcode.navigation.core.NavigatorStub -import com.getcode.navigation.core.NavigatorWrapper -import com.getcode.navigation.screens.ChildNavTab -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import xyz.flipchat.app.R - -@Parcelize -class ProfileTab(override val ordinal: Int): ChildNavTab, Parcelable { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @IgnoredOnParcel - override var childNav: NavigationLocator = NavigatorStub - - override val options: TabOptions - @Composable get() = TabOptions( - index = ordinal.toUShort(), - title = stringResource(R.string.title_profileTab), - icon = rememberVectorPainter(Icons.Outlined.Person) - ) - - @Composable - override fun Content() { - Navigator(ScreenRegistry.get(NavScreenProvider.OwnProfile)) { navigator -> - childNav = NavigatorWrapper(navigator) - SlideTransition(navigator) - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/LoginHome.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/LoginHome.kt deleted file mode 100644 index 387bcdf1f..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/LoginHome.kt +++ /dev/null @@ -1,166 +0,0 @@ -package xyz.flipchat.app.features.login - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.text.ClickableText -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Science -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.text.withStyle -import com.getcode.theme.CodeTheme -import com.getcode.theme.White -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.core.noRippleClickable -import com.getcode.view.LoadingSuccessState -import xyz.flipchat.app.R -import xyz.flipchat.app.util.ChromeTabsUtils - -@Composable -fun LoginHome( - isSpectatorJoinEnabled: Boolean = false, - isCreatingAccount: LoadingSuccessState = LoadingSuccessState(), - betaFlagsVisible: Boolean = false, - onLogoTapped: () -> Unit, - openBetaFlags: () -> Unit, - createAccount: () -> Unit, - login: () -> Unit, -) { - val context = LocalContext.current - - Box( - modifier = Modifier - .fillMaxSize() - .background(CodeTheme.colors.secondary) - .windowInsetsPadding(WindowInsets.navigationBars), - ) { - Column(modifier = Modifier.fillMaxSize()) { - Spacer(Modifier.weight(1f)) - - Column( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .fillMaxWidth(0.65f) - .noRippleClickable(enabled = !betaFlagsVisible) { onLogoTapped() }, - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.inset) - ) { - Image( - painter = painterResource(R.drawable.flipchat_logo), - contentDescription = "", - modifier = Modifier - ) - Text( - text = stringResource(R.string.app_name_without_variant), - style = CodeTheme.typography.displayMedium, - color = White - ) - } - - Spacer(Modifier.weight(1f)) - - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset), - onClick = createAccount, - isLoading = isCreatingAccount.loading, - isSuccess = isCreatingAccount.success, - text = if (isSpectatorJoinEnabled) { - stringResource(R.string.action_getStarted) - } else { - stringResource(R.string.action_createAccount) - }, - buttonState = ButtonState.Filled, - ) - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset), - onClick = login, - text = stringResource(R.string.action_logIn), - buttonState = ButtonState.Subtle, - ) - - - val bottomString = buildAnnotatedString { - append(stringResource(R.string.login_description_byTapping)) - append(" ") - append(stringResource(R.string.login_description_agreeToOur)) - append(" ") - pushStringAnnotation(tag = "tos", annotation = stringResource(R.string.app_tos)) - withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) { - append(stringResource(R.string.title_termsOfService)) - } - pop() - append(" ") - append(stringResource(R.string.core_and)) - append(" ") - pushStringAnnotation( - tag = "policy", - annotation = stringResource(R.string.app_privacy_policy) - ) - withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) { - append(stringResource(R.string.title_privacyPolicy)) - } - pop() - } - - ClickableText( - text = bottomString, - style = CodeTheme.typography.caption.copy( - textAlign = TextAlign.Center, - color = CodeTheme.colors.textSecondary - ), - modifier = Modifier - .padding(CodeTheme.dimens.grid.x4), - onClick = { offset -> - bottomString.getStringAnnotations(tag = "tos", start = offset, end = offset) - .firstOrNull()?.let { - ChromeTabsUtils.launchUrl(context, it.item) - } - bottomString.getStringAnnotations(tag = "policy", start = offset, end = offset) - .firstOrNull()?.let { - ChromeTabsUtils.launchUrl(context, it.item) - } - } - ) - } - - if (betaFlagsVisible) { - IconButton( - modifier = Modifier - .align(Alignment.TopEnd) - .statusBarsPadding() - .padding(top = CodeTheme.dimens.inset), - onClick = openBetaFlags - ) { - Icon(Icons.Filled.Science, contentDescription = null, tint = Color.White) - } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/LoginScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/LoginScreen.kt deleted file mode 100644 index 4588316df..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/LoginScreen.kt +++ /dev/null @@ -1,66 +0,0 @@ -package xyz.flipchat.app.features.login - -import android.os.Parcelable -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import cafe.adriel.voyager.core.registry.ScreenRegistry -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.hilt.getViewModel -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.core.LocalCodeNavigator -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import kotlin.time.Duration.Companion.seconds - -@Parcelize -data class LoginScreen(val seed: String? = null) : Screen, Parcelable { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @Composable - override fun Content() { - val vm = getViewModel() - val state by vm.stateFlow.collectAsState() - val navigator = LocalCodeNavigator.current - - LaunchedEffect(vm) { - vm.eventFlow - .filterIsInstance() - .onEach { delay(2.seconds) } - .onEach { navigator.replaceAll(ScreenRegistry.get(NavScreenProvider.AppHomeScreen())) } - .launchIn(this) - } - - if (seed != null) { -// SeedDeepLink(getViewModel(), seed) - } else { - LoginHome( - isCreatingAccount = state.creatingAccount, - betaFlagsVisible = state.betaOptionsVisible, - isSpectatorJoinEnabled = state.followerModeEnabled, - onLogoTapped = { vm.dispatchEvent(LoginViewModel.Event.OnLogoTapped) }, - openBetaFlags = { - navigator.push(ScreenRegistry.get(NavScreenProvider.BetaFlags)) - }, - createAccount = { - if (state.followerModeEnabled) { - vm.dispatchEvent(LoginViewModel.Event.CreateAccount) - } else { - navigator.push(ScreenRegistry.get(NavScreenProvider.CreateAccount.NameEntry())) - } - }, - login = { - navigator.push(ScreenRegistry.get(NavScreenProvider.Login.SeedInput)) - } - ) - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/LoginViewModel.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/LoginViewModel.kt deleted file mode 100644 index 25dd365a1..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/LoginViewModel.kt +++ /dev/null @@ -1,96 +0,0 @@ -package xyz.flipchat.app.features.login - -import androidx.lifecycle.viewModelScope -import com.getcode.manager.TopBarManager -import com.getcode.services.utils.onSuccessWithDelay -import com.flipcash.libs.coroutines.DispatcherProvider -import com.getcode.view.BaseViewModel2 -import com.getcode.view.LoadingSuccessState -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.filterNot -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import xyz.flipchat.app.auth.AuthManager -import xyz.flipchat.app.beta.Lab -import xyz.flipchat.app.beta.Labs -import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds - -@HiltViewModel -class LoginViewModel @Inject constructor( - private val authManager: AuthManager, - betaFlags: Labs, - dispatchers: DispatcherProvider, -) : BaseViewModel2( - initialState = State(), - updateStateForEvent = updateStateForEvent, - dispatchers = dispatchers, -) { - data class State( - val followerModeEnabled: Boolean = false, - val creatingAccount: LoadingSuccessState = LoadingSuccessState(), - val logoTapCount: Int = 0, - val betaOptionsVisible: Boolean = false, - ) - - sealed interface Event { - data object OnLogoTapped: Event - data object BetaOptionsUnlocked: Event - data class OnFollowerModeEnabled(val enabled: Boolean): Event - data object CreateAccount: Event - data object OnAccountCreated: Event - data object CreateFailed: Event - } - - init { - betaFlags.observe(Lab.FollowerMode) - .onEach { dispatchEvent(Event.OnFollowerModeEnabled(it)) } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { stateFlow.value.logoTapCount } - .filter { it >= TAP_THRESHOLD } - .filterNot { stateFlow.value.betaOptionsVisible } - .onEach { dispatchEvent(Event.BetaOptionsUnlocked) } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { - authManager.createAccount() - .onFailure { - dispatchEvent(Event.CreateFailed) - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = "Create Account Failed", - message = it.localizedMessage ?: "Something went wrong" - ) - ) - }.onSuccessWithDelay(2.seconds) { - dispatchEvent(Event.OnAccountCreated) - } - } - .launchIn(viewModelScope) - } - - internal companion object { - private const val TAP_THRESHOLD = 6 - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - Event.CreateAccount -> { state -> state.copy(creatingAccount = LoadingSuccessState(loading = true)) } - Event.OnAccountCreated -> { state -> state.copy(creatingAccount = LoadingSuccessState(loading = false, success = true)) } - Event.CreateFailed -> { state -> state.copy(creatingAccount = LoadingSuccessState(loading = false)) } - is Event.OnFollowerModeEnabled -> { state -> state.copy(followerModeEnabled = event.enabled) } - is Event.BetaOptionsUnlocked -> { state -> state.copy(betaOptionsVisible = true) } - is Event.OnLogoTapped -> { state -> - if (state.logoTapCount >= TAP_THRESHOLD) state - else state.copy(logoTapCount = state.logoTapCount + 1) - } - } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/accesskey/AccessKeyScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/accesskey/AccessKeyScreen.kt deleted file mode 100644 index 0dcf920b6..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/accesskey/AccessKeyScreen.kt +++ /dev/null @@ -1,305 +0,0 @@ -package xyz.flipchat.app.features.login.accesskey - -import android.Manifest -import android.os.Build -import android.os.Parcelable -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.MutableTransitionState -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.isSpecified -import cafe.adriel.voyager.core.registry.ScreenRegistry -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.hilt.getViewModel -import com.getcode.manager.BottomBarManager -import com.getcode.manager.TopBarManager -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.NamedScreen -import com.getcode.theme.CodeTheme -import com.getcode.theme.White -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.components.Cloudy -import com.getcode.ui.components.SelectionContainer -import com.getcode.ui.components.rememberSelectionState -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.core.addIf -import com.getcode.ui.core.measured -import com.getcode.util.permissions.PermissionResult -import com.getcode.util.permissions.getPermissionLauncher -import com.getcode.util.permissions.rememberPermissionHandler -import kotlinx.coroutines.delay -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import xyz.flipchat.app.R -import xyz.flipchat.app.util.launchAppSettings - -@Parcelize -class AccessKeyScreen: Screen, NamedScreen, Parcelable { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(R.string.title_accessKey) - - @Composable - override fun Content() { - val viewModel = getViewModel() - val navigator = LocalCodeNavigator.current - - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AppBarWithTitle( - title = name, - ) - AccessKeyScreenContent(viewModel) { - navigator.push(ScreenRegistry.get(NavScreenProvider.Login.NotificationPermission(true))) - } - } - - BackHandler { /* intercept */ } - } -} - - -@Composable -internal fun AccessKeyScreenContent(viewModel: LoginAccessKeyViewModel, onCompleted: () -> Unit) { - val navigator = LocalCodeNavigator.current - val context = LocalContext.current - val dataState by viewModel.uiFlow.collectAsState() - - var isExportSeedRequested by remember { mutableStateOf(false) } - var isStoragePermissionGranted by remember { mutableStateOf(false) } - val isAccessKeyVisible = remember { MutableTransitionState(false) } - - val onPermissionResult = { result: PermissionResult -> - isStoragePermissionGranted = result == PermissionResult.Granted - - if (!isStoragePermissionGranted) { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = context.getString(R.string.error_title_failedToSave), - message = context.getString(R.string.error_description_failedToSave), - type = TopBarManager.TopBarMessageType.ERROR, - secondaryText = context.getString(R.string.action_openSettings), - secondaryAction = { context.launchAppSettings() } - ) - ) - } - } - - val launcher = getPermissionLauncher(Manifest.permission.WRITE_EXTERNAL_STORAGE, onPermissionResult) - val permissionChecker = rememberPermissionHandler() - - LaunchedEffect(isExportSeedRequested, isStoragePermissionGranted) { - if (isExportSeedRequested && isStoragePermissionGranted) { - viewModel.saveImage() - .onSuccess { - delay(400) - onCompleted() - } - .onFailure { - isExportSeedRequested = false - } - } - } - - val onExportClick = { - isExportSeedRequested = true - - if (Build.VERSION.SDK_INT > 29) { - isStoragePermissionGranted = true - } else { - permissionChecker.request( - permission = Manifest.permission.WRITE_EXTERNAL_STORAGE, - onPermissionResult = onPermissionResult, - launcher = launcher - ) - } - - } - val onSkipClick = { - onCompleted() - } - - var buttonHeight by remember { - mutableStateOf(0.dp) - } - - val selectionState = rememberSelectionState( - content = dataState.words.joinToString(" ") - ) - - SelectionContainer( - modifier = Modifier - .fillMaxSize() - .windowInsetsPadding(WindowInsets.navigationBars), - state = selectionState, - ) { - Cloudy( - modifier = Modifier - .fillMaxSize(), - enabled = selectionState.shown - ) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = CodeTheme.dimens.inset) - .padding(vertical = CodeTheme.dimens.grid.x4) - ) { - Column( - modifier = Modifier - .align(Alignment.BottomCenter) - .measured { buttonHeight = it.height }, - ) { - CodeButton( - modifier = Modifier.fillMaxWidth(), - onClick = onExportClick, - text = stringResource(R.string.action_saveAccessKey), - buttonState = ButtonState.Filled, - isLoading = dataState.isLoading, - enabled = dataState.isEnabled, - isSuccess = dataState.isSuccess, - ) - - CodeButton( - modifier = Modifier.fillMaxWidth(), - onClick = { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = context.getString(R.string.prompt_title_wroteThemDown), - subtitle = context - .getString(R.string.prompt_description_wroteThemDown), - positiveText = context - .getString(R.string.action_yesWroteThemDown), - negativeText = "", - tertiaryText = context.getString(R.string.action_cancel), - onPositive = { onSkipClick() }, - onNegative = {} - ) - ) - }, - text = stringResource(R.string.action_wroteThemDownInstead), - buttonState = ButtonState.Subtle, - enabled = dataState.isEnabled, - ) - } - } - } - - Column( - modifier = Modifier - .align(Alignment.TopCenter) - .fillMaxHeight() - .addIf(buttonHeight.isSpecified) { Modifier.padding(bottom = buttonHeight + CodeTheme.dimens.grid.x4) }, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Column( - modifier = Modifier - // highly specific aspect ratio from iOS :) - .aspectRatio(0.607f, matchHeightConstraintsFirst = true) - .fillMaxWidth() - .weight(1f), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - AnimatedVisibility( - visibleState = isAccessKeyVisible, - enter = fadeIn(animationSpec = tween(300, 0)), - exit = fadeOut(animationSpec = tween(300, 0)) - ) { - dataState.accessKeyCroppedBitmap?.let { bitmap -> - Image( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .scale(selectionState.scale.value), - bitmap = bitmap.asImageBitmap(), - contentScale = ContentScale.Crop, - contentDescription = dataState.wordsFormatted, - ) - } - } - } - - val textAlpha by animateFloatAsState( - if (selectionState.shown) 0f else 1f, - label = "text alpha" - ) - - Text( - modifier = Modifier - .alpha(textAlpha) - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.grid.x5) - .padding( - top = CodeTheme.dimens.grid.x3, - bottom = CodeTheme.dimens.grid.x6 - ), - style = CodeTheme.typography.textSmall.copy(textAlign = TextAlign.Center), - color = White, - text = stringResource(R.string.subtitle_accessKeyDescription) - ) - } - - - BackHandler { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = context.getString(R.string.prompt_title_exitAccountCreation), - subtitle = context - .getString(R.string.prompt_description_exitAccountCreation), - positiveText = context.getString(R.string.action_exit), - negativeText = "", - tertiaryText = context.getString(R.string.action_cancel), - onPositive = { navigator.replaceAll(ScreenRegistry.get(NavScreenProvider.Login.Home())) }, - type = BottomBarManager.BottomBarMessageType.DESTRUCTIVE, - onNegative = {} - ) - ) - } - - LaunchedEffect(dataState.accessKeyCroppedBitmap) { - isAccessKeyVisible.targetState = dataState.accessKeyCroppedBitmap != null - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/accesskey/LoginAccessKeyViewModel.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/accesskey/LoginAccessKeyViewModel.kt deleted file mode 100644 index 43e621cf9..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/accesskey/LoginAccessKeyViewModel.kt +++ /dev/null @@ -1,22 +0,0 @@ -package xyz.flipchat.app.features.login.accesskey - -import com.getcode.libs.qr.QRCodeGenerator -import com.getcode.services.manager.MnemonicManager -import com.getcode.util.resources.ResourceHelper -import dagger.hilt.android.lifecycle.HiltViewModel -import xyz.flipchat.app.features.accesskey.BaseAccessKeyViewModel -import xyz.flipchat.app.util.media.MediaScanner -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject - -@HiltViewModel -class LoginAccessKeyViewModel @Inject constructor( - resources: ResourceHelper, - mnemonicManager: MnemonicManager, - mediaScanner: MediaScanner, - userManager: UserManager, - qrCodeGenerator: QRCodeGenerator -): BaseAccessKeyViewModel(resources, mnemonicManager, mediaScanner, userManager, qrCodeGenerator) { - - suspend fun saveImage(): Result = saveBitmapToFile().map { Unit } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/accesskey/SeedInputScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/accesskey/SeedInputScreen.kt deleted file mode 100644 index 79abbc0f9..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/accesskey/SeedInputScreen.kt +++ /dev/null @@ -1,189 +0,0 @@ -package xyz.flipchat.app.features.login.accesskey - -import android.os.Parcelable -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.constraintlayout.compose.ConstraintLayout -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.hilt.getViewModel -import com.getcode.navigation.core.CodeNavigator -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.screens.NamedScreen -import xyz.flipchat.app.R -import com.getcode.theme.CodeTheme -import com.getcode.theme.inputColors -import com.getcode.theme.topBarHeight -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.util.permissions.notificationPermissionCheck -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data object SeedInputScreen: Screen, NamedScreen, Parcelable { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - override val name: String - @Composable get() = stringResource(R.string.title_enterAccessKeyWords) - - @Composable - override fun Content() { - val viewModel: SeedInputViewModel = getViewModel() - val navigator = LocalCodeNavigator.current - Column { - AppBarWithTitle( - backButton = true, - onBackIconClicked = { navigator.pop() }, - title = name, - ) - SeedInput(viewModel) - } - } -} - -@Composable -private fun SeedInput(viewModel: SeedInputViewModel) { - val navigator: CodeNavigator = LocalCodeNavigator.current - val dataState by viewModel.uiFlow.collectAsState() - val focusManager = LocalFocusManager.current - val focusRequester = FocusRequester() - - val notificationPermissionCheck = notificationPermissionCheck(isShowError = false) { } - - Column( - modifier = Modifier - .fillMaxSize() - .windowInsetsPadding(WindowInsets.systemBars) - .padding(horizontal = CodeTheme.dimens.inset) - .padding(top = topBarHeight) - .padding(bottom = CodeTheme.dimens.grid.x4) - .verticalScroll(rememberScrollState()) - .imePadding(), - ) { - ConstraintLayout( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - ) { - val (input, wordCount, checkboxValid, captionText) = createRefs() - - Text( - modifier = Modifier - .constrainAs(captionText) { - top.linkTo(parent.top) - }, - style = CodeTheme.typography.textSmall.copy(textAlign = TextAlign.Center), - color = CodeTheme.colors.textSecondary, - text = stringResource(R.string.subtitle_loginDescription) - ) - - OutlinedTextField( - modifier = Modifier - .constrainAs(input) { - top.linkTo(captionText.bottom) - } - .padding(top = CodeTheme.dimens.inset) - .fillMaxWidth() - .height(120.dp) - .focusRequester(focusRequester), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), - visualTransformation = VisualTransformation.None, - value = dataState.wordsString, - onValueChange = { viewModel.onTextChange(it) }, - textStyle = CodeTheme.typography.textLarge.copy( - fontSize = 16.sp, - ), - colors = inputColors(), - ) - - Text( - text = dataState.wordCount.toString(), - color = CodeTheme.colors.textSecondary, - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier - .constrainAs(wordCount) { - bottom.linkTo(input.bottom) - } - .padding(bottom = CodeTheme.dimens.grid.x2, start = CodeTheme.dimens.grid.x2) - ) - - if (dataState.isValid) { - Image( - painter = painterResource(id = R.drawable.ic_checked_blue), - modifier = Modifier - .constrainAs(checkboxValid) { - start.linkTo(wordCount.end) - top.linkTo(wordCount.top) - bottom.linkTo(wordCount.bottom) - } - .padding(bottom = CodeTheme.dimens.grid.x2, start = CodeTheme.dimens.grid.x1) - .height(CodeTheme.dimens.grid.x3), - contentDescription = "" - ) - } - } - - if (dataState.isSuccess) { - notificationPermissionCheck(true) - } - - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding( - top = CodeTheme.dimens.grid.x3, - bottom = CodeTheme.dimens.grid.x4 - ), - onClick = { - focusManager.clearFocus() - viewModel.onSubmit(navigator) - }, - isLoading = dataState.isLoading, - isSuccess = dataState.isSuccess, - enabled = dataState.continueEnabled, - text = stringResource(R.string.action_logIn), - buttonState = ButtonState.Filled, - ) - } - - LaunchedEffect(Unit) { - focusRequester.requestFocus() - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/accesskey/SeedInputViewModel.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/accesskey/SeedInputViewModel.kt deleted file mode 100644 index 4222d04c4..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/accesskey/SeedInputViewModel.kt +++ /dev/null @@ -1,159 +0,0 @@ -package xyz.flipchat.app.features.login.accesskey - -import android.annotation.SuppressLint -import androidx.lifecycle.viewModelScope -import cafe.adriel.voyager.core.registry.ScreenRegistry -import com.getcode.crypt.MnemonicPhrase -import com.getcode.manager.BottomBarManager -import com.getcode.manager.TopBarManager -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.core.CodeNavigator -import xyz.flipchat.app.R -import xyz.flipchat.app.util.AccountManager -import com.getcode.services.manager.MnemonicManager -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.ErrorUtils -import com.getcode.view.BaseViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import xyz.flipchat.app.auth.AuthManager -import xyz.flipchat.services.analytics.FlipchatAnalyticsService -import java.util.Locale -import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds - -data class SeedInputUiModel( - val wordsString: String = "", - val wordCount: Int = 0, - val continueEnabled: Boolean = false, - val isValid: Boolean = false, - val isLoading: Boolean = false, - val isSuccess: Boolean = false, -) - -@HiltViewModel -class SeedInputViewModel @Inject constructor( - private val analytics: FlipchatAnalyticsService, - private val authManager: AuthManager, - private val resources: ResourceHelper, - private val mnemonicManager: MnemonicManager, - private val accountManager: AccountManager, -) : BaseViewModel(resources) { - val uiFlow = MutableStateFlow(SeedInputUiModel()) - private val mnemonicCode = mnemonicManager.mnemonicCode - - init { - viewModelScope.launch { - val token = accountManager.getToken() - if (token != null) { - analytics.unintentionalLogout() - ErrorUtils.handleError( - Throwable("We shouldn't be here. Login screen visible with associated account in AccountManager.") - ) - } - } - } - - fun onTextChange(wordsString: String) { - val isLoading = uiFlow.value.isLoading - val isSuccess = uiFlow.value.isSuccess - if (isLoading || isSuccess) return - - val userWordList = wordsString.lowercase(Locale.CANADA).split(" ") - val wordCount = getValidCount(userWordList, mnemonicCode.wordList) - uiFlow.update { - it.copy( - wordsString = wordsString, - wordCount = wordCount, - continueEnabled = wordCount == 12, - isValid = wordCount == 12 - ) - } - } - - fun onSubmit(navigator: CodeNavigator) { - val userWordList = - uiFlow.value.wordsString.trim().replace(Regex("(\\s)+"), " ").lowercase(Locale.getDefault()).split(" ") - val mnemonic = MnemonicPhrase.newInstance(userWordList) ?: return - - - CoroutineScope(Dispatchers.IO).launch { - val entropyB64: String - try { - entropyB64 = mnemonicManager.getEncodedBase64(mnemonic) - } catch (e: Exception) { - showError(navigator) - return@launch - } - - performLogin(navigator, entropyB64) - } - } - - @SuppressLint("CheckResult") - fun performLogin(navigator: CodeNavigator, entropyB64: String, deeplink: Boolean = false) { - viewModelScope.launch { - setState(isLoading = true, isSuccess = false, isContinueEnabled = false) - authManager.login(entropyB64) - .onFailure { - if (it is AuthManager.AuthManagerException.TimelockUnlockedException) { - TopBarManager.showMessage( - getString(R.string.error_title_timelockUnlocked), - getString(R.string.error_description_timelockUnlocked) - ) - navigator.popAll() - } else { - showError(navigator) - } - setState(isLoading = false, isSuccess = false, isContinueEnabled = true) - } - .onSuccess { - setState(isLoading = false, isSuccess = true, isContinueEnabled = false) - delay(if (deeplink) 0.seconds else 1.seconds) - navigator.replaceAll(ScreenRegistry.get(NavScreenProvider.AppHomeScreen())) - } - } - } - - private fun setState(isLoading: Boolean, isSuccess: Boolean, isContinueEnabled: Boolean) { - uiFlow.update { - it.copy( - isLoading = isLoading, - isSuccess = isSuccess, - continueEnabled = isContinueEnabled - ) - } - } - - override fun setIsLoading(isLoading: Boolean) { - uiFlow.update { - it.copy( - isLoading = isLoading, - continueEnabled = false - ) - } - } - - private fun getValidCount(userWordList: List, mnemonicWordList: List): Int { - return userWordList.filter { it in mnemonicWordList }.size - } - - private fun showError(navigator: CodeNavigator) { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = resources.getString(R.string.prompt_title_notFlipchatAccount), - subtitle = resources.getString(R.string.prompt_description_notFlipchatAccount), - positiveText = resources.getString(R.string.action_createNewFlipchatAccount), - negativeText = resources.getString(R.string.action_tryDifferentFlipchatAccount), - onPositive = { - navigator.replaceAll(ScreenRegistry.get(NavScreenProvider.Login.Home())) - } - ) - ) - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/permissions/NotificationPermission.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/permissions/NotificationPermission.kt deleted file mode 100644 index 07b25ac1d..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/permissions/NotificationPermission.kt +++ /dev/null @@ -1,131 +0,0 @@ -package xyz.flipchat.app.features.login.permissions - -import android.os.Parcelable -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.constraintlayout.compose.ConstraintLayout -import cafe.adriel.voyager.core.registry.ScreenRegistry -import cafe.adriel.voyager.core.screen.Screen -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.core.LocalCodeNavigator -import xyz.flipchat.app.R -import com.getcode.theme.CodeTheme -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import kotlinx.parcelize.Parcelize - -@Parcelize -data class NotificationPermissionScreen(val fromOnboarding: Boolean = false): Screen, Parcelable { - @Composable - override fun Content() { - NotificationPermission(fromOnboarding) - } - -} -@Composable -fun NotificationPermission(fromOnboarding: Boolean = false) { - val navigator = LocalCodeNavigator.current -// val analytics = LocalAnalytics.current - val onNotificationResult: (Boolean) -> Unit = { isGranted -> - if (isGranted) { - if (fromOnboarding) { -// analytics.action(Action.CompletedOnboarding) - } - if (navigator.lastModalItem is NotificationPermissionScreen) { - navigator.hide() - } else { - navigator.replaceAll(ScreenRegistry.get(NavScreenProvider.AppHomeScreen())) - } - } - } - val notificationPermissionCheck = - com.getcode.util.permissions.notificationPermissionCheck(onResult = { - onNotificationResult(it) - }) - - SideEffect { - notificationPermissionCheck(false) - } - - ConstraintLayout( - modifier = Modifier - .fillMaxSize() - .windowInsetsPadding(WindowInsets.navigationBars), - ) { - val (image, caption, button, buttonSkip) = createRefs() - - Image( - painter = painterResource(id = R.drawable.ic_notification_request), - contentDescription = "", - modifier = Modifier - .padding(horizontal = CodeTheme.dimens.grid.x8) - .padding(top = CodeTheme.dimens.grid.x10) - .fillMaxHeight(0.6f) - .fillMaxWidth() - .constrainAs(image) { - top.linkTo(parent.top) - bottom.linkTo(caption.top) - start.linkTo(parent.start) - end.linkTo(parent.end) - } - ) - - Text( - modifier = Modifier - .padding(horizontal = CodeTheme.dimens.inset) - .constrainAs(caption) { - top.linkTo(image.bottom) - bottom.linkTo(button.top) - start.linkTo(parent.start) - end.linkTo(parent.end) - }, - text = stringResource(R.string.permissions_description_push_messages), - style = CodeTheme.typography.textMedium - .copy(textAlign = TextAlign.Center), - ) - - CodeButton( - onClick = { notificationPermissionCheck(true) }, - text = stringResource(R.string.action_allowPushNotifications), - buttonState = ButtonState.Filled, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset) - .constrainAs(button) { - start.linkTo(parent.start) - end.linkTo(parent.end) - linkTo(button.bottom, buttonSkip.top, bias = 1.0F) - }, - ) - - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = CodeTheme.dimens.grid.x2) - .padding(horizontal = CodeTheme.dimens.inset) - .constrainAs(buttonSkip) { - linkTo(buttonSkip.bottom, parent.bottom, bias = 1.0F) - start.linkTo(parent.start) - end.linkTo(parent.end) - }, - onClick = { - onNotificationResult(true) - }, - text = stringResource(R.string.action_notNow), - buttonState = ButtonState.Subtle, - ) - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/register/AccessKeyModalScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/register/AccessKeyModalScreen.kt deleted file mode 100644 index 40de7cb50..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/register/AccessKeyModalScreen.kt +++ /dev/null @@ -1,48 +0,0 @@ -package xyz.flipchat.app.features.login.register - -import android.os.Parcelable -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import cafe.adriel.voyager.core.registry.ScreenRegistry -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.hilt.getViewModel -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.modal.FullScreenModalScreen -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.utils.DisableSheetGestures -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import xyz.flipchat.app.features.login.accesskey.AccessKeyScreenContent -import xyz.flipchat.app.features.login.accesskey.LoginAccessKeyViewModel - -@Parcelize -class AccessKeyModalScreen : FullScreenModalScreen, Parcelable { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @Composable - override fun ModalContent() { - val navigator = LocalCodeNavigator.current - val viewModel = getViewModel() - - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AppBarWithTitle(backButton = false) - AccessKeyScreenContent(viewModel) { - navigator.push(ScreenRegistry.get(NavScreenProvider.CreateAccount.Purchase)) - } - } - - BackHandler { /* intercept */ } - DisableSheetGestures() - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/register/PurchaseAccountScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/register/PurchaseAccountScreen.kt deleted file mode 100644 index 2aa924860..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/register/PurchaseAccountScreen.kt +++ /dev/null @@ -1,263 +0,0 @@ -package xyz.flipchat.app.features.login.register - -import android.os.Parcelable -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.CheckCircleOutline -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewModelScope -import cafe.adriel.voyager.hilt.getViewModel -import com.getcode.manager.TopBarManager -import com.getcode.model.ID -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.modal.FullScreenModalScreen -import com.getcode.services.utils.onSuccessWithDelay -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.components.chat.UserAvatar -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.theme.CodeScaffold -import com.getcode.ui.utils.DisableSheetGestures -import com.getcode.util.getActivity -import com.getcode.util.resources.ResourceHelper -import com.flipcash.libs.coroutines.DispatcherProvider -import com.getcode.view.BaseViewModel2 -import com.getcode.view.LoadingSuccessState -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.parcelize.Parcelize -import xyz.flipchat.app.R -import xyz.flipchat.app.auth.AuthManager -import xyz.flipchat.app.ui.LocalUserManager -import xyz.flipchat.controllers.PurchaseController -import xyz.flipchat.services.billing.BillingClient -import xyz.flipchat.services.billing.IapPaymentEvent -import xyz.flipchat.services.billing.IapProduct -import xyz.flipchat.services.billing.LocalBillingClient -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds - -@Parcelize -class PurchaseAccountScreen : FullScreenModalScreen, Parcelable { - - @Composable - override fun ModalContent() { - Column { - AppBarWithTitle( - backButton = false, - isInModal = true - ) - PurchaseAccountScreenContent(getViewModel()) - } - BackHandler { /** swallow **/ } - DisableSheetGestures() - } -} - -@Composable -private fun PurchaseAccountScreenContent(viewModel: PurchaseAccountViewModel) { - val navigator = LocalCodeNavigator.current - val context = LocalContext.current - val billingController = LocalBillingClient.current - val userManager = LocalUserManager.current - val composeScope = rememberCoroutineScope() - - val state by viewModel.stateFlow.collectAsState() - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .onEach { - navigator.hideWithResult(userManager?.userFlags?.isRegistered == true) - }.launchIn(this) - } - - CodeScaffold( - bottomBar = { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset) - .padding(bottom = CodeTheme.dimens.grid.x2) - .navigationBarsPadding(), - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x1) - ) { - CodeButton( - modifier = Modifier - .fillMaxWidth(), - buttonState = ButtonState.Filled, - isLoading = state.creatingAccount.loading, - isSuccess = state.creatingAccount.success, - text = stringResource(R.string.action_purchaseAccount), - ) { - composeScope.launch { - context.getActivity()?.let { - billingController.purchase(it, IapProduct.CreateAccount) - } - } - } - } - } - ) { padding -> - Box( - modifier = Modifier - .fillMaxSize() - .padding(padding) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset) - .align(Alignment.Center), - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), - horizontalAlignment = Alignment.CenterHorizontally - ) { - UserAvatar( - modifier = Modifier - .size(120.dp) - .clip(CircleShape), - data = state.userId, - overlay = { - Image( - modifier = Modifier.size(60.dp), - imageVector = Icons.Default.CheckCircleOutline, - colorFilter = ColorFilter.tint(Color.White), - contentDescription = null, - ) - } - ) - - Text( - modifier = Modifier.padding(top = CodeTheme.dimens.grid.x10), - text = stringResource(R.string.title_finalizeAccountCreation), - style = CodeTheme.typography.textLarge, - textAlign = TextAlign.Center, - color = CodeTheme.colors.textMain - ) - - Text( - text = stringResource( - R.string.subtitle_finalizeAccountCreation, - state.costOfAccount - ), - style = CodeTheme.typography.textMedium, - textAlign = TextAlign.Center, - color = CodeTheme.colors.textSecondary - ) - } - } - } -} - -@HiltViewModel -private class PurchaseAccountViewModel @Inject constructor( - private val userManager: UserManager, - private val authManager: AuthManager, - billingClient: BillingClient, - resources: ResourceHelper, - dispatchers: DispatcherProvider, -) : BaseViewModel2( - initialState = State(), - updateStateForEvent = updateStateForEvent, - dispatchers = dispatchers, -) { - data class State( - val userId: ID? = null, - val costOfAccount: String = "", - val creatingAccount: LoadingSuccessState = LoadingSuccessState(), - ) - - sealed interface Event { - data class OnUserIdChanged(val id: ID): Event - data class OnCostChanged(val cost: String): Event - data class OnCreatingChanged(val creating: Boolean, val created: Boolean = false) : Event - data object OnAccountCreated: Event - } - - init { - userManager.state - .mapNotNull { it.userId } - .onEach { dispatchEvent(Event.OnUserIdChanged(it)) } - .launchIn(viewModelScope) - - viewModelScope.launch { - dispatchEvent(Event.OnCostChanged(billingClient.costOf(IapProduct.CreateAccount))) - } - - billingClient.eventFlow - .mapNotNull { event -> - when (event) { - IapPaymentEvent.OnCancelled -> null - is IapPaymentEvent.OnError -> { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToPurchaseItem), - resources.getString(R.string.error_description_failedToPurchaseItem,) - ) - ) - null - } - is IapPaymentEvent.OnSuccess -> event - } - }.filterIsInstance() - .onEach { - dispatchEvent(Event.OnCreatingChanged(true)) - authManager.register(userManager.displayName!!) - .onSuccessWithDelay(2.seconds) { - dispatchEvent(Event.OnCreatingChanged(creating = false, created = true)) - delay(2.seconds) - dispatchEvent(Event.OnAccountCreated) - }.onFailure { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToCreateAccount), - resources.getString(R.string.error_description_failedToCreateAccount) - ) - ) - } - } - .launchIn(viewModelScope) - } - - companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - Event.OnAccountCreated -> { state -> state } - is Event.OnCostChanged -> { state -> state.copy(costOfAccount = event.cost) } - is Event.OnCreatingChanged -> { state -> state.copy(creatingAccount = LoadingSuccessState(loading = event.creating, success = event.created)) } - is Event.OnUserIdChanged -> { state -> state.copy(userId = event.id) } - } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/register/RegisterDisplayNameViewModel.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/register/RegisterDisplayNameViewModel.kt deleted file mode 100644 index 41f5c8fc5..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/register/RegisterDisplayNameViewModel.kt +++ /dev/null @@ -1,124 +0,0 @@ -package xyz.flipchat.app.features.login.register - -import androidx.compose.foundation.text.input.TextFieldState -import androidx.lifecycle.viewModelScope -import com.getcode.manager.TopBarManager -import com.flipcash.libs.coroutines.DispatcherProvider -import com.getcode.view.BaseViewModel2 -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import xyz.flipchat.app.auth.AuthManager -import xyz.flipchat.controllers.CodeController -import xyz.flipchat.services.user.AuthState -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject - -@HiltViewModel -class RegisterDisplayNameViewModel @Inject constructor( - authManager: AuthManager, - codeController: CodeController, - userManager: UserManager, - dispatchers: DispatcherProvider, -): BaseViewModel2( - initialState = State(), - updateStateForEvent = updateStateForEvent, - dispatchers = dispatchers, -) { - data class State( - val checkingDisplayName: Boolean = false, - val isValidDisplayName: Boolean = false, - val textFieldState: TextFieldState = TextFieldState(), - ) { - val canAdvance: Boolean - get() = textFieldState.text.isNotEmpty() - } - - sealed interface Event { - data object RegisterDisplayName : Event - data object OnSuccess : Event - data class OnError(val reason: String) : Event - } - - init { - eventFlow - .filterIsInstance() - .map { stateFlow.value } - .map { - val textFieldState = it.textFieldState - val text = textFieldState.text.toString().trim() - - if (userManager.authState == AuthState.Unregistered) { - userManager.set(displayName = text) - Result.success(userManager.userId!!) - } else { - authManager.register(text) - } - } - .onResult( - onError = { dispatchEvent(Event.OnError(it.localizedMessage ?: "Something went wrong")) }, - onSuccess = { dispatchEvent(Event.OnSuccess) } - ) - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .onEach { - codeController.fetchBalance() - .onFailure { it.printStackTrace() } - .onSuccess { codeController.requestAirdrop() } - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .onEach { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - title = "Create Account Failed", - message = it.reason - ) - ) - }.launchIn(viewModelScope) - } - - internal companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - Event.RegisterDisplayName -> { state -> - state.copy(checkingDisplayName = true) - } - is Event.OnError -> { state -> - state.copy(checkingDisplayName = false) - } - Event.OnSuccess -> { state -> - state.copy(isValidDisplayName = true) - } - } - } - } -} - -fun Flow>.onResult(onError: (Throwable) -> Unit = { }, onSuccess: (T) -> Unit = { }): Flow> { - return this.map { - it.onSuccess(onSuccess).onFailure(onError) - } -} - -fun Flow>.mapResult(block: (T) -> R): Flow> { - return this.map { - if (it.isSuccess) { - Result.success(block(it.getOrNull()!!)) - } else { - Result.failure(it.exceptionOrNull() ?: Throwable("mapResult failed")) - } - } -} - -fun Flow>.onError(block: (Throwable) -> Unit): Flow> { - return this.map { - it.onFailure(block) - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/register/RegisterInfoScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/register/RegisterInfoScreen.kt deleted file mode 100644 index 396301856..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/register/RegisterInfoScreen.kt +++ /dev/null @@ -1,174 +0,0 @@ -package xyz.flipchat.app.features.login.register - -import android.os.Parcelable -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Person -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.lifecycle.ViewModel -import cafe.adriel.voyager.core.registry.ScreenRegistry -import cafe.adriel.voyager.hilt.getViewModel -import com.getcode.model.ID -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.modal.FullScreenModalScreen -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.AppBarDefaults -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.components.chat.UserAvatar -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.theme.CodeScaffold -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.parcelize.Parcelize -import xyz.flipchat.app.R -import xyz.flipchat.services.billing.BillingClient -import xyz.flipchat.services.billing.IapProduct -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject - -@Parcelize -class RegisterInfoScreen : FullScreenModalScreen, Parcelable { - - @Composable - override fun ModalContent() { - val navigator = LocalCodeNavigator.current - - Column { - AppBarWithTitle( - backButton = false, - endContent = { - AppBarDefaults.Close { navigator.hide() } - } - ) - RegisterInfoScreenContent( - viewModel = getViewModel(), - onGetStarted = { - navigator.push( - ScreenRegistry.get( - NavScreenProvider.CreateAccount.NameEntry( - showInModal = true - ) - ) - ) - }, - onNotNow = { - navigator.hide() - } - ) - } - } -} - -@Composable -private fun RegisterInfoScreenContent( - viewModel: RegisterInfoViewModel, - onGetStarted: () -> Unit, - onNotNow: () -> Unit -) { - CodeScaffold( - bottomBar = { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset) - .padding(bottom = CodeTheme.dimens.grid.x2) - .navigationBarsPadding(), - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x1) - ) { - CodeButton( - modifier = Modifier - .fillMaxWidth(), - buttonState = ButtonState.Filled, - text = stringResource(R.string.action_getStarted) - ) { - onGetStarted() - } - - CodeButton( - modifier = Modifier.fillMaxWidth(), - buttonState = ButtonState.Subtle, - text = stringResource(R.string.action_notNow), - ) { - onNotNow() - } - } - } - ) { padding -> - Box( - modifier = Modifier - .fillMaxSize() - .padding(padding) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset) - .align(Alignment.Center), - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x2), - horizontalAlignment = Alignment.CenterHorizontally - ) { - UserAvatar( - modifier = Modifier - .size(120.dp) - .clip(CircleShape), - data = viewModel.userId, - overlay = { - Image( - modifier = Modifier.size(60.dp), - imageVector = Icons.Default.Person, - colorFilter = ColorFilter.tint(Color.White), - contentDescription = null, - ) - } - ) - - Text( - modifier = Modifier.padding(top = CodeTheme.dimens.grid.x10), - text = stringResource(R.string.title_createAccountToJoinRooms), - style = CodeTheme.typography.textLarge, - color = CodeTheme.colors.textMain, - textAlign = TextAlign.Center, - ) - - Text( - text = stringResource(R.string.title_newAccountsCost, viewModel.costOfAccount), - textAlign = TextAlign.Center, - style = CodeTheme.typography.textMedium, - color = CodeTheme.colors.textSecondary - ) - } - } - } -} - -@HiltViewModel -private class RegisterInfoViewModel @Inject constructor( - private val userManager: UserManager, - private val iapController: BillingClient -) : ViewModel() { - - val userId: ID? - get() = userManager.userId - - val costOfAccount: String - get() = iapController.costOf(IapProduct.CreateAccount) -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/register/RegisterModalScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/register/RegisterModalScreen.kt deleted file mode 100644 index 300ac6580..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/register/RegisterModalScreen.kt +++ /dev/null @@ -1,51 +0,0 @@ -package xyz.flipchat.app.features.login.register - -import android.os.Parcelable -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import cafe.adriel.voyager.core.registry.ScreenRegistry -import cafe.adriel.voyager.hilt.getViewModel -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.modal.FullScreenModalScreen -import com.getcode.ui.components.AppBarDefaults -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.utils.keyboardAsState -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.parcelize.Parcelize - -@Parcelize -class RegisterModalScreen : FullScreenModalScreen, Parcelable { - - @Composable - override fun ModalContent() { - val navigator = LocalCodeNavigator.current - val keyboardIsVisible by keyboardAsState() - val keyboard = LocalSoftwareKeyboardController.current - val composeScope = rememberCoroutineScope() - - Column { - AppBarWithTitle( - backButton = false, - endContent = { - AppBarDefaults.Close { - composeScope.launch { - if (keyboardIsVisible) { - keyboard?.hide() - delay(500) - } - navigator.hide() - } - } - } - ) - RegisterDisplayNameScreenContent(getViewModel()) { - navigator.push(ScreenRegistry.get(NavScreenProvider.CreateAccount.AccessKey(true))) - } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/register/RegisterScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/register/RegisterScreen.kt deleted file mode 100644 index 6bd5c532b..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/login/register/RegisterScreen.kt +++ /dev/null @@ -1,175 +0,0 @@ -package xyz.flipchat.app.features.login.register - -import android.os.Parcelable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardCapitalization -import cafe.adriel.voyager.core.registry.ScreenRegistry -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.hilt.getViewModel -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.theme.CodeTheme -import com.getcode.theme.inputColors -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.components.TextInput -import com.getcode.ui.core.rememberAnimationScale -import com.getcode.ui.core.scaled -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.theme.CodeScaffold -import com.getcode.ui.utils.ConstraintMode -import com.getcode.ui.utils.keyboardAsState -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.parcelize.Parcelize -import xyz.flipchat.app.R -import xyz.flipchat.app.features.login.register.RegisterDisplayNameViewModel.Event - -@Parcelize -class RegisterScreen : Screen, Parcelable { - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - - Column { - AppBarWithTitle( - backButton = true, - onBackIconClicked = navigator::pop - ) - RegisterDisplayNameScreenContent(getViewModel()) { - navigator.push(ScreenRegistry.get(NavScreenProvider.CreateAccount.AccessKey(false))) - } - } - } -} - -@Composable -internal fun RegisterDisplayNameScreenContent( - viewModel: RegisterDisplayNameViewModel, - onShowAccessKey: () -> Unit, -) { - val state by viewModel.stateFlow.collectAsState() - - val keyboardVisible by keyboardAsState() - val keyboardController = LocalSoftwareKeyboardController.current - val composeScope = rememberCoroutineScope() - val animationScale by rememberAnimationScale() - var isChecking by remember(state.checkingDisplayName) { mutableStateOf(state.checkingDisplayName) } - - val register = { - composeScope.launch { - isChecking = true - if (keyboardVisible) { - keyboardController?.hide() - delay(500.scaled(animationScale)) - } - viewModel.dispatchEvent(Event.RegisterDisplayName) - } - } - - LaunchedEffect(viewModel) { - viewModel.eventFlow - .filterIsInstance() - .onEach { delay(400.scaled(animationScale)) } - .onEach { onShowAccessKey() } - .launchIn(this) - } - - CodeScaffold( - modifier = Modifier - .fillMaxSize() - .imePadding(), - bottomBar = { - Box( - modifier = Modifier - .fillMaxWidth() - .navigationBarsPadding() - .padding(bottom = CodeTheme.dimens.grid.x3), - ) { - CodeButton( - enabled = state.canAdvance, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset), - buttonState = ButtonState.Filled, - text = stringResource(R.string.action_next), - isLoading = isChecking, - isSuccess = state.isValidDisplayName - ) { - register() - } - } - } - ) { padding -> - val focusRequester = remember { FocusRequester() } - Box( - modifier = Modifier - .fillMaxSize() - .padding(padding) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterStart) - .padding(horizontal = CodeTheme.dimens.inset), - verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.inset) - ) { - TextInput( - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - state = state.textFieldState, - colors = inputColors( - backgroundColor = Color.Transparent, - borderColor = Color.Transparent - ), - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.Sentences - ), - maxLines = 1, - constraintMode = ConstraintMode.AutoSize(minimum = CodeTheme.typography.displaySmall), - style = CodeTheme.typography.displayMedium, - placeholderStyle = CodeTheme.typography.displayMedium, - placeholder = stringResource(R.string.subtitle_yourName), - ) - - Text( - text = stringResource(R.string.subtitle_displayNameHint), - style = CodeTheme.typography.textMedium, - color = Color.White.copy(0.4f) - ) - } - } - - LaunchedEffect(focusRequester) { - focusRequester.requestFocus() - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/payments/MessageTipPaymentConfirmation.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/payments/MessageTipPaymentConfirmation.kt deleted file mode 100644 index dfec2f773..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/payments/MessageTipPaymentConfirmation.kt +++ /dev/null @@ -1,130 +0,0 @@ -package xyz.flipchat.app.features.payments - -import androidx.activity.compose.BackHandler -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.res.stringResource -import cafe.adriel.voyager.navigator.currentOrThrow -import com.getcode.model.Currency -import com.getcode.model.KinAmount -import com.getcode.model.Rate -import com.getcode.model.kin -import com.getcode.models.ConfirmationState -import com.getcode.models.MessageTipPaymentConfirmation -import com.getcode.network.LocalBalanceController -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.Modal -import com.getcode.ui.components.SlideToConfirm -import com.getcode.ui.components.picker.Picker -import com.getcode.ui.components.picker.PickerState -import com.getcode.ui.components.picker.rememberPickerState -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.util.resources.LocalResources -import com.getcode.utils.Kin -import com.getcode.utils.formatAmountString -import xyz.flipchat.app.R -import com.getcode.ui.components.R as uiR - -private val tipOptions = (1 .. 100).map { KinAmount.newInstance(it.kin, Rate.oneToOne) } - -@Composable -internal fun MessageTipPaymentConfirmation( - modifier: Modifier = Modifier, - confirmation: MessageTipPaymentConfirmation?, - onSend: (KinAmount) -> Unit, - onCancel: () -> Unit, -) { - val state by remember(confirmation?.state) { - derivedStateOf { confirmation?.state } - } - - val isSending by remember(state) { - derivedStateOf { state is ConfirmationState.Sending } - } - - val resources = LocalResources.current!! - - val pickerState = rememberPickerState( - items = tipOptions, - prefix = "⬢", - initialIndex = tipOptions.indexOfFirst { it.kin == 5.kin } - ) { item -> - formatAmountString( - resources = resources, - currency = Currency.Kin, - amount = item.kin.toKinValueDouble(), - suffix = resources.getKinSuffix() - ) - } - - val balanceController = LocalBalanceController.currentOrThrow - val currentBalance by balanceController.formattedBalance.collectAsState() - - BackHandler { - onCancel() - } - - Modal(modifier) { - if (state != null) { - MessageTipConfirmationContent( - pickerState = pickerState, - balance = currentBalance?.formattedValue, - isSending = isSending, - state = state, - onApproved = { - pickerState.selectedItem?.let { - onSend(it) - } - } - ) - val enabled = state !is ConfirmationState.Sending && state !is ConfirmationState.Sent - val alpha by animateFloatAsState(targetValue = if (enabled) 1f else 0f, label = "alpha") - CodeButton( - modifier = Modifier - .fillMaxWidth() - .alpha(alpha), - enabled = enabled, - buttonState = ButtonState.Subtle, - onClick = onCancel, - text = stringResource(id = android.R.string.cancel), - ) - } - } -} - -@Composable -private fun MessageTipConfirmationContent( - pickerState: PickerState, - balance: String?, - isSending: Boolean, - state: ConfirmationState?, - onApproved: () -> Unit -) { - Picker( - modifier = Modifier.fillMaxWidth(), - state = pickerState, - textStyle = CodeTheme.typography.displayMedium - ) - Text( - text = stringResource( - R.string.subtitle_balance, - balance.orEmpty(), - ), - style = CodeTheme.typography.textSmall.copy(color = CodeTheme.colors.tertiary), - ) - SlideToConfirm( - isLoading = isSending, - isSuccess = state is ConfirmationState.Sent, - onConfirm = onApproved, - label = stringResource(uiR.string.action_swipeToTip) - ) -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/payments/PaymentScaffold.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/payments/PaymentScaffold.kt deleted file mode 100644 index ad9e66e77..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/payments/PaymentScaffold.kt +++ /dev/null @@ -1,111 +0,0 @@ -package xyz.flipchat.app.features.payments - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment.Companion.BottomCenter -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import com.getcode.models.BillState -import com.getcode.theme.Black40 -import com.getcode.ui.utils.AnimationUtils -import com.getcode.ui.utils.ModalAnimationSpeed -import com.getcode.ui.core.rememberedClickable -import xyz.flipchat.services.LocalPaymentController - -@Composable -fun PaymentScaffold(content: @Composable () -> Unit) { - val payments = LocalPaymentController.current ?: error("CompositionLocal is null") - - val state by payments.state.collectAsState() - Box(modifier = Modifier.fillMaxSize()) { - content() - val scrimDetails by rememberConfirmationDetails(state.billState) - - val scrimAlpha by animateFloatAsState(if (scrimDetails.show) 1f else 0f, label = "scrim visibility") - - if (scrimDetails.show) { - Box( - modifier = Modifier - .fillMaxSize() - .alpha(scrimAlpha) - .background(Black40) - .rememberedClickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - if (scrimDetails.cancellable) { - payments.cancelPayment(fromUser = true) - } - } - ) - } - - // public payments - AnimatedContent( - modifier = Modifier.align(BottomCenter), - targetState = state.billState.publicPaymentConfirmation?.amount, // amount is constant across state changes - transitionSpec = AnimationUtils.modalAnimationSpec(speed = ModalAnimationSpeed.Fast), - label = "public payments", - ) { - if (it != null) { - Box( - contentAlignment = BottomCenter - ) { - PublicPaymentConfirmation( - confirmation = state.billState.publicPaymentConfirmation, - onSend = { payments.completePublicPayment() }, - onCancel = { payments.cancelPayment() } - ) - } - } - } - - AnimatedContent( - modifier = Modifier.align(BottomCenter), - targetState = state.billState.messageTipPaymentConfirmation?.metadata, - transitionSpec = AnimationUtils.modalAnimationSpec(speed = ModalAnimationSpeed.Fast), - label = "message tip payments", - ) { - if (it != null) { - Box( - contentAlignment = BottomCenter - ) { - MessageTipPaymentConfirmation( - confirmation = state.billState.messageTipPaymentConfirmation, - onSend = { amount -> payments.completeMessageTip(amount) }, - onCancel = { payments.cancelPayment() } - ) - } - } - } - } -} - -data class ScrimDetails(val show: Boolean, val cancellable: Boolean) - -@Composable -private fun rememberConfirmationDetails(billState: BillState): State { - return remember(billState) { - derivedStateOf { - val publicPaymentConfirmation = billState.publicPaymentConfirmation - val messageTipPaymentConfirmation = billState.messageTipPaymentConfirmation - - listOf( - publicPaymentConfirmation, - messageTipPaymentConfirmation - ).firstNotNullOfOrNull { it }?.let { conf -> - ScrimDetails(conf.showScrim, conf.cancellable) - } ?: ScrimDetails(show = false, cancellable = false) - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/payments/PublicPaymentConfirmation.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/payments/PublicPaymentConfirmation.kt deleted file mode 100644 index 3f372151c..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/payments/PublicPaymentConfirmation.kt +++ /dev/null @@ -1,96 +0,0 @@ -package xyz.flipchat.app.features.payments - -import androidx.activity.compose.BackHandler -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.getcode.model.KinAmount -import com.getcode.models.ConfirmationState -import com.getcode.models.PublicPaymentConfirmation -import com.getcode.theme.CodeTheme -import com.getcode.theme.bolded -import com.getcode.ui.components.Modal -import com.getcode.ui.components.PriceWithFlag -import com.getcode.ui.components.SlideToConfirm -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton - -@Composable -internal fun PublicPaymentConfirmation( - modifier: Modifier = Modifier, - confirmation: PublicPaymentConfirmation?, - onSend: () -> Unit, - onCancel: () -> Unit, -) { - val state by remember(confirmation?.state) { - derivedStateOf { confirmation?.state } - } - - val isSending by remember(state) { - derivedStateOf { state is ConfirmationState.Sending } - } - - val requestedAmount by remember(confirmation?.amount?.kin?.quarks) { - derivedStateOf { confirmation?.amount } - } - - BackHandler { - onCancel() - } - - - Modal(modifier) { - val amount = requestedAmount - if (state != null && amount != null) { - PaymentConfirmationContent( - amount = amount, - isSending = isSending, - state = state, - onApproved = onSend - ) - val enabled = state !is ConfirmationState.Sending && state !is ConfirmationState.Sent - val alpha by animateFloatAsState(targetValue = if (enabled) 1f else 0f, label = "alpha") - CodeButton( - modifier = Modifier.fillMaxWidth().alpha(alpha), - enabled = enabled, - buttonState = ButtonState.Subtle, - onClick = onCancel, - text = stringResource(id = android.R.string.cancel), - ) - } - } -} - -@Composable -private fun PaymentConfirmationContent( - amount: KinAmount, - isSending: Boolean, - state: ConfirmationState?, - onApproved: () -> Unit -) { - PriceWithFlag( - currencyCode = amount.rate.currency, - amount = amount, - iconSize = 24.dp - ) { - Text( - text = it, - color = Color.White, - style = CodeTheme.typography.displayMedium.bolded() - ) - } - SlideToConfirm( - isLoading = isSending, - isSuccess = state is ConfirmationState.Sent, - onConfirm = { onApproved() }, - ) -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/profile/ProfileScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/profile/ProfileScreen.kt deleted file mode 100644 index 72f6d717b..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/profile/ProfileScreen.kt +++ /dev/null @@ -1,393 +0,0 @@ -package xyz.flipchat.app.features.profile - -import android.app.Activity -import android.content.Context -import android.os.Parcelable -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.Crossfade -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Person -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.lifecycle.Lifecycle -import cafe.adriel.voyager.core.registry.ScreenRegistry -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.hilt.getViewModel -import com.getcode.manager.BottomBarManager -import com.getcode.model.ID -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.model.social.user.SocialProfile -import com.getcode.navigation.screens.ContextSheet -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.AppBarDefaults -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.components.chat.UserAvatar -import com.getcode.ui.components.user.social.SocialUserDisplay -import com.getcode.ui.core.ContextMenuAction -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.utils.RepeatOnLifecycle -import com.getcode.ui.utils.getActivity -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import xyz.flipchat.app.R -import xyz.flipchat.app.features.profile.components.ProfileContextAction -import xyz.flipchat.app.oauth.OAuthProvider -import xyz.flipchat.app.oauth.rememberLauncherForOAuth -import xyz.flipchat.app.ui.LocalUserManager -import xyz.flipchat.services.internal.data.mapper.nullIfEmpty - -@Parcelize -class ProfileScreen(val userId: ID? = null, val isInTab: Boolean) : Screen, Parcelable { - - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @Composable - override fun Content() { - val navigator = LocalCodeNavigator.current - val context = LocalContext.current - val userManager = LocalUserManager.current - val viewModel = getViewModel() - val state by viewModel.stateFlow.collectAsState() - val composeScope = rememberCoroutineScope() - - LaunchedEffect(viewModel, userId) { - if (userId != null) { - viewModel.dispatchEvent(ProfileViewModel.Event.OnLoadUser(userId)) - } else { - viewModel.dispatchEvent(ProfileViewModel.Event.OnLoadUser(userManager?.userId!!)) - } - } - - RepeatOnLifecycle(targetState = Lifecycle.State.RESUMED) { - viewModel.dispatchEvent(ProfileViewModel.Event.CheckUserLink) - } - - Column { - AppBarWithTitle( - backButton = !isInTab, - onBackIconClicked = { navigator.pop() }, - endContent = { - if (state.isSelf && isInTab) { - AppBarDefaults.Overflow { - navigator.show( - ContextSheet( - buildActions( - state = state, - dispatchEvent = viewModel::dispatchEvent, - navigateTo = { - composeScope.launch { - delay(200) - navigator.push(it) - } - }, - deleteAccount = { - confirmAccountDeletion( - context, - composeScope - ) { activity -> - viewModel.deleteAccount(activity) { - navigator.replaceAll(ScreenRegistry.get( - NavScreenProvider.Login.Home())) - } - } - } - ) - ) - ) - } - } - } - ) - ProfileContent( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - state = state, - isInTab = isInTab, - dispatchEvent = viewModel::dispatchEvent - ) - } - } -} - - -private fun confirmAccountDeletion( - context: Context, - composeScope: CoroutineScope, - onConfirmed: (Activity) -> Unit -) { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = context.getString(R.string.prompt_title_deleteAccount), - subtitle = context - .getString(R.string.prompt_description_deleteAccount), - positiveText = context.getString(R.string.action_permanentlyDeleteAccount), - tertiaryText = context.getString(R.string.action_cancel), - onPositive = { - composeScope.launch { - delay(150) - context.getActivity()?.let { - onConfirmed(it) - } - } - } - ) - ) -} - -@Composable -private fun ProfileContent( - modifier: Modifier = Modifier, - isInTab: Boolean, - state: ProfileViewModel.State, - dispatchEvent: (ProfileViewModel.Event) -> Unit -) { - val context = LocalContext.current - val uriHandler = LocalUriHandler.current - - Column( - modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally - ) { - UserAvatar( - modifier = Modifier - .padding( - top = CodeTheme.dimens.grid.x7, - bottom = CodeTheme.dimens.grid.x4 - ) - .size(120.dp) - .clip(CircleShape), - data = state.imageUrl.nullIfEmpty() ?: state.id, - overlay = { - Image( - modifier = Modifier.size(60.dp), - imageVector = Icons.Default.Person, - colorFilter = ColorFilter.tint(Color.White), - contentDescription = null, - ) - } - ) - - // SocialUserDisplay (title with platform logo) - if (state.linkedSocialProfile != null) { - SocialUserDisplay( - modifier = Modifier.fillMaxWidth(), - profile = state.linkedSocialProfile - ) - } else { - Text( - text = state.displayName, - style = CodeTheme.typography.textLarge, - color = CodeTheme.colors.textMain, - textAlign = TextAlign.Center - ) - } - - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - // Crossfade for linked vs unlinked content - Crossfade( - targetState = state.linkedSocialProfile, - animationSpec = tween(durationMillis = 300) - ) { profile -> - when (profile) { - SocialProfile.Unknown -> Unit - null -> { - // Unlinked state - if (state.canConnectAccount && state.isSelf) { - val xOAuthLauncher = - rememberLauncherForOAuth(OAuthProvider.X) { accessToken -> - dispatchEvent(ProfileViewModel.Event.LinkXAccount(accessToken)) - } - - AnimatedVisibility( - visible = true, - enter = fadeIn() + slideInVertically(initialOffsetY = { it / 2 }), - exit = fadeOut() + slideOutVertically(targetOffsetY = { it / 2 }) - ) { - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(top = CodeTheme.dimens.grid.x12) - .padding(horizontal = CodeTheme.dimens.inset), - buttonState = ButtonState.Filled, - onClick = { - xOAuthLauncher.launch( - OAuthProvider.X.launchIntent( - context - ) - ) - }, - content = { - Image( - painter = rememberVectorPainter( - image = ImageVector.vectorResource( - id = R.drawable.ic_twitter_x - ) - ), - colorFilter = ColorFilter.tint(Color.Black), - contentDescription = null - ) - Spacer(Modifier.width(CodeTheme.dimens.grid.x2)) - Text(text = stringResource(R.string.action_connectYourAccount)) - } - ) - } - } - } - else -> { - // Linked state - state.linkedSocialProfile?.let { linkedProfile -> - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - state.username?.let { username -> - AnimatedVisibility( - visible = true, - enter = fadeIn(), - exit = fadeOut() + shrinkVertically() - ) { - Text( - modifier = Modifier.fillMaxWidth(), - text = username, - style = CodeTheme.typography.textSmall, - color = CodeTheme.colors.textSecondary, - textAlign = TextAlign.Center - ) - } - } - - when (linkedProfile) { - is SocialProfile.X -> { - AnimatedVisibility( - visible = true, - enter = fadeIn(), - exit = fadeOut() + shrinkVertically() - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - modifier = Modifier - .fillMaxWidth() - .padding(top = CodeTheme.dimens.grid.x8), - text = "${linkedProfile.followerCountFormatted} Followers", - style = CodeTheme.typography.textSmall, - color = CodeTheme.colors.textSecondary, - textAlign = TextAlign.Center - ) - Text( - modifier = Modifier - .fillMaxWidth(0.70f) - .padding(top = CodeTheme.dimens.grid.x1), - text = linkedProfile.description, - style = CodeTheme.typography.textSmall, - color = CodeTheme.colors.textSecondary, - textAlign = TextAlign.Center - ) - } - } - } - - is SocialProfile.Unknown -> Unit - } - - if (!isInTab) { - AnimatedVisibility( - visible = true, - enter = fadeIn() + slideInVertically(initialOffsetY = { it / 2 }), - exit = fadeOut() + slideOutVertically(targetOffsetY = { it / 2 }) - ) { - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(top = CodeTheme.dimens.grid.x12) - .padding(horizontal = CodeTheme.dimens.inset), - buttonState = ButtonState.Filled, - onClick = { uriHandler.openUri(linkedProfile.profileUrl) }, - text = stringResource( - R.string.action_openProfileOnPlatform, - linkedProfile.platformTypeName - ) - ) - } - } - } - } - } - } - } - } - } -} - -private fun buildActions( - state: ProfileViewModel.State, - navigateTo: (Screen) -> Unit, - dispatchEvent: (ProfileViewModel.Event) -> Unit, - deleteAccount: () -> Unit, -): List { - return buildList { - if (state.isStaff) { - add( - ProfileContextAction.Labs { - navigateTo(ScreenRegistry.get(NavScreenProvider.BetaFlags)) - } - ) - } - - if (state.linkedSocialProfile != null) { - add( - ProfileContextAction.UnlinkSocialProfile(state.linkedSocialProfile) { - dispatchEvent(ProfileViewModel.Event.UnlinkSocialProfileRequested(state.linkedSocialProfile)) - } - ) - } - - add( - ProfileContextAction.DeleteAccount { - deleteAccount() - } - ) - } -} - diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/profile/ProfileViewModel.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/profile/ProfileViewModel.kt deleted file mode 100644 index 8c961eacc..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/profile/ProfileViewModel.kt +++ /dev/null @@ -1,223 +0,0 @@ -package xyz.flipchat.app.features.profile - -import android.app.Activity -import androidx.lifecycle.viewModelScope -import com.getcode.manager.BottomBarManager -import com.getcode.manager.TopBarManager -import com.getcode.model.ID -import com.getcode.model.social.user.SocialProfile -import com.getcode.util.resources.ResourceHelper -import com.flipcash.libs.coroutines.DispatcherProvider -import com.getcode.view.BaseViewModel2 -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.filterNot -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import xyz.flipchat.app.R -import xyz.flipchat.app.auth.AuthManager -import xyz.flipchat.app.beta.Lab -import xyz.flipchat.app.beta.Labs -import xyz.flipchat.app.features.login.register.onResult -import xyz.flipchat.controllers.ChatsController -import xyz.flipchat.controllers.ProfileController -import xyz.flipchat.services.domain.model.profile.UserProfile -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject - -@HiltViewModel -class ProfileViewModel @Inject constructor( - userManager: UserManager, - profileController: ProfileController, - labs: Labs, - resources: ResourceHelper, - private val authManager: AuthManager, - private val chatsController: ChatsController, - dispatchers: DispatcherProvider, -): BaseViewModel2( - initialState = State(), - updateStateForEvent = updateStateForEvent, - dispatchers = dispatchers, -) { - data class State( - val id: ID? = null, - val isSelf: Boolean = false, - private val name: String = "", - val linkedSocialProfile: SocialProfile? = SocialProfile.Unknown, - val isStaff: Boolean = false, - val canConnectAccount: Boolean = false, - val showConnectedSocial: Boolean = false, - ) { - val displayName: String - get() = linkedSocialProfile?.let { - when (it) { - is SocialProfile.Unknown -> null - is SocialProfile.X -> it.friendlyName - } - } ?: name - - val username: String? - get() = linkedSocialProfile?.let { - when (it) { - is SocialProfile.Unknown -> null - is SocialProfile.X -> "@${it.username}" - } - } - - val imageUrl: String? - get() = linkedSocialProfile?.let { - when (it) { - is SocialProfile.Unknown -> null - is SocialProfile.X -> it.profilePicUrl - } - } - } - - sealed interface Event { - data object CheckUserLink: Event - data class OnLoadUser(val id: ID): Event - data class OnUserLoaded(val isSelf: Boolean, val user: UserProfile): Event - data class OnStaffEmployed(val enabled: Boolean) : Event - data class CanConnectSocialChanged(val enabled: Boolean): Event - data class LinkXAccount(val accessToken: String?): Event - data class UnlinkSocialProfileRequested(val profile: SocialProfile): Event - data class UnlinkSocialProfile(val profile: SocialProfile): Event - } - - init { - // handle self user updates (connects/disconnects) - stateFlow - .filter { it.isSelf } - .flatMapLatest { userManager.state } - .mapNotNull { UserProfile(it.displayName.orEmpty(), it.linkedSocialProfiles) } - .distinctUntilChanged() - .onEach { dispatchEvent(Event.OnUserLoaded(true, it)) } - .launchIn(viewModelScope) - - userManager.state - .mapNotNull { it.flags } - .map { it.isStaff } - .onEach { dispatchEvent(Event.OnStaffEmployed(it)) } - .launchIn(viewModelScope) - - labs.observe(Lab.ConnectX) - .onEach { - dispatchEvent(Event.CanConnectSocialChanged(it)) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.id } - .distinctUntilChanged() - .map { id -> - if (userManager.isSelf(id)) { - Result.success(true to UserProfile(userManager.displayName.orEmpty(), userManager.socialProfiles)) - } else { - profileController.getProfile(id) - .map { false to it } - } - }.onResult( - onError = {}, - onSuccess = { (isSelf, profile) -> - dispatchEvent(Event.OnUserLoaded(isSelf, profile)) - } - ).launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .mapNotNull { stateFlow.value.id } - .map { profileController.getProfile(it) } - .launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.accessToken } - .map { profileController.linkXAccount(it.orEmpty()) } - .onResult( - onError = { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToLinkXAccount), - resources.getString(R.string.error_description_failedToLinkXAccount) - ) - ) - }, - ).launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.profile } - .filterNot { it is SocialProfile.Unknown } - .onEach { profile -> - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = resources.getString(R.string.prompt_title_disconnectSocialAccount, profile.platformTypeName), - subtitle = resources - .getString(R.string.prompt_description_disconnectSocialAccount, profile.platformTypeName), - positiveText = resources.getString(R.string.action_disconnectSocialAccount, profile.platformTypeName), - tertiaryText = resources.getString(R.string.action_cancel), - onPositive = { - dispatchEvent(Event.UnlinkSocialProfile(profile)) - } - ) - ) - }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .map { it.profile } - .onEach { - when (it) { - SocialProfile.Unknown -> Unit - is SocialProfile.X -> { - profileController.unlinkXAccount(it) - .onFailure { - TopBarManager.showMessage( - TopBarManager.TopBarMessage( - resources.getString(R.string.error_title_failedToUnlinkXAccount), - resources.getString(R.string.error_description_failedToUnlinkXAccount) - ) - ) - } - } - } - } - .launchIn(viewModelScope) - } - - fun deleteAccount(activity: Activity, onComplete: () -> Unit) = viewModelScope.launch { - authManager.deleteAndLogout(activity) - .onSuccess { - chatsController.closeEventStream() - onComplete() - } - } - - internal companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - is Event.CheckUserLink -> { state -> state } - is Event.OnLoadUser -> { state -> state.copy(id = event.id) } - is Event.OnStaffEmployed -> { state -> state.copy(isStaff = event.enabled) } - is Event.OnUserLoaded -> { state -> - state.copy( - isSelf = event.isSelf, - name = event.user.displayName, - linkedSocialProfile = event.user.socialProfiles - .firstOrNull() - ) - } - is Event.CanConnectSocialChanged -> { state -> state.copy(canConnectAccount = event.enabled) } - is Event.LinkXAccount -> { state -> state } - is Event.UnlinkSocialProfileRequested -> { state -> state } - is Event.UnlinkSocialProfile -> { state -> state } - } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/profile/components/ProfileContextSheet.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/profile/components/ProfileContextSheet.kt deleted file mode 100644 index 5e7924672..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/profile/components/ProfileContextSheet.kt +++ /dev/null @@ -1,56 +0,0 @@ -package xyz.flipchat.app.features.profile.components - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material.icons.outlined.LinkOff -import androidx.compose.material.icons.outlined.Science -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import com.getcode.model.social.user.SocialProfile -import com.getcode.ui.core.ContextMenuAction -import xyz.flipchat.app.R - -sealed interface ProfileContextAction : ContextMenuAction { - data class Labs(override val onSelect: () -> Unit) : ProfileContextAction, ContextMenuAction.Single { - override val isDestructive: Boolean = false - override val delayUponSelection: Boolean = true - override val title: String - @Composable get() = stringResource(R.string.title_betaFlags) - override val painter: Painter - @Composable get() = rememberVectorPainter(Icons.Outlined.Science) - } - - data class UnlinkSocialProfile( - val profile: SocialProfile, - override val onSelect: () -> Unit - ) : ProfileContextAction, ContextMenuAction.Single { - override val isDestructive: Boolean = true - override val delayUponSelection: Boolean = true - override val title: String - @Composable get() = when (profile) { - SocialProfile.Unknown -> "" - is SocialProfile.X -> "Disconnect X" - } - override val painter: Painter - @Composable get() = when (profile) { - SocialProfile.Unknown -> rememberVectorPainter(Icons.Outlined.LinkOff) - is SocialProfile.X -> rememberVectorPainter(image = ImageVector.vectorResource(id = R.drawable.ic_twitter_x)) - } - } - - data class DeleteAccount( - override val onSelect: () -> Unit - ): ProfileContextAction, ContextMenuAction.Single { - override val isDestructive: Boolean = true - override val delayUponSelection: Boolean = true - override val title: String - @Composable get() = stringResource(R.string.action_deleteMyAccount) - override val painter: Painter - @Composable get() = rememberVectorPainter(Icons.Default.Delete) - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/restricted/AppRestrictedScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/restricted/AppRestrictedScreen.kt deleted file mode 100644 index f874f22f5..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/restricted/AppRestrictedScreen.kt +++ /dev/null @@ -1,27 +0,0 @@ -package xyz.flipchat.app.features.restricted - -import android.os.Parcelable -import androidx.compose.runtime.Composable -import cafe.adriel.voyager.core.registry.ScreenRegistry -import cafe.adriel.voyager.core.screen.Screen -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.extensions.getActivityScopedViewModel -import com.getcode.ui.components.restrictions.ContentRestrictedView -import com.getcode.ui.core.RestrictionType -import kotlinx.parcelize.Parcelize -import xyz.flipchat.app.features.home.HomeViewModel - -@Parcelize -class AppRestrictedScreen(private val restrictionType: RestrictionType): Screen, Parcelable { - @Composable - override fun Content() { - val homeViewModel = getActivityScopedViewModel() - val navigator = LocalCodeNavigator.current - ContentRestrictedView(restrictionType) { - homeViewModel.logout(it) { - navigator.replaceAll(ScreenRegistry.get(NavScreenProvider.Login.Home())) - } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/settings/SettingsScreen.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/settings/SettingsScreen.kt deleted file mode 100644 index 77af1dc98..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/settings/SettingsScreen.kt +++ /dev/null @@ -1,149 +0,0 @@ -package xyz.flipchat.app.features.settings - -import android.os.Parcelable -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Science -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import cafe.adriel.voyager.core.registry.ScreenRegistry -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.hilt.getViewModel -import com.getcode.manager.BottomBarManager -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.components.SettingsRow -import com.getcode.ui.core.rememberAnimationScale -import com.getcode.ui.core.scaled -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.theme.CodeScaffold -import com.getcode.ui.utils.getActivity -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import xyz.flipchat.app.R - -@Parcelize -class SettingsScreen : Screen, Parcelable { - @IgnoredOnParcel - override val key: ScreenKey = uniqueScreenKey - - @Composable - override fun Content() { - val viewModel = getViewModel() - val navigator = LocalCodeNavigator.current - val context = LocalContext.current - val composeScope = rememberCoroutineScope() - val animationScale by rememberAnimationScale() - - val state by viewModel.stateFlow.collectAsState() - - CodeScaffold( - modifier = Modifier - .fillMaxSize() - .padding( - vertical = CodeTheme.dimens.grid.x2, - ), - topBar = { - AppBarWithTitle( - backButton = true, - onBackIconClicked = { - navigator.pop() - } - ) - }, - bottomBar = { - LogoutButton { - composeScope.launch { - delay(150.scaled(animationScale)) // wait for dismiss - context.getActivity()?.let { - viewModel.logout(it) { - navigator.replaceAll(ScreenRegistry.get(NavScreenProvider.Login.Home())) - } - } - } - } - } - ) { padding -> - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(padding) - ) { - item { - Box(modifier = Modifier.fillParentMaxWidth()) { - Image( - painter = painterResource(R.drawable.flipchat_logo), - contentDescription = "", - modifier = Modifier - .padding(vertical = CodeTheme.dimens.inset) - .align(Alignment.Center) - .size(CodeTheme.dimens.staticGrid.x12), - - ) - } - } - if (state.isStaff) { - item { - SettingsRow( - modifier = Modifier.padding(horizontal = CodeTheme.dimens.inset), - title = stringResource(R.string.title_betaFlags), - icon = rememberVectorPainter(Icons.Outlined.Science) - ) { - navigator.push(ScreenRegistry.get(NavScreenProvider.BetaFlags)) - } - } - } - } - } - } -} - -@Composable -private fun LogoutButton( - onConfirmed: () -> Unit -) { - val context = LocalContext.current - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset) - .navigationBarsPadding(), - buttonState = ButtonState.Filled, - text = stringResource(R.string.action_logout) - ) { - BottomBarManager.showMessage( - BottomBarManager.BottomBarMessage( - title = context.getString(R.string.prompt_title_logout), - subtitle = context - .getString(R.string.prompt_description_logout), - positiveText = context.getString(R.string.action_logout), - tertiaryText = context.getString(R.string.action_cancel), - onPositive = { - onConfirmed() - } - ) - ) - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/settings/SettingsViewModel.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/settings/SettingsViewModel.kt deleted file mode 100644 index 572142006..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/features/settings/SettingsViewModel.kt +++ /dev/null @@ -1,68 +0,0 @@ -package xyz.flipchat.app.features.settings - -import android.app.Activity -import androidx.lifecycle.viewModelScope -import com.flipcash.libs.coroutines.DispatcherProvider -import com.getcode.view.BaseViewModel2 -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import xyz.flipchat.app.auth.AuthManager -import xyz.flipchat.controllers.ChatsController -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject - -@HiltViewModel -class SettingsViewModel @Inject constructor( - private val authManager: AuthManager, - private val chatsController: ChatsController, - userManager: UserManager, - dispatchers: DispatcherProvider, -) : BaseViewModel2( - initialState = State(), - updateStateForEvent = updateStateForEvent, - dispatchers = dispatchers, -) { - data class State( - val isStaff: Boolean = false, - ) - - sealed interface Event { - data class OnStaffEmployed(val enabled: Boolean) : Event - } - - init { - userManager.state - .mapNotNull { it.flags } - .map { it.isStaff } - .onEach { dispatchEvent(Event.OnStaffEmployed(it)) } - .launchIn(viewModelScope) - } - - fun logout(activity: Activity, onComplete: () -> Unit) = viewModelScope.launch { - authManager.logout(activity) - .onSuccess { - chatsController.closeEventStream() - onComplete() - } - } - - fun deleteAccount(activity: Activity, onComplete: () -> Unit) = viewModelScope.launch { - authManager.deleteAndLogout(activity) - .onSuccess { - chatsController.closeEventStream() - onComplete() - } - } - - internal companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> - when (event) { - is Event.OnStaffEmployed -> { state -> state.copy(isStaff = event.enabled) } - } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/inject/ApiModule.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/inject/ApiModule.kt deleted file mode 100644 index 87cc8306a..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/inject/ApiModule.kt +++ /dev/null @@ -1,74 +0,0 @@ -package xyz.flipchat.app.inject - -import android.content.Context -import com.getcode.model.Currency -import xyz.flipchat.app.util.AccountAuthenticator -import com.getcode.services.db.CurrencyProvider -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.Kin -import com.mixpanel.android.mpmetrics.MixpanelAPI -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import io.reactivex.rxjava3.core.Scheduler -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.schedulers.Schedulers -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import xyz.flipchat.app.BuildConfig -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object ApiModule { - - @Singleton - @Provides - fun provideCompositeDisposable(): CompositeDisposable { - return CompositeDisposable() - } - - @Provides - fun provideScheduler(): Scheduler = Schedulers.io() - - @Singleton - @Provides - fun provideAccountAuthenticator( - @ApplicationContext context: Context, - ): AccountAuthenticator { - return AccountAuthenticator(context) - } - - @Singleton - @Provides - fun provideMixpanelApi(@ApplicationContext context: Context): MixpanelAPI { - return MixpanelAPI.getInstance(context, BuildConfig.MIXPANEL_API_KEY) - } - - @Singleton - @Provides - fun providesHttpLoggingInterceptor() = HttpLoggingInterceptor() - .apply { - level = HttpLoggingInterceptor.Level.BODY - } - - @Singleton - @Provides - fun providesOkHttpClient(httpLoggingInterceptor: HttpLoggingInterceptor): OkHttpClient = - OkHttpClient - .Builder() - .addInterceptor(httpLoggingInterceptor) - .build() - - @Singleton - @Provides - fun providesCurrencyProvider( - resources: ResourceHelper - ): CurrencyProvider = object : CurrencyProvider { - override suspend fun preferredCurrency(): Currency = Currency.Kin - override suspend fun defaultCurrency(): Currency = Currency.Kin - override fun suffix(currency: Currency?): String = resources.getKinSuffix() - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/inject/AppModule.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/inject/AppModule.kt deleted file mode 100644 index 088dc799a..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/inject/AppModule.kt +++ /dev/null @@ -1,167 +0,0 @@ -package xyz.flipchat.app.inject - -import android.content.ClipboardManager -import android.content.Context -import android.net.ConnectivityManager -import android.net.wifi.WifiManager -import android.telephony.TelephonyManager -import androidx.core.app.NotificationManagerCompat -import com.getcode.libs.emojis.EmojiQueryProvider -import com.getcode.libs.emojis.EmojiUsageController -import com.getcode.libs.emojis.EmojiUsageTracker -import com.getcode.libs.opengraph.OpenGraphCacheProvider -import com.getcode.libs.opengraph.OpenGraphParser -import com.getcode.libs.opengraph.cache.CacheProvider -import com.getcode.util.locale.LocaleHelper -import com.getcode.util.permissions.PermissionChecker -import com.getcode.util.resources.AndroidResources -import com.getcode.util.resources.AndroidSettingsHelper -import com.getcode.util.resources.ResourceHelper -import com.getcode.util.resources.SettingsHelper -import com.getcode.utils.CurrencyUtils -import com.mixpanel.android.mpmetrics.MixpanelAPI -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import xyz.flipchat.app.BuildConfig -import xyz.flipchat.app.beta.Labs -import xyz.flipchat.app.beta.LabsController -import xyz.flipchat.app.util.AndroidLocale -import xyz.flipchat.app.util.AndroidPermissions -import xyz.flipchat.app.util.FcTab -import xyz.flipchat.app.util.Router -import xyz.flipchat.app.util.RouterImpl -import xyz.flipchat.controllers.ChatsController -import xyz.flipchat.internal.EmojiQueryProviderImpl -import xyz.flipchat.services.analytics.FlipchatAnalyticsManager -import xyz.flipchat.services.analytics.FlipchatAnalyticsService -import xyz.flipchat.services.billing.BillingClient -import xyz.flipchat.services.billing.GooglePlayBillingClient -import xyz.flipchat.services.billing.StubBillingClient -import xyz.flipchat.services.internal.network.repository.iap.InAppPurchaseRepository -import xyz.flipchat.services.user.UserManager -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object AppModule { - @Provides - fun providesResourceHelper( - @ApplicationContext context: Context, - ): ResourceHelper = AndroidResources(context) - - @Provides - fun providesSettingsHelper( - @ApplicationContext context: Context, - ): SettingsHelper = AndroidSettingsHelper(context) - - @Provides - fun providesLocaleHelper( - @ApplicationContext context: Context, - currencyUtils: CurrencyUtils, - ): LocaleHelper = AndroidLocale(context, currencyUtils) - - @Provides - fun providesWifiManager( - @ApplicationContext context: Context, - ): WifiManager = - context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager - - @Provides - fun providesConnectivityManager( - @ApplicationContext context: Context, - ): ConnectivityManager = - context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - - @Provides - fun providesTelephonyManager( - @ApplicationContext context: Context, - ): TelephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager - - @Provides - fun providesPermissionChecker( - @ApplicationContext context: Context, - ): PermissionChecker = AndroidPermissions(context) - - @Provides - fun providesClipboard( - @ApplicationContext context: Context - ): ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - - - @Provides - fun providesNotificationManager( - @ApplicationContext context: Context - ): NotificationManagerCompat = NotificationManagerCompat.from(context) - - @Provides - fun providesAnalyticsService( - mixpanelAPI: MixpanelAPI - ): FlipchatAnalyticsService = FlipchatAnalyticsManager(mixpanelAPI) - - @Provides - fun providesDeeplinkRouter( - labs: Labs, - userManager: UserManager, - chatsController: ChatsController, - resources: ResourceHelper, - ): Router = RouterImpl( - labs = labs, - userManager = userManager, - chatsController = chatsController, - resources = resources, - tabIndexResolver = { resolved -> - when (resolved) { - FcTab.Chat -> FcTab.Chat.ordinal - FcTab.Cash -> FcTab.Cash.ordinal - FcTab.Settings -> FcTab.Settings.ordinal - FcTab.Profile -> FcTab.Profile.ordinal - } - }, - indexTabResolver = { index -> FcTab.entries[index] } - ) - - @Singleton - @Provides - fun providesBetaController( - @ApplicationContext context: Context - ): Labs = LabsController(context) - - @Singleton - @Provides - fun providesBillingController( - @ApplicationContext context: Context, - userManager: UserManager, - purchaseRepository: InAppPurchaseRepository - ): BillingClient = if (BuildConfig.DEBUG) { - StubBillingClient - } else { - GooglePlayBillingClient(context, userManager, purchaseRepository) - } - - @Singleton - @Provides - fun providesOpenGraphCache( - @ApplicationContext context: Context, - ): CacheProvider = OpenGraphCacheProvider(context) - - @Singleton - @Provides - fun providesOpenGraphParser( - cache: CacheProvider - ): OpenGraphParser = OpenGraphParser(cacheProvider = cache) - - @Singleton - @Provides - fun provideEmojiQueryProvider( - userManager: UserManager, - ): EmojiQueryProvider = EmojiQueryProviderImpl(userManager) - - @Singleton - @Provides - fun providesEmojiUsageController( - queryProvider: EmojiQueryProvider - ): EmojiUsageTracker = EmojiUsageController(queryProvider) -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/inject/TipModule.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/inject/TipModule.kt deleted file mode 100644 index 20f6df8ff..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/inject/TipModule.kt +++ /dev/null @@ -1,27 +0,0 @@ -package xyz.flipchat.app.inject - -import android.content.Context -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import dev.bmcreations.tipkit.engines.EventEngine -import dev.bmcreations.tipkit.engines.TipsEngine -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object TipModule { - @Provides - @Singleton - fun providesTipEngine( - eventEngine: EventEngine - ) = TipsEngine(eventEngine) - - @Singleton - @Provides - fun providesEventEngine( - @ApplicationContext context: Context - ) = EventEngine(context) -} diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/navigation/NavScreenProvider.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/navigation/NavScreenProvider.kt deleted file mode 100644 index 348b29a03..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/navigation/NavScreenProvider.kt +++ /dev/null @@ -1,126 +0,0 @@ -package xyz.flipchat.app.navigation - -import android.os.Parcel -import android.os.Parcelable -import androidx.compose.ui.graphics.Color -import cafe.adriel.voyager.core.registry.ScreenProvider -import com.getcode.model.ID -import com.getcode.ui.core.RestrictionType -import dev.theolm.rinku.DeepLink -import kotlinx.parcelize.Parceler -import kotlinx.parcelize.Parcelize - -sealed class NavScreenProvider : ScreenProvider { - - data class AppRestricted(val restrictionType: RestrictionType): NavScreenProvider() - - sealed class Login { - data class Home(val seed: String? = null) : NavScreenProvider() - data object SeedInput : NavScreenProvider() - data class NotificationPermission(val fromOnboarding: Boolean = false) : NavScreenProvider() - } - - sealed interface CreateAccount { - data object Start: NavScreenProvider() - data class NameEntry(val showInModal: Boolean = false) : NavScreenProvider() - data class AccessKey(val showInModal: Boolean = false) : NavScreenProvider() - data object Purchase: NavScreenProvider() - } - - data class AppHomeScreen(val deeplink: DeepLink? = null) : NavScreenProvider() - sealed class Room { - data object List : NavScreenProvider() - sealed class Lookup { - data object Entry : NavScreenProvider() - } - - data object Create: NavScreenProvider() - data class Messages( - val chatId: ID? = null, - val roomNumber: Long? = null, - ) : NavScreenProvider() - - data class Info( - val args: RoomInfoArgs = RoomInfoArgs(), - val returnToSender: Boolean = false - ) : NavScreenProvider() - - data class Preview( - val args: RoomInfoArgs = RoomInfoArgs(), - val returnToSender: Boolean = false - ) : NavScreenProvider() - - data class ChangeCover( - val id: ID - ) : NavScreenProvider() - - data class ChangeName( - val id: ID, - val title: String, - ): NavScreenProvider() - - data class ChangeDescription( - val id: ID, - val description: String, - ): NavScreenProvider() - } - - data object Balance : NavScreenProvider() - data object Settings : NavScreenProvider() - data object OwnProfile: NavScreenProvider() - data class UserProfile(val user: ID): NavScreenProvider() - - data object BetaFlags: NavScreenProvider() -} - -@Parcelize -data class LoginArgs( - val signInEntropy: String? = null, - val isPhoneLinking: Boolean = false, - val isNewAccount: Boolean = false, - val phoneNumber: String? = null -) : Parcelable - -@Parcelize -data class RoomInfoArgs( - val roomId: ID? = null, - val roomNumber: Long = 0, - val roomTitle: String? = null, - val roomDescription: String? = null, - val memberCount: Int = 0, - val ownerId: ID? = null, - val hostName: String? = null, - val messagingFeeQuarks: Long = 0, - val gradientColors: GradientColors = GradientColors( - Triple( - Color(0xFFFFBB00), - Color(0xFF7306B7), - Color(0xFF3E32C4), - ) - ) -) : Parcelable - -@Parcelize -data class GradientColors( - val triple: Triple -) : Parcelable { - private constructor(parcel: Parcel) : this( - Triple( - Color(parcel.readLong()), - Color(parcel.readLong()), - Color(parcel.readLong()) - ) - ) - - companion object : Parceler { - override fun GradientColors.write(parcel: Parcel, flags: Int) { - parcel.writeLong(triple.first.value.toLong()) - parcel.writeLong(triple.second.value.toLong()) - parcel.writeLong(triple.third.value.toLong()) - } - - override fun create(parcel: Parcel): GradientColors { - return GradientColors(parcel) - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/notifications/FcNotificationReceiver.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/notifications/FcNotificationReceiver.kt deleted file mode 100644 index 08d3f2e2c..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/notifications/FcNotificationReceiver.kt +++ /dev/null @@ -1,132 +0,0 @@ -package xyz.flipchat.app.notifications - -import android.annotation.SuppressLint -import android.app.Notification -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import androidx.core.app.Person -import androidx.core.app.RemoteInput -import com.getcode.vendor.Base58 -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlin.time.Clock -import xyz.flipchat.app.auth.AuthManager -import xyz.flipchat.chat.RoomController -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext - -@AndroidEntryPoint -class FcNotificationReceiver : BroadcastReceiver() { - - @Inject - lateinit var authManager: AuthManager - - @Inject - lateinit var userManager: UserManager - - @Inject - lateinit var roomController: RoomController - - @Inject - lateinit var notificationManager: NotificationManagerCompat - - override fun onReceive(context: Context, intent: Intent) { - val remoteInput = RemoteInput.getResultsFromIntent(intent) - if (remoteInput != null) { - val roomId = runCatching { - Base58.decode( - intent.getStringExtra(FcNotificationService.KEY_ROOM_ID).orEmpty() - ).toList() - }.getOrNull() - - val notificationId = intent.getIntExtra(FcNotificationService.KEY_NOTIFICATION_ID, -1).takeIf { it > 0 } - if (notificationId != null) { - val activeNotification = notificationManager.getActiveNotification(notificationId) - if (activeNotification != null) { - if (roomId != null) { - val message = - remoteInput.getCharSequence(FcNotificationService.KEY_TEXT_REPLY).toString() - authenticateIfNeeded { - goAsync { - roomController.sendMessage(roomId, message) - .onFailure { - it.printStackTrace() - }.onSuccess { - println("Message sent via notification!") - addReply(context, message, notificationId, activeNotification) - } - } - } - } - } - } - } - } - - @SuppressLint("MissingPermission") - private fun addReply( - context: Context, - text: String, - notificationId: Int, - activeNotification: Notification - ) { - val activeStyle = NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(activeNotification) ?: return - - // Recover builder from the active notification. - val recoveredBuilder = NotificationCompat.Builder(context, activeNotification) - - val person = Person.Builder() - .setName("You") - .build() - - val message = NotificationCompat.MessagingStyle.Message( - text, - Clock.System.now().toEpochMilliseconds(), - person - ) - - val newStyle = NotificationCompat.MessagingStyle(person) - .setConversationTitle(activeStyle.conversationTitle) - - activeStyle.messages.onEach { newStyle.addMessage(it) } - - newStyle.addMessage(message) - - // Set the new style to the recovered builder. - recoveredBuilder.setStyle(newStyle) - - // Update the active notification. - NotificationManagerCompat.from(context).notify(notificationId, recoveredBuilder.build()) - } - - private fun authenticateIfNeeded(block: () -> Unit) { - if (userManager.userId == null) { - authManager.init { block() } - } else { - block() - } - } -} - -fun BroadcastReceiver.goAsync( - context: CoroutineContext = EmptyCoroutineContext, - block: suspend CoroutineScope.() -> Unit -) { - val pendingResult = goAsync() - @OptIn(DelicateCoroutinesApi::class) // Must run globally; there's no teardown callback. - GlobalScope.launch(context) { - try { - block() - } finally { - pendingResult.finish() - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/notifications/FcNotificationService.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/notifications/FcNotificationService.kt deleted file mode 100644 index 06d03201f..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/notifications/FcNotificationService.kt +++ /dev/null @@ -1,237 +0,0 @@ -package xyz.flipchat.app.notifications - -import android.Manifest -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.media.RingtoneManager -import android.net.Uri -import android.os.Build -import androidx.compose.ui.graphics.toArgb -import androidx.core.app.ActivityCompat -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import androidx.core.app.Person -import androidx.core.app.RemoteInput -import com.getcode.ui.components.chat.utils.localizedText -import com.getcode.util.resources.ResourceHelper -import com.getcode.util.resources.ResourceType -import com.getcode.utils.CurrencyUtils -import com.getcode.utils.TraceType -import com.getcode.utils.base58 -import com.getcode.utils.trace -import com.google.firebase.messaging.FirebaseMessagingService -import com.google.firebase.messaging.RemoteMessage -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlin.time.Clock -import timber.log.Timber -import xyz.flipchat.app.MainActivity -import xyz.flipchat.app.R -import xyz.flipchat.app.auth.AuthManager -import xyz.flipchat.app.theme.FC_Primary -import xyz.flipchat.controllers.ChatsController -import xyz.flipchat.controllers.PushController -import xyz.flipchat.internal.db.FcAppDatabase -import xyz.flipchat.notifications.FcNotificationType -import xyz.flipchat.notifications.parse -import xyz.flipchat.services.user.AuthState -import xyz.flipchat.services.user.UserManager -import java.security.SecureRandom -import javax.inject.Inject - -@AndroidEntryPoint -class FcNotificationService : FirebaseMessagingService(), - CoroutineScope by CoroutineScope(Dispatchers.IO) { - - companion object { - const val KEY_NOTIFICATION_ID = "key_notification_id" - const val KEY_TEXT_REPLY = "key_text_reply" - const val KEY_ROOM_ID = "key_room_id" - } - - @Inject - lateinit var authManager: AuthManager - - @Inject - lateinit var userManager: UserManager - - @Inject - lateinit var pushController: PushController - - @Inject - lateinit var chatsController: ChatsController - - @Inject - lateinit var resources: ResourceHelper - - @Inject - lateinit var currencyUtils: CurrencyUtils - - @Inject - lateinit var notificationManager: NotificationManagerCompat - - private val db: FcAppDatabase - get() = FcAppDatabase.requireInstance() - - override fun onMessageReceived(message: RemoteMessage) { - super.onMessageReceived(message) - authenticateIfNeeded { handleMessage(message) } - } - - override fun onNewToken(token: String) { - super.onNewToken(token) - authenticateIfNeeded { - launch { - pushController.addToken(token) - .onSuccess { trace("push token updated", type = TraceType.Process) } - } - } - } - - private fun handleMessage(remoteMessage: RemoteMessage) { - launch { - trace("handling received message", type = TraceType.Silent) - if (remoteMessage.data.isNotEmpty()) { - Timber.d("Message data payload: ${remoteMessage.data}") - val notification = remoteMessage.parse() - - if (notification != null) { - val (type, titleKey, messageContent) = notification - if (type.isNotifiable()) { - val title = titleKey.localizedStringByKey(resources) ?: titleKey - val body = messageContent.localizedText( - resources = resources, - currencyUtils = currencyUtils - ) - - val result = buildNotification(type, title, body) - if (result != null) { - notify(result.first, result.second, type) - } - } - - when (type) { - is FcNotificationType.ChatMessage -> { - val roomId = type.id - if (roomId != null) { - launch { chatsController.updateRoom(roomId) } - } - } - - is FcNotificationType.Unknown -> Unit - } - } else { - val result = buildNotification( - FcNotificationType.Unknown(), - resources.getString(R.string.app_name), - "You have a new message." - ) - if (result != null) { - notify(result.first, result.second, FcNotificationType.Unknown()) - } - } - } - } - } - - private fun authenticateIfNeeded(block: () -> Unit) { - if (userManager.userId == null) { - authManager.init { block() } - } else { - block() - } - } - - private suspend fun buildNotification( - type: FcNotificationType, - title: String, - content: String, - ): Pair? { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - notificationManager.createNotificationChannel( - NotificationChannel( - type.name, - type.name, - NotificationManager.IMPORTANCE_DEFAULT - ) - ) - } - - if (type is FcNotificationType.ChatMessage && type.id == userManager.openRoom) { - return null - } - - - with(notificationManager) { - val (id, notification) = when (type) { - is FcNotificationType.ChatMessage -> { - val roomNumber = type.id?.let { db.conversationDao().findConversationRaw(it)?.roomNumber } - val isFullMember = db.conversationMembersDao() - .getMemberIn(memberId = userManager.userId.orEmpty(), type.id.orEmpty())?.isFullMember == true - - buildChatNotification( - applicationContext, - resources, - type, - roomNumber, - title, - content, - userManager.authState is AuthState.LoggedIn && isFullMember - ) - } - - is FcNotificationType.Unknown -> buildMiscNotification(applicationContext, type, title, content) - } - - return id to notification.build() - } - } - - private fun notify( - id: Int, - notification: Notification, - type: FcNotificationType, - ) { - if (ActivityCompat.checkSelfPermission( - this, - Manifest.permission.POST_NOTIFICATIONS - ) == PackageManager.PERMISSION_GRANTED - ) { - notificationManager.notify(id, notification) - trace( - tag = "Push", - message = "Push notification shown", - metadata = { - "category" to type.name - }, - type = TraceType.Process - ) - } else { - trace( - tag = "Push", - message = "Push notification NOT shown - missing permission", - metadata = { - "category" to type.name - }, - type = TraceType.Process - ) - } - } -} - -private fun String.localizedStringByKey(resources: ResourceHelper): String? { - val name = this.replace(".", "_") - val resId = resources.getIdentifier( - name, - ResourceType.String, - ).let { if (it == 0) null else it } - - return resId?.let { resources.getString(it) } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/notifications/NotificationHelper.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/notifications/NotificationHelper.kt deleted file mode 100644 index 788831a0c..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/notifications/NotificationHelper.kt +++ /dev/null @@ -1,157 +0,0 @@ -package xyz.flipchat.app.notifications - -import android.app.Notification -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.media.RingtoneManager -import android.net.Uri -import androidx.compose.ui.graphics.toArgb -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import androidx.core.app.Person -import androidx.core.app.RemoteInput -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.base58 -import kotlin.time.Clock -import xyz.flipchat.app.MainActivity -import xyz.flipchat.app.R -import xyz.flipchat.app.notifications.FcNotificationService.Companion.KEY_NOTIFICATION_ID -import xyz.flipchat.app.notifications.FcNotificationService.Companion.KEY_ROOM_ID -import xyz.flipchat.app.notifications.FcNotificationService.Companion.KEY_TEXT_REPLY -import xyz.flipchat.app.theme.FC_Primary -import xyz.flipchat.notifications.FcNotificationType -import java.security.SecureRandom - -internal fun NotificationManagerCompat.buildChatNotification( - context: Context, - resources: ResourceHelper, - type: FcNotificationType.ChatMessage, - roomNumber: Long?, - title: String, - content: String, - canReply: Boolean, -): Pair { - val sender = content.substringBefore(":").trim().ifEmpty { type.sender } ?: "Sender" - val messageBody = content.substringAfter(":").trim() - val person = Person.Builder() - .setName(sender) - .build() - - val message = NotificationCompat.MessagingStyle.Message( - messageBody, - Clock.System.now().toEpochMilliseconds(), - person - ) - - val notificationId = type.id?.base58.hashCode() - - val style = getActiveNotification(notificationId)?.let { - NotificationCompat.MessagingStyle.extractMessagingStyleFromNotification(it) - } ?: NotificationCompat.MessagingStyle(person) - .setConversationTitle(title) - .setGroupConversation(true) - - val updatedStyle = style.addMessage(message) - - val replyAction = if (type.id != null && canReply) { - // build direct reply action - val replyLabel: String = resources.getString(R.string.action_reply) - val remoteInput: RemoteInput = RemoteInput.Builder(KEY_TEXT_REPLY).run { - setLabel(replyLabel) - build() - } - - val resultIntent = Intent(context, FcNotificationReceiver::class.java).apply { - putExtra(KEY_ROOM_ID, type.id!!.base58) - putExtra(KEY_NOTIFICATION_ID, notificationId) - } - - val replyPendingIntent: PendingIntent = - PendingIntent.getBroadcast( - context, - type.id.hashCode(), - resultIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE - ) - - NotificationCompat.Action.Builder( - R.drawable.ic_reply, - context.getString(R.string.action_reply), - replyPendingIntent - ).addRemoteInput(remoteInput).build() - } else { - null - } - - val notificationBuilder: NotificationCompat.Builder = - NotificationCompat.Builder(context, type.name) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setStyle(updatedStyle) - .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)) - .setSmallIcon(R.drawable.ic_flipchat_notification) - .setColor(FC_Primary.toArgb()) - .setAutoCancel(true) - .setOnlyAlertOnce(true) - .setContentIntent(context.buildContentIntent(type.copy(roomNumber = roomNumber))) - - if (replyAction != null) { - notificationBuilder.addAction(replyAction) - } - - return notificationId to notificationBuilder -} - -internal fun NotificationManagerCompat.buildMiscNotification( - context: Context, - type: FcNotificationType, - title: String, - content: String -): Pair { - val notificationBuilder: NotificationCompat.Builder = - NotificationCompat.Builder(context, type.name) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)) - .setSmallIcon(R.drawable.ic_flipchat_notification) - .setColor(FC_Primary.toArgb()) - .setAutoCancel(true) - .setContentTitle(title) - .setContentText(content) - .setContentIntent(context.buildContentIntent(type)) - - val random = SecureRandom() - val notificationId = random.nextInt(256) - - return notificationId to notificationBuilder -} - -internal fun NotificationManagerCompat.getActiveNotification(notificationId: Int): Notification? { - val barNotifications = activeNotifications - for (notification in barNotifications) { - if (notification.id == notificationId) { - return notification.notification - } - } - return null -} - -internal fun Context.buildContentIntent( - type: FcNotificationType -): PendingIntent { - val launchIntent = when (type) { - is FcNotificationType.ChatMessage -> Intent(Intent.ACTION_VIEW).apply { - data = Uri.parse("https://app.flipchat.xyz/room/${type.roomNumber}") - } - - is FcNotificationType.Unknown -> Intent(this, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - } - - return PendingIntent.getActivity( - this, - type.ordinal, - launchIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/oauth/OAuthLauncher.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/oauth/OAuthLauncher.kt deleted file mode 100644 index 214a18176..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/oauth/OAuthLauncher.kt +++ /dev/null @@ -1,117 +0,0 @@ -package xyz.flipchat.app.oauth - -import android.app.Activity -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.ManagedActivityResultLauncher -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.ActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.runtime.Composable -import androidx.core.content.IntentSanitizer - -@Composable -fun rememberLauncherForOAuth( - provider: OAuthProvider, - onResult: (String?) -> Unit -): ManagedActivityResultLauncher { - val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - val error = result.data?.data?.getQueryParameter("error") - val errorDesc = result.data?.data?.getQueryParameter("error_description") - - when (result.resultCode) { - Activity.RESULT_OK -> { - val data: Intent? = result.data - data?.data?.let { uri -> - when (provider) { - is OAuthProvider.X -> { - if (error == null) { - val authCode = uri.getQueryParameter("code") - provider.exchangeAuthCodeForAccessToken(authCode) { - onResult(it) - } - } - } - } - } - } - Activity.RESULT_CANCELED -> { - println("OAuth flow canceled") - if (error != null) { - println("Error: $error - $errorDesc") - } - } - } - } - - return launcher -} - -internal class PrivateOauthResultActivity: ComponentActivity() { - - companion object { - const val OAUTH_URI = "oauth_uri" - const val REDIRECT_URI = "redirect_uri" - } - - private var redirectUri: String = "" - private var newIntentReceived = false - private var firstPass = false - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - println("onCreate OauthResultActivity") - val oauthUriString = intent.extras?.getString(OAUTH_URI) - if (oauthUriString == null) { - println("No oauth URI provided") - setResult(RESULT_CANCELED) - finish() - } else { - val oauthUri = Uri.parse(oauthUriString) - redirectUri = intent.extras?.getString(REDIRECT_URI).orEmpty() - if (redirectUri.isEmpty()) { - println("No redirect URI provided") - setResult(RESULT_CANCELED) - finish() - return - } - - startActivityForResult( - Intent(Intent.ACTION_VIEW, oauthUri), - 99 - ) - } - } - - override fun onResume() { - super.onResume() - if (firstPass) { - if (!newIntentReceived) { - // If user manually returns and no new intent was received, finish the activity - println("OAuth result not received, finishing activity") - setResult(RESULT_CANCELED) - finish() - } - } - - firstPass = true - } - - override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - println("onNewIntent from OAuth => ${intent.data}") - val sanitizedIntent = IntentSanitizer.Builder() - .allowAnyComponent() - .allowData { it.toString().startsWith(redirectUri) } - .build() - .sanitizeByFiltering(intent) - - println("onNewIntent sanitized intent => ${sanitizedIntent.data}") - newIntentReceived = true - setResult(RESULT_OK, sanitizedIntent) - finish() - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/oauth/OAuthProvider.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/oauth/OAuthProvider.kt deleted file mode 100644 index 6fc9ef371..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/oauth/OAuthProvider.kt +++ /dev/null @@ -1,113 +0,0 @@ -package xyz.flipchat.app.oauth - -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.util.Base64 -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import org.json.JSONObject -import timber.log.Timber -import xyz.flipchat.app.BuildConfig -import java.net.HttpURLConnection -import java.net.URL -import java.security.MessageDigest -import java.security.SecureRandom - -private const val baseRedirectUri = "flipchat://app.flipchat.xyz/oauth" - -sealed interface OAuthProvider { - fun launchIntent(context: Context): Intent - val redirectUri: String - - data object X : OAuthProvider { - private val codeVerifier = generateCodeVerifier() - internal val codeChallenge = generateCodeChallenge(codeVerifier) - internal val randomStateString = generateRandomStateString() - - override val redirectUri: String = "$baseRedirectUri/x" - - override fun launchIntent(context: Context): Intent = Intent( - context, - PrivateOauthResultActivity::class.java - ).apply { - putExtra(PrivateOauthResultActivity.OAUTH_URI, buildAuthUri(this@X).toString()) - putExtra(PrivateOauthResultActivity.REDIRECT_URI, redirectUri) - } - - fun exchangeAuthCodeForAccessToken(authCode: String?, onResult: (String?) -> Unit) { - val clientId = BuildConfig.X_CLIENT_ID - val requestBody = "grant_type=authorization_code" + - "&client_id=$clientId" + - "&code=$authCode" + - "&redirect_uri=$redirectUri" + - "&code_verifier=$codeVerifier" - - CoroutineScope(Dispatchers.IO).launch { - try { - val url = URL("https://api.twitter.com/2/oauth2/token") - val connection = url.openConnection() as HttpURLConnection - connection.requestMethod = "POST" - connection.setRequestProperty( - "Content-Type", - "application/x-www-form-urlencoded" - ) - connection.doOutput = true - - connection.outputStream.use { it.write(requestBody.toByteArray()) } - - val response = connection.inputStream.bufferedReader().readText() - Timber.d("TwitterOAuth", "Access Token Response: $response") - - val jsonObject = JSONObject(response) - val accessToken = jsonObject.optString("access_token") - onResult(accessToken) - } catch (e: Exception) { - Timber.e("TwitterOAuth", "Token exchange failed", e) - onResult(null) - } - } - } - - } -} - - -private fun generateCodeVerifier(): String { - val secureRandom = SecureRandom() - val bytes = ByteArray(32) - secureRandom.nextBytes(bytes) - return Base64.encodeToString(bytes, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP) -} - -private fun generateRandomStateString(): String { - val secureRandom = SecureRandom() - val bytes = ByteArray(32) - secureRandom.nextBytes(bytes) - return Base64.encodeToString(bytes, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP) -} - -fun generateCodeChallenge(codeVerifier: String): String { - val bytes = codeVerifier.toByteArray(Charsets.US_ASCII) - val messageDigest = MessageDigest.getInstance("SHA-256") - val digest = messageDigest.digest(bytes) - return Base64.encodeToString(digest, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP) -} - -private fun buildAuthUri(type: OAuthProvider): Uri { - return when (type) { - is OAuthProvider.X -> { - Uri.parse("https://twitter.com/i/oauth2/authorize").buildUpon() - .appendQueryParameter("client_id", BuildConfig.X_CLIENT_ID) - .appendQueryParameter("redirect_uri", type.redirectUri) - .appendQueryParameter("response_type", "code") - .appendQueryParameter("scope", "tweet.read users.read offline.access") - .appendQueryParameter("code_challenge", type.codeChallenge) - .appendQueryParameter("code_challenge_method", "S256") - .appendQueryParameter("state", type.randomStateString) - .build() - } - } -} - diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/theme/FlipchatTheme.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/theme/FlipchatTheme.kt deleted file mode 100644 index 88f90fc08..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/theme/FlipchatTheme.kt +++ /dev/null @@ -1,91 +0,0 @@ -package xyz.flipchat.app.theme - -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight -import com.getcode.theme.Alert -import com.getcode.theme.BannerSuccess -import com.getcode.theme.BetaIndicator -import com.getcode.theme.Black40 -import com.getcode.theme.BrandLight -import com.getcode.theme.BrandMuted -import com.getcode.theme.BrandOverlay -import com.getcode.theme.CashBill -import com.getcode.theme.CashBillDecor -import com.getcode.theme.CodeTypography -import com.getcode.theme.ColorScheme -import com.getcode.theme.DesignSystem -import com.getcode.theme.Error -import com.getcode.theme.Gray50 -import com.getcode.theme.TextMain -import com.getcode.theme.Warning -import com.getcode.theme.White -import com.getcode.theme.codeTypography - -val FC_Primary = Color(0xFF362774) -private val FC_Secondary = Color(0xFF443091) -private val FC_Tertiary = Color(0xFF7D6CC3) -private val FC_TextWithPrimary = Color(0xFFD2C6FF) -private val FC_Accent = Color(0xFFC372FF) - -private val colors = ColorScheme( - brand = FC_Primary, - brandLight = BrandLight, - brandSubtle = FC_Secondary, - brandMuted = BrandMuted, - brandDark = Color(0xFF2C2158), - brandOverlay = BrandOverlay, - brandContainer = FC_Primary, - secondary = FC_Secondary, - tertiary = FC_Tertiary, - indicator = FC_Accent, - action = Gray50, - onAction = White, - background = FC_Primary, - onBackground = White, - surface = Color(0xFF28176E), - surfaceError = Error, - surfaceVariant = FC_Secondary, - onSurface = White, - error = Error, - errorText = Alert, - success = FC_Accent, - successText = FC_Accent, - textMain = TextMain, - textSecondary = FC_TextWithPrimary, - border = BrandLight, - divider = FC_Secondary, - dividerVariant = FC_Tertiary, - trackColor = Color(0xFF241A4B), - toggleUncheckedTrackColor = Color(0xFF241A4B), - cashBill = CashBill, - cashBillDecorColor = CashBillDecor, - betaIndicator = BetaIndicator, - bannerThemed = Color(0xFF28176E), - bannerError = Error, - bannerWarning = Warning, - bannerSuccess = BannerSuccess, - scrim = Black40, - accessKey = GradientSpec( - background = FC_Primary, - colors = listOf( - Color(0xFF1E1B4B), - Color(0xFF3730A3), - Color(0xFF7C3AED), - Color(0xFFC084FC), - ), - stops = listOf(0f, 0.3f, 0.6f, 1f) - ) -) - -@Composable -fun FlipchatTheme(content: @Composable () -> Unit) { - DesignSystem( - colorScheme = colors, - // override code type system to make screen title's slightly bigger - typography = codeTypography.copy( - screenTitle = codeTypography.displayExtraSmall.copy(fontWeight = FontWeight.W500) - ), - content = content - ) -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/ui/AmountWithKeypad.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/ui/AmountWithKeypad.kt deleted file mode 100644 index faf7fd228..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/ui/AmountWithKeypad.kt +++ /dev/null @@ -1,69 +0,0 @@ -package xyz.flipchat.app.ui - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.text.AmountAnimatedInputUiModel -import com.getcode.ui.components.text.AmountArea -import com.getcode.ui.theme.CodeKeyPad -import com.getcode.utils.network.LocalNetworkObserver -import xyz.flipchat.app.R - -@Composable -fun AmountWithKeypad( - modifier: Modifier = Modifier, - amountAnimatedModel: AmountAnimatedInputUiModel, - prefix: String = "", - placeholder: String = "", - isKin: Boolean = false, - hint: String = "", - onNumberPressed: (Int) -> Unit, - onBackspace: () -> Unit, -) { - val networkObserver = LocalNetworkObserver.current - val networkState by networkObserver.state.collectAsState() - - Column( - modifier = modifier, - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .weight(0.65f) - ) { - AmountArea( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.Center) - .padding(horizontal = CodeTheme.dimens.inset), - amountPrefix = prefix, - amountText = "0", - placeholder = placeholder, - captionText = hint, - currencyResId = if (isKin) R.drawable.ic_currency_kin else null, - isAltCaptionKinIcon = false, - uiModel = amountAnimatedModel, - isAnimated = true, - isClickable = false, - networkState = networkState, - textStyle = CodeTheme.typography.displayLarge, - ) - } - - CodeKeyPad( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = CodeTheme.dimens.inset) - .weight(1f), - onNumber = onNumberPressed, - onClear = onBackspace, - ) - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/ui/Locals.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/ui/Locals.kt deleted file mode 100644 index 798217323..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/ui/Locals.kt +++ /dev/null @@ -1,9 +0,0 @@ -package xyz.flipchat.app.ui - -import androidx.compose.runtime.staticCompositionLocalOf -import xyz.flipchat.app.beta.Labs -import xyz.flipchat.app.beta.NoOpLabs -import xyz.flipchat.services.user.UserManager - -val LocalUserManager = staticCompositionLocalOf { null } -val LocalLabs = staticCompositionLocalOf { NoOpLabs } \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/ui/navigation/AppScreenContent.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/ui/navigation/AppScreenContent.kt deleted file mode 100644 index 59f3dd4d0..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/ui/navigation/AppScreenContent.kt +++ /dev/null @@ -1,137 +0,0 @@ -package xyz.flipchat.app.ui.navigation - -import androidx.compose.runtime.Composable -import cafe.adriel.voyager.core.registry.ScreenRegistry -import xyz.flipchat.app.navigation.NavScreenProvider -import xyz.flipchat.app.features.balance.BalanceScreen -import xyz.flipchat.app.features.beta.BetaFlagsScreen -import xyz.flipchat.app.features.chat.conversation.ConversationScreen -import xyz.flipchat.app.features.chat.cover.CoverChargeScreen -import xyz.flipchat.app.features.chat.create.CreateRoomScreen -import xyz.flipchat.app.features.chat.description.RoomDescriptionScreen -import xyz.flipchat.app.features.chat.info.RoomInfoScreen -import xyz.flipchat.app.features.chat.list.RoomListScreen -import xyz.flipchat.app.features.chat.lookup.LookupRoomScreen -import xyz.flipchat.app.features.chat.name.RoomNameScreen -import xyz.flipchat.app.features.home.TabbedHomeScreen -import xyz.flipchat.app.features.login.LoginScreen -import xyz.flipchat.app.features.login.accesskey.AccessKeyScreen -import xyz.flipchat.app.features.login.accesskey.SeedInputScreen -import xyz.flipchat.app.features.login.permissions.NotificationPermissionScreen -import xyz.flipchat.app.features.login.register.AccessKeyModalScreen -import xyz.flipchat.app.features.login.register.PurchaseAccountScreen -import xyz.flipchat.app.features.login.register.RegisterInfoScreen -import xyz.flipchat.app.features.login.register.RegisterModalScreen -import xyz.flipchat.app.features.login.register.RegisterScreen -import xyz.flipchat.app.features.profile.ProfileScreen -import xyz.flipchat.app.features.restricted.AppRestrictedScreen -import xyz.flipchat.app.features.settings.SettingsScreen - -@Composable -fun AppScreenContent(content: @Composable () -> Unit) { - ScreenRegistry { - register { - AppRestrictedScreen(it.restrictionType) - } - - register { - LoginScreen(it.seed) - } - - register { - RegisterInfoScreen() - } - - register { - if (it.showInModal) { - RegisterModalScreen() - } else { - RegisterScreen() - } - } - - register { - if (it.showInModal) { - AccessKeyModalScreen() - } else { - AccessKeyScreen() - } - } - - register { - PurchaseAccountScreen() - } - - register { - NotificationPermissionScreen(it.fromOnboarding) - } - - register { - SeedInputScreen - } - - register { - BalanceScreen() - } - - register { - TabbedHomeScreen(it.deeplink) - } - - register { - CreateRoomScreen() - } - - register { - RoomListScreen() - } - - register { - LookupRoomScreen() - } - - register { - ConversationScreen( - chatId = it.chatId, - roomNumber = it.roomNumber, - ) - } - - register { - RoomInfoScreen(it.args, false, it.returnToSender) - } - - register { - RoomInfoScreen(it.args, true, it.returnToSender) - } - - register { - CoverChargeScreen(it.id) - } - - register { - RoomNameScreen(it.id, it.title) - } - - register { - RoomDescriptionScreen(it.id, it.description) - } - - register { - ProfileScreen(isInTab = true) - } - - register { - ProfileScreen(it.user, isInTab = false) - } - - register { - SettingsScreen() - } - - register { - BetaFlagsScreen() - } - } - content() -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/ui/navigation/MainRoot.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/ui/navigation/MainRoot.kt deleted file mode 100644 index 84e5378a0..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/ui/navigation/MainRoot.kt +++ /dev/null @@ -1,124 +0,0 @@ -package xyz.flipchat.app.ui.navigation - -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.requiredHeight -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.res.painterResource -import cafe.adriel.voyager.core.registry.ScreenRegistry -import cafe.adriel.voyager.core.screen.Screen -import cafe.adriel.voyager.core.screen.ScreenKey -import cafe.adriel.voyager.core.screen.uniqueScreenKey -import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.currentOrThrow -import xyz.flipchat.app.R -import xyz.flipchat.app.navigation.NavScreenProvider -import com.getcode.navigation.extensions.getActivityScopedViewModel -import xyz.flipchat.app.ui.LocalUserManager -import com.getcode.theme.CodeTheme -import com.getcode.ui.theme.CodeCircularProgressIndicator -import dev.theolm.rinku.DeepLink -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import timber.log.Timber -import xyz.flipchat.app.features.home.HomeViewModel -import xyz.flipchat.services.user.AuthState - -internal class MainRoot(private val deepLink: () -> DeepLink?) : Screen { - - override val key: ScreenKey = uniqueScreenKey - - private fun readResolve(): Any = this - - @Composable - override fun Content() { - val navigator = LocalNavigator.currentOrThrow - val userManager = LocalUserManager.currentOrThrow - var showLoading by remember { mutableStateOf(false) } - val homeViewModel = getActivityScopedViewModel() - val router = homeViewModel.router - - Box( - modifier = Modifier - .fillMaxSize() - .background(CodeTheme.colors.secondary), - contentAlignment = Alignment.Center, - ) { - Column( - modifier = Modifier - .fillMaxWidth(0.65f), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - Image( - painter = painterResource(R.drawable.flipchat_logo), - contentDescription = null, - ) - } - - Spacer(modifier = Modifier.requiredHeight(CodeTheme.dimens.inset)) - val loadingAlpha by animateFloatAsState( - if (showLoading) 1f else 0f, - label = "loading visibility" - ) - CodeCircularProgressIndicator( - modifier = Modifier.alpha(loadingAlpha) - ) - } - } - - - LaunchedEffect(userManager) { - userManager.state - .map { it.authState } - .distinctUntilChanged() - .onEach { state -> - Timber.d("sessionState=$state") - when (state) { - AuthState.LoggedInAwaitingUser -> { - delay(500) - showLoading = true - } - AuthState.Unregistered, - AuthState.LoggedIn -> { - val screens = router.processDestination(deepLink()) - - if (screens.isNotEmpty()) { - navigator.replaceAll(screens) - } else { - navigator.replace(ScreenRegistry.get(NavScreenProvider.AppHomeScreen())) - } - } - AuthState.LoggedOut -> { - navigator.replace(ScreenRegistry.get(NavScreenProvider.Login.Home())) - } - AuthState.Unknown -> { - navigator.replace(ScreenRegistry.get(NavScreenProvider.Login.Home())) - } - } - }.launchIn(this) - } - } -} - diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/ui/room/RoomCard.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/ui/room/RoomCard.kt deleted file mode 100644 index 21e210b36..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/ui/room/RoomCard.kt +++ /dev/null @@ -1,217 +0,0 @@ -package xyz.flipchat.app.ui.room - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredHeight -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Constraints -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.getcode.model.Currency -import xyz.flipchat.app.R -import xyz.flipchat.app.theme.FlipchatTheme -import com.getcode.theme.CodeTheme -import com.getcode.theme.dropShadow -import com.getcode.ui.utils.ConstraintMode -import com.getcode.ui.utils.Geometry -import com.getcode.ui.utils.constrain -import com.getcode.ui.core.measured -import com.getcode.ui.utils.withDropShadow -import com.getcode.util.resources.LocalResources -import com.getcode.utils.Kin -import com.getcode.utils.decodeBase58 -import com.getcode.utils.formatAmountString -import xyz.flipchat.app.data.RoomInfo - - -private class RoomCardGeometry(width: Dp, height: Dp) : Geometry(width, height) { - - val topSpacer: Dp - get() = size.height * 0.06f - - val iconWidth: Dp - get() = size.width * 0.2f - - val iconHeight: Dp - get() = size.width * 0.2f - - val titleTopPadding: Dp - get() = size.height * 0.2f - - val titleBottomPadding: Dp - get() = size.height * 0.2f - - val bottomSpacer: Dp - get() = size.height * 0.1f -} - - -@Composable -fun RoomCard( - modifier: Modifier = Modifier, - roomInfo: RoomInfo, -) { - Box( - modifier = modifier - .dropShadow(blurRadius = 40.dp, color = Color.Black.copy(alpha = 0.30f)) - .aspectRatio(0.65f) - .clip(CodeTheme.shapes.small) - .background(Color(0xFFD9D9D9)) - .background( - brush = Brush.verticalGradient( - colorStops = arrayOf( - 0.14f to roomInfo.gradientColors.first, - 0.38f to roomInfo.gradientColors.second, - 0.67f to roomInfo.gradientColors.third, - ), - ), - ) - .background( - brush = Brush.radialGradient( - colors = listOf(Color.White.copy(0.65f), Color.Transparent), - center = Offset(-200f, -200f), - radius = 1800f - ), - ), - contentAlignment = Alignment.Center - ) { - BoxWithConstraints { - val maxWidth = maxWidth - val geometry = RoomCardGeometry(maxWidth, maxHeight) - Column( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Spacer(Modifier.requiredHeight(geometry.topSpacer)) - Image( - modifier = Modifier - .size( - geometry.iconWidth, - geometry.iconHeight - ), - painter = painterResource(R.drawable.flipchat_logo), - contentDescription = null - ) - Spacer(Modifier.requiredHeight(geometry.titleTopPadding)) - - val titleStyle = CodeTheme.typography.displaySmall - var textSize by remember { mutableStateOf(titleStyle.fontSize) } - var titleSize by remember { mutableStateOf(DpSize.Zero) } - - Box(modifier = Modifier.measured { titleSize = it }) { - Text( - modifier = Modifier - .padding(horizontal = CodeTheme.dimens.grid.x4) - .constrain( - mode = ConstraintMode.AutoSize(CodeTheme.typography.displaySmall), - text = roomInfo.title, - style = CodeTheme.typography.displaySmall.copy(textAlign = TextAlign.Center), - frameConstraints = Constraints( - maxWidth = with(LocalDensity.current) { maxWidth.roundToPx() }, - maxHeight = with(LocalDensity.current) { titleSize.height.roundToPx() }, - ) - ) { textSize = it }, - text = roomInfo.title, - style = CodeTheme.typography.displaySmall - .copy(fontSize = textSize, lineHeight = 24.sp, textAlign = TextAlign.Center) - .withDropShadow(), - color = Color.White, - maxLines = 3 - ) - } - Spacer(Modifier.requiredHeight(geometry.titleBottomPadding)) - Column( - modifier = Modifier - .padding(horizontal = CodeTheme.dimens.grid.x6), - horizontalAlignment = Alignment.CenterHorizontally - ) { - if (roomInfo.hostName != null) { - Text( - text = stringResource( - R.string.title_roomCardHostedBy, - roomInfo.hostName - ), - style = CodeTheme.typography.textSmall.withDropShadow(), - color = Color.White.copy(0.80f) - ) - } - Text( - text = pluralStringResource( - R.plurals.title_roomCardMemberCount, - roomInfo.memberCount, - roomInfo.memberCount - ), - style = CodeTheme.typography.textSmall.withDropShadow(), - color = Color.White.copy(0.80f) - ) - Text( - text = stringResource( - R.string.title_roomCardJoinCost, - formatAmountString( - resources = LocalResources.current!!, - currency = Currency.Kin, - amount = roomInfo.messagingFee.quarks.toDouble(), - suffix = stringResource(R.string.core_kin) - ) - ), - textAlign = TextAlign.Center, - style = CodeTheme.typography.textSmall.withDropShadow(), - color = Color.White.copy(0.80f) - ) - } - Spacer(Modifier.requiredHeight(geometry.bottomSpacer)) - } - } - } -} - -val id = "4T7DtS9CEZKVJrBgujQLcjBYnMqZSzZV6CqJewME6zVp".decodeBase58().toList() - -@Preview -@Composable -private fun Preview_RoomCard() { - FlipchatTheme { - Box(modifier = Modifier.size(375.dp, 812.dp)) { - RoomCard( - modifier = Modifier.align(Alignment.Center), - roomInfo = RoomInfo( - id = id, - title = "Room #237", - hostName = "Ivy", - memberCount = 24, - roomNumber = 0 - ) - ) - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/AccountAuthenticator.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/AccountAuthenticator.kt deleted file mode 100644 index d379f44cb..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/AccountAuthenticator.kt +++ /dev/null @@ -1,96 +0,0 @@ -package xyz.flipchat.app.util - - -import android.accounts.AbstractAccountAuthenticator -import android.accounts.Account -import android.accounts.AccountAuthenticatorResponse -import android.accounts.NetworkErrorException -import android.content.Context -import android.os.Bundle -import androidx.core.os.bundleOf -import com.getcode.utils.trace -import android.accounts.AccountManager as AndroidAccountManager - - -class AccountAuthenticator( - private val context: Context, -) : AbstractAccountAuthenticator(context) { - @Throws(NetworkErrorException::class) - override fun addAccount( - response: AccountAuthenticatorResponse, - accountType: String, - authTokenType: String, - requiredFeatures: Array, - options: Bundle - ): Bundle = Bundle() - - @Throws(NetworkErrorException::class) - override fun confirmCredentials( - arg0: AccountAuthenticatorResponse, - arg1: Account, arg2: Bundle - ): Bundle? = null - - override fun editProperties(arg0: AccountAuthenticatorResponse, arg1: String): Bundle? = null - - @Throws(NetworkErrorException::class) - override fun getAuthToken( - response: AccountAuthenticatorResponse, - account: Account, - authTokenType: String, - options: Bundle - ): Bundle { - // Extract the username and password from the Account Manager, then, generate token - val am = AndroidAccountManager.get(context) - var authToken = am.peekAuthToken(account, authTokenType) - trace("authenticator: authToken ${authToken != null}, $authTokenType") - // Lets give another try to authenticate the user - if (null != authToken) { - if (authToken.isEmpty()) { - val password = am.getPassword(account) - if (password != null) { - authToken = "test" - } - } - } - - // If we get an authToken - we return it - if (null != authToken) { - if (authToken.isNotEmpty()) { - val result = Bundle() - result.putString(AndroidAccountManager.KEY_ACCOUNT_NAME, account.name) - result.putString(AndroidAccountManager.KEY_ACCOUNT_TYPE, account.type) - result.putString(AndroidAccountManager.KEY_AUTHTOKEN, authToken) - return result - } - } - - trace( - message = "authenticator failure", - error = Throwable("Failed to retrieve authToken from AccountManager") - ) - // If we get here, then we couldn't access the user's password - return Bundle() - } - - override fun getAuthTokenLabel(arg0: String): String? { - return "entropy" - } - - @Throws(NetworkErrorException::class) - override fun hasFeatures( - arg0: AccountAuthenticatorResponse, arg1: Account, - arg2: Array - ): Bundle { - // This call is used to query whether the Authenticator supports - // specific features. We don't expect to get called, so we always - // return false (no) for any queries. - val result = bundleOf(AndroidAccountManager.KEY_BOOLEAN_RESULT to false) - return result - } - - @Throws(NetworkErrorException::class) - override fun updateCredentials( - arg0: AccountAuthenticatorResponse, - arg1: Account, arg2: String, arg3: Bundle - ): Bundle? = null -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/AccountManager.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/AccountManager.kt deleted file mode 100644 index 1f0b0df15..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/AccountManager.kt +++ /dev/null @@ -1,13 +0,0 @@ -package xyz.flipchat.app.util - -import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject - -class AccountManager @Inject constructor( - @ApplicationContext private val context: Context -) { - suspend fun getToken(): String? { - return AccountUtils.getToken(context)?.token - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/AccountUtils.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/AccountUtils.kt deleted file mode 100644 index 73b277e96..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/AccountUtils.kt +++ /dev/null @@ -1,245 +0,0 @@ -package xyz.flipchat.app.util - -import android.accounts.Account -import android.accounts.AccountManager -import android.accounts.AuthenticatorException -import android.content.Context -import android.os.Handler -import android.os.HandlerThread -import androidx.core.os.bundleOf -import com.getcode.utils.TraceType -import com.getcode.utils.network.retryable -import com.getcode.utils.trace -import io.reactivex.rxjava3.annotations.NonNull -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.subjects.SingleSubject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlin.time.Clock -import xyz.flipchat.app.BuildConfig -import java.util.Optional -import kotlin.coroutines.resume - - -object AccountUtils { - private const val ACCOUNT_TYPE = BuildConfig.APPLICATION_ID - private const val ACCOUNT_REGISTERED = "fc_account_registered" - private const val ACCOUNT_UNREGISTERED = "fc_account_unregistered" - - fun addAccount( - context: Context, - name: String, - password: String, - token: String, - isUnregistered: Boolean, - ) { - val accountManager: AccountManager = AccountManager.get(context) - val account = Account(name, ACCOUNT_TYPE) - - val data = bundleOf(AccountManager.KEY_AUTH_TOKEN_LABEL to if (isUnregistered) ACCOUNT_UNREGISTERED else ACCOUNT_REGISTERED) - accountManager.addAccountExplicitly(account, password, data) - accountManager.setAuthToken(account, ACCOUNT_TYPE, token) - } - - suspend fun removeAccounts(context: Context): @NonNull Single { - return getAccount(context) - .map { - if (it.second == null) return@map false - val am: AccountManager = AccountManager.get(context) - am.removeAccountExplicitly(it.second) - } - } - - suspend fun updateAccount(context: Context, name: String): Result { - return runCatching { - val account = getAccount(context) - .mapOptional { - Optional.ofNullable(it.second) - }.blockingGet() ?: throw Throwable("Unable to get account") - val am: AccountManager = AccountManager.get(context) - - suspendCancellableCoroutine { cont -> - am.renameAccount(/* account = */ account, - /* newName = */ name, - /* callback = */ { future -> - try { - val bundle = future?.result - val updated = bundle?.name == name - if (updated) { - cont.resume(Unit) - } else { - cont.resumeWith(Result.failure(Throwable("Failed to update name"))) - } - } catch (e: AuthenticatorException) { - e.printStackTrace() - cont.resumeWith(Result.failure(e)) - } - }, - /* handler = */ handler - ) - } - } - } - - private suspend fun getAccount(context: Context): @NonNull Single> { - val subject = SingleSubject.create>() - return subject.doOnSubscribe { - CoroutineScope(Dispatchers.IO).launch { - val result = retryable( - call = { getAccountNoActivity(context) }, - onRetry = { currentAttempt -> - trace( - tag = "Account", - message = "Retrying call", - metadata = { - "count" to currentAttempt - }, - type = TraceType.Process, - ) - } - ) - subject.onSuccess(result ?: (null to null)) - } - } - } - - private val handler: Handler by lazy { Handler(handlerThread.looper) } - - private val handlerThread: HandlerThread by lazy { - HandlerThread("RenetikBackgroundThread").apply { - setUncaughtExceptionHandler { _, e -> run { throw RuntimeException(e) } } - start() - } - } - - private suspend fun getAccountNoActivity( - context: Context - ): Pair? = suspendCancellableCoroutine { cont -> - trace("getAuthToken", type = TraceType.Silent) - val am: AccountManager = AccountManager.get(context) - val account = am.getAccountsByType(ACCOUNT_TYPE).firstOrNull() - if (account == null) { - trace( - "no associated account found", - type = TraceType.Error - ) - cont.resume(null) - return@suspendCancellableCoroutine - } - val start = Clock.System.now() - am.getAuthToken( - account, ACCOUNT_TYPE, null, false, - { future -> - try { - val bundle = future?.result - val authToken = bundle?.getString(AccountManager.KEY_AUTHTOKEN) - - val end = Clock.System.now() - trace("auth token fetch took ${end.toEpochMilliseconds() - start.toEpochMilliseconds()} ms") - - cont.resume(authToken.orEmpty() to account) - } catch (e: AuthenticatorException) { - trace( - message = "failed to read account", - error = e, - type = TraceType.Error - ) - cont.resume(null) - } - }, handler - ) - } - - suspend fun getUserId(context: Context): UserIdResult? { - val accountManager = AccountManager.get(context) - val accounts = accountManager.getAccountsByType(ACCOUNT_TYPE) - fun getPassword(a: Account?): String? { - if (a != null) { - val pw = runCatching { accountManager.getPassword(a) } - .getOrNull()?.takeIf { it.isNotEmpty() } - if (pw != null) { - return pw - } - } - return null - } - - val account = accounts.firstOrNull() - - val label = account?.let { accountManager.getUserData(it, AccountManager.KEY_AUTH_TOKEN_LABEL) } - - getPassword(account)?.let { - if (label == ACCOUNT_UNREGISTERED) { - return UserIdResult.Unregistered(it) - } else { - return UserIdResult.Registered(it) - } - } - - val (_, acct) = retryable( - call = { getAccountNoActivity(context) }, - onRetry = { currentAttempt -> - trace( - tag = "Account", - message = "Retrying call", - metadata = { - "count" to currentAttempt - }, - type = TraceType.Process, - ) - } - ) ?: return null - - return getPassword(acct)?.let { - if (label == ACCOUNT_UNREGISTERED) { - UserIdResult.Unregistered(it) - } else { - UserIdResult.Registered(it) - } - } - } - - suspend fun getToken(context: Context): TokenResult? { - val accountManager = AccountManager.get(context) - val accounts = accountManager.getAccountsByType(ACCOUNT_TYPE) - val account = accounts.firstOrNull() - if (account != null) { - val token = runCatching { accountManager.peekAuthToken(account, ACCOUNT_TYPE) } - .getOrNull()?.takeIf { it.isNotEmpty() } - if (token != null) { - return TokenResult.Account(token) - } - } - - val token = retryable( - call = { getAccountNoActivity(context) }, - onRetry = { currentAttempt -> - trace( - tag = "Account", - message = "Retrying call", - metadata = { - "count" to currentAttempt - }, - type = TraceType.Process, - ) - } - - )?.first ?: return null - - return TokenResult.Account(token) - } -} - -sealed interface TokenResult { - val token: String - - data class Account(override val token: String) : TokenResult - data class Code(override val token: String) : TokenResult -} - -sealed interface UserIdResult { - data class Registered(val userId: String): UserIdResult - data class Unregistered(val userId: String): UserIdResult -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/AndroidLocale.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/AndroidLocale.kt deleted file mode 100644 index d24ab47c4..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/AndroidLocale.kt +++ /dev/null @@ -1,21 +0,0 @@ -package xyz.flipchat.app.util - -import android.content.Context -import com.getcode.model.Currency -import com.getcode.util.locale.LocaleHelper -import com.getcode.util.locale.LocaleUtils -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject - -class AndroidLocale @Inject constructor( - @ApplicationContext private val context: Context, - private val currencyUtils: com.getcode.utils.CurrencyUtils, -): LocaleHelper { - override fun getDefaultCurrencyName(): String { - return LocaleUtils.getDefaultCurrency(context) - } - - override fun getDefaultCountry(): String { - return LocaleUtils.getDefaultCountry(context) - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/AndroidPermissions.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/AndroidPermissions.kt deleted file mode 100644 index bbc1f43d1..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/AndroidPermissions.kt +++ /dev/null @@ -1,27 +0,0 @@ -package xyz.flipchat.app.util - -import android.content.Context -import android.content.pm.PackageManager -import androidx.core.content.ContextCompat -import com.getcode.util.permissions.PermissionChecker -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject - -class AndroidPermissions @Inject constructor( - @ApplicationContext private val context: Context -) : PermissionChecker { - override fun isGranted(permission: String): Boolean { - return check(permission) == PackageManager.PERMISSION_GRANTED - } - - override fun isDenied(permission: String): Boolean { - return check(permission) == PackageManager.PERMISSION_DENIED - } - - override fun check(permission: String): Int { - return ContextCompat.checkSelfPermission( - context, - permission, - ) - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/AuthenticatorService.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/AuthenticatorService.kt deleted file mode 100644 index 24f45e657..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/AuthenticatorService.kt +++ /dev/null @@ -1,23 +0,0 @@ -package xyz.flipchat.app.util - -import android.accounts.AccountManager -import android.app.Service -import android.content.Intent -import android.os.IBinder -import dagger.hilt.android.AndroidEntryPoint - - -@AndroidEntryPoint -class AuthenticatorService : Service() { - private val accountAuthenticator: AccountAuthenticator by lazy { - AccountAuthenticator(this) - } - - override fun onBind(intent: Intent): IBinder? { - var binder: IBinder? = null - if (intent.action == AccountManager.ACTION_AUTHENTICATOR_INTENT) { - binder = accountAuthenticator.iBinder - } - return binder - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/Bitmap.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/Bitmap.kt deleted file mode 100644 index 9efff6490..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/Bitmap.kt +++ /dev/null @@ -1,29 +0,0 @@ -package xyz.flipchat.app.util - -import android.graphics.Bitmap -import com.getcode.utils.ErrorUtils -import com.getcode.utils.timedTrace -import java.io.File -import java.io.FileOutputStream - -internal fun Bitmap.save(destination: File, name: () -> String): Boolean { - val filename = name() - if (!destination.exists()) { - if (!destination.mkdirs()) { - return false - } - } - val dest = File(destination, filename) - - return timedTrace("saving bitmap") { - try { - FileOutputStream(dest).use { out -> - compress(Bitmap.CompressFormat.PNG, 90, out) - } - } catch (e: Exception) { - ErrorUtils.handleError(e) - return@timedTrace false - } - return@timedTrace true - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/ChromeTabsUtils.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/ChromeTabsUtils.kt deleted file mode 100644 index 2e31886c4..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/ChromeTabsUtils.kt +++ /dev/null @@ -1,72 +0,0 @@ -package xyz.flipchat.app.util - -import android.content.ComponentName -import android.content.Context -import android.net.Uri -import androidx.browser.customtabs.CustomTabColorSchemeParams -import androidx.browser.customtabs.CustomTabsClient -import androidx.browser.customtabs.CustomTabsIntent -import androidx.browser.customtabs.CustomTabsServiceConnection -import androidx.browser.customtabs.CustomTabsSession -import androidx.compose.ui.graphics.Color -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.toBitmap -import xyz.flipchat.app.R -import com.getcode.theme.Brand -import com.getcode.ui.utils.toAGColor - -object ChromeTabsUtils { - fun launchUrl( - context: Context, - url: String, - showBack: Boolean = false - ) { - val mCustomTabsServiceConnection: CustomTabsServiceConnection? - var mClient: CustomTabsClient? - var mCustomTabsSession: CustomTabsSession? = null - mCustomTabsServiceConnection = object : CustomTabsServiceConnection() { - override fun onCustomTabsServiceConnected( - componentName: ComponentName, - customTabsClient: CustomTabsClient - ) { - mClient = customTabsClient - mClient?.warmup(0L) - mCustomTabsSession = mClient?.newSession(null) - } - - override fun onServiceDisconnected(name: ComponentName) { - mClient = null - } - } - CustomTabsClient.bindCustomTabsService( - context, - "com.android.chrome", - mCustomTabsServiceConnection - ) - val builder: CustomTabsIntent.Builder = CustomTabsIntent.Builder(mCustomTabsSession) - .setShowTitle(false) - .setShareState(CustomTabsIntent.SHARE_STATE_OFF) - .setInstantAppsEnabled(false) - .setColorSchemeParams( - CustomTabsIntent.COLOR_SCHEME_DARK, - CustomTabColorSchemeParams.Builder() - .setToolbarColor(Brand.toAGColor()) - .setNavigationBarDividerColor(Color.Transparent.toAGColor()) - .setNavigationBarColor(Color.Transparent.toAGColor()) - .build() - ) - - if (showBack) { - ContextCompat.getDrawable( - context, - R.drawable.ic_arrow_back - )?.toBitmap()?.let { backArrow -> - builder.setCloseButtonIcon(backArrow) - } - } - - val customTabsIntent = builder.build() - - customTabsIntent.launchUrl(context, Uri.parse(url)) - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/Context.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/Context.kt deleted file mode 100644 index 884e8f3cb..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/Context.kt +++ /dev/null @@ -1,14 +0,0 @@ -package xyz.flipchat.app.util - -import android.content.Context -import androidx.core.content.ContextCompat - -fun Context.launchAppSettings() { - val intent = IntentUtils.appSettings() - ContextCompat.startActivity(this, intent, null) -} - -fun Context.dialNumber(number: String) { - val intent = IntentUtils.dialNumber(number) - ContextCompat.startActivity(this, intent, null) -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/IntentUtils.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/IntentUtils.kt deleted file mode 100644 index 560f648a0..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/IntentUtils.kt +++ /dev/null @@ -1,36 +0,0 @@ -package xyz.flipchat.app.util - -import android.content.Intent -import android.net.Uri -import android.provider.Settings -import xyz.flipchat.app.BuildConfig - - -object IntentUtils { - - fun appSettings() = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { - data = Uri.fromParts("package", BuildConfig.APPLICATION_ID, null) - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - - fun dialNumber(number: String) = Intent(Intent.ACTION_DIAL).apply { - data = Uri.parse("tel:$number") - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - - fun shareRoom(roomNumber: Long): Intent { - val shareLink = "https://app.flipchat.xyz/room/$roomNumber" - - val sendIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, shareLink) - type = "text/plain" - } - - val shareIntent = Intent.createChooser(sendIntent, null).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - - return shareIntent - } -} diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/Router.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/Router.kt deleted file mode 100644 index b1fd4c7c9..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/Router.kt +++ /dev/null @@ -1,231 +0,0 @@ -package xyz.flipchat.app.util - -import androidx.core.net.toUri -import cafe.adriel.voyager.core.registry.ScreenRegistry -import cafe.adriel.voyager.core.screen.Screen -import com.getcode.model.ID -import xyz.flipchat.app.navigation.NavScreenProvider -import xyz.flipchat.app.navigation.RoomInfoArgs -import com.getcode.navigation.screens.ChildNavTab -import com.getcode.util.resources.ResourceHelper -import com.getcode.vendor.Base58 -import dev.theolm.rinku.DeepLink -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import xyz.flipchat.app.beta.Labs -import xyz.flipchat.app.features.home.tabs.CashTab -import xyz.flipchat.app.features.home.tabs.ChatTab -import xyz.flipchat.app.features.home.tabs.ProfileTab -import xyz.flipchat.controllers.ChatsController -import xyz.flipchat.internal.db.FcAppDatabase -import xyz.flipchat.services.extensions.titleOrFallback -import xyz.flipchat.services.user.UserManager - -interface Router { - suspend fun checkTabs() - val rootTabs: List - fun getInitialTabIndex(deeplink: DeepLink?): Int - suspend fun processDestination(deeplink: DeepLink?): List - fun processType(deeplink: DeepLink?): DeeplinkType? - fun tabForIndex(index: Int): FcTab -} - -enum class FcTab { - Chat, Cash, Settings, Profile -} - -sealed interface DeeplinkType { - data class Login(val entropy: String) : DeeplinkType - data class OpenRoomByNumber(val number: Long, val messageId: ID? = null) : DeeplinkType - data class OpenRoomById(val id: ID, val messageId: ID? = null) : DeeplinkType -} - -class RouterImpl( - private val labs: Labs, - private val userManager: UserManager, - private val chatsController: ChatsController, - private val resources: ResourceHelper, - private val tabIndexResolver: (FcTab) -> Int, - private val indexTabResolver: (Int) -> FcTab, -) : Router, CoroutineScope by CoroutineScope(Dispatchers.IO) { - companion object { - val chats = listOf("chats") - val cash = listOf("cash") - val settings = listOf("settings") - val profile = listOf("profile") - - val login = listOf("login") - val room = listOf("room", "id", "number") - } - - private val db: FcAppDatabase - get() = FcAppDatabase.requireInstance() - - override fun tabForIndex(index: Int) = indexTabResolver(index) - - private val commonTabs: List = - listOf(ChatTab(0), CashTab(1), ProfileTab(2)) - - private val tabs = MutableStateFlow(commonTabs) - - override suspend fun checkTabs() { - // no-op for now - } - - override val rootTabs: List - get() = tabs.value - - override fun getInitialTabIndex(deeplink: DeepLink?): Int { - return deeplink?.let { - when { - deeplink.pathSegments.isEmpty() -> tabIndexResolver(FcTab.Chat) - chats.contains(deeplink.pathSegments[0]) -> tabIndexResolver(FcTab.Chat) - room.contains(deeplink.pathSegments[0]) -> tabIndexResolver(FcTab.Chat) - cash.contains(deeplink.pathSegments[0]) -> tabIndexResolver(FcTab.Cash) - settings.contains(deeplink.pathSegments[0]) -> tabIndexResolver(FcTab.Settings) - profile.contains(deeplink.pathSegments[0]) -> tabIndexResolver(FcTab.Profile) - else -> 0 - } - } ?: 0 - } - - override suspend fun processDestination(deeplink: DeepLink?): List { - return deeplink?.let { - val type = processType(deeplink) ?: return emptyList() - when (type) { - is DeeplinkType.Login -> listOf(ScreenRegistry.get(NavScreenProvider.AppHomeScreen(deeplink))) - - is DeeplinkType.OpenRoomByNumber -> { - val conversation = db.conversationDao().findConversationRaw(type.number) - val screens = mutableListOf(ScreenRegistry.get(NavScreenProvider.AppHomeScreen())) - if (conversation != null) { - screens.add(ScreenRegistry.get(NavScreenProvider.Room.Messages(conversation.id))) - } else { - val lookup = chatsController.lookupRoom(type.number).getOrNull() - if (lookup != null) { - val (room, members) = lookup - val moderator = members.firstOrNull { it.isModerator } - - val args = RoomInfoArgs( - roomId = room.id, - roomNumber = room.roomNumber, - roomTitle = room.titleOrFallback(resources,), - roomDescription = room.description, - memberCount = members.count(), - ownerId = room.ownerId, - hostName = moderator?.identity?.displayName, - messagingFeeQuarks = room.messagingFee.quarks, - ) - - screens.add( - ScreenRegistry.get( - NavScreenProvider.Room.Preview(args = args, returnToSender = true) - ) - ) - } - } - - screens - } - - is DeeplinkType.OpenRoomById -> { - val conversation = db.conversationDao().findConversationRaw(type.id) - val screens = mutableListOf(ScreenRegistry.get(NavScreenProvider.AppHomeScreen())) - if (conversation != null) { - screens.add(ScreenRegistry.get(NavScreenProvider.Room.Messages(conversation.id))) - } else { - val lookup = chatsController.lookupRoom(type.id).getOrNull() - if (lookup != null) { - val (room, members) = lookup - val moderator = members.firstOrNull { it.isModerator } - - val args = RoomInfoArgs( - roomId = room.id, - roomNumber = room.roomNumber, - roomTitle = room.titleOrFallback(resources,), - roomDescription = room.description, - memberCount = members.count(), - ownerId = room.ownerId, - hostName = moderator?.identity?.displayName, - messagingFeeQuarks = room.messagingFee.quarks, - ) - - screens.add( - ScreenRegistry.get( - NavScreenProvider.Room.Preview(args = args, returnToSender = true) - ) - ) - } - } - - screens - } - } - } ?: emptyList() - } - - override fun processType(deeplink: DeepLink?): DeeplinkType? { - return deeplink?.let { - when (deeplink.pathSegments.size) { - 1 -> { - when { - login.contains(deeplink.pathSegments[0]) -> { - var entropy = runCatching { - deeplink.data.toUri().getQueryParameter("data") - }.getOrNull() - - // if not found at data check `e` - if (entropy == null) { - entropy = runCatching { - deeplink.data.toUri().getQueryParameter("e") - }.getOrNull() ?: return null - } - - DeeplinkType.Login(entropy) - } - - else -> null - } - } - - 2 -> { - when { - room.contains(deeplink.pathSegments[0]) -> { - when (val specifier = deeplink.pathSegments[0]) { - "room", - "number" -> { - val number = runCatching { - deeplink.pathSegments[1].toLongOrNull() - }.getOrNull() ?: return null - - val messageId = runCatching { - deeplink.data.toUri().getQueryParameter("m")?.let { Base58.decode(it).toList() } - }.getOrNull() - - DeeplinkType.OpenRoomByNumber(number = number, messageId = messageId) - } - "id" -> { - val id = runCatching { - deeplink.pathSegments[1] - }.getOrNull()?.let { Base58.decode(it).toList() } ?: return null - - val messageId = runCatching { - deeplink.data.toUri().getQueryParameter("m")?.let { Base58.decode(it).toList() } - }.getOrNull() - - DeeplinkType.OpenRoomById(id = id, messageId = messageId) - } - else -> return null - } - } - - else -> null - } - } - - else -> null - } - } - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/media/MediaScanner.kt b/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/media/MediaScanner.kt deleted file mode 100644 index d9e0aa950..000000000 --- a/apps/flipchatApp/src/main/kotlin/xyz/flipchat/app/util/media/MediaScanner.kt +++ /dev/null @@ -1,15 +0,0 @@ -package xyz.flipchat.app.util.media - -import android.content.Context -import android.media.MediaScannerConnection -import dagger.hilt.android.qualifiers.ApplicationContext -import java.io.File -import javax.inject.Inject - -class MediaScanner @Inject constructor( - @ApplicationContext private val context: Context -) { - fun scan(directory: File) { - MediaScannerConnection.scanFile(context, arrayOf(directory.toString()), null, null) - } -} \ No newline at end of file diff --git a/apps/flipchatApp/src/main/res/drawable-nodpi/ic_access_key_bg.webp b/apps/flipchatApp/src/main/res/drawable-nodpi/ic_access_key_bg.webp deleted file mode 100644 index dbf2ae661..000000000 Binary files a/apps/flipchatApp/src/main/res/drawable-nodpi/ic_access_key_bg.webp and /dev/null differ diff --git a/apps/flipchatApp/src/main/res/drawable/flipchat_logo.xml b/apps/flipchatApp/src/main/res/drawable/flipchat_logo.xml deleted file mode 100644 index c7288771c..000000000 --- a/apps/flipchatApp/src/main/res/drawable/flipchat_logo.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/apps/flipchatApp/src/main/res/drawable/ic_door_open.xml b/apps/flipchatApp/src/main/res/drawable/ic_door_open.xml deleted file mode 100644 index 17f9ad764..000000000 --- a/apps/flipchatApp/src/main/res/drawable/ic_door_open.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/apps/flipchatApp/src/main/res/drawable/ic_fc_balance.xml b/apps/flipchatApp/src/main/res/drawable/ic_fc_balance.xml deleted file mode 100644 index 367c69b08..000000000 --- a/apps/flipchatApp/src/main/res/drawable/ic_fc_balance.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/apps/flipchatApp/src/main/res/drawable/ic_fc_chats.xml b/apps/flipchatApp/src/main/res/drawable/ic_fc_chats.xml deleted file mode 100644 index 88e02a31e..000000000 --- a/apps/flipchatApp/src/main/res/drawable/ic_fc_chats.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - diff --git a/apps/flipchatApp/src/main/res/drawable/ic_flipchat_logo_access_key.xml b/apps/flipchatApp/src/main/res/drawable/ic_flipchat_logo_access_key.xml deleted file mode 100644 index 2bb9c7197..000000000 --- a/apps/flipchatApp/src/main/res/drawable/ic_flipchat_logo_access_key.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/apps/flipchatApp/src/main/res/drawable/ic_flipchat_notification.xml b/apps/flipchatApp/src/main/res/drawable/ic_flipchat_notification.xml deleted file mode 100644 index ed7023765..000000000 --- a/apps/flipchatApp/src/main/res/drawable/ic_flipchat_notification.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/apps/flipchatApp/src/main/res/drawable/ic_launcher_foreground.xml b/apps/flipchatApp/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index 77fc00c44..000000000 --- a/apps/flipchatApp/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/apps/flipchatApp/src/main/res/drawable/ic_notification_request.png b/apps/flipchatApp/src/main/res/drawable/ic_notification_request.png deleted file mode 100644 index bb845f710..000000000 Binary files a/apps/flipchatApp/src/main/res/drawable/ic_notification_request.png and /dev/null differ diff --git a/apps/flipchatApp/src/main/res/drawable/ic_reply.xml b/apps/flipchatApp/src/main/res/drawable/ic_reply.xml deleted file mode 100644 index f5f525d45..000000000 --- a/apps/flipchatApp/src/main/res/drawable/ic_reply.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/apps/flipchatApp/src/main/res/drawable/splash.xml b/apps/flipchatApp/src/main/res/drawable/splash.xml deleted file mode 100644 index 1cc928369..000000000 --- a/apps/flipchatApp/src/main/res/drawable/splash.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - \ No newline at end of file diff --git a/apps/flipchatApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/apps/flipchatApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 7353dbd1f..000000000 --- a/apps/flipchatApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/apps/flipchatApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/apps/flipchatApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 7353dbd1f..000000000 --- a/apps/flipchatApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/apps/flipchatApp/src/main/res/mipmap-hdpi/ic_launcher.webp b/apps/flipchatApp/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index e8655fc13..000000000 Binary files a/apps/flipchatApp/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/apps/flipchatApp/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/apps/flipchatApp/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index 2704887ed..000000000 Binary files a/apps/flipchatApp/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/apps/flipchatApp/src/main/res/mipmap-mdpi/ic_launcher.webp b/apps/flipchatApp/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 8b153e243..000000000 Binary files a/apps/flipchatApp/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/apps/flipchatApp/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/apps/flipchatApp/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 2ee000bbe..000000000 Binary files a/apps/flipchatApp/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/apps/flipchatApp/src/main/res/mipmap-xhdpi/ic_launcher.webp b/apps/flipchatApp/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index c4f441b88..000000000 Binary files a/apps/flipchatApp/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/apps/flipchatApp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/apps/flipchatApp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index b7cda1902..000000000 Binary files a/apps/flipchatApp/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/apps/flipchatApp/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/apps/flipchatApp/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 0cba69e0f..000000000 Binary files a/apps/flipchatApp/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/apps/flipchatApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/apps/flipchatApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index db85d88a2..000000000 Binary files a/apps/flipchatApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/apps/flipchatApp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/apps/flipchatApp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index 5214661e0..000000000 Binary files a/apps/flipchatApp/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/apps/flipchatApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/apps/flipchatApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index e9ad5d50e..000000000 Binary files a/apps/flipchatApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/apps/flipchatApp/src/main/res/values/colors.xml b/apps/flipchatApp/src/main/res/values/colors.xml deleted file mode 100644 index 1d56a948f..000000000 --- a/apps/flipchatApp/src/main/res/values/colors.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - #FF8383 - #FFA1A1 - #FFF383 - #FFF6A3 - #FFF6A3 - #65F57C - - - #666666 - #8D8D94 - #BABBC2 - #E6E6EB - #F0F0F5 - - - #0F0C1F - #7379A0 - #565C86 - #443091 - - #0F0C1F - #8785A9 - #66000000 - #B3000000 - #00000000 - #1D1A30 - - #FF000000 - #FFFFFFFF - #05FFFFFF - \ No newline at end of file diff --git a/apps/flipchatApp/src/main/res/values/ic_launcher_background.xml b/apps/flipchatApp/src/main/res/values/ic_launcher_background.xml deleted file mode 100644 index 9493af536..000000000 --- a/apps/flipchatApp/src/main/res/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #443091 - \ No newline at end of file diff --git a/apps/flipchatApp/src/main/res/values/plurals.xml b/apps/flipchatApp/src/main/res/values/plurals.xml deleted file mode 100644 index 541af4858..000000000 --- a/apps/flipchatApp/src/main/res/values/plurals.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - 0 Speakers - 1 Speaker - %d Speakers - - @string/title_roomInfoSpeakerCountEmpty - @string/title_roomInfoSpeakerCountSingle - @string/title_roomInfoSpeakerCountMany - @string/title_roomInfoSpeakerCountMany - @string/title_roomInfoSpeakerCountMany - @string/title_roomInfoSpeakerCountMany - - - 0 Listeners - 1 Listener - %d Listeners - - @string/title_roomInfoListenerCountEmpty - @string/title_roomInfoListenerCountSingle - @string/title_roomInfoListenerCountMany - @string/title_roomInfoListenerCountMany - @string/title_roomInfoListenerCountMany - @string/title_roomInfoListenerCountMany - - - - \ No newline at end of file diff --git a/apps/flipchatApp/src/main/res/values/strings-universal.xml b/apps/flipchatApp/src/main/res/values/strings-universal.xml deleted file mode 100644 index 507e2f760..000000000 --- a/apps/flipchatApp/src/main/res/values/strings-universal.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - Flipchat - Flipchat - \@flipchatapp - FlipchatAccount - - https://app.flipchat.xyz - https://flipchat.xyz/terms - https://flipchat.xyz/privacy - \ No newline at end of file diff --git a/apps/flipchatApp/src/main/res/values/strings.xml b/apps/flipchatApp/src/main/res/values/strings.xml deleted file mode 100644 index 141f700d2..000000000 --- a/apps/flipchatApp/src/main/res/values/strings.xml +++ /dev/null @@ -1,269 +0,0 @@ - - - Your Name - Flipchat Name - Give people an idea what your Flipchat is about - - Nevermind - - Get Started - - By tapping any button above you - - Send %1$s to Start Chatting - Loading your balance and transaction history - Loading your chats - Join Flipchats - Start Listening - Find a Flipchat - Chat - - Speakers - Listeners - Listener - None yet - - Something Went Wrong - You were unable to create a new Flipchat at this time. Please try again later - - Flipchat Doesn\’t Exist Yet - Please try a different Flipchat number - - Something Went Wrong - This user could not be made a speaker. Please check your network connection and try again - - User Hasn\'t Created Their Account - Users need to create an account before becoming a speaker. Please ask the user to create a Flipchat account - - Something Went Wrong - This user could not be removed as a speaker. Please check your network connection and try again - - Something Went Wrong - Your new message fee couldn\’t be saved. Please check your network connection and try again - - Something Went Wrong - You were unable to join %1$s at this time. Please try again later - - Something Went Wrong - You were unable to follow %1$s at this time. Please try again later - - Something Went Wrong - You were unable to get payment information at this time. Please try again later - - Something Went Wrong - You were unable to leave the room at this time. Please try again later - - Something Went Wrong - This message could not be deleted. Please check your network connection and try again - - Something Went Wrong - This user could not be removed. Please check your network connection and try again - - Something Went Wrong - This user\'s message could not be reported. Please check your network connection and try again - - Something Went Wrong - This user\'s could not be muted. Please check your network connection and try again - - Something Went Wrong - This user could not be block. Please check your network connection and try again - - Something Went Wrong - This user could not be unblocked. Please check your network connection and try again - - Something Went Wrong - Please check your network connection and try again - - Something Went Wrong - Please check your network connection and try again - - Something Went Wrong - We were unable to open this link. Please try again - - Purchase Failed - Something went wrong during payment. Please try again later - - Inappropriate Flipchat Name - Flipchat names need to be appropriate for all ages - - Something Went Wrong - We were unable to change the Flipchat name at this time. Please try again later - - Something Went Wrong - We were unable to change the Flipchat description at this time. Please try again later - - Something Went Wrong - You were unable to reopen this Flipchat at this time. Please try again later - - Something Went Wrong - You were unable to close this Flipchat at this time. Please try again later - - Something Went Wrong - You were unable to send a tip at this time. Please try again later - - Something Went Wrong - We were unable to link your X account at this time. Please try again later - Something Went Wrong - We were unable to unlink your X account at this time. Please try again later - - Connect Your X - Identity in %1$s is based on your X identity. - %1$s I’d like to connect my X - Message %1$s to Connect - Balance - Flipchats - Settings - Profile - Enter Flipchat Number - Create a New Flipchat - Create a New Flipchat: ⬢ %1$s - Join a Flipchat - Enter Flipchat Number - Join Flipchat - Change Cover Charge - Change Listener Message Fee - - Join %1$s - Join Room: ⬢ %1$s - Pay to Chat: ⬢ %1$s - Listener Message: ⬢ %1$s - - Leave Flipchat? - You will need to pay to get back in, but we won\’t tell people you left - Leave %1$s - - Delete Message? - Their messages will be deleted for everyone - Delete - - Remove %1$s? - They will be able to rejoin after waiting an hour, but will have to pay the cover charge again - Remove - - Mute %1$s? - They will no longer be able send messages in this room - Mute - - Make %1$s a Speaker? - They will be able to message for free - Make a Speaker - - Remove %1$s as a Speaker? - They will no longer be able to message for free - Remove as Speaker - - Block %1$s? - All messages from this user will be hidden - Block - - Report - This message will be forwarded to Flipchat. This contact will not be notified - - Report Sent - Your report was sent successfully - - Receive push notifications when people message you. - - Hosted by %1$s - Cover Charge: ⬢ %1$s - - 0 People Inside - 1 Person Inside - %d People Inside - - @string/title_roomCardMemberCountEmpty - @string/title_roomCardMemberCountSingle - @string/title_roomCardMemberCountMany - @string/title_roomCardMemberCountMany - @string/title_roomCardMemberCountMany - @string/title_roomCardMemberCountMany - - - 0 people here - 1 person here - %d people here - - @string/title_conversationMemberCountEmpty - @string/title_conversationMemberCountSingle - @string/title_conversationMemberCountMany - @string/title_conversationMemberCountMany - @string/title_conversationMemberCountMany - @string/title_conversationMemberCountMany - - - Save - Save Changes - This is how you\’ll show up in chats - Enter a name appropriate for all ages - - You\'ve been muted - The host has temporarily closed this flipchat. Only they can send messages until they reopen it - Your Flipchat is currently open - Your Flipchat is currently closed - This Flipchat is currently open - This Flipchat is currently closed - Change - Reopen - Close - Reopen Flipchat? - People will be able to send messages again - Close Flipchat Temporarily - Reopen Flipchat - Close Flipchat Temporarily? - Only you will be able to send messages until you reopen the flipchat - Close Temporarily - Reopen Flipchat - - Your Access Key is the only way to access your account. Please keep it private and safe. - Warning! This image gives access to your Flipchat account. Do not share this image with anyone else. Keep it secure and safe. - Please allow Flipchat access to Photos in Settings in order to save your Access Key - - Access Key No Longer Usable in Flipchat - Your Access Key has initiated an unlock. As a result, you will no longer be able to use this Access Key in Flipchat - Not a Flipchat Account - Only accounts created through Flipchat are currently supported - Create a New Flipchat Account - Try a Different Flipchat Account - - Tap the Google Lens icon to open the QR code to log into Flipchat. Alternatively you can log in manually by entering the 12 words in the Flipchat Log In screen. - - Log Out? - You will need to enter your Access Key to get back into this account - - Labs - - Not Now - Create an Account to Join Flipchats - Purchase Your Account - New accounts cost %1$s - Finalize Account Creation - Accounts on Flipchat must be purchased for %1$s to reduce spam - Change Cover Charge - Change Listener Message Fee - Add Flipchat Name - Edit Flipchat Name - Change Flipchat Name - Change Flipchat Description - Customize - Customize Flipchat - Leave This Flipchat - Share a Link to Your Flipchat - Share a Link to This Flipchat - - Delete My Account - Permanently Delete Account? - This will permanently delete your Flipchat account - Permanently Delete My Account - - Balance: ⬢ %1$s - You - - We\'ve made some changes to improve the experience. You\'ll need to update the app to keep using Flipchat. - Connect Your Account - - Disconnect Your %1$s Account? - You will no longer have a profile picture or connected %1$s account - Disconnect %1$s Account - - Open Profile on %1$s - \ No newline at end of file diff --git a/apps/flipchatApp/src/main/res/values/themes.xml b/apps/flipchatApp/src/main/res/values/themes.xml deleted file mode 100644 index aec3a4945..000000000 --- a/apps/flipchatApp/src/main/res/values/themes.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - diff --git a/apps/flipchatApp/src/main/res/xml/authenticator.xml b/apps/flipchatApp/src/main/res/xml/authenticator.xml deleted file mode 100644 index 415269cbc..000000000 --- a/apps/flipchatApp/src/main/res/xml/authenticator.xml +++ /dev/null @@ -1,7 +0,0 @@ - - \ No newline at end of file diff --git a/buildSrc/src/main/java/GradleNamespace.kt b/buildSrc/src/main/java/GradleNamespace.kt index 8dac9cd49..54a404248 100644 --- a/buildSrc/src/main/java/GradleNamespace.kt +++ b/buildSrc/src/main/java/GradleNamespace.kt @@ -1,5 +1,4 @@ object Gradle { const val codeNamespace = "com.getcode" - const val flipchatNamespace = "xyz.flipchat" const val flipcashNamespace = "com.flipcash" } \ No newline at end of file diff --git a/buildSrc/src/main/java/Packaging.kt b/buildSrc/src/main/java/Packaging.kt index 4dd0d653b..e2c8fcaf6 100644 --- a/buildSrc/src/main/java/Packaging.kt +++ b/buildSrc/src/main/java/Packaging.kt @@ -24,21 +24,10 @@ sealed class Packaging( val versionCode: Int? = versionCodeOverride - object Code : Packaging( - majorVersion = 2, - minorVersion = 1, - patchVersion = 14, - ) - object Flipcash : Packaging( majorVersion = 2026, // release year minorVersion = 5, // release month patchVersion = 3, // cycle in minor version ) - object Flipchat : Packaging( - majorVersion = 1, - minorVersion = 0, - patchVersion = 10, - ) } diff --git a/definitions/code/models/.gitignore b/definitions/code/models/.gitignore deleted file mode 100644 index 9f2a07880..000000000 --- a/definitions/code/models/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -build/ -.gradle/ diff --git a/definitions/code/models/build.gradle.kts b/definitions/code/models/build.gradle.kts deleted file mode 100644 index 125465d15..000000000 --- a/definitions/code/models/build.gradle.kts +++ /dev/null @@ -1,90 +0,0 @@ -import org.apache.tools.ant.taskdefs.condition.Os -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - id(Plugins.android_library) - id(Plugins.kotlin_android) - id("com.google.protobuf") -} - -val archSuffix = if (Os.isFamily(Os.FAMILY_MAC)) ":osx-x86_64" else "" - -version = "0.0.1" -group = "com.codeinc.gen" - -dependencies { - protobuf(project(":definitions:code:protos")) - - implementation(Libs.grpc_protobuf_lite) - implementation(Libs.grpc_stub) - - // Kotlin Generation - implementation(Libs.grpc_kotlin) - implementation(Libs.protobuf_kotlin_lite) - implementation(Libs.kotlinx_coroutines_core) -} - -kotlin { - jvmToolchain(Versions.java.toInt()) -} - -android { - namespace = "${Gradle.codeNamespace}.service.models" - compileSdk = Android.compileSdkVersion - defaultConfig { - minSdk = Android.minSdkVersion - testInstrumentationRunner = Android.testInstrumentationRunner - } -} - -kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(Versions.java)) - } - - compilerOptions { - jvmTarget.set(JvmTarget.fromTarget(Versions.java)) - optIn.addAll( - "kotlin.time.ExperimentalTime", - "kotlin.ExperimentalUnsignedTypes", - "kotlin.RequiresOptIn" - ) - } -} - -protobuf { - protoc { - artifact = "com.google.protobuf:protoc:${Versions.protobuf}$archSuffix" - } - plugins { - create("java") { - artifact = "io.grpc:protoc-gen-grpc-java:${Versions.grpc}" - } - create("grpc") { - artifact = "io.grpc:protoc-gen-grpc-java:${Versions.grpc}" - } - create("grpckt") { - artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar" - } - } - generateProtoTasks { - all().forEach { - it.plugins { - create("java") { - option("lite") - } - create("grpc") { - option("lite") - } - create("grpckt") { - option("lite") - } - } - it.builtins { - create("kotlin") { - option("lite") - } - } - } - } -} diff --git a/definitions/code/protos/.gitignore b/definitions/code/protos/.gitignore deleted file mode 100644 index 9f2a07880..000000000 --- a/definitions/code/protos/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -build/ -.gradle/ diff --git a/definitions/code/protos/build.gradle.kts b/definitions/code/protos/build.gradle.kts deleted file mode 100644 index a42e252c7..000000000 --- a/definitions/code/protos/build.gradle.kts +++ /dev/null @@ -1,11 +0,0 @@ -// todo: maybe use variants / configurations to do both stub & stub-lite here - -// Note: We use the java-library plugin to get the protos into the artifact for this subproject -// because there doesn't seem to be an better way. -plugins { - `java-library` -} - -java { - sourceSets.getByName("main").resources.srcDir("src/main/proto") -} diff --git a/definitions/code/protos/src/main/proto/account/v1/account_service.proto b/definitions/code/protos/src/main/proto/account/v1/account_service.proto deleted file mode 100644 index d8cdef961..000000000 --- a/definitions/code/protos/src/main/proto/account/v1/account_service.proto +++ /dev/null @@ -1,239 +0,0 @@ -syntax = "proto3"; - -package code.account.v1; - -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/account/v1;account"; -option java_package = "com.codeinc.gen.account.v1"; -option objc_class_prefix = "CPBAccountV1"; - -import "common/v1/model.proto"; -import "transaction/v2/transaction_service.proto"; -import "google/protobuf/timestamp.proto"; - - -service Account { - // IsCodeAccount returns whether an owner account is a Code account. This hints - // to the client whether the account can be logged in, used for making payments, - // etc. - rpc IsCodeAccount(IsCodeAccountRequest) returns (IsCodeAccountResponse); - - // GetTokenAccountInfos returns token account metadata relevant to the Code owner - // account. - rpc GetTokenAccountInfos(GetTokenAccountInfosRequest) returns (GetTokenAccountInfosResponse); - - // LinkAdditionalAccounts allows a client to declare additional accounts to - // be tracked and used within Code. The accounts declared in this RPC are not - // managed by Code (ie. not a Timelock account), created externally and cannot - // be linked automatically (ie. authority derived off user 12 words). - rpc LinkAdditionalAccounts(LinkAdditionalAccountsRequest) returns (LinkAdditionalAccountsResponse); -} - -message IsCodeAccountRequest { - // The owner account to check against. - common.v1.SolanaAccountId owner = 1; - - - // The signature is of serialize(IsCodeAccountRequest) without this field set - // using the private key of the owner account. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 2; - -} - -message IsCodeAccountResponse { - Result result = 1; - enum Result { - // The account is a Code account. - OK = 0; - // The account is not a Code account. - NOT_FOUND = 1; - // The account exists, but at least one timelock account is unlocked. - UNLOCKED_TIMELOCK_ACCOUNT = 2; - } -} - -message GetTokenAccountInfosRequest { - // The owner account, which can also be thought of as a parent account for this - // RPC that links to one or more token accounts. - common.v1.SolanaAccountId owner = 1; - - - // The signature is of serialize(GetTokenAccountInfosRequest) without this field set - // using the private key of the owner account. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 2; - -} - -message GetTokenAccountInfosResponse { - Result result = 1; - enum Result { - OK = 0; - NOT_FOUND = 1; - } - - map token_account_infos = 2; -} - -message LinkAdditionalAccountsRequest { - // The owner account to link to - common.v1.SolanaAccountId owner = 1; - - - // The authority account derived off the user's 12 words, which contains - // the USDC ATA (and potentially others in the future) that will be used - // in swaps. - common.v1.SolanaAccountId swap_authority = 2; - - - // Signature values for each account provided in this request. Each signature - // must be generated without this array set. The expected ordering of signatures: - // 1. owner - // 2. swap_authority - repeated common.v1.Signature signatures = 3 ; - -} - -message LinkAdditionalAccountsResponse { - Result result = 1; - enum Result { - // Supports idempotency, and will be returned as long as the request exactly - // matches a previous execution. - OK = 0; - // The action has been denied (eg. owner account not phone verified) - DENIED = 1; - // An account being linked is not valid - INVALID_ACCOUNT = 2; - } -} - -message TokenAccountInfo { - // The token account's address - common.v1.SolanaAccountId address = 1; - - - // The owner of the token account, which can also be thought of as a parent - // account that links to one or more token accounts. This is provided when - // available. - common.v1.SolanaAccountId owner = 2; - - // The token account's authority, which has access to moving funds for the - // account. This can be the owner account under certain circumstances (eg. - // ATA, primary account). This is provided when available. - common.v1.SolanaAccountId authority = 3; - - // The type of token account, which infers its intended use. - common.v1.AccountType account_type = 4; - - - // The account's derivation index for applicable account types. When this field - // doesn't apply, a zero value is provided. - uint64 index = 5; - - // The source of truth for the balance calculation. - BalanceSource balance_source = 6; - enum BalanceSource { - // The account's balance could not be determined. This may be returned when - // the data source is unstable and a reliable balance cannot be determined. - BALANCE_SOURCE_UNKNOWN = 0; - // The account's balance was fetched directly from a finalized state on the - // blockchain. - BALANCE_SOURCE_BLOCKCHAIN = 1; - // The account's balance was calculated using cached values in Code. Accuracy - // is only guaranteed when management_state is LOCKED. - BALANCE_SOURCE_CACHE = 2; - } - - // The balance in quarks, as observed by Code. This may not reflect the value - // on the blockchain and could be non-zero even if the account hasn't been created. - // Use balance_source to determine how this value was calculated. - uint64 balance = 7; - - // The state of the account as it pertains to Code's ability to manage funds. - ManagementState management_state = 8; - enum ManagementState { - // The state of the account is unknown. This may be returned when the - // data source is unstable and a reliable state cannot be determined. - MANAGEMENT_STATE_UNKNOWN = 0; - // Code does not maintain a management state and won't move funds for this - // account. - MANAGEMENT_STATE_NONE = 1; - // The account is in the process of transitioning to the LOCKED state. - MANAGEMENT_STATE_LOCKING = 2; - // The account's funds are locked and Code has co-signing authority. - MANAGEMENT_STATE_LOCKED = 3; - // The account is in the process of transitioning to the UNLOCKED state. - MANAGEMENT_STATE_UNLOCKING = 4; - // The account's funds are unlocked and Code no longer has co-signing - // authority. The account must transition to the LOCKED state to have - // management capabilities. - MANAGEMENT_STATE_UNLOCKED = 5; - // The account is in the process of transitioning to the CLOSED state. - MANAGEMENT_STATE_CLOSING = 6; - // The account has been closed and doesn't exist on the blockchain. - // Subsequently, it also has a zero balance. - MANAGEMENT_STATE_CLOSED = 7; - } - - // The state of the account on the blockchain. - BlockchainState blockchain_state = 9; - enum BlockchainState { - // The state of the account is unknown. This may be returned when the - // data source is unstable and a reliable state cannot be determined. - BLOCKCHAIN_STATE_UNKNOWN = 0; - // The account does not exist on the blockchain. - BLOCKCHAIN_STATE_DOES_NOT_EXIST = 1; - // The account is created and exists on the blockchain. - BLOCKCHAIN_STATE_EXISTS = 2; - } - - // For temporary incoming accounts only. Flag indicates whether client must - // actively try rotating it by issuing a ReceivePaymentsPrivately intent. In - // general, clients should wait as long as possible until this flag is true - // or requiring the funds to send their next payment. - bool must_rotate = 10; - - // Whether an account is claimed. This only applies to relevant account types - // (eg. REMOTE_SEND_GIFT_CARD). - ClaimState claim_state = 11; - enum ClaimState { - // The account doesn't have a concept of being claimed, or the state - // could not be fetched by server. - CLAIM_STATE_UNKNOWN = 0; - // The account has not yet been claimed. - CLAIM_STATE_NOT_CLAIMED = 1; - // The account is claimed. Attempting to claim it will fail. - CLAIM_STATE_CLAIMED = 2; - // The account hasn't been claimed, but is expired. Funds will move - // back to the issuer. Attempting to claim it will fail. - CLAIM_STATE_EXPIRED = 3; - } - - // For account types used as an intermediary for sending money between two - // users (eg. REMOTE_SEND_GIFT_CARD), this represents the original exchange - // data used to fund the account. Over time, this value will become stale: - // 1. Exchange rates will fluctuate, so the total fiat amount will differ. - // 2. External entities can deposit additional funds into the account, so - // the balance, in quarks, may be greater than the original quark value. - // 3. The balance could have been received, so the total balance can show - // as zero. - transaction.v2.ExchangeData original_exchange_data = 12; - - // The token account's mint - common.v1.SolanaAccountId mint = 13; - - // Reserved for the number of decimals configured for the mint - reserved 14; - - // Reserved for a user-friendly display name for the mint - reserved 15; - - // The relationship with a third party that this account has established with. - // This only applies to relevant account types (eg. RELATIONSHIP). - common.v1.Relationship relationship = 16; - - // Time the account was created, if available. For Code accounts, this is - // the time of intent submission. Otherwise, for external accounts, it is - // the tiem created on the blockchain. - google.protobuf.Timestamp created_at = 17; -} diff --git a/definitions/code/protos/src/main/proto/badge/v1/badge_service.proto b/definitions/code/protos/src/main/proto/badge/v1/badge_service.proto deleted file mode 100644 index 5885e2fe2..000000000 --- a/definitions/code/protos/src/main/proto/badge/v1/badge_service.proto +++ /dev/null @@ -1,25 +0,0 @@ -syntax = "proto3"; -package code.badge.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/badge/v1;badge"; -option java_package = "com.codeinc.gen.badge.v1"; -option objc_class_prefix = "CPBBadgeV1"; -import "common/v1/model.proto"; - -service Badge { - // ResetBadgeCount resets an owner account's app icon badge count back to zero - rpc ResetBadgeCount(ResetBadgeCountRequest) returns (ResetBadgeCountResponse); -} -message ResetBadgeCountRequest { - // The owner account to clear badge count - common.v1.SolanaAccountId owner = 1; - // The signature is of serialize(ResetBadgeCountRequest) without this field set - // using the private key of the owner account. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 2; -} -message ResetBadgeCountResponse { - Result result = 1; - enum Result { - OK = 0; - } -} diff --git a/definitions/code/protos/src/main/proto/chat/v1/chat_service.proto b/definitions/code/protos/src/main/proto/chat/v1/chat_service.proto deleted file mode 100644 index ebfc6d1a3..000000000 --- a/definitions/code/protos/src/main/proto/chat/v1/chat_service.proto +++ /dev/null @@ -1,256 +0,0 @@ -syntax = "proto3"; -package code.chat.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/chat/v1;chat"; -option java_package = "com.codeinc.gen.chat.v1"; -option objc_class_prefix = "CPBChatV1"; -import "common/v1/model.proto"; -import "transaction/v2/transaction_service.proto"; -import "google/protobuf/timestamp.proto"; - -// Deprecated: Use the v2 service -service Chat { - // GetChats gets the set of chats for an owner account - rpc GetChats(GetChatsRequest) returns (GetChatsResponse); - // GetMessages gets the set of messages for a chat - rpc GetMessages(GetMessagesRequest) returns (GetMessagesResponse); - // AdvancePointer advances a pointer in chat history - rpc AdvancePointer(AdvancePointerRequest) returns (AdvancePointerResponse); - // SetMuteState configures the mute state of a chat - rpc SetMuteState(SetMuteStateRequest) returns (SetMuteStateResponse); - // SetSubscriptionState configures the susbscription state of a chat - rpc SetSubscriptionState(SetSubscriptionStateRequest) returns (SetSubscriptionStateResponse); - // - // Experimental PoC two-way chat APIs below - // - rpc StreamChatEvents(stream StreamChatEventsRequest) returns (stream StreamChatEventsResponse); - rpc SendMessage(SendMessageRequest) returns (SendMessageResponse); -} -message GetChatsRequest { - common.v1.SolanaAccountId owner = 1; - common.v1.Signature signature = 2; - uint32 page_size = 3; - Cursor cursor = 4; - Direction direction = 5; - enum Direction { - ASC = 0; - DESC = 1; - } -} -message GetChatsResponse { - Result result = 1; - enum Result { - OK = 0; - NOT_FOUND = 1; - } - repeated ChatMetadata chats = 2 ; -} -message GetMessagesRequest { - ChatId chat_id = 1; - common.v1.SolanaAccountId owner = 2; - common.v1.Signature signature = 3; - uint32 page_size = 4; - Cursor cursor = 5; - Direction direction = 6; - enum Direction { - ASC = 0; - DESC = 1; - } -} -message GetMessagesResponse { - Result result = 1; - enum Result { - OK = 0; - NOT_FOUND = 1; - } - repeated ChatMessage messages = 2 ; -} -message AdvancePointerRequest { - ChatId chat_id = 1; - Pointer pointer = 2; - common.v1.SolanaAccountId owner = 3; - common.v1.Signature signature = 4; -} -message AdvancePointerResponse { - Result result = 1; - enum Result { - OK = 0; - CHAT_NOT_FOUND = 1; - MESSAGE_NOT_FOUND = 2; - } -} -message SetMuteStateRequest { - ChatId chat_id = 1; - bool is_muted = 2; - common.v1.SolanaAccountId owner = 3; - common.v1.Signature signature = 4; -} -message SetMuteStateResponse { - Result result = 1; - enum Result { - OK = 0; - CHAT_NOT_FOUND = 1; - CANT_MUTE = 2; - } -} -message SetSubscriptionStateRequest { - ChatId chat_id = 1; - bool is_subscribed = 2; - common.v1.SolanaAccountId owner = 3; - common.v1.Signature signature = 4; -} -message SetSubscriptionStateResponse { - Result result = 1; - enum Result { - OK = 0; - CHAT_NOT_FOUND = 1; - CANT_UNSUBSCRIBE = 2; - } -} -message OpenChatEventStream { - ChatId chat_id = 1; - common.v1.SolanaAccountId owner = 2; - common.v1.Signature signature = 3; -} -message ChatStreamEvent { - repeated ChatMessage messages = 1; - repeated Pointer pointers = 2; -} -message ChatStreamEventBatch { - repeated ChatStreamEvent events = 2 ; -} -message StreamChatEventsRequest { - oneof type { - OpenChatEventStream open_stream = 1; - common.v1.ClientPong pong = 2; - } -} -message StreamChatEventsResponse { - oneof type { - ChatStreamEventBatch events = 1; - common.v1.ServerPing ping = 2; - } -} -message SendMessageRequest { - common.v1.SolanaAccountId owner = 1; - common.v1.Signature signature = 2; - ChatId chat_id = 3; - // todo: What field type should this be? Maybe the chat message itself with fields missing? - repeated Content content = 4 ; -} -message SendMessageResponse { - Result result = 1; - enum Result { - OK = 0; - } - ChatMessage message = 2; -} -message ChatId { - bytes value = 1 ; -} -message ChatMessageId { - bytes value = 1 ; -} -message ChatMemberId { - // todo: Public key for now - bytes value = 1 ; -} -message Pointer { - Kind kind = 1; - enum Kind { - UNKNOWN = 0; - READ = 1; - DELIVERED = 2; - SENT = 3; // Probably always inferred by OK result in SendMessageResponse - } - ChatMessageId value = 2; - ChatMemberId user = 3; -} -message ChatMetadata { - ChatId chat_id = 1; - // Recommended chat title inferred by the type of chat - oneof title { - ServerLocalizedContent localized = 2; - common.v1.Domain domain = 3; - } - // Pointer in the chat indicating the most recently read message by the user - Pointer read_pointer = 4; - // Estimated number of unread messages in this chat - uint32 num_unread = 5; - // Has the user muted this chat? - bool is_muted = 6; - // Is the user subscribed to this chat? - bool is_subscribed = 7; - // Can the user mute this chat? - bool can_mute = 8; - // Can the user unsubscribe from this chat? - bool can_unsubscribe = 9; - // Cursor value for this chat for reference in subsequent GetChatsRequest - Cursor cursor = 10; - // Is this a verified chat? - // - // Note: It's possible to have two chats with the same title, but with - // different verification statuses. They should be treated separately. - bool is_verified = 11; -} -message ChatMessage { - // Unique ID for this message - ChatMessageId message_id = 1; - // Timestamp this message was generated at - google.protobuf.Timestamp ts = 2; - // Ordered message content. A message may have more than one piece of content. - repeated Content content = 3 ; - // Cursor value for this message for reference in subsequent GetMessagesRequest - Cursor cursor = 4; - ChatMemberId sender = 5; -} -message Content { - oneof type { - ServerLocalizedContent server_localized = 1; - ExchangeDataContent exchange_data = 2; - NaclBoxEncryptedContent nacl_box = 3; - TextContent text = 4; - ThankYouContent thank_you = 5; - } -} -message ServerLocalizedContent { - // When server-side localization is in place, clients will always see the - // localized text. - string key_or_text = 1 ; -} -message ExchangeDataContent { - Verb verb = 1; - enum Verb { - UNKNOWN = 0; - GAVE = 1; - RECEIVED = 2; - WITHDREW = 3; - DEPOSITED = 4; - SENT = 5; - RETURNED = 6; - SPENT = 7; - PAID = 8; - PURCHASED = 9; - RECEIVED_TIP = 10; - SENT_TIP = 11; - } - oneof exchange_data { - transaction.v2.ExchangeData exact = 2; - transaction.v2.ExchangeDataWithoutRate partial = 3; - } -} -message NaclBoxEncryptedContent { - common.v1.SolanaAccountId peer_public_key = 1; - bytes nonce = 2 ; - bytes encrypted_payload = 3 ; -} -message TextContent { - string text = 1 ; -} -message ThankYouContent { - // todo: May need additional metdata (who is being thanked, which tip, etc) -} -// Opaque cursor used across paged APIs. Underlying bytes may change as paging -// strategies evolve. -message Cursor { - bytes value = 1 ; -} diff --git a/definitions/code/protos/src/main/proto/chat/v2/chat_service.proto b/definitions/code/protos/src/main/proto/chat/v2/chat_service.proto deleted file mode 100644 index 9b5df3813..000000000 --- a/definitions/code/protos/src/main/proto/chat/v2/chat_service.proto +++ /dev/null @@ -1,420 +0,0 @@ -syntax = "proto3"; -package code.chat.v2; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/chat/v2;chat"; -option java_package = "com.codeinc.gen.chat.v2"; -option objc_class_prefix = "CPBChatV2"; -import "common/v1/model.proto"; -import "transaction/v2/transaction_service.proto"; -import "google/protobuf/timestamp.proto"; - -service Chat { - // GetChats gets the set of chats for an owner account using a paged API. - // This RPC is aware of all identities tied to the owner account. - rpc GetChats(GetChatsRequest) returns (GetChatsResponse); - // GetMessages gets the set of messages for a chat member using a paged API - rpc GetMessages(GetMessagesRequest) returns (GetMessagesResponse); - // StreamChatEvents streams chat events in real-time. Chat events include - // messages, pointer updates, etc. - // - // The streaming protocol is follows: - // 1. Client initiates a stream by sending an OpenChatEventStream message. - // 2. If an error is encoutered, a ChatStreamEventError message will be - // returned by server and the stream will be closed. - // 3. Server will immediately flush initial chat state. - // 4. New chat events will be pushed to the stream in real time as they - // are received. - // - // This RPC supports a keepalive protocol as follows: - // 1. Client initiates a stream by sending an OpenChatEventStream message. - // 2. Upon stream initialization, server begins the keepalive protocol. - // 3. Server sends a ping to the client. - // 4. Client responds with a pong as fast as possible, making note of - // the delay for when to expect the next ping. - // 5. Steps 3 and 4 are repeated until the stream is explicitly terminated - // or is deemed to be unhealthy. - // - // Client notes: - // * Client should be careful to process events async, so any responses to pings are - // not delayed. - // * Clients should implement a reasonable backoff strategy upon continued timeout - // failures. - // * Clients that abuse pong messages may have their streams terminated by server. - // - // At any point in the stream, server will respond with events in real time as - // they are observed. Events sent over the stream should not affect the ping/pong - // protocol timings. - rpc StreamChatEvents(stream StreamChatEventsRequest) returns (stream StreamChatEventsResponse); - // StartChat starts a chat. The RPC call is idempotent and will use existing - // chats whenever applicable within the context of message routing. - rpc StartChat(StartChatRequest) returns (StartChatResponse); - // SendMessage sends a message to a chat. - rpc SendMessage(SendMessageRequest) returns (SendMessageResponse); - // AdvancePointer advances a pointer in message history for a chat member. - rpc AdvancePointer(AdvancePointerRequest) returns (AdvancePointerResponse); - // SetMuteState configures a chat member's mute state. - rpc SetMuteState(SetMuteStateRequest) returns (SetMuteStateResponse); - // NotifyIsTypingRequest notifies a chat that the sending member is typing. - // - // These requests are transient, and may be dropped at any point. - rpc NotifyIsTyping(NotifyIsTypingRequest) returns (NotifyIsTypingResponse); -} -message GetChatsRequest { - common.v1.SolanaAccountId owner = 1; - common.v1.Signature signature = 2; - uint32 page_size = 3; - Cursor cursor = 4; - Direction direction = 5; - enum Direction { - ASC = 0; - DESC = 1; - } -} -message GetChatsResponse { - Result result = 1; - enum Result { - OK = 0; - } - repeated Metadata chats = 2 ; -} -message GetMessagesRequest { - common.v1.ChatId chat_id = 1; - common.v1.SolanaAccountId owner = 2; - common.v1.Signature signature = 3; - uint32 page_size = 4; - Cursor cursor = 5; - Direction direction = 6; - enum Direction { - ASC = 0; - DESC = 1; - } -} -message GetMessagesResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - } - repeated Message messages = 2 ; -} -message OpenChatEventStream { - common.v1.ChatId chat_id = 1; - common.v1.SolanaAccountId owner = 2; - common.v1.Signature signature = 3; -} -message ChatStreamEvent { - oneof type { - Message message = 1; - Pointer pointer = 2; - IsTyping is_typing = 3; - } -} -message ChatStreamEventBatch { - repeated ChatStreamEvent events = 2 ; -} -message ChatStreamEventError { - Code code = 1; - enum Code { - DENIED = 0; - } -} -message StreamChatEventsRequest { - oneof type { - OpenChatEventStream open_stream = 1; - common.v1.ClientPong pong = 2; - } -} -message StreamChatEventsResponse { - oneof type { - ChatStreamEventBatch events = 1; - common.v1.ServerPing ping = 2; - ChatStreamEventError error = 3; - } -} -message StartChatRequest { - common.v1.SolanaAccountId owner = 1; - common.v1.Signature signature = 2; - oneof parameters { - StartTwoWayChatParameters two_way_chat = 3; - } -} -// StartTwoWayChatParameters contains the parameters required to start -// or recover a two way chat between the caller and the specified 'other_user'. -// -// The 'other_user' is currently the 'tip_address', normally retrieved from -// user.Identity.GetTwitterUser(username). -message StartTwoWayChatParameters { - // The account id of the user the caller wishes to chat with. - // - // This will be the `tip` (or equivalent) address. - common.v1.SolanaAccountId other_user = 1; - // The intent_id of the payment that initiated the chat/friendship. - // - // This field is optional. It is used as an optimization when the server has not - // yet observed the establishment of a friendship. In this case, the server will - // use the provided intent_id to verify the friendship. - // - // This is most likely to occur when initiating a chat with a user for the first - // time. - common.v1.IntentId intent_id = 2; -} -message StartChatResponse { - Result result = 1; - enum Result { - OK = 0; - // DENIED indicates the caller is not allowed to start/join the chat. - DENIED = 1; - // INVALID_PRAMETER indicates one of the parameters is invalid. - INVALID_PARAMETER = 2; - // PENDING indicates that the payment (for chat) intent is pending confirmation - // before the service will permit the creation of the chat. This can happen in - // cases where the block chain is particularly slow (beyond our RPC timeouts). - PENDING = 3; - // MISSING_IDENTITY indicates that there is no identity for the user (creator). - MISSING_IDENTITY = 4; - // USER_NOT_FOUND indicates that (one of) the target user's was not found. - USER_NOT_FOUND = 5; - } - // The chat to use if the RPC was successful. - Metadata chat = 2; -} -message SendMessageRequest { - common.v1.ChatId chat_id = 1; - // Allowed content types that can be sent by client: - // - TextContent - // - ThankYouContent - repeated Content content = 2 ; - common.v1.SolanaAccountId owner = 3; - common.v1.Signature signature = 4; -} -message SendMessageResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - INVALID_CONTENT_TYPE = 2; - } - // The chat message that was sent if the RPC was succesful, which includes - // server-side metadata like the generated message ID and official timestamp - Message message = 2; -} -message AdvancePointerRequest { - common.v1.ChatId chat_id = 1; - Pointer pointer = 2; - common.v1.SolanaAccountId owner = 3; - common.v1.Signature signature = 4; -} -message AdvancePointerResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - MESSAGE_NOT_FOUND = 2; - } -} -message SetMuteStateRequest { - common.v1.ChatId chat_id = 1; - bool is_muted = 2; - common.v1.SolanaAccountId owner = 3; - common.v1.Signature signature = 4; -} -message SetMuteStateResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - CANT_MUTE = 2; - } -} -message NotifyIsTypingRequest { - common.v1.ChatId chat_id = 1; - bool is_typing = 2; - common.v1.SolanaAccountId owner = 3; - common.v1.Signature signature = 4; -} -message NotifyIsTypingResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - } -} -message MessageId { - // A lexicographically sortable ID that can be used to sort source of - // chat history. - bytes value = 1 ; -} -message MemberId { - // The publically available 'deposit' address of the user. - bytes value = 1 ; -} -enum ChatType { - UNKNOWN_CHAT_TYPE = 0; - TWO_WAY = 1; - // GROUP = 3; -} -enum Platform { - UNKNOWN_PLATFORM = 0; - TWITTER = 1; -} -enum PointerType { - UNKNOWN_POINTER_TYPE = 0; - SENT = 1; // Always inferred by OK result in SendMessageResponse or message presence in a chat - DELIVERED = 2; - READ = 3; -} -// A chat -// -// todo: Support is_verified in a clean way -message Metadata { - common.v1.ChatId chat_id = 1; - // The type of chat - ChatType type = 2 ; - // Cursor value for this chat for reference in subsequent GetChatsRequest - Cursor cursor = 3; - // The chat title, which is _only_ set by server if an explicit title - // was set. Otherwise, clients should fill in an appropriate chat title. - string title = 4 ; - // The members in this chat. - repeated Member members = 5 ; - // Whether or not the chat is muted (from the perspective of the caller). - bool is_muted = 6; - // Whether or not the chat is mutable (from the persective of the caller). - bool muteable = 7; - // Number of (estimated) unread message (from the perspective of the caller). - uint32 num_unread = 8; -} -// A message in a chat -message Message { - // Globally unique ID for this message - MessageId message_id = 1; - // The chat member that sent the message. For NOTIFICATION chats, this field - // is omitted since the chat has exactly 1 member. - MemberId sender_id = 2; - // Ordered message content. A message may have more than one piece of content. - repeated Content content = 3 ; - // Timestamp this message was generated at. This value is also encoded in - // any time-based UUID message IDs. - google.protobuf.Timestamp ts = 4; - // Cursor value for this message for reference in a paged GetMessagesRequest - Cursor cursor = 5; -} -// A user in a chat -message Member { - // Public AccountId (for is self...is derived via deposit address) - MemberId member_id = 1; - // The chat member's identity if it has been revealed. - // - // Multiple identities here? Well really only needs twitter/other handles - // Repeated PlatformHandles (where code doesn't matter)? - MemberIdentity identity = 2; - // Chat message state for this member. - // - // If set, the list may contain DELIVERED and READ pointers. SENT pointers - // are only shared between the sender and server, to indicate persistence. - // - // The server may wish to omit all pointers in various types of group chats - // or as relief valves. - repeated Pointer pointers = 3 ; - // If the member is the caller (where applicable), will be set to true. - bool is_self = 4; -} -// Identity to an external social platform that can be linked to a Code account -message MemberIdentity { - // The external social platform linked to this chat member - Platform platform = 1 ; - // The chat member's username on the external social platform. - string username = 2 ; - // TODO: If we need the profile pic, we need display name...both can maybe be removed? - // If present, the display name of the user. - string display_name = 3 ; - // If present, the URL of the users profile pic. - string profile_pic_url = 4 ; -} -// Pointer in a chat indicating a user's message history state in a chat. -message Pointer { - // The type of pointer indicates which user's message history state can be - // inferred from the pointer value. It is also possible to infer cross-pointer - // state. For example, if a chat member has a READ pointer for a message with - // ID N, then the DELIVERED pointer must be at least N. - PointerType type = 1 ; - // Everything at or before this message ID is considered to have the state - // inferred by the type of pointer. - MessageId value = 2; - // The chat member associated with this pointer state - MemberId member_id = 3; -} -// Content for a chat message -message Content { - oneof type { - TextContent text = 1; - LocalizedContent localized = 2; - ExchangeDataContent exchange_data = 3; - NaclBoxEncryptedContent nacl_box = 4; - } -} -// Raw text content -message TextContent { - string text = 1 ; -} -// Text content that is either a localization key that should be translated on -// client, or a server-side translated piece of text. -message LocalizedContent { - string key_or_text = 1 ; -} -// Exchange data content for movement of a value of Kin -message ExchangeDataContent { - enum Verb { - UNKNOWN = 0; - GAVE = 1; - RECEIVED = 2; - WITHDREW = 3; - DEPOSITED = 4; - SENT = 5; - RETURNED = 6; - SPENT = 7; - PAID = 8; - PURCHASED = 9; - RECEIVED_TIP = 10; - SENT_TIP = 11; - } - // Verb describing how the amount of Kin was exchanged - // - // Note: The current definition is not suitable outside a NOTIFICATION chat - // as not enough context is provided as to which member this verb is - // associated with. - Verb verb = 1 ; - // An amount of Kin being exchanged - oneof exchange_data { - transaction.v2.ExchangeData exact = 2; - transaction.v2.ExchangeDataWithoutRate partial = 3; - } - // An ID that can be referenced to the source of the exchange of Kin - oneof reference { - common.v1.IntentId intent = 4; - common.v1.Signature signature = 5; - } - // TODO: We need to be able to provide the parties involved, such that - // such that client can render it correctly. -} -// Encrypted piece of content using NaCl box encryption -message NaclBoxEncryptedContent { - // The sender's public key that is used to derive the shared private key for - // decryption for message content. - common.v1.SolanaAccountId peer_public_key = 1; - // Globally random nonce that is unique to this encrypted piece of content - bytes nonce = 2 ; - // The encrypted piece of message content - bytes encrypted_payload = 3 ; -} -// Opaque cursor used across paged APIs. Underlying bytes may change as paging -// strategies evolve. Expected length value will vary based on the RPC being -// executed. -message Cursor { - bytes value = 1 ; -} -message IsTyping { - MemberId member_id = 1; - // is_typing indicates whether or not the user is typing. - // If false, the user has explicitly stopped typing. - bool is_typing = 2; -} diff --git a/definitions/code/protos/src/main/proto/common/v1/model.proto b/definitions/code/protos/src/main/proto/common/v1/model.proto deleted file mode 100644 index 177fb318e..000000000 --- a/definitions/code/protos/src/main/proto/common/v1/model.proto +++ /dev/null @@ -1,139 +0,0 @@ -syntax = "proto3"; -package code.common.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/common/v1;common"; -option java_package = "com.codeinc.gen.common.v1"; -option objc_class_prefix = "CPBCommonV1"; -import "google/protobuf/duration.proto"; -import "google/protobuf/timestamp.proto"; - -// AccountType associates a type to an account, which infers how an account is used -// within the Code ecosystem. -enum AccountType { - UNKNOWN = 0; - PRIMARY = 1; - TEMPORARY_INCOMING = 2; - TEMPORARY_OUTGOING = 3; - BUCKET_1_KIN = 4; - BUCKET_10_KIN = 5; - BUCKET_100_KIN = 6; - BUCKET_1_000_KIN = 7; - BUCKET_10_000_KIN = 8; - BUCKET_100_000_KIN = 9; - BUCKET_1_000_000_KIN = 10; - LEGACY_PRIMARY_2022 = 11; - REMOTE_SEND_GIFT_CARD = 12; - RELATIONSHIP = 13; - SWAP = 14; -} -// SolanaAccountId is a raw binary Ed25519 public key for a Solana account -message SolanaAccountId { - bytes value = 1 ; -} -// InstructionAccount is an account public key used within the context of -// an instruction. -message InstructionAccount { - SolanaAccountId account = 1; - bool is_signer = 2; - bool is_writable = 3; -} -// Transaction is a raw binary Solana transaction -message Transaction { - // Maximum size taken from: https://github.com/solana-labs/solana/blob/39b3ac6a8d29e14faa1de73d8b46d390ad41797b/sdk/src/packet.rs#L9-L13 - bytes value = 1 ; -} -// Blockhash is a raw binary Solana blockchash -message Blockhash { - bytes value = 1 ; -} -// Signature is a raw binary Ed25519 signature -message Signature { - bytes value = 1 ; -} -// IntentId is a client-side generated ID that maps to an intent to perform actions -// on the blockchain fulfilled by the Code sequencer. -message IntentId { - bytes value = 1 ; -} -// UserId is a globally unique identifier for a user within Code -// -// Note: Users outside Code are modelled as relationship accounts -message UserId { - bytes value = 1 ; -} -message ChatId { - // Sufficient space is left for a consistent hash value, though other types - // of values may be used. - bytes value = 1 ; -} -// DataContainerId is a globally unique identifier for a container where a user -// can store a copy of their data -message DataContainerId { - bytes value = 1 ; -} -// DeviceToken is an opaque token used to verify whether a device real -message DeviceToken { - string value = 1 ; -} -// AppInstallId is a unque ID tied to a client app installation. It does not -// identify a device. Value should remain private and not be shared across -// installs. -message AppInstallId { - string value = 1 ; -} -// PhoneNumber is an E.164 phone number -message PhoneNumber { - // Regex provided by Twilio here: https://www.twilio.com/docs/glossary/what-e164#regex-matching-for-e164 - string value = 1; -} -// Domain is a hostname -message Domain { - string value = 1 ; -} -// Relationship is a set of identifiers that a user can establish a relationship -// with. -message Relationship { - oneof type { - common.v1.Domain domain = 1; - } -} -// Hash is a raw binary 32 byte hash value -message Hash { - bytes value = 1 ; -} -// Locale is a user locale consisting of a combination of language, script and region -message Locale { - string value = 1; -} -// UUID is a 16 byte UUID value -message UUID { - bytes value = 1 ; -} -// Request is a generic wrapper for gRPC requests -message Request { - string version = 1; - string service = 2; - string method = 3; - bytes body = 4; -} -// Response is a generic wrapper for gRPC responses -message Response { - Result result = 1; - bytes body = 2; - string message = 3; - enum Result { - OK = 0; - ERROR = 1; - } -} -message ServerPing { - // Timestamp the ping was sent on the stream, for client to get a sense - // of potential network latency - google.protobuf.Timestamp timestamp = 1; - // The delay server will apply before sending the next ping - google.protobuf.Duration ping_delay = 2; -} -message ClientPong { - // Timestamp the Pong was sent on the stream, for server to get a sense - // of potential network latency - google.protobuf.Timestamp timestamp = 1; -} diff --git a/definitions/code/protos/src/main/proto/contact/v1/contact_list_service.proto b/definitions/code/protos/src/main/proto/contact/v1/contact_list_service.proto deleted file mode 100644 index 0eca54b93..000000000 --- a/definitions/code/protos/src/main/proto/contact/v1/contact_list_service.proto +++ /dev/null @@ -1,106 +0,0 @@ -syntax = "proto3"; -package code.contact.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/contact/v1;contact"; -option java_package = "com.codeinc.gen.contact.v1"; -option objc_class_prefix = "CPBContactV1"; -import "common/v1/model.proto"; - -service ContactList { - // AddContacts adds a batch of contacts to a user's contact list - rpc AddContacts(AddContactsRequest) returns (AddContactsResponse); - // RemoveContacts removes a batch of contacts from a user's contact list - rpc RemoveContacts(RemoveContactsRequest) returns (RemoveContactsResponse); - // GetContacts gets a subset of contacts from a user's contact list - rpc GetContacts(GetContactsRequest) returns (GetContactsResponse); -} -message AddContactsRequest { - // The public key of the owner account that signed this request message. - common.v1.SolanaAccountId owner_account_id = 1; - // The signature is of serialize(AddContactsRequest) without this field set - // using the private key of owner_account_id. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 2; - // The data container for the copy of the contact list being added to. - common.v1.DataContainerId container_id = 3; - // The set of contacts to add to the contact list - repeated common.v1.PhoneNumber contacts = 4 ; - -} -message AddContactsResponse { - Result result = 1; - enum Result { - OK = 0; - } - // The contacts' current status keyed by phone number. This is an optimization - // so that clients can populate initial state without needing an extra network - // call. - map contact_status = 2; -} -message RemoveContactsRequest { - // The public key of the owner account that signed this request message. - common.v1.SolanaAccountId owner_account_id = 1; - // The signature is of serialize(RemoveContactsRequest) without this field - // set using the private key of owner_account_id. This provides an - // authentication mechanism to the RPC. - common.v1.Signature signature = 2; - // The data container for the copy of the contact list being removed from. - common.v1.DataContainerId container_id = 3; - // The set of contacts to remove from the contact list - repeated common.v1.PhoneNumber contacts = 4 ; -} -message RemoveContactsResponse { - Result result = 1; - enum Result { - OK = 0; - } -} -message GetContactsRequest { - // The public key of the owner account that signed this request message. - common.v1.SolanaAccountId owner_account_id = 1; - // The signature is of serialize(GetContactsRequest) without this field set - // using the private key of owner_account_id. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 2; - // The data container for the copy of the contact list being fetched. - common.v1.DataContainerId container_id = 3; - // The page token, which is retreived from a previous response, to get the next - // set of contacts. The first page is returned when not set. - PageToken page_token = 4; - // Filter out contacts that have an association with Code. This includes users - // that have both been invited and registered with the app. - bool include_only_in_app_contacts = 5; -} -message GetContactsResponse { - Result result = 1; - enum Result { - OK = 0; - } - // A page of contacts - repeated Contact contacts = 2; - // The page token to include in a subsequent request to get the next set of - // contacts. This will not be set for the last response in the list of - // pages. - PageToken next_page_token = 3; -} -message Contact { - // The contact's phone number - common.v1.PhoneNumber phone_number = 1; - // The contact's current status - ContactStatus status = 2; -} -message ContactStatus { - // Flag to indicate whether a user has registered with Code and used the app - // at least once. - bool is_registered = 1; - // Flag to indicate whether a user has been invited to Code. - // - // todo: This field will be deprecated after the invite phase is complete. - bool is_invited = 2; - // Flag to indicate whether a user's invitation to Code has been revoked. - // - // todo: This field will be deprecated after the invite phase is complete. - bool is_invite_revoked = 3; -} -message PageToken { - bytes value = 1 ; -} diff --git a/definitions/code/protos/src/main/proto/currency/v1/currency_service.proto b/definitions/code/protos/src/main/proto/currency/v1/currency_service.proto deleted file mode 100644 index 5458a224b..000000000 --- a/definitions/code/protos/src/main/proto/currency/v1/currency_service.proto +++ /dev/null @@ -1,29 +0,0 @@ -syntax = "proto3"; -package code.currency.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/currency/v1;currency"; -option java_package = "com.codeinc.gen.currency.v1"; -option objc_class_prefix = "CPBCurrencyV1"; - -import "google/protobuf/timestamp.proto"; -service Currency { - // GetAllRates returns the exchange rates for Kin against all available currencies - rpc GetAllRates(GetAllRatesRequest) returns (GetAllRatesResponse); -} -message GetAllRatesRequest { - // If timestamp is included, the returned rate will be the most recent available - // exchange rate prior to the provided timestamp within the same day. Otherwise, - // the latest rates will be returned. - google.protobuf.Timestamp timestamp = 1; -} -message GetAllRatesResponse { - Result result = 1; - enum Result { - OK = 0; - // No currency data is available for the requested timestamp. - MISSING_DATA = 1; - } - // The time the exchange rates were observed - google.protobuf.Timestamp as_of = 2; - // The price of 1 Kin in different currencies, keyed on 3- or 4- letter lowercase currency code. - map rates = 3 ; -} diff --git a/definitions/code/protos/src/main/proto/device/v1/device_service.proto b/definitions/code/protos/src/main/proto/device/v1/device_service.proto deleted file mode 100644 index 1b8e420a7..000000000 --- a/definitions/code/protos/src/main/proto/device/v1/device_service.proto +++ /dev/null @@ -1,49 +0,0 @@ -syntax = "proto3"; -package code.device.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/device/v1;device"; -option java_package = "com.codeinc.gen.device.v1"; -option objc_class_prefix = "CPBDevicetV1"; -import "common/v1/model.proto"; - -service Device { - // RegisterLoggedInAccounts registers a set of owner accounts logged for - // an app install. Currently, a single login is enforced per app install. - // After using GetLoggedInAccounts to detect stale logins, clients can use - // this RPC to update the set of accounts with valid login sessions. - rpc RegisterLoggedInAccounts(RegisterLoggedInAccountsRequest) returns (RegisterLoggedInAccountsResponse); - // GetLoggedInAccounts gets the set of logged in accounts for an app install. - // Clients can use this RPC to detect stale logins for boot out of the app. - rpc GetLoggedInAccounts(GetLoggedInAccountsRequest) returns (GetLoggedInAccountsResponse); -} -message RegisterLoggedInAccountsRequest { - common.v1.AppInstallId app_install = 1; - // The set of owners logged into the app install. Setting an empty value - // indicates there are no logged in users. We allow for more than one owner - // in the spec with a repeated field to be flexible in the future. - repeated common.v1.SolanaAccountId owners = 2 ; - // Signature values must appear in the exact order their respecitive signing - // owner account appears in the owners field. All signatures should be generated - // without any other signature values set. - repeated common.v1.Signature signatures = 3 ; -} -message RegisterLoggedInAccountsResponse { - Result result = 1; - enum Result { - OK = 0; - INVALID_OWNER = 1; - } - // Set of invalid owner accounts detected in the request. An owner account - // can be invalid for several reasons: not phone verified, timelock account - // unlocked, etc. Value is set when result is INVALID_OWNER. - repeated common.v1.SolanaAccountId invalid_owners = 2 ; -} -message GetLoggedInAccountsRequest { - common.v1.AppInstallId app_install = 1; -} -message GetLoggedInAccountsResponse { - Result result = 1; - enum Result { - OK = 0; - } - repeated common.v1.SolanaAccountId owners = 2 ; -} diff --git a/definitions/code/protos/src/main/proto/invite/v2/invite_service.proto b/definitions/code/protos/src/main/proto/invite/v2/invite_service.proto deleted file mode 100644 index c3676cc62..000000000 --- a/definitions/code/protos/src/main/proto/invite/v2/invite_service.proto +++ /dev/null @@ -1,91 +0,0 @@ -syntax = "proto3"; -package code.invite.v2; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/invite/v2;invite"; -option java_package = "com.codeinc.gen.invite.v2"; -option objc_class_prefix = "CPBInviteV2"; -import "common/v1/model.proto"; - -service Invite { - // GetInviteCount gets the number of invites that a user can send out. - rpc GetInviteCount(GetInviteCountRequest) returns (GetInviteCountResponse); - // InvitePhoneNumber invites someone to join via their phone number. A phone number - // can only be invited once by a unique user or invite code. This is to avoid having - // a phone number consuming more than one invite count globally. - rpc InvitePhoneNumber(InvitePhoneNumberRequest) returns (InvitePhoneNumberResponse); - // GetInvitationStatus gets a phone number's invitation status. - rpc GetInvitationStatus(GetInvitationStatusRequest) returns (GetInvitationStatusResponse); -} -message GetInviteCountRequest { - // The user to query for their invite count - common.v1.UserId user_id = 1; -} -message GetInviteCountResponse { - Result result = 1; - enum Result { - OK = 0; - } - // The number of invites the user is allowed to issue. - uint32 invite_count = 2; -} -message InvitePhoneNumberRequest { - // The source for the invite. One of these values must be present - oneof source { - common.v1.UserId user = 1; - InviteCode invite_code = 3; - } - // The phone number receiving the invite. - common.v1.PhoneNumber receiver = 2; -} -message InvitePhoneNumberResponse { - Result result = 1; - enum Result { - OK = 0; - // The source exceeded their invite count and is restricted from issuing - // further invites. - INVITE_COUNT_EXCEEDED = 1; - // The receiver phone number has already been invited. Regardless of who - // invited it, the source's invite count is not decremented when this is - // returned. - ALREADY_INVITED = 2; - // The source user has not been invited. - USER_NOT_INVITED = 3; - // The receiver phone number failed validation. - INVALID_RECEIVER_PHONE_NUMBER = 4; - // The invite code doesn't exist. - INVITE_CODE_NOT_FOUND = 5; - // The invite code has been revoked. - INVITE_CODE_REVOKED = 6; - // The invite code has expired. - INVITE_CODE_EXPIRED = 7; - } -} -message GetInvitationStatusRequest { - // The user being queried for their invitation status. - common.v1.UserId user_id = 1; -} -message GetInvitationStatusResponse { - Result result = 1; - enum Result { - OK = 0; - } - // The user's invitation status - InvitationStatus status = 2; -} -message InviteCode { - // Regex for invite codes - string value = 1 ; -} -message PageToken { - bytes value = 1 ; -} -enum InvitationStatus { - // The phone number has never been invited. - NOT_INVITED = 0; - // The phone number has been invited at least once. - INVITED = 1; - // The phone number has been invited and used the app at least once via a - // phone verified account creation or login. - REGISTERED = 2; - // The phone number was invited, but revoked at a later time. - REVOKED = 3; -} diff --git a/definitions/code/protos/src/main/proto/messaging/v1/messaging_service.proto b/definitions/code/protos/src/main/proto/messaging/v1/messaging_service.proto deleted file mode 100644 index 5e4a222d1..000000000 --- a/definitions/code/protos/src/main/proto/messaging/v1/messaging_service.proto +++ /dev/null @@ -1,330 +0,0 @@ -syntax = "proto3"; -package code.messaging.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/messaging/v1;messaging"; -option java_package = "com.codeinc.gen.messaging.v1"; -option objc_class_prefix = "CPBMessagingV1"; -import "common/v1/model.proto"; -import "transaction/v2/transaction_service.proto"; - -import "google/protobuf/timestamp.proto"; -service Messaging { - // OpenMessageStream opens a stream of messages. Messages are routed using the - // public key of a rendezvous keypair derived by both the sender and the - // recipient of the messages. The sender may be a client or server. - // - // Messages are expected to be acked once they have been processed by the client. - // Ack'd messages will no longer be delivered on future OpenMessageStream calls, - // and are eligible for deletion from the service. Clients should, however, handle - // duplicate delivery of messages. - // - // For grabbing a bill, the expected flow is as follows: - // 1. The payment sender creates a cash scan code - // 2. The payment sender calls OpenMessageStream on the rendezvous public key, which is - // derived by using sha256(scan payload) as the keypair seed. - // 3. The payment recipient scans the code and uses SendMessage to send their account ID - // back to the sender via the rendezvous public key. - // 4. The payment sender receives the message, submits the intent, and closes the stream. - // - // For receiving a bill of requested value, the expected flow is as follows: - // 1. The payment recipient uses SendMessage to send their account ID and payment amount to - // the sender via the rendezvous public key, which is derived by using sha256(scan payload) - // as the keypair seed. - // 2. The payment recipient calls OpenMessageStream on the rendezvous public key to listen - // for status messages generated by client/server. It must ignore the original message it sent - // as part of step 1. - // 3. The payment recipient creates a payment request scan code - // 4. The payment sender calls PollMessages on the rendezvous public key. This is ok because - // we know the message exists per step 1, and doesn't actually incur a long poll. This is a - // required hack because we don't have the infrastructure in place to allow multiple listens - // on the same stream, and the recipient needs real-time status updates. - // 5. The payment sender receives the message (any status messages are ignored), and submits the - // intent. - // 6. The payment recipient observes status message (eg. IntentSubmitted, ClientRejectedPayment, - // WebhookCalled) for payment state. - // 7. The payment recipient closes the stream once the payment hits a terminal state, or times out. - // - // For logging in, the expected flow is as follows: - // 1. The third party uses SendMessage to send their login challenge to the user via the rendezvous - // public key, which is derived by using sha256(scan payload) as the keypair seed. - // 2. The third party calls OpenMessageStream on the rendezvous public key to listen for status - // messages generated by server. It must ignore the original message it sent as part of step 1. - // 3. The third party creates a login scan code - // 4. The user logging in calls PollMessages on the rendezvous public key. This is ok because - // we know the message exists per step 1, and doesn't actually incur a long poll. This is a - // required hack because we don't have the infrastructure in place to allow multiple listens - // on the same stream, and the recipient needs real-time status updates. - // 5. The user logging in receives the message (any status messages are ignored), verifies it, - // then submits a login attempt. - // 6. The third party observes status message (eg. IntentSubmitted, ClientRejectedLogin, - // WebhookCalled) for login state. - // 7. The third party closes the stream once the login hits a terminal state, or times out. - rpc OpenMessageStream(OpenMessageStreamRequest) returns (stream OpenMessageStreamResponse); - // OpenMessageStreamWithKeepAlive is like OpenMessageStream, but enables a ping/pong - // keepalive to determine the health of the stream at both the client and server. - // - // The keepalive protocol is as follows: - // 1. Client initiates a stream by sending an OpenMessageStreamRequest. - // 2. Upon stream initialization, server begins the keepalive protocol. - // 3. Server sends a ping to the client. - // 4. Client responds with a pong as fast as possible, making note of - // the delay for when to expect the next ping. - // 5. Steps 3 and 4 are repeated until the stream is explicitly terminated - // or is deemed to be unhealthy. - // - // Client notes: - // * Client should be careful to process messages async, so any responses to pings are - // not delayed. - // * Clients should implement a reasonable backoff strategy upon continued timeout failures. - // * Clients that abuse pong messages may have their streams terminated by server. - // - // At any point in the stream, server will respond with messages in real time as - // they are observed. Messages sent over the stream should not affect the ping/pong - // protocol timings. Individual protocols for payment flows remain the same, and are - // documented in OpenMessageStream. - // - // Note: This API will enforce OpenMessageStreamRequest.signature is set as part of migration - // to this newer protocol - rpc OpenMessageStreamWithKeepAlive(stream OpenMessageStreamWithKeepAliveRequest) returns (stream OpenMessageStreamWithKeepAliveResponse); - // PollMessages is like OpenMessageStream, but uses a polling flow for receiving - // messages. Updates are not real-time and depedent on the polling interval. - // This RPC supports all message types. - // - // This is a temporary RPC until OpenMessageStream can be built out generically on - // both client and server, while supporting things like multiple listeners. - rpc PollMessages(PollMessagesRequest) returns (PollMessagesResponse); - // AckMessages acks one or more messages that have been successfully delivered to - // the client. - rpc AckMessages(AckMessagesRequest) returns (AckMesssagesResponse); - // SendMessage sends a message. - rpc SendMessage(SendMessageRequest) returns (SendMessageResponse); -} -message OpenMessageStreamRequest { - RendezvousKey rendezvous_key = 1; - // The signature is of serialize(OpenMessageStreamRequest) using rendezvous_key. - // - // todo: Make required once clients migrate - common.v1.Signature signature = 2; -} -message OpenMessageStreamResponse { - repeated Message messages = 1 ; -} -message OpenMessageStreamWithKeepAliveRequest { - oneof request_or_pong { - OpenMessageStreamRequest request = 1; - common.v1.ClientPong pong = 2; - } -} -message OpenMessageStreamWithKeepAliveResponse { - oneof response_or_ping { - OpenMessageStreamResponse response = 1; - common.v1.ServerPing ping = 2; - } -} -message PollMessagesRequest { - RendezvousKey rendezvous_key = 1; - // The signature is of serialize(PollMessagesRequest) using rendezvous_key. - common.v1.Signature signature = 2; -} -message PollMessagesResponse { - repeated Message messages = 1 ; -} -message AckMessagesRequest { - RendezvousKey rendezvous_key = 1; - repeated MessageId message_ids = 2 ; -} -message AckMesssagesResponse { - Result result = 1; - enum Result { - OK = 0; - } -} -message SendMessageRequest { - // The message to send. Types of messages clients can send are restricted. - Message message = 1; - // The rendezvous key that the message should be routed to. - RendezvousKey rendezvous_key = 2; - // The signature is of serialize(Message) using the PrivateKey of the keypair. - common.v1.Signature signature = 3; -} -message SendMessageResponse { - Result result = 1; - enum Result { - OK = 0; - NO_ACTIVE_STREAM = 1; - } - // Set if result == OK. - MessageId message_id = 2; -} -// RendezvousKey is a unique key pair, typically derived from a scan code payload, -// which is used to establish a secure communication channel anonymously to coordinate -// a flow using messages. -message RendezvousKey { - bytes value = 1 ; -} -// MessageId identifies a message. It is only guaranteed to be unique when -// paired with a destination (i.e. the rendezvous public key). -message MessageId { - bytes value = 1 ; -} -// Request that a pulled out bill be sent to the requested address. -// -// This message type is only initiated by clients. -message RequestToGrabBill { - // Requestor is the Kin token account on Solana to which a payment should be sent. - common.v1.SolanaAccountId requestor_account = 1; -} -// Request that a bill of a requested value is created and sent to the requested -// address. -// -// This message type is only initiated by clients. -message RequestToReceiveBill { - // Requestor is the Kin token account on Solana to which a payment should be sent. - common.v1.SolanaAccountId requestor_account = 1; - // The exchange data for the requested bill value. - oneof exchange_data { - // An exact amount of Kin. Payment is guaranteed to transfer the specified - // quarks in the requested currency and exchange rate. - // - // Only supports Kin. Use exchange_data.partial for fiat amounts. - transaction.v2.ExchangeData exact = 2; - // Fiat amount request. The amount of Kin is determined at time of payment - // with a recent exchange rate provided by the paying client and validatd - // by server. - // - // Only supports fiat amounts. Use exchange_data.exact for Kin. - transaction.v2.ExchangeDataWithoutRate partial = 3; - } - // - // Optional fields below to identify a domain requesting to receive a bill. - // Verification of the domain is optional. When verified, clients can establish - // relationships and third parties will by able to identify users with that - // account after payment is made. - // - // Note on field requirements: - // - Verified: All of domain, verifier, signature and rendezvous_key are required - // - Unverified: Only domain is requried - // - // The third-party's domain name, which is its primary identifier. Server - // guarantees to perform domain verification against the verifier account. - common.v1.Domain domain = 4; - // Owner account owned by the third party used in domain verification. - common.v1.SolanaAccountId verifier = 5; - // Signature of this message using the verifier private key, which in addition - // to domain verification, authenticates the third party. - common.v1.Signature signature = 6; - // Rendezvous key to avoid replay attacks - RendezvousKey rendezvous_key = 7; - // Additional fee payments splitting the requested amount. This is in addition - // to the hard-coded Code $0.01 USD fee. - repeated transaction.v2.AdditionalFeePayment additional_fees = 8; -} -// A status update on a stream to indicate a scan code was scanned. This can appear -// multiple times for the same stream. -// -// This message type is only initiated by client -message CodeScanned { - // Timestamp the client scanned the code - google.protobuf.Timestamp timestamp = 1; -} -// Payment is rejected by the client -// -// This message type is only initiated by clients -message ClientRejectedPayment { - common.v1.IntentId intent_id = 1; -} -// Intent was submitted via SubmitIntent -// -// This message type is only initiated by server -message IntentSubmitted { - common.v1.IntentId intent_id = 1; - // Metadata is available for intents where it can be safely propagated publicly. - // Anything else requires an additional authenticated RPC call (eg. login). - transaction.v2.Metadata metadata = 2; -} -// Webhook was successfully called -// -// This message type is only initiated by server -message WebhookCalled { - // Estimated time webhook was received - google.protobuf.Timestamp timestamp = 1; -} -// Request that an account logs in -// -// This message type is only initiated by third-parties through the SDK. -message RequestToLogin { - // The third-party's domain name, which is its primary identifier. Server - // guarantees to perform domain verification against the verifier account. - // - // Clients should expect subdomains for future feature compatiblity, but must - // use the ASCII base domain in the RELATIONSHIP account derivation strategy. - common.v1.Domain domain = 1; - // Deprecated nonce value, which is replaced by the rendezvous_key field which - // is effectively derived off a random nonce. - reserved 2; - // Reserved for a timestamp field, which may be used in the future. - reserved 3; - // Owner account owned by the third party used in domain verification. - common.v1.SolanaAccountId verifier = 4; - // Signature of this message using the verifier private key, which in addition - // to domain verification, authenticates the third party. - common.v1.Signature signature = 5; - // Rendezvous key to avoid replay attacks - RendezvousKey rendezvous_key = 6; -} -// Login is rejected by the client -// -// This message type is only initiated by user clients -message ClientRejectedLogin { - // Timestamp the login was rejected - google.protobuf.Timestamp timestamp = 4; -} -// Client has received an aidrop from server -// -// This message type is only initiated by server. -message AirdropReceived { - // The type of airdrop received - transaction.v2.AirdropType airdrop_type = 1; - // Exchange data relating to the amount of Kin and fiat value of the airdrop - transaction.v2.ExchangeData exchange_data = 2; - // Time the airdrop was received - google.protobuf.Timestamp timestamp = 3; -} -message Message { - // MessageId is the Id of the message. This ID is generated by the - // server, and will _always_ be set when receiving a message. - // - // Server generates the message to: - // 1. Reserve the ability for any future ID changes - // 2. Prevent clients attempting to collide message IDs. - MessageId id = 1; - // The signature sent from SendMessageRequest, which will be injected by server. - // This enables clients to ensure no MITM attacks were performed to hijack contents - // of the typed message. This is only applicable for messages not generated by server. - common.v1.Signature send_message_request_signature = 3; - // Next field number is 13 - oneof kind { - // - // Section: Cash - // - RequestToGrabBill request_to_grab_bill = 2; - // - // Section: Payment Requests - // - RequestToReceiveBill request_to_receive_bill = 5; - CodeScanned code_scanned = 6; - ClientRejectedPayment client_rejected_payment = 7; - IntentSubmitted intent_submitted = 8; - WebhookCalled webhook_called = 9; - // - // Section: Login - // - RequestToLogin request_to_login = 10; - ClientRejectedLogin client_rejected_login = 12; - // - // Section: Airdrops - // - AirdropReceived airdrop_received = 4; - } - // Reserved for deprecated LoginAttempt field - reserved 11; -} diff --git a/definitions/code/protos/src/main/proto/micropayment/v1/micro_payment_service.proto b/definitions/code/protos/src/main/proto/micropayment/v1/micro_payment_service.proto deleted file mode 100644 index fc502ae5b..000000000 --- a/definitions/code/protos/src/main/proto/micropayment/v1/micro_payment_service.proto +++ /dev/null @@ -1,107 +0,0 @@ -syntax = "proto3"; -package code.micropayment.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/micropayment/v1;micropayment"; -option java_package = "com.codeinc.gen.micropayment.v1"; -option objc_class_prefix = "APBMicroPaymentV1"; -import "common/v1/model.proto"; - -// todo: Migrate this to a generic "request" service -service MicroPayment { - // GetStatus gets basic request status - rpc GetStatus(GetStatusRequest) returns (GetStatusResponse); - // RegisterWebhook registers a webhook for a request - // - // todo: Once Kik codes can encode the entire payment request details, we can - // remove the messaging service component and have a Create RPC that - // reserves the intent ID with payment details, plus registers the webhook - // at the same time. Until that's possible, we're stuck with two RPC calls. - rpc RegisterWebhook(RegisterWebhookRequest) returns (RegisterWebhookResponse); - // Codify adds a trial micro paywall to any URL - rpc Codify(CodifyRequest) returns (CodifyResponse); - // GetPathMetadata gets codified website metadata for a given path - // - // Important Note: This RPC's current implementation is insecure and - // it's sole design is to enable PoC and trials. - rpc GetPathMetadata(GetPathMetadataRequest) returns (GetPathMetadataResponse); -} -message GetStatusRequest { - common.v1.IntentId intent_id = 1; -} -message GetStatusResponse { - // Does the payment request exist? - bool exists = 1; - // Has the user scanned the code at least once? - bool code_scanned = 2; - // Has the user sumbmitted a payment? - bool intent_submitted = 3; -} -message RegisterWebhookRequest { - common.v1.IntentId intent_id = 1; - string url = 2 ; -} -message RegisterWebhookResponse { - Result result = 1; - enum Result { - OK = 0; - // A webhook has already been registered - ALREADY_REGISTERED = 1; - // A request does not exist for the provided intent ID - REQUEST_NOT_FOUND = 2; - // A user has already submitted a payment - INTENT_EXISTS = 3; - // The webhook URL is invalid - INVALID_URL = 4; - } -} -message CodifyRequest { - // The URL to Codify - string url = 1 ; - // ISO 4217 alpha-3 currency code the payment should be made in - string currency = 2; - // The amount that should be paid in the native currency - double native_amount = 3; - // The verified owner account public key - common.v1.SolanaAccountId owner_account = 4; - // The primary account public key where payment will be sent - common.v1.SolanaAccountId primary_account = 5; -; - // The signature is of serialize(CodifyRequest) without this field set using the - // private key of the owner account. This provides an authentication mechanism - // to the RPC and can be used to validate payment details. - common.v1.Signature signature = 6; -} -message CodifyResponse { - Result result = 1; - enum Result { - OK = 0; - // The URL to Codify is invalid - INVALID_URL = 1; - // The primary account is invalid - INVALID_ACCOUNT = 2; - // The currency isn't supported for micro payments - UNSUPPORTED_CURRENCY = 3; - // The payment amount exceeds the minimum/maximum allowed amount - NATIVE_AMOUNT_EXCEEDS_LIMIT = 4; - } - // The URL to view the content with a Code micro paywall - string codified_url = 2; -} -message GetPathMetadataRequest { - string path = 1; -} -message GetPathMetadataResponse { - Result result = 1; - enum Result { - OK = 0; - NOT_FOUND = 1; - } - // The account where the payment should be sent to - common.v1.SolanaAccountId destination = 2; - - // ISO 4217 alpha-3 currency code the payment should be made in - string currency = 3; - // The amount that should be paid in the native currency - double native_amount = 4; - // The URL to redirect upon successful payment - string redirct_url = 5; -} diff --git a/definitions/code/protos/src/main/proto/phone/v1/phone_verification_service.proto b/definitions/code/protos/src/main/proto/phone/v1/phone_verification_service.proto deleted file mode 100644 index a7390c097..000000000 --- a/definitions/code/protos/src/main/proto/phone/v1/phone_verification_service.proto +++ /dev/null @@ -1,117 +0,0 @@ -syntax = "proto3"; -package code.phone.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/phone/v1;phone"; -option java_package = "com.codeinc.gen.phone.v1"; -option objc_class_prefix = "CPBPhoneV1"; -import "common/v1/model.proto"; - -service PhoneVerification { - // SendVerificationCode sends a verification code to the provided phone number - // over SMS. If an active verification is already taking place, the existing code - // will be resent. - rpc SendVerificationCode(SendVerificationCodeRequest) returns (SendVerificationCodeResponse); - // CheckVerificationCode validates a verification code. On success, a one-time use - // token to link an owner account is provided. - rpc CheckVerificationCode(CheckVerificationCodeRequest) returns (CheckVerificationCodeResponse); - // GetAssociatedPhoneNumber gets the latest verified phone number linked to an owner account. - rpc GetAssociatedPhoneNumber(GetAssociatedPhoneNumberRequest) returns (GetAssociatedPhoneNumberResponse); -} -message SendVerificationCodeRequest { - // The phone number to send a verification code over SMS to. - common.v1.PhoneNumber phone_number = 1; - // Device token for antispam measures against fake devices - common.v1.DeviceToken device_token = 2; -} -message SendVerificationCodeResponse { - Result result = 1; - enum Result { - OK = 0; - // The phone number is not invited and cannot use Code. The SMS will not - // be sent until the user is invited. This result is only valid during - // the invitation stage of the application and won't apply after general - // public release. - NOT_INVITED = 1; - // SMS is rate limited (eg. by IP, phone number, etc) and was not sent. - // These will be set generously such that real users won't actually hit - // the limits. - RATE_LIMITED = 2; - // The phone number is not real because it fails Twilio lookup. - INVALID_PHONE_NUMBER = 3; - // The phone number is valid, but it maps to an unsupported type of phone - // like a landline or eSIM. - UNSUPPORTED_PHONE_TYPE = 4; - // The country associated with the phone number is not supported (eg. it - // is on the sanctioned list). - UNSUPPORTED_COUNTRY = 5; - // The device is not supported (eg. it fails device attestation checks) - UNSUPPORTED_DEVICE = 6; - } -} -message CheckVerificationCodeRequest { - // The phone number being verified. - common.v1.PhoneNumber phone_number = 1; - // The verification code received via SMS. - VerificationCode code = 2; -} -message CheckVerificationCodeResponse { - Result result = 1; - enum Result { - OK = 0; - // The provided verification code is invalid. The user may retry - // enterring the code if this is received. When max attempts are - // received, NO_VERIFICATION will be returned. - INVALID_CODE = 1; - // There is no verification in progress for the phone number. Several - // reasons this can occur include a verification being expired or having - // reached a maximum check threshold. The client must initiate a new - // verification using SendVerificationCode. - NO_VERIFICATION = 2; - // The call is rate limited (eg. by IP, phone number, etc). The code is - // not verified. - RATE_LIMITED = 3; - } - // The token used to associate an owner account to a user using the verified - // phone number. - PhoneLinkingToken linking_token = 2; -} -message GetAssociatedPhoneNumberRequest { - // The public key of the owner account that is being queried for a linked - // phone number. - common.v1.SolanaAccountId owner_account_id = 1; - // The signature is of serialize(GetAssociatedPhoneNumberRequest) without - // this field set using the private key of owner_account_id. This provides - // an authentication mechanism to the RPC. - common.v1.Signature signature = 2; -} -message GetAssociatedPhoneNumberResponse { - Result result = 1; - enum Result { - OK = 0; - // A phone number is not associated with the provided owner account. - NOT_FOUND = 1; - // The phone number exists, but is no longer invited - NOT_INVITED = 2; - // The phone number exists, but at least one timelock account is unlocked - UNLOCKED_TIMELOCK_ACCOUNT = 3; - } - // The latest phone number associated with the owner account. - common.v1.PhoneNumber phone_number = 2; - // State that determines whether a phone number is linked to the owner - // account. A phone number is linked if we can treat it as an alias. - // This is notably different from association, which answers the question - // of whether the number was linked at any point in time. - bool is_linked = 3; -} -message VerificationCode { - // A 4-10 digit numerical code. - string value = 2 ; -} -// A one-time use token that can be provided to the Identity service to link an -// owner account to a user with the verified phone number. The client should -// treat this token as opaque. -message PhoneLinkingToken { - // The verified phone number. - common.v1.PhoneNumber phone_number = 1; - // The code that verified the phone number. - VerificationCode code = 2; -} diff --git a/definitions/code/protos/src/main/proto/push/v1/push_service.proto b/definitions/code/protos/src/main/proto/push/v1/push_service.proto deleted file mode 100644 index 9f070916e..000000000 --- a/definitions/code/protos/src/main/proto/push/v1/push_service.proto +++ /dev/null @@ -1,74 +0,0 @@ -syntax = "proto3"; -package code.push.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/push/v1;push"; -option java_package = "com.codeinc.gen.push.v1"; -option objc_class_prefix = "APBPushV1"; -import "common/v1/model.proto"; - -service Push { - // AddToken stores a push token in a data container. The call is idempotent - // and adding an existing valid token will not fail. Token types will be - // validated against the user agent and any mismatches will result in an - // INVALID_ARGUMENT status error. - // - // The token will be unlinked from any and all other accounts that it was - // previously bound to. - rpc AddToken(AddTokenRequest) returns (AddTokenResponse); - // RemoveToken removes the provided push token from the account. - // - // The provided token must be bound to the current account. - // Otherwise, the RPC will succeed with without removal. - rpc RemoveToken(RemoveTokenRequest) returns (RemoveTokenResponse); -} -enum TokenType { - UNKNOWN = 0; - // FCM registration token for an Android device - FCM_ANDROID = 1; - // FCM registration token or an iOS device - FCM_APNS = 2; -} -message AddTokenRequest { - // The public key of the owner account that signed this request message. - common.v1.SolanaAccountId owner_account_id = 1; - // The signature is of serialize(AddTokenRequest) without this field set - // using the private key of owner_account_id. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 2; - // The data container where the push token will be stored. - common.v1.DataContainerId container_id = 3; - // The push token to store - string push_token = 4 ; - // The type of push token - TokenType token_type = 5; - // The instance of the app install where the push token was generated. Ideally, - // the push token is unique to the install. - common.v1.AppInstallId app_install = 6; -} -message AddTokenResponse { - Result result = 1; - enum Result { - OK = 0; - // The push token is invalid and wasn't stored. - INVALID_PUSH_TOKEN = 1; - } -} -message RemoveTokenRequest { - // The public key of the owner account that signed this request message. - common.v1.SolanaAccountId owner_account_id = 1; - // The signature is of serialize(AddTokenRequest) without this field set - // using the private key of owner_account_id. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 2; - // The data container where the push token was stored. - common.v1.DataContainerId container_id = 3; - // The push token to remove. - string push_token = 4 ; - // The type of push token to remove. - TokenType token_type = 5; -} -message RemoveTokenResponse { - Result result = 1; - enum Result { - OK = 0; - } -} diff --git a/definitions/code/protos/src/main/proto/transaction/v2/transaction_service.proto b/definitions/code/protos/src/main/proto/transaction/v2/transaction_service.proto deleted file mode 100644 index 285a54819..000000000 --- a/definitions/code/protos/src/main/proto/transaction/v2/transaction_service.proto +++ /dev/null @@ -1,1162 +0,0 @@ -syntax = "proto3"; -package code.transaction.v2; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2;transaction"; -option java_package = "com.codeinc.gen.transaction.v2"; -option objc_class_prefix = "APBTransactionV2"; -import "common/v1/model.proto"; -import "google/protobuf/timestamp.proto"; - -service Transaction { - // SubmitIntent is the mechanism for client and server to agree upon a set of - // client actions to execute on the blockchain using the Code sequencer for - // fulfillment. - // - // Transactions are never exchanged between client and server. Instead, the - // required accounts and arguments for instructions known to each actor are - // exchanged to allow independent and local transaction construction. - // - // Client and server are expected to fully validate the intent. Proofs will - // be provided for any parameter requiring one. Signatures should only be - // generated after approval of all transactions. - // - // This RPC is not a traditional streaming endpoint. It bundles two unary calls - // to enable DB-level transaction semantics. - // - // The high-level happy path flow for the RPC is as follows: - // 1. Client initiates a stream and sends SubmitIntentRequest.SubmitActions - // 2. Server validates the intent, its actions and metadata - // 3a. If there are transactions requiring the user's signature, then server - // returns SubmitIntentResponse.ServerParameters - // 3b. Otherwise, server returns SubmitIntentResponse.Success and closes the - // stream - // 4. For each transaction requiring the user's signature, the client locally - // constructs it, performs validation and collects the signature - // 5. Client sends SubmitIntentRequest.SubmitSignatures with the signature - // list generated from 4 - // 6. Server validates all signatures are submitted and are the expected values - // using locally constructed transactions. - // 7. Server returns SubmitIntentResponse.Success and closes the stream - // In the error case: - // * Server will return SubmitIntentResponse.Error and close the stream - // * Client will close the stream - rpc SubmitIntent(stream SubmitIntentRequest) returns (stream SubmitIntentResponse); - // GetIntentMetadata gets basic metadata on an intent. It can also be used - // to fetch the status of submitted intents. Metadata exists only for intents - // that have been successfully submitted. - rpc GetIntentMetadata(GetIntentMetadataRequest) returns (GetIntentMetadataResponse); - // GetPrivacyUpgradeStatus gets the status of a private transaction and the - // ability to upgrade it to permanent privacy. - rpc GetPrivacyUpgradeStatus(GetPrivacyUpgradeStatusRequest) returns (GetPrivacyUpgradeStatusResponse); - // GetPrioritizedIntentsForPrivacyUpgrade allows clients to get private - // intent actions that can be upgraded in a secure and verifiable manner. - rpc GetPrioritizedIntentsForPrivacyUpgrade(GetPrioritizedIntentsForPrivacyUpgradeRequest) returns (GetPrioritizedIntentsForPrivacyUpgradeResponse); - // GetLimits gets limits for money moving intents for an owner account in an - // identity-aware manner - rpc GetLimits(GetLimitsRequest) returns (GetLimitsResponse); - // GetPaymentHistory gets an owner account's payment history inferred from intents - // - // Deprecated: Payment history has migrated to chats - rpc GetPaymentHistory(GetPaymentHistoryRequest) returns (GetPaymentHistoryResponse); - // CanWithdrawToAccount provides hints to clients for submitting withdraw intents. - // The RPC indicates if a withdrawal is possible, and how it should be performed. - rpc CanWithdrawToAccount(CanWithdrawToAccountRequest) returns (CanWithdrawToAccountResponse); - // Airdrop airdrops Kin to the requesting account - rpc Airdrop(AirdropRequest) returns (AirdropResponse); - // Swap performs an on-chain swap. The high-level flow mirrors SubmitIntent - // closely. However, due to the time-sensitive nature and unreliability of - // swaps, they do not fit within the broader intent system. This results in - // a few key differences: - // * Transactions are submitted on a best-effort basis outside of the Code - // Sequencer within the RPC handler - // * Balance changes are applied after the transaction has finalized - // * Transactions use recent blockhashes over a nonce - // - // The transaction will have the following instruction format: - // 1. ComputeBudget::SetComputeUnitLimit - // 2. ComputeBudget::SetComputeUnitPrice - // 3. SwapValidator::PreSwap - // 4. Dynamic swap instruction - // 5. SwapValidator::PostSwap - // - // Note: Currently limited to swapping USDC to Kin. - // Note: Kin is deposited into the primary account. - rpc Swap(stream SwapRequest) returns (stream SwapResponse); - // DeclareFiatOnrampPurchaseAttempt is called whenever a user attempts to use a fiat - // onramp to purchase crypto for use in Code. - rpc DeclareFiatOnrampPurchaseAttempt(DeclareFiatOnrampPurchaseAttemptRequest) returns (DeclareFiatOnrampPurchaseAttemptResponse); -} -// -// Request and Response Definitions -// -message SubmitIntentRequest { - oneof request { - SubmitActions submit_actions = 1; - SubmitSignatures submit_signatures = 2; - } - message SubmitActions { - // The globally unique client generated intent ID. Use the original intent - // ID when operating on actions that mutate the intent. - common.v1.IntentId id = 1; - // The verified owner account public key - common.v1.SolanaAccountId owner = 2; - // Additional metadata that describes the high-level intention - Metadata metadata = 3; - // The set of all ordered actions required to fulfill the intent - repeated Action actions = 4 ; - // The signature is of serialize(SubmitActions) without this field set using the - // private key of the owner account. This provides an authentication mechanism - // to the RPC. - common.v1.Signature signature = 5; - // Device token for antispam measures against fake devices - common.v1.DeviceToken device_token = 6; - } - message SubmitSignatures { - // The set of all signatures for each transaction requiring signature from the - // authority accounts. - repeated common.v1.Signature signatures = 1 ; - } -} -message SubmitIntentResponse { - oneof response { - ServerParameters server_parameters = 1; - Success success = 2; - Error error = 3; - } - message ServerParameters { - // The set of all server paremeters required to fill missing transaction - // details. Server guarantees to provide a message for each client action - // in an order consistent with the received action list. - repeated ServerParameter server_parameters = 1 ; - } - message Success { - Code code = 1; - enum Code { - // The intent was successfully created and is now scheduled. - OK = 0; - } - // todo: Revisit if we need side-effects. Clients are effecitively doing - // local simulation now with the privacy solution. - } - message Error { - Code code = 1; - enum Code { - // Denied by a guard (spam, money laundering, etc) - DENIED = 0; - // The intent is invalid. - INVALID_INTENT = 1; - // There is an issue with provided signatures. - SIGNATURE_ERROR = 2; - // Server detected client has stale state. - STALE_STATE = 3; - } - repeated ErrorDetails error_details = 2; - } -} -message GetIntentMetadataRequest { - // The intent ID to query - common.v1.IntentId intent_id = 1; - // The verified owner account public key when not signing with the rendezvous - // key. Only owner accounts involved in the intent can access the metadata. - common.v1.SolanaAccountId owner = 2; - // The signature is of serialize(GetIntentStatusRequest) without this field set - // using the private key of the rendezvous or owner account. This provides an - // authentication mechanism to the RPC. - common.v1.Signature signature = 3; -} -message GetIntentMetadataResponse { - Result result = 1; - enum Result { - OK = 0; - NOT_FOUND = 1; - } - Metadata metadata = 2; -} -message GetPrivacyUpgradeStatusRequest { - // The intent ID - common.v1.IntentId intent_id = 1; - // The action ID for private transaction - uint32 action_id = 2; -} -message GetPrivacyUpgradeStatusResponse { - Result result = 1; - enum Result { - OK = 0; - // The provided intent ID doesn't exist - INTENT_NOT_FOUND = 1; - // The provided action ID doesn't exist - ACTION_NOT_FOUND = 2; - // The provided action doesn't map to a private transaction - INVALID_ACTION = 3; - } - Status status = 2; - enum Status { - UNKNOWN = 0; - // The transaction for the temporary private transaction was submitted and - // finalized. The opportunity to upgrade was missed. - TEMPORARY_TRANSACTION_FINALIZED = 1; - // The next block of transactions hasn't been created. Wait and try again - // later. - WAITING_FOR_NEXT_BLOCK = 2; - // The transaction can be upgraded to permanent privacy - READY_FOR_UPGRADE = 3; - // The transaction has already been upgraded - ALREADY_UPGRADED = 4; - } -} -message GetPrioritizedIntentsForPrivacyUpgradeRequest { - // The owner account to query against for upgradeable intents. - common.v1.SolanaAccountId owner = 1; - // The maximum number of intents to return in the response. Default is 10. - uint32 limit = 2; - // The signature is of serialize(GetPrioritizedIntentsForPrivacyUpgradeRequest) - // without this field set using the private key of the owner account. This - // provides an authentication mechanism to the RPC. - common.v1.Signature signature = 3; -} -message GetPrioritizedIntentsForPrivacyUpgradeResponse { - Result result = 1; - enum Result { - OK = 0; - NOT_FOUND = 1; - } - // Ordered from highest to lowest priority - repeated UpgradeableIntent items = 2 ; -} -message GetLimitsRequest { - // The owner account whose limits will be calculated. Any other owner accounts - // linked with the same identity of the owner will also be applied. - common.v1.SolanaAccountId owner = 1; - // The signature is of serialize(GetLimitsRequest) without this field set - // using the private key of the owner account. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 2; - // All transactions starting at this time will be incorporated into the consumed - // limit calculation. Clients should set this to the start of the current day in - // the client's current time zone (because server has no knowledge of this atm). - google.protobuf.Timestamp consumed_since = 3; -} -message GetLimitsResponse { - Result result = 1; - enum Result { - OK = 0; - } - // Send limits keyed by currency - map send_limits_by_currency = 2; - // Deposit limits - DepositLimit deposit_limit = 3; - // Micro payment limits keyed by currency - map micro_payment_limits_by_currency = 4; - // Buy module limits keyed by currency - map buy_module_limits_by_currency = 5; -} -message GetPaymentHistoryRequest { - // The owner account to get payment history for - common.v1.SolanaAccountId owner = 1; - // An optional history cursor indicating where in the history to resume from. - Cursor cursor = 2; - // The number of results to return per request. Default is 100. - uint32 page_size = 3; - // The order in which to return history items from the cursor. - Direction direction = 4; - enum Direction { - // ASC direction returns all history items in ascending order. - ASC = 0; - // DESC direction returns all history items in descending order. - DESC = 1; - } - // The signature is of serialize(GetPaymentHistoryRequest) without this field set - // using the private key of the owner account. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 5; -} -message GetPaymentHistoryResponse { - Result result = 1; - enum Result { - OK = 0; - NOT_FOUND = 1; - } - repeated PaymentHistoryItem items = 2 ; -} -message CanWithdrawToAccountRequest { - common.v1.SolanaAccountId account = 1; -} -message CanWithdrawToAccountResponse { - // Metadata so the client knows how to withdraw to the account. Server cannot - // provide precalculated addresses in this response to maintain non-custodial - // status. - AccountType account_type = 2; - enum AccountType { - Unknown = 0; // Server cannot determine - TokenAccount = 1; // Client uses the address as is in SubmitIntent - OwnerAccount = 2; // Client locally derives the ATA to use in SubmitIntent - } - // Server-controlled flag to indicate if the account can be withdrawn to. - // There are several reasons server may deny it, including: - // - Wrong type of Code account - // - Not wanting to subsidize the creation of an ATA - // - Unsupported external account type (eg. token account but of the wrong mint) - // This is guaranteed to be false when account_type = Unknown. - bool is_valid_payment_destination = 1; - // Token account requires initialization before the withdrawal can occur. - // Server has chosen not to subsidize the fees. The response is guaranteed - // to have set is_valid_payment_destination = false in this case. - bool requires_initialization = 3; -} -message AirdropRequest { - // The type of airdrop to claim - AirdropType airdrop_type = 1 ; - // The owner account to airdrop Kin to - common.v1.SolanaAccountId owner = 2; - // The signature is of serialize(AirdropRequest) without this field set - // using the private key of the owner account. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 3; -} -message AirdropResponse { - Result result = 1; - enum Result { - OK = 0; - // Airdrops are unavailable - UNAVAILABLE = 1; - // The airdrop has already been claimed by the owner - ALREADY_CLAIMED = 2; - } - // Exchange data for the amount of Kin airdropped when successful - ExchangeData exchange_data = 2; -} -message SwapRequest { - oneof request { - Initiate initiate = 1; - SubmitSignature submit_signature = 2; - } - message Initiate { - // The verified owner account public key - common.v1.SolanaAccountId owner = 1; - // The user authority account that will sign to authorize the swap. Ideally, - // this is an account derived off the owner account that is solely responsible - // for swapping. - common.v1.SolanaAccountId swap_authority = 2; - // Maximum amount to swap from the source mint, in quarks. If value is set to zero, - // the entire amount will be swapped. - uint64 limit = 3; - // Whether the client wants the RPC to wait for blockchain status. If false, - // then the RPC will return Success when the swap is submitted to the blockchain. - // Otherwise, the RPC will observe and report back the status of the transaction. - bool wait_for_blockchain_status = 4; - // The signature is of serialize(Initiate) without this field set using the - // private key of the owner account. This provides an authentication mechanism - // to the RPC. - common.v1.Signature signature = 5; - } - message SubmitSignature { - // The signature for the locally constructed swap transaction - common.v1.Signature signature = 1; - } -} -message SwapResponse { - oneof response { - ServerParameters server_parameters = 1; - Success success = 2; - Error error = 3; - } - message ServerParameters { - // Subisdizer account that will be paying for the swap - common.v1.SolanaAccountId payer = 1; - // Recent blockhash - common.v1.Blockhash recent_blockhash = 2; - // Compute unit limit provided to the ComputeBudget::SetComputeUnitLimit - // instruction. If the value is 0, then the instruction can be omitted. - uint32 compute_unit_limit = 3; - // Compute unit price provided in the ComputeBudget::SetComputeUnitPrice - // instruction. If the value is 0, then the instruction can be omitted. - uint64 compute_unit_price = 4; - // On-chain program that will be performing the swap - common.v1.SolanaAccountId swap_program = 5; - // Accounts provided to the swap instruction - repeated common.v1.InstructionAccount swap_ixn_accounts = 6 ; - // Instruction data for the swap instruction - bytes swap_ixn_data = 7 ; - // Maximum quarks that will be sent out of the source account after - // executing the swap. If not, the validation instruction will cause - // the transaction to fail. - uint64 max_to_send = 8; - // Minimum quarks that will be received into the destination account - // after executing the swap. If not, the validation instruction will - // cause the transaction to fail. - uint64 min_to_receive = 9; - // Nonce to use in swap validator state account PDA - common.v1.SolanaAccountId nonce = 10; - } - message Success { - Code code = 1; - enum Code { - // The swap was submitted to the blockchain. - SWAP_SUBMITTED = 0; - // The swap was finalized on the blockchain. - SWAP_FINALIZED = 1; - } - } - message Error { - Code code = 1; - enum Code { - // Denied by a guard (spam, money laundering, etc) - DENIED = 0; - // There is an issue with the provided signature. - SIGNATURE_ERROR = 2; - // The swap failed server-side validation - INVALID_SWAP = 3; - // The submitted swap transaction failed. Attempt the swap again. - SWAP_FAILED = 4; - } - repeated ErrorDetails error_details = 2; - } -} -message DeclareFiatOnrampPurchaseAttemptRequest { - // The owner account invoking the buy module - common.v1.SolanaAccountId owner = 1; - // The amount being purchased - ExchangeDataWithoutRate purchase_amount = 2; - // A nonce value unique to the purchase. If it's included in a memo for the - // transaction for the deposit to the owner, then purchase_amount will be used - // for display values. Otherwise, the amount will be inferred from the transaction. - common.v1.UUID nonce = 3; - // The signature is of serialize(DeclareFiatOnrampPurchaseAttemptRequest) without - // this field set using the private key of the owner account. This provides an - // authentication mechanism to the RPC. - common.v1.Signature signature = 4; -} -message DeclareFiatOnrampPurchaseAttemptResponse { - Result result = 1; - enum Result { - OK = 0; - // The owner account is not valid (ie. it isn't a Code account) - INVALID_OWNER = 1; - // The currency isn't supported - UNSUPPORTED_CURRENCY = 2; - // The amount specified exceeds limits - AMOUNT_EXCEEDS_MAXIMUM = 3; - } -} -// -// Metadata definitions -// -// Metadata describes the high-level details of an intent -message Metadata { - oneof type { - OpenAccountsMetadata open_accounts = 1; - SendPrivatePaymentMetadata send_private_payment = 2; - ReceivePaymentsPrivatelyMetadata receive_payments_privately = 3; - UpgradePrivacyMetadata upgrade_privacy = 4; - MigrateToPrivacy2022Metadata migrate_to_privacy_2022 = 5; - SendPublicPaymentMetadata send_public_payment = 6; - ReceivePaymentsPubliclyMetadata receive_payments_publicly = 7; - EstablishRelationshipMetadata establish_relationship = 8; - } -} -// Open a set of accounts. Currently, clients should only use this for new users -// to open all required accounts up front (buckets, incoming, and outgoing). -// -// Action Spec: -// -// actions = [OpenAccountAction(PRIMARY)] -// for account in [TEMPORARY_INCOMING, TEMPORARY_OUTGOING, BUCKET_1_KIN, ... , BUCKET_1_000_000_KIN] -// actions.push_back(OpenAccountAction(account)) -// actions.push_back(CloseDormantAccount(account)) -message OpenAccountsMetadata { - // Nothing is currently required -} -// Sends a payment to a destination account with initial temporary privacy. Clients -// should also reorganize their bucket accounts and rotate their temporary outgoing -// account. -// -// Action Spec (In Person Cash Payment or Withdrawal or Tip): -// -// actions = [ -// // Section 1: Transfer ExchangeData.Quarks from BUCKET_X_KIN accounts to TEMPORARY_OUTGOING account with reogranizations -// -// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize), -// TemporaryPrivacyTransferAction(BUCKET_X_KIN, TEMPORARY_OUTGOING[index], multiple * bucketSize), -// ..., -// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize), -// TemporaryPrivacyTransferAction(BUCKET_X_KIN, TEMPORARY_OUTGOING[index], multiple * bucketSize), -// -// // Section 2: Rotate TEMPORARY_OUTGOING account -// -// // Below must appear last in this exact order -// NoPrivacyWithdrawAction(TEMPORARY_OUTGOING[index], destination, ExchangeData.Quarks), -// OpenAccountAction(TEMPORARY_OUTGOING[index + 1]), -// CloseDormantAccount(TEMPORARY_OUTGOING[index + 1]), -// ] -// -// Action Spec (Remote Send): -// -// actions = [ -// // Section 1: Open REMOTE_SEND_GIFT_CARD account -// -// OpenAccountAction(REMOTE_SEND_GIFT_CARD), -// -// // Section 2: Transfer ExchangeData.Quarks from BUCKET_X_KIN accounts to TEMPORARY_OUTGOING account with reogranizations -// -// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize), -// TemporaryPrivacyTransferAction(BUCKET_X_KIN, TEMPORARY_OUTGOING[index], multiple * bucketSize), -// ..., -// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize), -// TemporaryPrivacyTransferAction(BUCKET_X_KIN, TEMPORARY_OUTGOING[index], multiple * bucketSize), -// -// // Section 3: Rotate TEMPORARY_OUTGOING account -// -// // Below must appear last in this exact order -// NoPrivacyWithdrawAction(TEMPORARY_OUTGOING[index], REMOTE_SEND_GIFT_CARD, ExchangeData.Quarks), -// OpenAccountAction(TEMPORARY_OUTGOING[index + 1]), -// CloseDormantAccount(TEMPORARY_OUTGOING[index + 1]), -// -// // Section 4: Close REMOTE_SEND_GIFT_CARD if not redeemed after period of time -// -// CloseDormantAccount(REMOTE_SEND_GIFT_CARD), -// -// Action Spec (Micro Payment): -// -// actions = [ -// // Section 1: Transfer ExchangeData.Quarks from BUCKET_X_KIN accounts to TEMPORARY_OUTGOING account with reogranizations -// -// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize), -// TemporaryPrivacyTransferAction(BUCKET_X_KIN, TEMPORARY_OUTGOING[index], multiple * bucketSize), -// ..., -// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize), -// TemporaryPrivacyTransferAction(BUCKET_X_KIN, TEMPORARY_OUTGOING[index], multiple * bucketSize), -// -// // Section 2: Fee payments -// -// // Hard-coded Code $0.01 USD fee to a dynamic fee account -// FeePayment(TEMPORARY_OUTGOING[index], codeFeeAccount, $0.01 USD of Kin), -// -// // Additional fees, exactly as specified in the original payment request -// FeePayment(TEMPORARY_OUTGOING[index], additionalFeeAccount0, additionalFeeQuarks0), -// ... -// FeePayment(TEMPORARY_OUTGOING[index], additionalFeeAccountN, additionalFeeQuarksN), -// -// // Section 3: Rotate TEMPORARY_OUTGOING account -// -// // Below must appear last in this exact order -// NoPrivacyWithdrawAction(TEMPORARY_OUTGOING[index], destination, ExchangeData.Quarks - $0.01 USD of Kin - additionalFeeQuarks0 - ... - additionalFeeQuarksN), -// OpenAccountAction(TEMPORARY_OUTGOING[index + 1]), -// CloseDormantAccount(TEMPORARY_OUTGOING[index + 1]), -// ] -message SendPrivatePaymentMetadata { - // The destination token account to send funds to - common.v1.SolanaAccountId destination = 1; - // The exchange data of total funds being sent to the destination - ExchangeData exchange_data = 2; - // Is the payment a withdrawal? For destinations that are not Code temporary - // accounts, this must be set to true. - bool is_withdrawal = 3; - // Is the payment for a remote send? - bool is_remote_send = 4; - // Is the payment for a tip? - bool is_tip = 5; - // If is_tip is true, the user being tipped - TippedUser tipped_user = 6; - // Is the payment for a chat? - bool is_chat = 7; - // If is_chat is true, the chat being paid for. - common.v1.ChatId chat_id = 8; -} -// Send a payment to a destination account publicly. -// -// Action Spec: -// -// source = PRIMARY or RELATIONSHIP -// actions = [NoPrivacyTransferAction(source, destination, ExchangeData.Quarks)] -message SendPublicPaymentMetadata { - // The primary or relatinship account where funds will be sent from. The primary - // account is assumed if this field is not set for backwards compatibility with - // old clients. - common.v1.SolanaAccountId source = 4; - // The destination token account to send funds to. This cannot be a Code - // temporary account. - common.v1.SolanaAccountId destination = 1; - // The exchange data of total funds being sent to the destination - ExchangeData exchange_data = 2; - // Is the payment a withdrawal? Currently, this is always true. - bool is_withdrawal = 3; -} -// Receive funds into an organizer with initial temporary privacy. Clients should -// also reorganize their bucket accounts and rotate their temporary incoming account -// as applicable. Only accounts owned and derived by a user's 12 words should operate -// as a source in this intent type to guarantee privacy upgradeability. -// -// Action Spec (Payment): -// -// actions = [ -// // Section 1: Transfer Quarks from TEMPORARY_INCOMING account to BUCKET_X_KIN accounts with reorganizations -// -// TemporaryPrivacyTransferAction(TEMPORARY_INCOMING[index], BUCKET_X_KIN, multiple * bucketSize), -// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize), -// ..., -// TemporaryPrivacyTransferAction(TEMPORARY_INCOMING[index], BUCKET_X_KIN, multiple * bucketSize), -// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize), -// -// // Section 2: Rotate TEMPORARY_INCOMING account -// -// // Below must appear last in this exact order -// CloseEmptyAccountAction(TEMPORARY_INCOMING[index]), -// OpenAccountAction(TEMPORARY_INCOMING[index + 1]) -// CloseDormantAccount(TEMPORARY_INCOMING[index + 1]), -// ] -// -// Action Spec (Deposit): -// -// source = PRIMARY or RELATIONSHIP -// actions = [ -// TemporaryPrivacyTransferAction(source, BUCKET_X_KIN, multiple * bucketSize), -// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize), -// ..., -// TemporaryPrivacyTransferAction(source, BUCKET_X_KIN, multiple * bucketSize), -// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize), -// ] -message ReceivePaymentsPrivatelyMetadata { - // The temporary incoming, primary or relationship account to receive funds from - common.v1.SolanaAccountId source = 1; - // The exact amount of Kin in quarks being received - uint64 quarks = 2; - // Is the receipt of funds from a deposit? If true, the source account must - // be a primary or relationship account. Otherwise, it must be from a temporary - // incoming account. - bool is_deposit = 3; -} -// Receive funds into a user-owned account publicly. All use cases of this intent -// close the account, so all funds must be moved. Use this intent to receive payments -// from an account not owned by a user's 12 words into a temporary incoming account, -// which will guarantee privacy upgradeability. -// -// Action Spec (Remote Send): -// -// actions = [NoPrivacyWithdrawAction(REMOTE_SEND_GIFT_CARD, TEMPORARY_INCOMING[latest_index], quarks)] -message ReceivePaymentsPubliclyMetadata { - // The remote send gift card to receive funds from - common.v1.SolanaAccountId source = 1; - // The exact amount of Kin in quarks being received - uint64 quarks = 2; - // Is the receipt of funds from a remote send gift card? Currently, this is - // the only use case for this intent and validation enforces the flag to true. - bool is_remote_send = 3; - // If is_remote_send is true, is the gift card being voided? The user owner - // account's 12 words that issued the gift card may only set this flag to true. - // Functionally, this doesn't affect the intent, but rather if we decide to show - // it in a user-friendly payment history. - bool is_issuer_voiding_gift_card = 4; - // If is_remote_send is true, the original exchange data that was provided as - // part of creating the gift card account. This is purely a server-provided value. - // SubmitIntent will disallow this being set. - ExchangeData exchange_data = 5; -} -// Upgrade existing private transactions from temporary to permanent privacy. -message UpgradePrivacyMetadata { - // Nothing is currently required -} -// Migrates existing users prior to the privacy implementation by: -// 1. If there are funds in the LEGACY_PRIMARY_2022 account, then move them -// to the new PRIMARY account, so the client can later simulate a deposit -// by submitting a *separate* ReceivePaymentsPrivately intent. -// 2. Close the LEGACY_PRIMARY_2022 account. -// -// Prereqs: -// - OpenAccounts intent has been submitted -// -// Action spec: -// -// if balance == 0 { -// actions = [CloseEmptyAccountAction(LEGACY_PRIMARY_2022)] -// } else { -// actions = [NoPrivacyWitdraw(LEGACY_PRIMARY_2022, PRIMARY, Quarks)] -// } -message MigrateToPrivacy2022Metadata { - // The exact amount of Kin in quarks being migrated. Set this to zero if - // the account is empty. - uint64 quarks = 1; -} -// Establishes a long-lived private relationship between a user and another -// entity. -// -// Prereqs: -// - OpenAccounts intent has been submitted -// -// Action spec: -// -// actions = [OpenAccountAction(RELATIONSHIP)] -message EstablishRelationshipMetadata { - common.v1.Relationship relationship = 1; -} -// -// Action Definitions -// -// Action is a well-defined, ordered and small set of transactions for a unit of work -// that the client wants to perform on the blockchain. Clients provide parameters known -// to them in the action. -message Action { - // The ID of this action, which is unique within an intent. It must match - // the index of the action's location in the SubmitAction's actions field. - uint32 id = 1; - // The type of action to perform. - oneof type { - OpenAccountAction open_account = 2; - CloseEmptyAccountAction close_empty_account = 3; - CloseDormantAccountAction close_dormant_account = 4; - NoPrivacyTransferAction no_privacy_transfer = 5; - NoPrivacyWithdrawAction no_privacy_withdraw = 6; - TemporaryPrivacyTransferAction temporary_privacy_transfer = 7; - TemporaryPrivacyExchangeAction temporary_privacy_exchange = 8; - PermanentPrivacyUpgradeAction permanent_privacy_upgrade = 9; - FeePaymentAction fee_payment = 10; - } -} -// Transaction 1 -// Instructions: -// 1. system::AdvanceNonce -// 2. timelock::Initialize -// Client Signature Required: No -// -// All OpenAccountActions for non-primary accounts must be followed with an associated -// CloseDormantAccountAction to enable server to perform cleanup. -message OpenAccountAction { - // The type of account, which will dictate its intended use - common.v1.AccountType account_type = 1; - // The owner of the account. For accounts liked to a user's 12 words, this is - // the verified parent owner account public key. All other account types should - // set this to the authority value. - common.v1.SolanaAccountId owner = 2; - // The index used to for accounts that are derived from owner - uint64 index = 3; - // The public key of the private key that has authority over the opened token account - common.v1.SolanaAccountId authority = 4; - // The token account being opened - common.v1.SolanaAccountId token = 5; - // The signature is of serialize(OpenAccountAction) without this field set - // using the private key of the authority account. This provides a proof - // of authorization to link authority to owner. - common.v1.Signature authority_signature = 6; -} -// Transaction 1 -// Instructions: -// 1. system::AdvanceNonce -// 2. timelock::BurnDustWithAuthority (max 1 Kin) -// 3. timelock::CloseAccounts -// Client Signature Required: Yes -message CloseEmptyAccountAction { - // The type of account being closed - common.v1.AccountType account_type = 1; - // The public key of the private key that has authority over the token account - // that should be closed - common.v1.SolanaAccountId authority = 2; - // The token account being closed - common.v1.SolanaAccountId token = 3; -} -// Transaction 1 -// Instructions: -// 1. system::AdvanceNonce -// 2. memo::Memo -// 3. timelock::RevokeLockWithAuthority -// 4. timelock::DeactivateLock -// 5. timelock::Withdraw (token -> primary) -// 6. timelock::CloseAccounts -// Client Signature Required: Yes -message CloseDormantAccountAction { - // The type of account being closed - common.v1.AccountType account_type = 1; - // The public key of the private key that has authority over the token account - // that should be closed - common.v1.SolanaAccountId authority = 2; - // The token account being closed - common.v1.SolanaAccountId token = 3; - // The destination where funds are withdrawn to - common.v1.SolanaAccountId destination = 4 ; -} -// Transaction 1 -// Instructions: -// 1. system::AdvanceNonce -// 2. memo::Memo -// 3. timelock::TransferWithAuthority (source -> destination) -// Client Signature Required: Yes -message NoPrivacyTransferAction { - // The public key of the private key that has authority over source - common.v1.SolanaAccountId authority = 1; - // The source account where funds are transferred from - common.v1.SolanaAccountId source = 2; - // The destination account where funds are transferred to - common.v1.SolanaAccountId destination = 3; - // The Kin quark amount to transfer - uint64 amount = 4; -} -// Transaction 1 -// Instructions: -// 1. system::AdvanceNonce -// 2. memo::Memo -// 3. timelock::RevokeLockWithAuthority -// 4. timelock::DeactivateLock -// 5. timelock::Withdraw (source -> destination) -// 6. timelock::CloseAccounts -// Client Signature Required: Yes -message NoPrivacyWithdrawAction { - // The public key of the private key that has authority over source - common.v1.SolanaAccountId authority = 1; - // The source account where funds are transferred from - common.v1.SolanaAccountId source = 2; - // The destination account where funds are transferred to - common.v1.SolanaAccountId destination = 3; - // The intended Kin quark amount to withdraw - uint64 amount = 4; - // Whether the account is closed afterwards. This is always true, since there - // are no current se cases to leave it open. - bool should_close = 5; -} -// Transaction 1 -// Instructions: -// 1. system::AdvanceNonce -// 2. memo::Memo -// 3. splitter::TransferWithCommitment (treasury -> destination) -// Client Signature Required: No -// -// Transaction 2 -// Instructions: -// 1. system::AdvanceNonce -// 2. memo::Memo -// 3. timelock::TransferWithAuthority (source -> commitment) -// Client Signature Required: Yes -message TemporaryPrivacyTransferAction { - // The public key of the private key that has authority over source - common.v1.SolanaAccountId authority = 1; - // The source account where funds are transferred from - common.v1.SolanaAccountId source = 2; - // The destination account where funds are transferred to - common.v1.SolanaAccountId destination = 3; - // The Kin quark amount to transfer - uint64 amount = 4; -} -// Transaction 1 -// Instructions: -// 1. system::AdvanceNonce -// 2. memo::Memo -// 3. splitter::TransferWithCommitment (treasury -> destination) -// Client Signature Required: No -// -// Transaction 2 -// Instructions: -// 1. system::AdvanceNonce -// 2. memo::Memo -// 3. timelock::TransferWithAuthority (source -> commitment) -// Client Signature Required: Yes -message TemporaryPrivacyExchangeAction { - // The public key of the private key that has authority over source - common.v1.SolanaAccountId authority = 1; - // The source account where funds are exchanged from - common.v1.SolanaAccountId source = 2; - // The destination account where funds are exchanged to - common.v1.SolanaAccountId destination = 3; - // The Kin quark amount to exchange - uint64 amount = 4; -} -// Transaction 1 -// Instructions: -// 1. system::AdvanceNonce -// 2. memo::Memo -// 3. timelock::TransferWithAuthority (source -> different commitment) -// Client Signature Required: Yes -message PermanentPrivacyUpgradeAction { - // The action ID of the temporary private transfer or exchange to upgrade - uint32 action_id = 1; -} -// Transaction 1 -// Instructions: -// 1. system::AdvanceNonce -// 2. memo::Memo -// 3. timelock::TransferWithAuthority (source -> fee account) -// Client Signature Required: Yes -// -// Note: This is exactly a NoPrivacyTransferAction, but with specialized metadata -// for fees. -message FeePaymentAction { - // The type of fee being operated on - FeeType type = 4; - enum FeeType { - CODE = 0; // Hardcoded $0.01 USD fee to a dynamic fee account specified by server - THIRD_PARTY = 1; // Third party fee specified at time of payment request - } - // The public key of the private key that has authority over source - common.v1.SolanaAccountId authority = 1; - // The source account where funds are transferred from - common.v1.SolanaAccountId source = 2; - // The Kin quark amount to transfer - uint64 amount = 3; - // The destination where the fee payment is being made for fees outside of - // Code. - common.v1.SolanaAccountId destination = 5; -} -// -// Server Parameter Definitions -// -// ServerParameter are a set of parameters known and returned by server that -// enables clients to complete transaction construction. Any necessary proofs, -// which are required to be locally verifiable, are also provided to ensure -// safe use in the event of a malicious server. -message ServerParameter { - // The action the server parameters belong to - uint32 action_id = 1; - // The set of nonces used for the action. Server will only provide values - // for transactions requiring client signatures. - repeated NoncedTransactionMetadata nonces = 2 ; - // The type of server parameter which maps to the type of action requested - oneof type { - OpenAccountServerParameter open_account = 3; - CloseEmptyAccountServerParameter close_empty_account = 4; - CloseDormantAccountServerParameter close_dormant_account = 5; - NoPrivacyTransferServerParameter no_privacy_transfer = 6; - NoPrivacyWithdrawServerParameter no_privacy_withdraw = 7; - TemporaryPrivacyTransferServerParameter temporary_privacy_transfer = 8; - TemporaryPrivacyExchangeServerParameter temporary_privacy_exchange = 9; - PermanentPrivacyUpgradeServerParameter permanent_privacy_upgrade = 10; - FeePaymentServerParameter fee_payment = 11; - } -} -message NoncedTransactionMetadata { - // The nonce account to use in the system::AdvanceNonce instruction - common.v1.SolanaAccountId nonce = 1; - // The blockhash to set in the transaction - common.v1.Blockhash blockhash = 2; -} -message OpenAccountServerParameter { - // There are no transactions requiring client signatures -} -message CloseEmptyAccountServerParameter { - // There are no action-specific server parameters -} -message CloseDormantAccountServerParameter { - // There are no action-specific server parameters -} -message NoPrivacyTransferServerParameter { - // There are no action-specific server parameters -} -message NoPrivacyWithdrawServerParameter { - // There are no action-specific server parameters -} -message TemporaryPrivacyTransferServerParameter { - // The treasury that will be used to split payments and provide a level of privacy - common.v1.SolanaAccountId treasury = 1; - // A recent root server observed from the treasury - common.v1.Hash recent_root = 2; -} -message TemporaryPrivacyExchangeServerParameter { - // The treasury that will be used to split payments and provide a level of privacy - common.v1.SolanaAccountId treasury = 1; - // A recent root server observed from the treasury - common.v1.Hash recent_root = 2; -} -message PermanentPrivacyUpgradeServerParameter { - // The new commitment that is being paid - common.v1.SolanaAccountId new_commitment = 1; - // The new commitment account's transcript. This is purely needed by client - // to validate merkle_root with commitment PDA logic. - common.v1.Hash new_commitment_transcript = 2; - // The new commitment account's destination. This is purely needed by client - // to validate merkle_root with commitment PDA logic. - common.v1.SolanaAccountId new_commitment_destination = 3; - // The new commitment account's payment amount. This is purely needed by client - // to validate merkle_root with commitment PDA logic. - uint64 new_commitment_amount = 4; - // The merkle root, which was the recent root used in the new commitment account - common.v1.Hash merkle_root = 5; - // The merkle proof that validates the original commitment occurred prior to - // the new commitment server is asking client to pay - repeated common.v1.Hash merkle_proof = 6 ; -} -message FeePaymentServerParameter { - // The destination account where Code fee payments should be sent. This will - // only be set when the corresponding FeePaymentAction Type is CODE. - common.v1.SolanaAccountId code_destination = 1; -} -// -// Structured Error Definitions -// -message ErrorDetails { - oneof type { - ReasonStringErrorDetails reason_string = 1; - InvalidSignatureErrorDetails invalid_signature = 2; - DeniedErrorDetails denied = 3; - } -} -message ReasonStringErrorDetails { - // Human readable string indicating the failure. - string reason = 1 ; -} -message InvalidSignatureErrorDetails { - // The action whose signature mismatched - uint32 action_id = 1; - // The transaction the server expected to have signed. - common.v1.Transaction expected_transaction = 2; - // The signature that was provided by the client. - common.v1.Signature provided_signature = 3; -} -message DeniedErrorDetails { - Code code = 1; - enum Code { - // Reason code not yet defined - UNSPECIFIED = 0; - // Phone number has exceeded its free account allocation - TOO_MANY_FREE_ACCOUNTS_FOR_PHONE_NUMBER = 1; - // Device has exceeded its free account allocation - TOO_MANY_FREE_ACCOUNTS_FOR_DEVICE = 2; - // The country associated with the phone number with the account is not - // supported (eg. it is on the sanctioned list). - UNSUPPORTED_COUNTRY = 3; - // The device is not supported (eg. it fails device attestation checks) - UNSUPPORTED_DEVICE = 4; - } - // Human readable string indicating the failure. - string reason = 2 ; -} -// -// Other Model Definitions -// -// UpgradeableIntent is an intent whose actions can be upgraded. -message UpgradeableIntent { - // The intent ID - common.v1.IntentId id = 1; - // The set of private actions that can be upgraded - repeated UpgradeablePrivateAction actions = 2 ; - message UpgradeablePrivateAction { - // The transaction blob that was signed by the client. Clients *MUST* use - // the source and destination account in the timelock::TransferWithAuthority - // instruction to validate all fields provided by server by locally computing - // the expected addresses. - common.v1.Transaction transaction_blob = 1; - // The client's signature for the transaction. Clients MUST use this to - // locally validate the transaction blob provided by server. - common.v1.Signature client_signature = 2; - // The action ID of this transaction - uint32 action_id = 3; - // The source account's type, which hints how to efficiently derive source - common.v1.AccountType source_account_type = 4; -; - // The source account's derivation index, which hints how to efficiently derive source - uint64 source_derivation_index = 5; - // The original destination account that was paid by the treasury - common.v1.SolanaAccountId original_destination = 6; - // The original quark amount for the action - uint64 original_amount = 7; - // The treasury used for this the private action - common.v1.SolanaAccountId treasury = 8; - // The recent root observed at the time of intent creation for this private action - common.v1.Hash recent_root = 9; - } -} -message PaymentHistoryItem { - // The cursor position of this item. - Cursor cursor = 1; - // Exchange data related to the payment - ExchangeData exchange_data = 2; - // Is this payment a send or receive? - PaymentType payment_type = 3; - enum PaymentType { - UNKNOWN = 0; - SEND = 1; - RECEIVE = 2; - } - // If the payment was a SEND, was it a withdraw? - bool is_withdraw = 4; - // If the payment was a RECEIVE, was it a deposit? - bool is_deposit = 5; - // The timestamp of the payment - google.protobuf.Timestamp timestamp = 6; - // Was the payment involved in a remote send? - bool is_remote_send = 7; - // If payment_type is RECEIVE and is_remote_send is true, was the funds being - // returned back to the issuer? - bool is_returned = 8; - // If payment_type is RECEIVE, is this receive an airdrop part of a reward, incentive, etc.? - bool is_airdrop = 9; - // If is_airdrop is true, the type of airdrop received. - AirdropType airdrop_type = 10; - // Is this a micro payment? - bool is_micro_payment = 11; - // The intent ID associated with this history item - common.v1.IntentId intent_id = 12; -} -// ExchangeData defines an amount of Kin with currency exchange data -message ExchangeData { - // ISO 4217 alpha-3 currency code. - string currency = 1; - // The agreed upon exchange rate. This might not be the same as the - // actual exchange rate at the time of intent or fund transfer. - double exchange_rate = 2; - // The agreed upon transfer amount in the currency the payment was made - // in. - double native_amount = 3; - // The exact amount of quarks to send. This will be used as the source of - // truth for validating transaction transfer amounts. - uint64 quarks = 4; -} -message ExchangeDataWithoutRate { - // ISO 4217 alpha-3 currency code. - string currency = 1; - // The agreed upon transfer amount in the currency the payment was made - // in. - double native_amount = 2; -} -message AdditionalFeePayment { - // Destination Kin token account where the fee payment will be made - common.v1.SolanaAccountId destination = 1; - // Fee percentage, in basis points, of the total quark amount of a payment. - uint32 fee_bps = 2 ; -} -message SendLimit { - // Remaining limit to apply on the next transaction - float next_transaction = 1; - // Maximum allowed on a per-transaction basis - float max_per_transaction = 2; - // Maximum allowed on a per-day basis - float max_per_day = 3; -} -message DepositLimit { - // Maximum quarks that may be deposited at any time. Server will guarantee - // this threshold will be below enforced dollar value limits, while also - // ensuring sufficient funds are available for a full organizer that supports - // max payment sends. Total dollar value limits may be spread across many deposits. - uint64 max_quarks = 1; -} -message MicroPaymentLimit { - // Maximum native amount that can be applied per micro payment transaction - float max_per_transaction = 1; - // Minimum native amount that can be applied per micro payment transaction - float min_per_transaction = 2; -} -message BuyModuleLimit { - // Minimum amount that can be purchased through the buy module - float min_per_transaction = 1; - // Maximum amount that can be purchased through the buy module - float max_per_transaction = 2; -} -message TippedUser { - Platform platform = 1 ; - enum Platform { - UNKNOWN = 0; - TWITTER = 1; - } - string username = 2 ; -} -message FriendedUser { - Platform platform = 1 ; - enum Platform { - UNKNOWN = 0; - TWITTER = 1; - } - string username = 2 ; -} -message Cursor { - bytes value = 1 ; -} -enum AirdropType { - UNKNOWN = 0; - // Reward for giving someone else their first Kin - GIVE_FIRST_KIN = 1; - // Airdrop for getting a user started with first Kin balance - GET_FIRST_KIN = 2; -} diff --git a/definitions/code/protos/src/main/proto/user/v1/identity_service.proto b/definitions/code/protos/src/main/proto/user/v1/identity_service.proto deleted file mode 100644 index 0ba2b2514..000000000 --- a/definitions/code/protos/src/main/proto/user/v1/identity_service.proto +++ /dev/null @@ -1,316 +0,0 @@ -syntax = "proto3"; -package code.user.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/user/v1;user"; -option java_package = "com.codeinc.gen.user.v1"; -option objc_class_prefix = "CPBUserV1"; -import "common/v1/model.proto"; -import "phone/v1/phone_verification_service.proto"; -import "transaction/v2/transaction_service.proto"; - -service Identity { - // LinkAccount links an owner account to the user identified and authenticated - // by a one-time use token. - // - // Notably, this RPC has the following side effects: - // * A new user is automatically created if one doesn't exist. - // * Server will create a new data container for at least every unique - // owner account linked to the user. - rpc LinkAccount(LinkAccountRequest) returns (LinkAccountResponse); - // UnlinkAccount removes links from an owner account. It will NOT remove - // existing associations between users, owner accounts and identifying - // features. - // - // The following associations will remain intact to ensure owner accounts - // can continue to be used with a consistent login experience: - // * the user continues to be associated to existing owner accounts and - // identifying features - // - // Client can continue mainting their current login session. Their current - // user and data container will remain the same. - // - // The call is guaranteed to be idempotent. It will not fail if the link is - // already removed by either a previous call to this RPC or by a more recent - // call to LinkAccount. A failure will only occur if the link between a user - // and the owner accout or identifying feature never existed. - rpc UnlinkAccount(UnlinkAccountRequest) returns (UnlinkAccountResponse); - // GetUser gets user information given a user identifier and an owner account. - rpc GetUser(GetUserRequest) returns (GetUserResponse); - // UpdatePreferences updates user preferences. - rpc UpdatePreferences(UpdatePreferencesRequest) returns (UpdatePreferencesResponse); - // LoginToThirdPartyApp logs a user into a third party app for a given intent - // ID. If the original request requires payment, then SubmitIntent must be called. - rpc LoginToThirdPartyApp(LoginToThirdPartyAppRequest) returns (LoginToThirdPartyAppResponse); - // GetLoginForThirdPartyApp gets a login for a third party app from an existing - // request. This endpoint supports all paths where login is possible (login on payment, - // raw login, etc.). - rpc GetLoginForThirdPartyApp(GetLoginForThirdPartyAppRequest) returns (GetLoginForThirdPartyAppResponse); - // GetTwitterUser gets Twitter user information - // - // Note 1: This RPC will only return results for Twitter users that have - // accounts linked with Code. - // - // Note 2: This RPC is heavily cached, and may not reflect real-time Twitter - // information. - rpc GetTwitterUser(GetTwitterUserRequest) returns (GetTwitterUserResponse); -} -message LinkAccountRequest { - // The public key of the owner account that will be linked to a user. - common.v1.SolanaAccountId owner_account_id = 1; - // The signature is of serialize(LinkAccountRequest) without this field set - // using the private key of owner_account_id. This validates that the client - // actually owns the account. - common.v1.Signature signature = 2; - // A one-time use token that identifies and authenticates the user. - oneof token { - // A token received after successfully verifying a phone number via a - // SMS code using the phone verification service. - phone.v1.PhoneLinkingToken phone = 3; - } -} -message LinkAccountResponse { - Result result = 1; - enum Result { - OK = 0; - // The provided token is invalid. A token may be invalid for a number of - // reasons including: it's already been used, has been modified by the - // client or has expired. - INVALID_TOKEN = 1; - // The client is rate limited (eg. by IP, user ID, etc). The client should - // retry at a later time. - RATE_LIMITED = 2; - } - // The user that was linked to the owner account - User user = 2; - // The data container where the user can store a copy of their data - common.v1.DataContainerId data_container_id = 3; - // Field 4 is the deprecated kin_token_account_details - reserved 4; - // Metadata about the user based for the instance of their view - oneof metadata { - // Metadata that corresponds to a phone-based identifying feature. - PhoneMetadata phone = 5; - } -} -message UnlinkAccountRequest { - // The public key of the owner account that will be unliked. - common.v1.SolanaAccountId owner_account_id = 1; - // The signature is of serialize(UnlinkAccountRequest) without this field set - // using the private key of owner_account_id. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 2; - oneof identifying_feature { - // The phone number associated with the owner account. - common.v1.PhoneNumber phone_number = 4; - } -} -message UnlinkAccountResponse { - Result result = 1; - enum Result { - OK = 0; - // The client attempted to unlink an owner account or identifying feature - // that never had a valid association. - NEVER_ASSOCIATED = 1; - } -} -message GetUserRequest { - // The public key of the owner account that signed this request message. - common.v1.SolanaAccountId owner_account_id = 1; - // The signature is of serialize(GetUserRequest) without this field set - // using the private key of owner_account_id. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 2; - // The user's indentifying feature, which maps to an instance of a view. - oneof identifying_feature { - common.v1.PhoneNumber phone_number = 3; - } -} -message GetUserResponse { - Result result = 1; - enum Result { - OK = 0; - // The user doesn't exist - NOT_FOUND = 1; - // The user is no longer invited - NOT_INVITED = 2; - // The user exists, but at least one of their timelock accounts is unlocked - UNLOCKED_TIMELOCK_ACCOUNT = 3; - } - // The user associated with the identifier - User user = 2; - // The data container where the user can store a copy of their data - common.v1.DataContainerId data_container_id = 3; - // Field 4 is the deprecated kin_token_account_details - reserved 4; - // Metadata about the user based for the instance of their view - oneof metadata { - // Metadata that corresponds to a phone-based identifying feature. - PhoneMetadata phone = 5; - } - // Whether client internal flags are enabled for this user - bool enable_internal_flags = 6; - // Set of which airdrops the user is eligible to receive - repeated transaction.v2.AirdropType eligible_airdrops = 7; - // Wether the buy module is enabled for this user - bool enable_buy_module = 8; -} -message UpdatePreferencesRequest { - // The public key of the owner account that signed this request message. - common.v1.SolanaAccountId owner_account_id = 1; - // The data container for the copy of the contact list being added to. - common.v1.DataContainerId container_id = 2; - // The signature is of serialize(UpdatePreferencesRequest) without this field set - // using the private key of owner_account_id. - common.v1.Signature signature = 3; - // The user's locale, which is used for server-side localization of things like - // chat messages, pushes, etc. If no locale is set, or the provided locale isn't - // supported, then English is used as the default fallback. - // - // Note: This is required since it's the only preference. In the future, we'll add - // optional fields, where the subset of fields provided will be the ones that - // are updated. - common.v1.Locale locale = 4; -} -message UpdatePreferencesResponse { - Result result = 1; - enum Result { - OK = 0; - // The provided locale couldn't be parsed or recognized and is invalid. - INVALID_LOCALE = 1; - } -} -message LoginToThirdPartyAppRequest { - // The intent ID identifying the instance of the login flow. - common.v1.IntentId intent_id = 1; - // The relationship authority account logging in. - common.v1.SolanaAccountId user_id = 2; - // Signature of this message using the user private key, which authenticates - // the user. - common.v1.Signature signature = 3; -} -message LoginToThirdPartyAppResponse { - Result result = 1; - enum Result { - // This supports idempotency. The same login with the same user will result - // in OK. - OK = 0; - // There is no request for the provided intent ID. - REQUEST_NOT_FOUND = 1; - // The request requires a payment. Call SubmitIntent instead. - PAYMENT_REQUIRED = 2; - // The request exists, but doesn't support login. - LOGIN_NOT_SUPPORTED = 3; - // A login with a different user already exists - DIFFERENT_LOGIN_EXISTS = 4; - // The provided account is not valid for login. It must be a relationship - // account with the correct identifier specified in the original request. - INVALID_ACCOUNT = 5; - } -} -message GetLoginForThirdPartyAppRequest { - // The intent ID identifying the instance of the login flow. - common.v1.IntentId intent_id = 1; - // Owner account owned by the third party used in domain verification. - common.v1.SolanaAccountId verifier = 2; - // Signature of this message using the verifier private key, which in addition - // to domain verification, authenticates the third party. - common.v1.Signature signature = 3; -} -message GetLoginForThirdPartyAppResponse { - Result result = 1; - enum Result { - OK = 0; - // There is no request for the provided intent ID. - REQUEST_NOT_FOUND = 1; - // The request exists, but doesn't support login. - LOGIN_NOT_SUPPORTED = 2; - // The intent supports login, but it hasn't been submitted. There is no - // logged in user yet. - NO_USER_LOGGED_IN = 3; - } - // The relationship authority account that logged in. - common.v1.SolanaAccountId user_id = 2; -} -message GetTwitterUserRequest { - oneof query { - // The Twitter username to query against - string username = 1 ; - // The tip address to query against - common.v1.SolanaAccountId tip_address = 2; - } - // An optional set of authentication information that allows for more - // information to be returned in the request. - common.v1.SolanaAccountId requestor = 10; - common.v1.Signature signature = 11; -} -message GetTwitterUserResponse { - Result result = 1; - enum Result { - OK = 0; - // The Twitter user doesn't exist or isn't linked with a Code account - NOT_FOUND = 1; - } - TwitterUser twitter_user = 2; -} -// User is the highest order of a form of identity within Code. -// -// Note: Users outside Code are modelled as relationship accounts -message User { - // The user's ID - common.v1.UserId id = 1; - // The identifying features that are associated with the user - View view = 2; -} -// View is a well-defined set of identifying features. It is contrained to having -// exactly one feature set at a time, for now. -message View { - // The phone number associated with a user. - // - // Note: This field is mandatory as of right now, since it's the only one - // supported to date. - common.v1.PhoneNumber phone_number = 1; -} -message PhoneMetadata { - // State that determines whether a phone number is linked to the owner - // account. A phone number is linked if we can treat it as an alias. - // This is notably different from association, which answers the question - // of whether the number was linked at any point in time. - bool is_linked = 1; -} -message TwitterUser { - // Public key for a token account where tips are routed - // - // TODO(tip_rename): Candidate for renaming to something more generic. - common.v1.SolanaAccountId tip_address = 1; - // The user's username on Twitter - string username = 2 ; - // The user's friendly name on Twitter - string name = 3 ; - // URL to the user's Twitter profile picture - string profile_pic_url = 4 ; - // The type of Twitter verification associated with the user - VerifiedType verified_type = 5; - enum VerifiedType { - NONE = 0; - BLUE = 1; - BUSINESS = 2; - GOVERNMENT = 3; - } - // The number of followers the user has on Twitter - uint32 follower_count = 6; - // The cost of establishing the friendship (regardless if caller is a friend). - // - // This should not be cached for an extended period, as exchange rate / value - // may change at any time. - transaction.v2.ExchangeDataWithoutRate friendship_cost = 7; - // =========================================================== - // The rest of the fields require authentication to be present. - // =========================================================== - // Indicates the user is a friend of the caller. - bool is_friend = 10; - // The ChatId used to communicate with this friend. - // - // This will always be set for authenticated users. - // If is_friend=false, this ChatId should be used when crafting - // the intent. - common.v1.ChatId friend_chat_id = 11; -} diff --git a/definitions/flipchat-vm/models/.gitignore b/definitions/flipchat-vm/models/.gitignore deleted file mode 100644 index 9f2a07880..000000000 --- a/definitions/flipchat-vm/models/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -build/ -.gradle/ diff --git a/definitions/flipchat-vm/models/build.gradle.kts b/definitions/flipchat-vm/models/build.gradle.kts deleted file mode 100644 index 6c39a61e6..000000000 --- a/definitions/flipchat-vm/models/build.gradle.kts +++ /dev/null @@ -1,90 +0,0 @@ -import org.apache.tools.ant.taskdefs.condition.Os -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - id(Plugins.android_library) - id(Plugins.kotlin_android) - id("com.google.protobuf") -} - -val archSuffix = if (Os.isFamily(Os.FAMILY_MAC)) ":osx-x86_64" else "" - -version = "0.0.1" -group = "com.codeinc.gen" - -dependencies { - protobuf(project(":definitions:flipchat-vm:protos")) - - implementation(Libs.grpc_protobuf_lite) - implementation(Libs.grpc_stub) - - // Kotlin Generation - implementation(Libs.grpc_kotlin) - implementation(Libs.protobuf_kotlin_lite) - implementation(Libs.kotlinx_coroutines_core) -} - -kotlin { - jvmToolchain(Versions.java.toInt()) -} - -android { - namespace = "${Gradle.codeNamespace}.service.models" - compileSdk = Android.compileSdkVersion - defaultConfig { - minSdk = Android.minSdkVersion - testInstrumentationRunner = Android.testInstrumentationRunner - } -} - -kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(Versions.java)) - } - - compilerOptions { - jvmTarget.set(JvmTarget.fromTarget(Versions.java)) - optIn.addAll( - "kotlin.time.ExperimentalTime", - "kotlin.ExperimentalUnsignedTypes", - "kotlin.RequiresOptIn" - ) - } -} - -protobuf { - protoc { - artifact = "com.google.protobuf:protoc:${Versions.protobuf}$archSuffix" - } - plugins { - create("java") { - artifact = "io.grpc:protoc-gen-grpc-java:${Versions.grpc}" - } - create("grpc") { - artifact = "io.grpc:protoc-gen-grpc-java:${Versions.grpc}" - } - create("grpckt") { - artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar" - } - } - generateProtoTasks { - all().forEach { - it.plugins { - create("java") { - option("lite") - } - create("grpc") { - option("lite") - } - create("grpckt") { - option("lite") - } - } - it.builtins { - create("kotlin") { - option("lite") - } - } - } - } -} diff --git a/definitions/flipchat-vm/protos/.gitignore b/definitions/flipchat-vm/protos/.gitignore deleted file mode 100644 index 9f2a07880..000000000 --- a/definitions/flipchat-vm/protos/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -build/ -.gradle/ diff --git a/definitions/flipchat-vm/protos/build.gradle.kts b/definitions/flipchat-vm/protos/build.gradle.kts deleted file mode 100644 index a42e252c7..000000000 --- a/definitions/flipchat-vm/protos/build.gradle.kts +++ /dev/null @@ -1,11 +0,0 @@ -// todo: maybe use variants / configurations to do both stub & stub-lite here - -// Note: We use the java-library plugin to get the protos into the artifact for this subproject -// because there doesn't seem to be an better way. -plugins { - `java-library` -} - -java { - sourceSets.getByName("main").resources.srcDir("src/main/proto") -} diff --git a/definitions/flipchat-vm/protos/src/main/proto/account/v1/code_account_service.proto b/definitions/flipchat-vm/protos/src/main/proto/account/v1/code_account_service.proto deleted file mode 100644 index d36ed1db8..000000000 --- a/definitions/flipchat-vm/protos/src/main/proto/account/v1/code_account_service.proto +++ /dev/null @@ -1,239 +0,0 @@ -syntax = "proto3"; - -package code.account.v1; - -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/account/v1;account"; -option java_package = "com.codeinc.gen.account.v1"; -option objc_class_prefix = "CPBAccountV1"; - -import "common/v1/code_model.proto"; -import "transaction/v2/code_transaction_service.proto"; -import "google/protobuf/timestamp.proto"; - - -service Account { - // IsCodeAccount returns whether an owner account is a Code account. This hints - // to the client whether the account can be logged in, used for making payments, - // etc. - rpc IsCodeAccount(IsCodeAccountRequest) returns (IsCodeAccountResponse); - - // GetTokenAccountInfos returns token account metadata relevant to the Code owner - // account. - rpc GetTokenAccountInfos(GetTokenAccountInfosRequest) returns (GetTokenAccountInfosResponse); - - // LinkAdditionalAccounts allows a client to declare additional accounts to - // be tracked and used within Code. The accounts declared in this RPC are not - // managed by Code (ie. not a Timelock account), created externally and cannot - // be linked automatically (ie. authority derived off user 12 words). - rpc LinkAdditionalAccounts(LinkAdditionalAccountsRequest) returns (LinkAdditionalAccountsResponse); -} - -message IsCodeAccountRequest { - // The owner account to check against. - common.v1.SolanaAccountId owner = 1; - - - // The signature is of serialize(IsCodeAccountRequest) without this field set - // using the private key of the owner account. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 2; - -} - -message IsCodeAccountResponse { - Result result = 1; - enum Result { - // The account is a Code account. - OK = 0; - // The account is not a Code account. - NOT_FOUND = 1; - // The account exists, but at least one timelock account is unlocked. - UNLOCKED_TIMELOCK_ACCOUNT = 2; - } -} - -message GetTokenAccountInfosRequest { - // The owner account, which can also be thought of as a parent account for this - // RPC that links to one or more token accounts. - common.v1.SolanaAccountId owner = 1; - - - // The signature is of serialize(GetTokenAccountInfosRequest) without this field set - // using the private key of the owner account. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 2; - -} - -message GetTokenAccountInfosResponse { - Result result = 1; - enum Result { - OK = 0; - NOT_FOUND = 1; - } - - map token_account_infos = 2; -} - -message LinkAdditionalAccountsRequest { - // The owner account to link to - common.v1.SolanaAccountId owner = 1; - - - // The authority account derived off the user's 12 words, which contains - // the USDC ATA (and potentially others in the future) that will be used - // in swaps. - common.v1.SolanaAccountId swap_authority = 2; - - - // Signature values for each account provided in this request. Each signature - // must be generated without this array set. The expected ordering of signatures: - // 1. owner - // 2. swap_authority - repeated common.v1.Signature signatures = 3 ; - -} - -message LinkAdditionalAccountsResponse { - Result result = 1; - enum Result { - // Supports idempotency, and will be returned as long as the request exactly - // matches a previous execution. - OK = 0; - // The action has been denied (eg. owner account not phone verified) - DENIED = 1; - // An account being linked is not valid - INVALID_ACCOUNT = 2; - } -} - -message TokenAccountInfo { - // The token account's address - common.v1.SolanaAccountId address = 1; - - - // The owner of the token account, which can also be thought of as a parent - // account that links to one or more token accounts. This is provided when - // available. - common.v1.SolanaAccountId owner = 2; - - // The token account's authority, which has access to moving funds for the - // account. This can be the owner account under certain circumstances (eg. - // ATA, primary account). This is provided when available. - common.v1.SolanaAccountId authority = 3; - - // The type of token account, which infers its intended use. - common.v1.AccountType account_type = 4; - - - // The account's derivation index for applicable account types. When this field - // doesn't apply, a zero value is provided. - uint64 index = 5; - - // The source of truth for the balance calculation. - BalanceSource balance_source = 6; - enum BalanceSource { - // The account's balance could not be determined. This may be returned when - // the data source is unstable and a reliable balance cannot be determined. - BALANCE_SOURCE_UNKNOWN = 0; - // The account's balance was fetched directly from a finalized state on the - // blockchain. - BALANCE_SOURCE_BLOCKCHAIN = 1; - // The account's balance was calculated using cached values in Code. Accuracy - // is only guaranteed when management_state is LOCKED. - BALANCE_SOURCE_CACHE = 2; - } - - // The balance in quarks, as observed by Code. This may not reflect the value - // on the blockchain and could be non-zero even if the account hasn't been created. - // Use balance_source to determine how this value was calculated. - uint64 balance = 7; - - // The state of the account as it pertains to Code's ability to manage funds. - ManagementState management_state = 8; - enum ManagementState { - // The state of the account is unknown. This may be returned when the - // data source is unstable and a reliable state cannot be determined. - MANAGEMENT_STATE_UNKNOWN = 0; - // Code does not maintain a management state and won't move funds for this - // account. - MANAGEMENT_STATE_NONE = 1; - // The account is in the process of transitioning to the LOCKED state. - MANAGEMENT_STATE_LOCKING = 2; - // The account's funds are locked and Code has co-signing authority. - MANAGEMENT_STATE_LOCKED = 3; - // The account is in the process of transitioning to the UNLOCKED state. - MANAGEMENT_STATE_UNLOCKING = 4; - // The account's funds are unlocked and Code no longer has co-signing - // authority. The account must transition to the LOCKED state to have - // management capabilities. - MANAGEMENT_STATE_UNLOCKED = 5; - // The account is in the process of transitioning to the CLOSED state. - MANAGEMENT_STATE_CLOSING = 6; - // The account has been closed and doesn't exist on the blockchain. - // Subsequently, it also has a zero balance. - MANAGEMENT_STATE_CLOSED = 7; - } - - // The state of the account on the blockchain. - BlockchainState blockchain_state = 9; - enum BlockchainState { - // The state of the account is unknown. This may be returned when the - // data source is unstable and a reliable state cannot be determined. - BLOCKCHAIN_STATE_UNKNOWN = 0; - // The account does not exist on the blockchain. - BLOCKCHAIN_STATE_DOES_NOT_EXIST = 1; - // The account is created and exists on the blockchain. - BLOCKCHAIN_STATE_EXISTS = 2; - } - - // For temporary incoming accounts only. Flag indicates whether client must - // actively try rotating it by issuing a ReceivePaymentsPrivately intent. In - // general, clients should wait as long as possible until this flag is true - // or requiring the funds to send their next payment. - bool must_rotate = 10; - - // Whether an account is claimed. This only applies to relevant account types - // (eg. REMOTE_SEND_GIFT_CARD). - ClaimState claim_state = 11; - enum ClaimState { - // The account doesn't have a concept of being claimed, or the state - // could not be fetched by server. - CLAIM_STATE_UNKNOWN = 0; - // The account has not yet been claimed. - CLAIM_STATE_NOT_CLAIMED = 1; - // The account is claimed. Attempting to claim it will fail. - CLAIM_STATE_CLAIMED = 2; - // The account hasn't been claimed, but is expired. Funds will move - // back to the issuer. Attempting to claim it will fail. - CLAIM_STATE_EXPIRED = 3; - } - - // For account types used as an intermediary for sending money between two - // users (eg. REMOTE_SEND_GIFT_CARD), this represents the original exchange - // data used to fund the account. Over time, this value will become stale: - // 1. Exchange rates will fluctuate, so the total fiat amount will differ. - // 2. External entities can deposit additional funds into the account, so - // the balance, in quarks, may be greater than the original quark value. - // 3. The balance could have been received, so the total balance can show - // as zero. - transaction.v2.ExchangeData original_exchange_data = 12; - - // The token account's mint - common.v1.SolanaAccountId mint = 13; - - // Reserved for the number of decimals configured for the mint - reserved 14; - - // Reserved for a user-friendly display name for the mint - reserved 15; - - // The relationship with a third party that this account has established with. - // This only applies to relevant account types (eg. RELATIONSHIP). - common.v1.Relationship relationship = 16; - - // Time the account was created, if available. For Code accounts, this is - // the time of intent submission. Otherwise, for external accounts, it is - // the tiem created on the blockchain. - google.protobuf.Timestamp created_at = 17; -} diff --git a/definitions/flipchat-vm/protos/src/main/proto/badge/v1/code_badge_service.proto b/definitions/flipchat-vm/protos/src/main/proto/badge/v1/code_badge_service.proto deleted file mode 100644 index f26d20ce4..000000000 --- a/definitions/flipchat-vm/protos/src/main/proto/badge/v1/code_badge_service.proto +++ /dev/null @@ -1,25 +0,0 @@ -syntax = "proto3"; -package code.badge.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/badge/v1;badge"; -option java_package = "com.codeinc.gen.badge.v1"; -option objc_class_prefix = "CPBBadgeV1"; -import "common/v1/code_model.proto"; - -service Badge { - // ResetBadgeCount resets an owner account's app icon badge count back to zero - rpc ResetBadgeCount(ResetBadgeCountRequest) returns (ResetBadgeCountResponse); -} -message ResetBadgeCountRequest { - // The owner account to clear badge count - common.v1.SolanaAccountId owner = 1; - // The signature is of serialize(ResetBadgeCountRequest) without this field set - // using the private key of the owner account. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 2; -} -message ResetBadgeCountResponse { - Result result = 1; - enum Result { - OK = 0; - } -} diff --git a/definitions/flipchat-vm/protos/src/main/proto/chat/v1/code_chat_service.proto b/definitions/flipchat-vm/protos/src/main/proto/chat/v1/code_chat_service.proto deleted file mode 100644 index 51c4a40bc..000000000 --- a/definitions/flipchat-vm/protos/src/main/proto/chat/v1/code_chat_service.proto +++ /dev/null @@ -1,256 +0,0 @@ -syntax = "proto3"; -package code.chat.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/chat/v1;chat"; -option java_package = "com.codeinc.gen.chat.v1"; -option objc_class_prefix = "CPBChatV1"; -import "common/v1/code_model.proto"; -import "transaction/v2/code_transaction_service.proto"; -import "google/protobuf/timestamp.proto"; - -// Deprecated: Use the v2 service -service Chat { - // GetChats gets the set of chats for an owner account - rpc GetChats(GetChatsRequest) returns (GetChatsResponse); - // GetMessages gets the set of messages for a chat - rpc GetMessages(GetMessagesRequest) returns (GetMessagesResponse); - // AdvancePointer advances a pointer in chat history - rpc AdvancePointer(AdvancePointerRequest) returns (AdvancePointerResponse); - // SetMuteState configures the mute state of a chat - rpc SetMuteState(SetMuteStateRequest) returns (SetMuteStateResponse); - // SetSubscriptionState configures the susbscription state of a chat - rpc SetSubscriptionState(SetSubscriptionStateRequest) returns (SetSubscriptionStateResponse); - // - // Experimental PoC two-way chat APIs below - // - rpc StreamChatEvents(stream StreamChatEventsRequest) returns (stream StreamChatEventsResponse); - rpc SendMessage(SendMessageRequest) returns (SendMessageResponse); -} -message GetChatsRequest { - common.v1.SolanaAccountId owner = 1; - common.v1.Signature signature = 2; - uint32 page_size = 3; - Cursor cursor = 4; - Direction direction = 5; - enum Direction { - ASC = 0; - DESC = 1; - } -} -message GetChatsResponse { - Result result = 1; - enum Result { - OK = 0; - NOT_FOUND = 1; - } - repeated ChatMetadata chats = 2 ; -} -message GetMessagesRequest { - ChatId chat_id = 1; - common.v1.SolanaAccountId owner = 2; - common.v1.Signature signature = 3; - uint32 page_size = 4; - Cursor cursor = 5; - Direction direction = 6; - enum Direction { - ASC = 0; - DESC = 1; - } -} -message GetMessagesResponse { - Result result = 1; - enum Result { - OK = 0; - NOT_FOUND = 1; - } - repeated ChatMessage messages = 2 ; -} -message AdvancePointerRequest { - ChatId chat_id = 1; - Pointer pointer = 2; - common.v1.SolanaAccountId owner = 3; - common.v1.Signature signature = 4; -} -message AdvancePointerResponse { - Result result = 1; - enum Result { - OK = 0; - CHAT_NOT_FOUND = 1; - MESSAGE_NOT_FOUND = 2; - } -} -message SetMuteStateRequest { - ChatId chat_id = 1; - bool is_muted = 2; - common.v1.SolanaAccountId owner = 3; - common.v1.Signature signature = 4; -} -message SetMuteStateResponse { - Result result = 1; - enum Result { - OK = 0; - CHAT_NOT_FOUND = 1; - CANT_MUTE = 2; - } -} -message SetSubscriptionStateRequest { - ChatId chat_id = 1; - bool is_subscribed = 2; - common.v1.SolanaAccountId owner = 3; - common.v1.Signature signature = 4; -} -message SetSubscriptionStateResponse { - Result result = 1; - enum Result { - OK = 0; - CHAT_NOT_FOUND = 1; - CANT_UNSUBSCRIBE = 2; - } -} -message OpenChatEventStream { - ChatId chat_id = 1; - common.v1.SolanaAccountId owner = 2; - common.v1.Signature signature = 3; -} -message ChatStreamEvent { - repeated ChatMessage messages = 1; - repeated Pointer pointers = 2; -} -message ChatStreamEventBatch { - repeated ChatStreamEvent events = 2 ; -} -message StreamChatEventsRequest { - oneof type { - OpenChatEventStream open_stream = 1; - common.v1.ClientPong pong = 2; - } -} -message StreamChatEventsResponse { - oneof type { - ChatStreamEventBatch events = 1; - common.v1.ServerPing ping = 2; - } -} -message SendMessageRequest { - common.v1.SolanaAccountId owner = 1; - common.v1.Signature signature = 2; - ChatId chat_id = 3; - // todo: What field type should this be? Maybe the chat message itself with fields missing? - repeated Content content = 4 ; -} -message SendMessageResponse { - Result result = 1; - enum Result { - OK = 0; - } - ChatMessage message = 2; -} -message ChatId { - bytes value = 1 ; -} -message ChatMessageId { - bytes value = 1 ; -} -message ChatMemberId { - // todo: Public key for now - bytes value = 1 ; -} -message Pointer { - Kind kind = 1; - enum Kind { - UNKNOWN = 0; - READ = 1; - DELIVERED = 2; - SENT = 3; // Probably always inferred by OK result in SendMessageResponse - } - ChatMessageId value = 2; - ChatMemberId user = 3; -} -message ChatMetadata { - ChatId chat_id = 1; - // Recommended chat title inferred by the type of chat - oneof title { - ServerLocalizedContent localized = 2; - common.v1.Domain domain = 3; - } - // Pointer in the chat indicating the most recently read message by the user - Pointer read_pointer = 4; - // Estimated number of unread messages in this chat - uint32 num_unread = 5; - // Has the user muted this chat? - bool is_muted = 6; - // Is the user subscribed to this chat? - bool is_subscribed = 7; - // Can the user mute this chat? - bool can_mute = 8; - // Can the user unsubscribe from this chat? - bool can_unsubscribe = 9; - // Cursor value for this chat for reference in subsequent GetChatsRequest - Cursor cursor = 10; - // Is this a verified chat? - // - // Note: It's possible to have two chats with the same title, but with - // different verification statuses. They should be treated separately. - bool is_verified = 11; -} -message ChatMessage { - // Unique ID for this message - ChatMessageId message_id = 1; - // Timestamp this message was generated at - google.protobuf.Timestamp ts = 2; - // Ordered message content. A message may have more than one piece of content. - repeated Content content = 3 ; - // Cursor value for this message for reference in subsequent GetMessagesRequest - Cursor cursor = 4; - ChatMemberId sender = 5; -} -message Content { - oneof type { - ServerLocalizedContent server_localized = 1; - ExchangeDataContent exchange_data = 2; - NaclBoxEncryptedContent nacl_box = 3; - TextContent text = 4; - ThankYouContent thank_you = 5; - } -} -message ServerLocalizedContent { - // When server-side localization is in place, clients will always see the - // localized text. - string key_or_text = 1 ; -} -message ExchangeDataContent { - Verb verb = 1; - enum Verb { - UNKNOWN = 0; - GAVE = 1; - RECEIVED = 2; - WITHDREW = 3; - DEPOSITED = 4; - SENT = 5; - RETURNED = 6; - SPENT = 7; - PAID = 8; - PURCHASED = 9; - RECEIVED_TIP = 10; - SENT_TIP = 11; - } - oneof exchange_data { - transaction.v2.ExchangeData exact = 2; - transaction.v2.ExchangeDataWithoutRate partial = 3; - } -} -message NaclBoxEncryptedContent { - common.v1.SolanaAccountId peer_public_key = 1; - bytes nonce = 2 ; - bytes encrypted_payload = 3 ; -} -message TextContent { - string text = 1 ; -} -message ThankYouContent { - // todo: May need additional metdata (who is being thanked, which tip, etc) -} -// Opaque cursor used across paged APIs. Underlying bytes may change as paging -// strategies evolve. -message Cursor { - bytes value = 1 ; -} diff --git a/definitions/flipchat-vm/protos/src/main/proto/chat/v2/chat_service.proto b/definitions/flipchat-vm/protos/src/main/proto/chat/v2/chat_service.proto deleted file mode 100644 index bce403d8e..000000000 --- a/definitions/flipchat-vm/protos/src/main/proto/chat/v2/chat_service.proto +++ /dev/null @@ -1,488 +0,0 @@ -syntax = "proto3"; -package code.chat.v2; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/chat/v2;chat"; -option java_package = "com.codeinc.gen.chat.v2"; -option objc_class_prefix = "CPBChatV2"; -import "common/v1/code_model.proto"; -import "transaction/v2/code_transaction_service.proto"; -import "google/protobuf/timestamp.proto"; - -service Chat { - // GetChats gets the set of chats for an owner account using a paged API. - // This RPC is aware of all identities tied to the owner account. - rpc GetChats(GetChatsRequest) returns (GetChatsResponse); - // GetMessages gets the set of messages for a chat member using a paged API - rpc GetMessages(GetMessagesRequest) returns (GetMessagesResponse); - // StreamChatEvents streams chat events in real-time. Chat events include - // messages, pointer updates, etc. - // - // The streaming protocol is follows: - // 1. Client initiates a stream by sending an OpenChatEventStream message. - // 2. If an error is encoutered, a ChatStreamEventError message will be - // returned by server and the stream will be closed. - // 3. Server will immediately flush initial chat state. - // 4. New chat events will be pushed to the stream in real time as they - // are received. - // - // This RPC supports a keepalive protocol as follows: - // 1. Client initiates a stream by sending an OpenChatEventStream message. - // 2. Upon stream initialization, server begins the keepalive protocol. - // 3. Server sends a ping to the client. - // 4. Client responds with a pong as fast as possible, making note of - // the delay for when to expect the next ping. - // 5. Steps 3 and 4 are repeated until the stream is explicitly terminated - // or is deemed to be unhealthy. - // - // Client notes: - // * Client should be careful to process events async, so any responses to pings are - // not delayed. - // * Clients should implement a reasonable backoff strategy upon continued timeout - // failures. - // * Clients that abuse pong messages may have their streams terminated by server. - // - // At any point in the stream, server will respond with events in real time as - // they are observed. Events sent over the stream should not affect the ping/pong - // protocol timings. - rpc StreamChatEvents(stream StreamChatEventsRequest) returns (stream StreamChatEventsResponse); - // StartChat starts a chat. The RPC call is idempotent and will use existing - // chats whenever applicable within the context of message routing. - rpc StartChat(StartChatRequest) returns (StartChatResponse); - // SendMessage sends a message to a chat - rpc SendMessage(SendMessageRequest) returns (SendMessageResponse); - // AdvancePointer advances a pointer in message history for a chat member - rpc AdvancePointer(AdvancePointerRequest) returns (AdvancePointerResponse); - // RevealIdentity reveals a chat member's identity if it is anonymous. A chat - // message will be inserted on success. - rpc RevealIdentity(RevealIdentityRequest) returns (RevealIdentityResponse); - // SetMuteState configures a chat member's mute state - rpc SetMuteState(SetMuteStateRequest) returns (SetMuteStateResponse); - // SetSubscriptionState configures a chat member's susbscription state - rpc SetSubscriptionState(SetSubscriptionStateRequest) returns (SetSubscriptionStateResponse); - // NotifyIsTypingRequest notifies a chat that the sending member is typing. - // - // These requests are transient, and may be dropped at any point. - rpc NotifyIsTyping(NotifyIsTypingRequest) returns (NotifyIsTypingResponse); -} -message GetChatsRequest { - common.v1.SolanaAccountId owner = 1; - common.v1.Signature signature = 2; - uint32 page_size = 3; - Cursor cursor = 4; - Direction direction = 5; - enum Direction { - ASC = 0; - DESC = 1; - } -} -message GetChatsResponse { - Result result = 1; - enum Result { - OK = 0; - NOT_FOUND = 1; - } - repeated ChatMetadata chats = 2 ; -} -message GetMessagesRequest { - ChatId chat_id = 1; - ChatMemberId member_id = 2; - common.v1.SolanaAccountId owner = 3; - common.v1.Signature signature = 4; - uint32 page_size = 5; - Cursor cursor = 6; - Direction direction = 7; - enum Direction { - ASC = 0; - DESC = 1; - } -} -message GetMessagesResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - CHAT_NOT_FOUND = 2; - MESSAGE_NOT_FOUND = 3; - } - repeated ChatMessage messages = 2 ; -} -message OpenChatEventStream { - ChatId chat_id = 1; - ChatMemberId member_id = 2 ; - common.v1.SolanaAccountId owner = 3; - common.v1.Signature signature = 4; -} -message ChatStreamEvent { - oneof type { - ChatMessage message = 1; - Pointer pointer = 2; - IsTyping is_typing = 3; - } -} -message ChatStreamEventBatch { - repeated ChatStreamEvent events = 2 ; -} -message ChatStreamEventError { - Code code = 1; - enum Code { - DENIED = 0; - CHAT_NOT_FOUND = 1; - } -} -message StreamChatEventsRequest { - oneof type { - OpenChatEventStream open_stream = 1; - common.v1.ClientPong pong = 2; - } -} -message StreamChatEventsResponse { - oneof type { - ChatStreamEventBatch events = 1; - common.v1.ServerPing ping = 2; - ChatStreamEventError error = 3; - } -} -message StartChatRequest { - common.v1.SolanaAccountId owner = 1; - common.v1.Signature signature = 2; - oneof parameters { - StartTipChatParameters tip_chat = 3; - // GroupChatParameters group_chat = 4; - } -} -// Starts a two-way chat between a tipper and tippee. Chat members are -// inferred from the 12 word public keys involved in the intent. Only -// the tippee can start the chat, and the tipper is anonymous if this -// is the first between the involved Code users. -message StartTipChatParameters { - // The tip's intent ID, which can be extracted from the reference in - // an ExchangeDataContent message content where the verb is RECEIVED_TIP. - common.v1.IntentId intent_id = 1; -} -message StartChatResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - INVALID_PARAMETER = 2; - } - // The chat to use if the RPC was successful - ChatMetadata chat = 2; -} -message SendMessageRequest { - ChatId chat_id = 1; - ChatMemberId member_id = 2 ; - // Allowed content types that can be sent by client: - // - TextContent - // - ThankYouContent - repeated Content content = 3 ; - common.v1.SolanaAccountId owner = 4; - common.v1.Signature signature = 5; -} -message SendMessageResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - CHAT_NOT_FOUND = 2; - INVALID_CHAT_TYPE = 3; - INVALID_CONTENT_TYPE = 4; - } - // The chat message that was sent if the RPC was succesful, which includes - // server-side metadata like the generated message ID and official timestamp - ChatMessage message = 2; -} -message AdvancePointerRequest { - ChatId chat_id = 1; - Pointer pointer = 2; - common.v1.SolanaAccountId owner = 3; - common.v1.Signature signature = 4; -} -message AdvancePointerResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - CHAT_NOT_FOUND = 2; - MESSAGE_NOT_FOUND = 3; - INVALID_POINTER_TYPE = 4; - } -} -message RevealIdentityRequest { - ChatId chat_id = 1; - ChatMemberId member_id = 2; - ChatMemberIdentity identity = 3; - common.v1.SolanaAccountId owner = 4; - common.v1.Signature signature = 5; -} -message RevealIdentityResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - CHAT_NOT_FOUND = 2; - DIFFERENT_IDENTITY_REVEALED = 3; - } - // The chat message that was sent if the RPC was successful - ChatMessage message = 2; -} -message SetMuteStateRequest { - ChatId chat_id = 1; - ChatMemberId member_id = 2 ; - bool is_muted = 3; - common.v1.SolanaAccountId owner = 4; - common.v1.Signature signature = 5; -} -message SetMuteStateResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - CHAT_NOT_FOUND = 2; - CANT_MUTE = 3; - } -} -message SetSubscriptionStateRequest { - ChatId chat_id = 1; - ChatMemberId member_id = 2 ; - bool is_subscribed = 3; - common.v1.SolanaAccountId owner = 4; - common.v1.Signature signature = 5; -} -message SetSubscriptionStateResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - CHAT_NOT_FOUND = 2; - CANT_UNSUBSCRIBE = 3; - } -} -message NotifyIsTypingRequest { - ChatId chat_id = 1; - ChatMemberId member_id = 2 ; - bool is_typing = 3; - common.v1.SolanaAccountId owner = 4; - common.v1.Signature signature = 5; -} -message NotifyIsTypingResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - CHAT_NOT_FOUND = 2; - } -} -message ChatId { - // Sufficient space is left for a consistent hash value, though other types - // of values may be used. - bytes value = 1 ; -} -message ChatMessageId { - // Guaranteed to be a time-based UUID. This should be used to construct a - // consistently ordered message history based on time using a simple byte - // comparison. - bytes value = 1 ; -} -message ChatMemberId { - // Globally random UUID - bytes value = 1 ; -} -enum ChatType { - UNKNOWN_CHAT_TYPE = 0; - NOTIFICATION = 1; - TWO_WAY = 2; - // GROUP = 3; -} -enum Platform { - UNKNOWN_PLATFORM = 0; - TWITTER = 1; -} -enum PointerType { - UNKNOWN_POINTER_TYPE = 0; - SENT = 1; // Always inferred by OK result in SendMessageResponse or message presence in a chat - DELIVERED = 2; - READ = 3; -} -// A chat -// -// todo: Support is_verified in a clean way -message ChatMetadata { - // Globally unique ID for this chat - ChatId chat_id = 1; - // The type of chat - ChatType type = 2 ; - // The chat title, which will be localized by server when applicable - string title = 3 ; - // The members in this chat - // - // For NOTIFICATION chats, this list has exactly 1 item - // For TWO_WAY chats, this list has exactly 2 items - // - // todo: If we support group chats, then we'll likely return the first page - // or a prioritized list. The remaining members would be fetched via - // a new RPC. - repeated ChatMember members = 4 ; - // Can the user mute this chat? - bool can_mute = 5; - // Can the user unsubscribe from this chat? - bool can_unsubscribe = 6; - // Cursor value for this chat for reference in subsequent GetChatsRequest - Cursor cursor = 7; -} -// A message in a chat -message ChatMessage { - // Globally unique ID for this message - ChatMessageId message_id = 1; - // The chat member that sent the message. For NOTIFICATION chats, this field - // is omitted since the chat has exactly 1 member. - ChatMemberId sender_id = 2; - // Ordered message content. A message may have more than one piece of content. - repeated Content content = 3 ; - // Timestamp this message was generated at. This value is also encoded in - // any time-based UUID message IDs. - google.protobuf.Timestamp ts = 4; - // Cursor value for this message for reference in a paged GetMessagesRequest - Cursor cursor = 5; -} -// A user in a chat -message ChatMember { - // Globally unique ID for this chat member - ChatMemberId member_id = 1; - // Is this chat member yourself? This enables client to identify which member_id - // is themselves. - bool is_self = 2; - // The chat member's identity if it has been revealed. - ChatMemberIdentity identity = 3; - // Chat message state for this member. This list will have DELIVERED and READ - // pointers, if they exist. SENT pointers should be inferred by persistence - // on server. - repeated Pointer pointers = 4 ; - // Estimated number of unread messages for the chat member in this chat - // - // Only valid when is_self = true - uint32 num_unread = 5; - // Has the chat member muted this chat? - // - // Only valid when is_self = true - bool is_muted = 6; - // Is the chat member subscribed to this chat? - // - // Only valid when is_self = true - bool is_subscribed = 7; -} -// Identity to an external social platform that can be linked to a Code account -message ChatMemberIdentity { - // The external social platform linked to this chat member - Platform platform = 1 ; - // The chat member's username on the external social platform - string username = 2 ; - // If present, the URL of the users profile pic. - string profile_pic_url = 3 ; -} -// Pointer in a chat indicating a user's message history state in a chat. -message Pointer { - // The type of pointer indicates which user's message history state can be - // inferred from the pointer value. It is also possible to infer cross-pointer - // state. For example, if a chat member has a READ pointer for a message with - // ID N, then the DELIVERED pointer must be at least N. - PointerType type = 1 ; - // Everything at or before this message ID is considered to have the state - // inferred by the type of pointer. - ChatMessageId value = 2; - // The chat member associated with this pointer state - ChatMemberId member_id = 3; -} -// Content for a chat message -message Content { - oneof type { - TextContent text = 1; - LocalizedContent localized = 2; - ExchangeDataContent exchange_data = 3; - NaclBoxEncryptedContent nacl_box = 4; - ThankYouContent thank_you = 5; - IdentityRevealedContent identity_revealed = 6; - } -} -// Raw text content -message TextContent { - string text = 1 ; -} -// Text content that is either a localization key that should be translated on -// client, or a server-side translated piece of text. -message LocalizedContent { - string key_or_text = 1 ; -} -// Exchange data content for movement of a value of Kin -message ExchangeDataContent { - enum Verb { - UNKNOWN = 0; - GAVE = 1; - RECEIVED = 2; - WITHDREW = 3; - DEPOSITED = 4; - SENT = 5; - RETURNED = 6; - SPENT = 7; - PAID = 8; - PURCHASED = 9; - RECEIVED_TIP = 10; - SENT_TIP = 11; - } - // Verb describing how the amount of Kin was exchanged - // - // Note: The current definition is not suitable outside a NOTIFICATION chat - // as not enough context is provided as to which member this verb is - // associated with. - Verb verb = 1 ; - // An amount of Kin being exchanged - oneof exchange_data { - transaction.v2.ExchangeData exact = 2; - transaction.v2.ExchangeDataWithoutRate partial = 3; - } - // An ID that can be referenced to the source of the exchange of Kin - oneof reference { - common.v1.IntentId intent = 4; - common.v1.Signature signature = 5; - } -} -// Encrypted piece of content using NaCl box encryption -message NaclBoxEncryptedContent { - // The sender's public key that is used to derive the shared private key for - // decryption for message content. - common.v1.SolanaAccountId peer_public_key = 1; - // Globally random nonce that is unique to this encrypted piece of content - bytes nonce = 2 ; - // The encrypted piece of message content - bytes encrypted_payload = 3 ; -} -// Thank you content that is used to thank Code users for tips -message ThankYouContent { - // The tip intent that is being thanked. - common.v1.IntentId tip_intent = 1; - // Reserved for the thanker, which is only required if we support GROUP chats. - // Otherwise, it can be inferred from the sender in a TWO_WAY chat. - reserved 2; - // Reserved for the thankee, which is only required if we support GROUP chats. - // Otherwise, it can be inferred from the sender in a TWO_WAY chat. - reserved 3; -} -// Identity revealed content that is inserted into chat whenever a chat member -// reveals their identity -message IdentityRevealedContent { - // The chat member who revealed their identity - ChatMemberId member_id = 1; - // The identity that was revealed - ChatMemberIdentity identity = 2; -} -// Opaque cursor used across paged APIs. Underlying bytes may change as paging -// strategies evolve. Expected length value will vary based on the RPC being -// executed. -message Cursor { - bytes value = 1 ; -} -message IsTyping { - ChatMemberId member_id = 1; - // is_typing indicates whether or not the user is typing. - // If false, the user has explicitly stopped typing. - bool is_typing = 2; -} diff --git a/definitions/flipchat-vm/protos/src/main/proto/common/v1/code_model.proto b/definitions/flipchat-vm/protos/src/main/proto/common/v1/code_model.proto deleted file mode 100644 index 6d681311e..000000000 --- a/definitions/flipchat-vm/protos/src/main/proto/common/v1/code_model.proto +++ /dev/null @@ -1,134 +0,0 @@ -syntax = "proto3"; -package code.common.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/common/v1;common"; -option java_package = "com.codeinc.gen.common.v1"; -option objc_class_prefix = "CPBCommonV1"; -import "google/protobuf/duration.proto"; -import "google/protobuf/timestamp.proto"; - -// AccountType associates a type to an account, which infers how an account is used -// within the Code ecosystem. -enum AccountType { - UNKNOWN = 0; - PRIMARY = 1; - TEMPORARY_INCOMING = 2; - TEMPORARY_OUTGOING = 3; - BUCKET_1_KIN = 4; - BUCKET_10_KIN = 5; - BUCKET_100_KIN = 6; - BUCKET_1_000_KIN = 7; - BUCKET_10_000_KIN = 8; - BUCKET_100_000_KIN = 9; - BUCKET_1_000_000_KIN = 10; - LEGACY_PRIMARY_2022 = 11; - REMOTE_SEND_GIFT_CARD = 12; - RELATIONSHIP = 13; - SWAP = 14; -} -// SolanaAccountId is a raw binary Ed25519 public key for a Solana account -message SolanaAccountId { - bytes value = 1 ; -} -// InstructionAccount is an account public key used within the context of -// an instruction. -message InstructionAccount { - SolanaAccountId account = 1; - bool is_signer = 2; - bool is_writable = 3; -} -// Transaction is a raw binary Solana transaction -message Transaction { - // Maximum size taken from: https://github.com/solana-labs/solana/blob/39b3ac6a8d29e14faa1de73d8b46d390ad41797b/sdk/src/packet.rs#L9-L13 - bytes value = 1 ; -} -// Blockhash is a raw binary Solana blockchash -message Blockhash { - bytes value = 1 ; -} -// Signature is a raw binary Ed25519 signature -message Signature { - bytes value = 1 ; -} -// IntentId is a client-side generated ID that maps to an intent to perform actions -// on the blockchain fulfilled by the Code sequencer. -message IntentId { - bytes value = 1 ; -} -// UserId is a globally unique identifier for a user within Code -// -// Note: Users outside Code are modelled as relationship accounts -message UserId { - bytes value = 1 ; -} -// DataContainerId is a globally unique identifier for a container where a user -// can store a copy of their data -message DataContainerId { - bytes value = 1 ; -} -// DeviceToken is an opaque token used to verify whether a device real -message DeviceToken { - string value = 1 ; -} -// AppInstallId is a unque ID tied to a client app installation. It does not -// identify a device. Value should remain private and not be shared across -// installs. -message AppInstallId { - string value = 1 ; -} -// PhoneNumber is an E.164 phone number -message PhoneNumber { - // Regex provided by Twilio here: https://www.twilio.com/docs/glossary/what-e164#regex-matching-for-e164 - string value = 1; -} -// Domain is a hostname -message Domain { - string value = 1 ; -} -// Relationship is a set of identifiers that a user can establish a relationship -// with. -message Relationship { - oneof type { - common.v1.Domain domain = 1; - } -} -// Hash is a raw binary 32 byte hash value -message Hash { - bytes value = 1 ; -} -// Locale is a user locale consisting of a combination of language, script and region -message Locale { - string value = 1; -} -// UUID is a 16 byte UUID value -message UUID { - bytes value = 1 ; -} -// Request is a generic wrapper for gRPC requests -message Request { - string version = 1; - string service = 2; - string method = 3; - bytes body = 4; -} -// Response is a generic wrapper for gRPC responses -message Response { - Result result = 1; - bytes body = 2; - string message = 3; - enum Result { - OK = 0; - ERROR = 1; - } -} -message ServerPing { - // Timestamp the ping was sent on the stream, for client to get a sense - // of potential network latency - google.protobuf.Timestamp timestamp = 1; - // The delay server will apply before sending the next ping - google.protobuf.Duration ping_delay = 2; -} -message ClientPong { - // Timestamp the Pong was sent on the stream, for server to get a sense - // of potential network latency - google.protobuf.Timestamp timestamp = 1; -} diff --git a/definitions/flipchat-vm/protos/src/main/proto/contact/v1/code_contact_list_service.proto b/definitions/flipchat-vm/protos/src/main/proto/contact/v1/code_contact_list_service.proto deleted file mode 100644 index c28545e59..000000000 --- a/definitions/flipchat-vm/protos/src/main/proto/contact/v1/code_contact_list_service.proto +++ /dev/null @@ -1,106 +0,0 @@ -syntax = "proto3"; -package code.contact.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/contact/v1;contact"; -option java_package = "com.codeinc.gen.contact.v1"; -option objc_class_prefix = "CPBContactV1"; -import "common/v1/code_model.proto"; - -service ContactList { - // AddContacts adds a batch of contacts to a user's contact list - rpc AddContacts(AddContactsRequest) returns (AddContactsResponse); - // RemoveContacts removes a batch of contacts from a user's contact list - rpc RemoveContacts(RemoveContactsRequest) returns (RemoveContactsResponse); - // GetContacts gets a subset of contacts from a user's contact list - rpc GetContacts(GetContactsRequest) returns (GetContactsResponse); -} -message AddContactsRequest { - // The public key of the owner account that signed this request message. - common.v1.SolanaAccountId owner_account_id = 1; - // The signature is of serialize(AddContactsRequest) without this field set - // using the private key of owner_account_id. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 2; - // The data container for the copy of the contact list being added to. - common.v1.DataContainerId container_id = 3; - // The set of contacts to add to the contact list - repeated common.v1.PhoneNumber contacts = 4 ; - -} -message AddContactsResponse { - Result result = 1; - enum Result { - OK = 0; - } - // The contacts' current status keyed by phone number. This is an optimization - // so that clients can populate initial state without needing an extra network - // call. - map contact_status = 2; -} -message RemoveContactsRequest { - // The public key of the owner account that signed this request message. - common.v1.SolanaAccountId owner_account_id = 1; - // The signature is of serialize(RemoveContactsRequest) without this field - // set using the private key of owner_account_id. This provides an - // authentication mechanism to the RPC. - common.v1.Signature signature = 2; - // The data container for the copy of the contact list being removed from. - common.v1.DataContainerId container_id = 3; - // The set of contacts to remove from the contact list - repeated common.v1.PhoneNumber contacts = 4 ; -} -message RemoveContactsResponse { - Result result = 1; - enum Result { - OK = 0; - } -} -message GetContactsRequest { - // The public key of the owner account that signed this request message. - common.v1.SolanaAccountId owner_account_id = 1; - // The signature is of serialize(GetContactsRequest) without this field set - // using the private key of owner_account_id. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 2; - // The data container for the copy of the contact list being fetched. - common.v1.DataContainerId container_id = 3; - // The page token, which is retreived from a previous response, to get the next - // set of contacts. The first page is returned when not set. - PageToken page_token = 4; - // Filter out contacts that have an association with Code. This includes users - // that have both been invited and registered with the app. - bool include_only_in_app_contacts = 5; -} -message GetContactsResponse { - Result result = 1; - enum Result { - OK = 0; - } - // A page of contacts - repeated Contact contacts = 2; - // The page token to include in a subsequent request to get the next set of - // contacts. This will not be set for the last response in the list of - // pages. - PageToken next_page_token = 3; -} -message Contact { - // The contact's phone number - common.v1.PhoneNumber phone_number = 1; - // The contact's current status - ContactStatus status = 2; -} -message ContactStatus { - // Flag to indicate whether a user has registered with Code and used the app - // at least once. - bool is_registered = 1; - // Flag to indicate whether a user has been invited to Code. - // - // todo: This field will be deprecated after the invite phase is complete. - bool is_invited = 2; - // Flag to indicate whether a user's invitation to Code has been revoked. - // - // todo: This field will be deprecated after the invite phase is complete. - bool is_invite_revoked = 3; -} -message PageToken { - bytes value = 1 ; -} diff --git a/definitions/flipchat-vm/protos/src/main/proto/currency/v1/code_currency_service.proto b/definitions/flipchat-vm/protos/src/main/proto/currency/v1/code_currency_service.proto deleted file mode 100644 index 5458a224b..000000000 --- a/definitions/flipchat-vm/protos/src/main/proto/currency/v1/code_currency_service.proto +++ /dev/null @@ -1,29 +0,0 @@ -syntax = "proto3"; -package code.currency.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/currency/v1;currency"; -option java_package = "com.codeinc.gen.currency.v1"; -option objc_class_prefix = "CPBCurrencyV1"; - -import "google/protobuf/timestamp.proto"; -service Currency { - // GetAllRates returns the exchange rates for Kin against all available currencies - rpc GetAllRates(GetAllRatesRequest) returns (GetAllRatesResponse); -} -message GetAllRatesRequest { - // If timestamp is included, the returned rate will be the most recent available - // exchange rate prior to the provided timestamp within the same day. Otherwise, - // the latest rates will be returned. - google.protobuf.Timestamp timestamp = 1; -} -message GetAllRatesResponse { - Result result = 1; - enum Result { - OK = 0; - // No currency data is available for the requested timestamp. - MISSING_DATA = 1; - } - // The time the exchange rates were observed - google.protobuf.Timestamp as_of = 2; - // The price of 1 Kin in different currencies, keyed on 3- or 4- letter lowercase currency code. - map rates = 3 ; -} diff --git a/definitions/flipchat-vm/protos/src/main/proto/device/v1/code_device_service.proto b/definitions/flipchat-vm/protos/src/main/proto/device/v1/code_device_service.proto deleted file mode 100644 index 37e2daf74..000000000 --- a/definitions/flipchat-vm/protos/src/main/proto/device/v1/code_device_service.proto +++ /dev/null @@ -1,49 +0,0 @@ -syntax = "proto3"; -package code.device.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/device/v1;device"; -option java_package = "com.codeinc.gen.device.v1"; -option objc_class_prefix = "CPBDevicetV1"; -import "common/v1/code_model.proto"; - -service Device { - // RegisterLoggedInAccounts registers a set of owner accounts logged for - // an app install. Currently, a single login is enforced per app install. - // After using GetLoggedInAccounts to detect stale logins, clients can use - // this RPC to update the set of accounts with valid login sessions. - rpc RegisterLoggedInAccounts(RegisterLoggedInAccountsRequest) returns (RegisterLoggedInAccountsResponse); - // GetLoggedInAccounts gets the set of logged in accounts for an app install. - // Clients can use this RPC to detect stale logins for boot out of the app. - rpc GetLoggedInAccounts(GetLoggedInAccountsRequest) returns (GetLoggedInAccountsResponse); -} -message RegisterLoggedInAccountsRequest { - common.v1.AppInstallId app_install = 1; - // The set of owners logged into the app install. Setting an empty value - // indicates there are no logged in users. We allow for more than one owner - // in the spec with a repeated field to be flexible in the future. - repeated common.v1.SolanaAccountId owners = 2 ; - // Signature values must appear in the exact order their respecitive signing - // owner account appears in the owners field. All signatures should be generated - // without any other signature values set. - repeated common.v1.Signature signatures = 3 ; -} -message RegisterLoggedInAccountsResponse { - Result result = 1; - enum Result { - OK = 0; - INVALID_OWNER = 1; - } - // Set of invalid owner accounts detected in the request. An owner account - // can be invalid for several reasons: not phone verified, timelock account - // unlocked, etc. Value is set when result is INVALID_OWNER. - repeated common.v1.SolanaAccountId invalid_owners = 2 ; -} -message GetLoggedInAccountsRequest { - common.v1.AppInstallId app_install = 1; -} -message GetLoggedInAccountsResponse { - Result result = 1; - enum Result { - OK = 0; - } - repeated common.v1.SolanaAccountId owners = 2 ; -} diff --git a/definitions/flipchat-vm/protos/src/main/proto/invite/v2/code_invite_service.proto b/definitions/flipchat-vm/protos/src/main/proto/invite/v2/code_invite_service.proto deleted file mode 100644 index 19936594d..000000000 --- a/definitions/flipchat-vm/protos/src/main/proto/invite/v2/code_invite_service.proto +++ /dev/null @@ -1,91 +0,0 @@ -syntax = "proto3"; -package code.invite.v2; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/invite/v2;invite"; -option java_package = "com.codeinc.gen.invite.v2"; -option objc_class_prefix = "CPBInviteV2"; -import "common/v1/code_model.proto"; - -service Invite { - // GetInviteCount gets the number of invites that a user can send out. - rpc GetInviteCount(GetInviteCountRequest) returns (GetInviteCountResponse); - // InvitePhoneNumber invites someone to join via their phone number. A phone number - // can only be invited once by a unique user or invite code. This is to avoid having - // a phone number consuming more than one invite count globally. - rpc InvitePhoneNumber(InvitePhoneNumberRequest) returns (InvitePhoneNumberResponse); - // GetInvitationStatus gets a phone number's invitation status. - rpc GetInvitationStatus(GetInvitationStatusRequest) returns (GetInvitationStatusResponse); -} -message GetInviteCountRequest { - // The user to query for their invite count - common.v1.UserId user_id = 1; -} -message GetInviteCountResponse { - Result result = 1; - enum Result { - OK = 0; - } - // The number of invites the user is allowed to issue. - uint32 invite_count = 2; -} -message InvitePhoneNumberRequest { - // The source for the invite. One of these values must be present - oneof source { - common.v1.UserId user = 1; - InviteCode invite_code = 3; - } - // The phone number receiving the invite. - common.v1.PhoneNumber receiver = 2; -} -message InvitePhoneNumberResponse { - Result result = 1; - enum Result { - OK = 0; - // The source exceeded their invite count and is restricted from issuing - // further invites. - INVITE_COUNT_EXCEEDED = 1; - // The receiver phone number has already been invited. Regardless of who - // invited it, the source's invite count is not decremented when this is - // returned. - ALREADY_INVITED = 2; - // The source user has not been invited. - USER_NOT_INVITED = 3; - // The receiver phone number failed validation. - INVALID_RECEIVER_PHONE_NUMBER = 4; - // The invite code doesn't exist. - INVITE_CODE_NOT_FOUND = 5; - // The invite code has been revoked. - INVITE_CODE_REVOKED = 6; - // The invite code has expired. - INVITE_CODE_EXPIRED = 7; - } -} -message GetInvitationStatusRequest { - // The user being queried for their invitation status. - common.v1.UserId user_id = 1; -} -message GetInvitationStatusResponse { - Result result = 1; - enum Result { - OK = 0; - } - // The user's invitation status - InvitationStatus status = 2; -} -message InviteCode { - // Regex for invite codes - string value = 1 ; -} -message PageToken { - bytes value = 1 ; -} -enum InvitationStatus { - // The phone number has never been invited. - NOT_INVITED = 0; - // The phone number has been invited at least once. - INVITED = 1; - // The phone number has been invited and used the app at least once via a - // phone verified account creation or login. - REGISTERED = 2; - // The phone number was invited, but revoked at a later time. - REVOKED = 3; -} diff --git a/definitions/flipchat-vm/protos/src/main/proto/messaging/v1/code_messaging_service.proto b/definitions/flipchat-vm/protos/src/main/proto/messaging/v1/code_messaging_service.proto deleted file mode 100644 index e75005d68..000000000 --- a/definitions/flipchat-vm/protos/src/main/proto/messaging/v1/code_messaging_service.proto +++ /dev/null @@ -1,330 +0,0 @@ -syntax = "proto3"; -package code.messaging.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/messaging/v1;messaging"; -option java_package = "com.codeinc.gen.messaging.v1"; -option objc_class_prefix = "CPBMessagingV1"; -import "common/v1/code_model.proto"; -import "transaction/v2/code_transaction_service.proto"; - -import "google/protobuf/timestamp.proto"; -service Messaging { - // OpenMessageStream opens a stream of messages. Messages are routed using the - // public key of a rendezvous keypair derived by both the sender and the - // recipient of the messages. The sender may be a client or server. - // - // Messages are expected to be acked once they have been processed by the client. - // Ack'd messages will no longer be delivered on future OpenMessageStream calls, - // and are eligible for deletion from the service. Clients should, however, handle - // duplicate delivery of messages. - // - // For grabbing a bill, the expected flow is as follows: - // 1. The payment sender creates a cash scan code - // 2. The payment sender calls OpenMessageStream on the rendezvous public key, which is - // derived by using sha256(scan payload) as the keypair seed. - // 3. The payment recipient scans the code and uses SendMessage to send their account ID - // back to the sender via the rendezvous public key. - // 4. The payment sender receives the message, submits the intent, and closes the stream. - // - // For receiving a bill of requested value, the expected flow is as follows: - // 1. The payment recipient uses SendMessage to send their account ID and payment amount to - // the sender via the rendezvous public key, which is derived by using sha256(scan payload) - // as the keypair seed. - // 2. The payment recipient calls OpenMessageStream on the rendezvous public key to listen - // for status messages generated by client/server. It must ignore the original message it sent - // as part of step 1. - // 3. The payment recipient creates a payment request scan code - // 4. The payment sender calls PollMessages on the rendezvous public key. This is ok because - // we know the message exists per step 1, and doesn't actually incur a long poll. This is a - // required hack because we don't have the infrastructure in place to allow multiple listens - // on the same stream, and the recipient needs real-time status updates. - // 5. The payment sender receives the message (any status messages are ignored), and submits the - // intent. - // 6. The payment recipient observes status message (eg. IntentSubmitted, ClientRejectedPayment, - // WebhookCalled) for payment state. - // 7. The payment recipient closes the stream once the payment hits a terminal state, or times out. - // - // For logging in, the expected flow is as follows: - // 1. The third party uses SendMessage to send their login challenge to the user via the rendezvous - // public key, which is derived by using sha256(scan payload) as the keypair seed. - // 2. The third party calls OpenMessageStream on the rendezvous public key to listen for status - // messages generated by server. It must ignore the original message it sent as part of step 1. - // 3. The third party creates a login scan code - // 4. The user logging in calls PollMessages on the rendezvous public key. This is ok because - // we know the message exists per step 1, and doesn't actually incur a long poll. This is a - // required hack because we don't have the infrastructure in place to allow multiple listens - // on the same stream, and the recipient needs real-time status updates. - // 5. The user logging in receives the message (any status messages are ignored), verifies it, - // then submits a login attempt. - // 6. The third party observes status message (eg. IntentSubmitted, ClientRejectedLogin, - // WebhookCalled) for login state. - // 7. The third party closes the stream once the login hits a terminal state, or times out. - rpc OpenMessageStream(OpenMessageStreamRequest) returns (stream OpenMessageStreamResponse); - // OpenMessageStreamWithKeepAlive is like OpenMessageStream, but enables a ping/pong - // keepalive to determine the health of the stream at both the client and server. - // - // The keepalive protocol is as follows: - // 1. Client initiates a stream by sending an OpenMessageStreamRequest. - // 2. Upon stream initialization, server begins the keepalive protocol. - // 3. Server sends a ping to the client. - // 4. Client responds with a pong as fast as possible, making note of - // the delay for when to expect the next ping. - // 5. Steps 3 and 4 are repeated until the stream is explicitly terminated - // or is deemed to be unhealthy. - // - // Client notes: - // * Client should be careful to process messages async, so any responses to pings are - // not delayed. - // * Clients should implement a reasonable backoff strategy upon continued timeout failures. - // * Clients that abuse pong messages may have their streams terminated by server. - // - // At any point in the stream, server will respond with messages in real time as - // they are observed. Messages sent over the stream should not affect the ping/pong - // protocol timings. Individual protocols for payment flows remain the same, and are - // documented in OpenMessageStream. - // - // Note: This API will enforce OpenMessageStreamRequest.signature is set as part of migration - // to this newer protocol - rpc OpenMessageStreamWithKeepAlive(stream OpenMessageStreamWithKeepAliveRequest) returns (stream OpenMessageStreamWithKeepAliveResponse); - // PollMessages is like OpenMessageStream, but uses a polling flow for receiving - // messages. Updates are not real-time and depedent on the polling interval. - // This RPC supports all message types. - // - // This is a temporary RPC until OpenMessageStream can be built out generically on - // both client and server, while supporting things like multiple listeners. - rpc PollMessages(PollMessagesRequest) returns (PollMessagesResponse); - // AckMessages acks one or more messages that have been successfully delivered to - // the client. - rpc AckMessages(AckMessagesRequest) returns (AckMesssagesResponse); - // SendMessage sends a message. - rpc SendMessage(SendMessageRequest) returns (SendMessageResponse); -} -message OpenMessageStreamRequest { - RendezvousKey rendezvous_key = 1; - // The signature is of serialize(OpenMessageStreamRequest) using rendezvous_key. - // - // todo: Make required once clients migrate - common.v1.Signature signature = 2; -} -message OpenMessageStreamResponse { - repeated Message messages = 1 ; -} -message OpenMessageStreamWithKeepAliveRequest { - oneof request_or_pong { - OpenMessageStreamRequest request = 1; - common.v1.ClientPong pong = 2; - } -} -message OpenMessageStreamWithKeepAliveResponse { - oneof response_or_ping { - OpenMessageStreamResponse response = 1; - common.v1.ServerPing ping = 2; - } -} -message PollMessagesRequest { - RendezvousKey rendezvous_key = 1; - // The signature is of serialize(PollMessagesRequest) using rendezvous_key. - common.v1.Signature signature = 2; -} -message PollMessagesResponse { - repeated Message messages = 1 ; -} -message AckMessagesRequest { - RendezvousKey rendezvous_key = 1; - repeated MessageId message_ids = 2 ; -} -message AckMesssagesResponse { - Result result = 1; - enum Result { - OK = 0; - } -} -message SendMessageRequest { - // The message to send. Types of messages clients can send are restricted. - Message message = 1; - // The rendezvous key that the message should be routed to. - RendezvousKey rendezvous_key = 2; - // The signature is of serialize(Message) using the PrivateKey of the keypair. - common.v1.Signature signature = 3; -} -message SendMessageResponse { - Result result = 1; - enum Result { - OK = 0; - NO_ACTIVE_STREAM = 1; - } - // Set if result == OK. - MessageId message_id = 2; -} -// RendezvousKey is a unique key pair, typically derived from a scan code payload, -// which is used to establish a secure communication channel anonymously to coordinate -// a flow using messages. -message RendezvousKey { - bytes value = 1 ; -} -// MessageId identifies a message. It is only guaranteed to be unique when -// paired with a destination (i.e. the rendezvous public key). -message MessageId { - bytes value = 1 ; -} -// Request that a pulled out bill be sent to the requested address. -// -// This message type is only initiated by clients. -message RequestToGrabBill { - // Requestor is the Kin token account on Solana to which a payment should be sent. - common.v1.SolanaAccountId requestor_account = 1; -} -// Request that a bill of a requested value is created and sent to the requested -// address. -// -// This message type is only initiated by clients. -message RequestToReceiveBill { - // Requestor is the Kin token account on Solana to which a payment should be sent. - common.v1.SolanaAccountId requestor_account = 1; - // The exchange data for the requested bill value. - oneof exchange_data { - // An exact amount of Kin. Payment is guaranteed to transfer the specified - // quarks in the requested currency and exchange rate. - // - // Only supports Kin. Use exchange_data.partial for fiat amounts. - transaction.v2.ExchangeData exact = 2; - // Fiat amount request. The amount of Kin is determined at time of payment - // with a recent exchange rate provided by the paying client and validatd - // by server. - // - // Only supports fiat amounts. Use exchange_data.exact for Kin. - transaction.v2.ExchangeDataWithoutRate partial = 3; - } - // - // Optional fields below to identify a domain requesting to receive a bill. - // Verification of the domain is optional. When verified, clients can establish - // relationships and third parties will by able to identify users with that - // account after payment is made. - // - // Note on field requirements: - // - Verified: All of domain, verifier, signature and rendezvous_key are required - // - Unverified: Only domain is requried - // - // The third-party's domain name, which is its primary identifier. Server - // guarantees to perform domain verification against the verifier account. - common.v1.Domain domain = 4; - // Owner account owned by the third party used in domain verification. - common.v1.SolanaAccountId verifier = 5; - // Signature of this message using the verifier private key, which in addition - // to domain verification, authenticates the third party. - common.v1.Signature signature = 6; - // Rendezvous key to avoid replay attacks - RendezvousKey rendezvous_key = 7; - // Additional fee payments splitting the requested amount. This is in addition - // to the hard-coded Code $0.01 USD fee. - repeated transaction.v2.AdditionalFeePayment additional_fees = 8; -} -// A status update on a stream to indicate a scan code was scanned. This can appear -// multiple times for the same stream. -// -// This message type is only initiated by client -message CodeScanned { - // Timestamp the client scanned the code - google.protobuf.Timestamp timestamp = 1; -} -// Payment is rejected by the client -// -// This message type is only initiated by clients -message ClientRejectedPayment { - common.v1.IntentId intent_id = 1; -} -// Intent was submitted via SubmitIntent -// -// This message type is only initiated by server -message IntentSubmitted { - common.v1.IntentId intent_id = 1; - // Metadata is available for intents where it can be safely propagated publicly. - // Anything else requires an additional authenticated RPC call (eg. login). - transaction.v2.Metadata metadata = 2; -} -// Webhook was successfully called -// -// This message type is only initiated by server -message WebhookCalled { - // Estimated time webhook was received - google.protobuf.Timestamp timestamp = 1; -} -// Request that an account logs in -// -// This message type is only initiated by third-parties through the SDK. -message RequestToLogin { - // The third-party's domain name, which is its primary identifier. Server - // guarantees to perform domain verification against the verifier account. - // - // Clients should expect subdomains for future feature compatiblity, but must - // use the ASCII base domain in the RELATIONSHIP account derivation strategy. - common.v1.Domain domain = 1; - // Deprecated nonce value, which is replaced by the rendezvous_key field which - // is effectively derived off a random nonce. - reserved 2; - // Reserved for a timestamp field, which may be used in the future. - reserved 3; - // Owner account owned by the third party used in domain verification. - common.v1.SolanaAccountId verifier = 4; - // Signature of this message using the verifier private key, which in addition - // to domain verification, authenticates the third party. - common.v1.Signature signature = 5; - // Rendezvous key to avoid replay attacks - RendezvousKey rendezvous_key = 6; -} -// Login is rejected by the client -// -// This message type is only initiated by user clients -message ClientRejectedLogin { - // Timestamp the login was rejected - google.protobuf.Timestamp timestamp = 4; -} -// Client has received an aidrop from server -// -// This message type is only initiated by server. -message AirdropReceived { - // The type of airdrop received - transaction.v2.AirdropType airdrop_type = 1; - // Exchange data relating to the amount of Kin and fiat value of the airdrop - transaction.v2.ExchangeData exchange_data = 2; - // Time the airdrop was received - google.protobuf.Timestamp timestamp = 3; -} -message Message { - // MessageId is the Id of the message. This ID is generated by the - // server, and will _always_ be set when receiving a message. - // - // Server generates the message to: - // 1. Reserve the ability for any future ID changes - // 2. Prevent clients attempting to collide message IDs. - MessageId id = 1; - // The signature sent from SendMessageRequest, which will be injected by server. - // This enables clients to ensure no MITM attacks were performed to hijack contents - // of the typed message. This is only applicable for messages not generated by server. - common.v1.Signature send_message_request_signature = 3; - // Next field number is 13 - oneof kind { - // - // Section: Cash - // - RequestToGrabBill request_to_grab_bill = 2; - // - // Section: Payment Requests - // - RequestToReceiveBill request_to_receive_bill = 5; - CodeScanned code_scanned = 6; - ClientRejectedPayment client_rejected_payment = 7; - IntentSubmitted intent_submitted = 8; - WebhookCalled webhook_called = 9; - // - // Section: Login - // - RequestToLogin request_to_login = 10; - ClientRejectedLogin client_rejected_login = 12; - // - // Section: Airdrops - // - AirdropReceived airdrop_received = 4; - } - // Reserved for deprecated LoginAttempt field - reserved 11; -} diff --git a/definitions/flipchat-vm/protos/src/main/proto/micropayment/v1/code_micro_payment_service.proto b/definitions/flipchat-vm/protos/src/main/proto/micropayment/v1/code_micro_payment_service.proto deleted file mode 100644 index 68a38719e..000000000 --- a/definitions/flipchat-vm/protos/src/main/proto/micropayment/v1/code_micro_payment_service.proto +++ /dev/null @@ -1,107 +0,0 @@ -syntax = "proto3"; -package code.micropayment.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/micropayment/v1;micropayment"; -option java_package = "com.codeinc.gen.micropayment.v1"; -option objc_class_prefix = "APBMicroPaymentV1"; -import "common/v1/code_model.proto"; - -// todo: Migrate this to a generic "request" service -service MicroPayment { - // GetStatus gets basic request status - rpc GetStatus(GetStatusRequest) returns (GetStatusResponse); - // RegisterWebhook registers a webhook for a request - // - // todo: Once Kik codes can encode the entire payment request details, we can - // remove the messaging service component and have a Create RPC that - // reserves the intent ID with payment details, plus registers the webhook - // at the same time. Until that's possible, we're stuck with two RPC calls. - rpc RegisterWebhook(RegisterWebhookRequest) returns (RegisterWebhookResponse); - // Codify adds a trial micro paywall to any URL - rpc Codify(CodifyRequest) returns (CodifyResponse); - // GetPathMetadata gets codified website metadata for a given path - // - // Important Note: This RPC's current implementation is insecure and - // it's sole design is to enable PoC and trials. - rpc GetPathMetadata(GetPathMetadataRequest) returns (GetPathMetadataResponse); -} -message GetStatusRequest { - common.v1.IntentId intent_id = 1; -} -message GetStatusResponse { - // Does the payment request exist? - bool exists = 1; - // Has the user scanned the code at least once? - bool code_scanned = 2; - // Has the user sumbmitted a payment? - bool intent_submitted = 3; -} -message RegisterWebhookRequest { - common.v1.IntentId intent_id = 1; - string url = 2 ; -} -message RegisterWebhookResponse { - Result result = 1; - enum Result { - OK = 0; - // A webhook has already been registered - ALREADY_REGISTERED = 1; - // A request does not exist for the provided intent ID - REQUEST_NOT_FOUND = 2; - // A user has already submitted a payment - INTENT_EXISTS = 3; - // The webhook URL is invalid - INVALID_URL = 4; - } -} -message CodifyRequest { - // The URL to Codify - string url = 1 ; - // ISO 4217 alpha-3 currency code the payment should be made in - string currency = 2; - // The amount that should be paid in the native currency - double native_amount = 3; - // The verified owner account public key - common.v1.SolanaAccountId owner_account = 4; - // The primary account public key where payment will be sent - common.v1.SolanaAccountId primary_account = 5; -; - // The signature is of serialize(CodifyRequest) without this field set using the - // private key of the owner account. This provides an authentication mechanism - // to the RPC and can be used to validate payment details. - common.v1.Signature signature = 6; -} -message CodifyResponse { - Result result = 1; - enum Result { - OK = 0; - // The URL to Codify is invalid - INVALID_URL = 1; - // The primary account is invalid - INVALID_ACCOUNT = 2; - // The currency isn't supported for micro payments - UNSUPPORTED_CURRENCY = 3; - // The payment amount exceeds the minimum/maximum allowed amount - NATIVE_AMOUNT_EXCEEDS_LIMIT = 4; - } - // The URL to view the content with a Code micro paywall - string codified_url = 2; -} -message GetPathMetadataRequest { - string path = 1; -} -message GetPathMetadataResponse { - Result result = 1; - enum Result { - OK = 0; - NOT_FOUND = 1; - } - // The account where the payment should be sent to - common.v1.SolanaAccountId destination = 2; - - // ISO 4217 alpha-3 currency code the payment should be made in - string currency = 3; - // The amount that should be paid in the native currency - double native_amount = 4; - // The URL to redirect upon successful payment - string redirct_url = 5; -} diff --git a/definitions/flipchat-vm/protos/src/main/proto/phone/v1/code_phone_verification_service.proto b/definitions/flipchat-vm/protos/src/main/proto/phone/v1/code_phone_verification_service.proto deleted file mode 100644 index e82bde7dc..000000000 --- a/definitions/flipchat-vm/protos/src/main/proto/phone/v1/code_phone_verification_service.proto +++ /dev/null @@ -1,117 +0,0 @@ -syntax = "proto3"; -package code.phone.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/phone/v1;phone"; -option java_package = "com.codeinc.gen.phone.v1"; -option objc_class_prefix = "CPBPhoneV1"; -import "common/v1/code_model.proto"; - -service PhoneVerification { - // SendVerificationCode sends a verification code to the provided phone number - // over SMS. If an active verification is already taking place, the existing code - // will be resent. - rpc SendVerificationCode(SendVerificationCodeRequest) returns (SendVerificationCodeResponse); - // CheckVerificationCode validates a verification code. On success, a one-time use - // token to link an owner account is provided. - rpc CheckVerificationCode(CheckVerificationCodeRequest) returns (CheckVerificationCodeResponse); - // GetAssociatedPhoneNumber gets the latest verified phone number linked to an owner account. - rpc GetAssociatedPhoneNumber(GetAssociatedPhoneNumberRequest) returns (GetAssociatedPhoneNumberResponse); -} -message SendVerificationCodeRequest { - // The phone number to send a verification code over SMS to. - common.v1.PhoneNumber phone_number = 1; - // Device token for antispam measures against fake devices - common.v1.DeviceToken device_token = 2; -} -message SendVerificationCodeResponse { - Result result = 1; - enum Result { - OK = 0; - // The phone number is not invited and cannot use Code. The SMS will not - // be sent until the user is invited. This result is only valid during - // the invitation stage of the application and won't apply after general - // public release. - NOT_INVITED = 1; - // SMS is rate limited (eg. by IP, phone number, etc) and was not sent. - // These will be set generously such that real users won't actually hit - // the limits. - RATE_LIMITED = 2; - // The phone number is not real because it fails Twilio lookup. - INVALID_PHONE_NUMBER = 3; - // The phone number is valid, but it maps to an unsupported type of phone - // like a landline or eSIM. - UNSUPPORTED_PHONE_TYPE = 4; - // The country associated with the phone number is not supported (eg. it - // is on the sanctioned list). - UNSUPPORTED_COUNTRY = 5; - // The device is not supported (eg. it fails device attestation checks) - UNSUPPORTED_DEVICE = 6; - } -} -message CheckVerificationCodeRequest { - // The phone number being verified. - common.v1.PhoneNumber phone_number = 1; - // The verification code received via SMS. - VerificationCode code = 2; -} -message CheckVerificationCodeResponse { - Result result = 1; - enum Result { - OK = 0; - // The provided verification code is invalid. The user may retry - // enterring the code if this is received. When max attempts are - // received, NO_VERIFICATION will be returned. - INVALID_CODE = 1; - // There is no verification in progress for the phone number. Several - // reasons this can occur include a verification being expired or having - // reached a maximum check threshold. The client must initiate a new - // verification using SendVerificationCode. - NO_VERIFICATION = 2; - // The call is rate limited (eg. by IP, phone number, etc). The code is - // not verified. - RATE_LIMITED = 3; - } - // The token used to associate an owner account to a user using the verified - // phone number. - PhoneLinkingToken linking_token = 2; -} -message GetAssociatedPhoneNumberRequest { - // The public key of the owner account that is being queried for a linked - // phone number. - common.v1.SolanaAccountId owner_account_id = 1; - // The signature is of serialize(GetAssociatedPhoneNumberRequest) without - // this field set using the private key of owner_account_id. This provides - // an authentication mechanism to the RPC. - common.v1.Signature signature = 2; -} -message GetAssociatedPhoneNumberResponse { - Result result = 1; - enum Result { - OK = 0; - // A phone number is not associated with the provided owner account. - NOT_FOUND = 1; - // The phone number exists, but is no longer invited - NOT_INVITED = 2; - // The phone number exists, but at least one timelock account is unlocked - UNLOCKED_TIMELOCK_ACCOUNT = 3; - } - // The latest phone number associated with the owner account. - common.v1.PhoneNumber phone_number = 2; - // State that determines whether a phone number is linked to the owner - // account. A phone number is linked if we can treat it as an alias. - // This is notably different from association, which answers the question - // of whether the number was linked at any point in time. - bool is_linked = 3; -} -message VerificationCode { - // A 4-10 digit numerical code. - string value = 2 ; -} -// A one-time use token that can be provided to the Identity service to link an -// owner account to a user with the verified phone number. The client should -// treat this token as opaque. -message PhoneLinkingToken { - // The verified phone number. - common.v1.PhoneNumber phone_number = 1; - // The code that verified the phone number. - VerificationCode code = 2; -} diff --git a/definitions/flipchat-vm/protos/src/main/proto/push/v1/code_push_service.proto b/definitions/flipchat-vm/protos/src/main/proto/push/v1/code_push_service.proto deleted file mode 100644 index c054b730a..000000000 --- a/definitions/flipchat-vm/protos/src/main/proto/push/v1/code_push_service.proto +++ /dev/null @@ -1,74 +0,0 @@ -syntax = "proto3"; -package code.push.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/push/v1;push"; -option java_package = "com.codeinc.gen.push.v1"; -option objc_class_prefix = "APBPushV1"; -import "common/v1/code_model.proto"; - -service Push { - // AddToken stores a push token in a data container. The call is idempotent - // and adding an existing valid token will not fail. Token types will be - // validated against the user agent and any mismatches will result in an - // INVALID_ARGUMENT status error. - // - // The token will be unlinked from any and all other accounts that it was - // previously bound to. - rpc AddToken(AddTokenRequest) returns (AddTokenResponse); - // RemoveToken removes the provided push token from the account. - // - // The provided token must be bound to the current account. - // Otherwise, the RPC will succeed with without removal. - rpc RemoveToken(RemoveTokenRequest) returns (RemoveTokenResponse); -} -enum TokenType { - UNKNOWN = 0; - // FCM registration token for an Android device - FCM_ANDROID = 1; - // FCM registration token or an iOS device - FCM_APNS = 2; -} -message AddTokenRequest { - // The public key of the owner account that signed this request message. - common.v1.SolanaAccountId owner_account_id = 1; - // The signature is of serialize(AddTokenRequest) without this field set - // using the private key of owner_account_id. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 2; - // The data container where the push token will be stored. - common.v1.DataContainerId container_id = 3; - // The push token to store - string push_token = 4 ; - // The type of push token - TokenType token_type = 5; - // The instance of the app install where the push token was generated. Ideally, - // the push token is unique to the install. - common.v1.AppInstallId app_install = 6; -} -message AddTokenResponse { - Result result = 1; - enum Result { - OK = 0; - // The push token is invalid and wasn't stored. - INVALID_PUSH_TOKEN = 1; - } -} -message RemoveTokenRequest { - // The public key of the owner account that signed this request message. - common.v1.SolanaAccountId owner_account_id = 1; - // The signature is of serialize(AddTokenRequest) without this field set - // using the private key of owner_account_id. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 2; - // The data container where the push token was stored. - common.v1.DataContainerId container_id = 3; - // The push token to remove. - string push_token = 4 ; - // The type of push token to remove. - TokenType token_type = 5; -} -message RemoveTokenResponse { - Result result = 1; - enum Result { - OK = 0; - } -} diff --git a/definitions/flipchat-vm/protos/src/main/proto/transaction/v2/code_transaction_service.proto b/definitions/flipchat-vm/protos/src/main/proto/transaction/v2/code_transaction_service.proto deleted file mode 100644 index 9f5b74883..000000000 --- a/definitions/flipchat-vm/protos/src/main/proto/transaction/v2/code_transaction_service.proto +++ /dev/null @@ -1,1036 +0,0 @@ -syntax = "proto3"; -package code.transaction.v2; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2;transaction"; -option java_package = "com.codeinc.gen.transaction.v2"; -option objc_class_prefix = "APBTransactionV2"; -import "common/v1/code_model.proto"; -import "google/protobuf/any.proto"; -import "google/protobuf/timestamp.proto"; - -service Transaction { - // SubmitIntent is the mechanism for client and server to agree upon a set of - // client actions to execute on the blockchain using the Code sequencer for - // fulfillment. - // - // Transactions and virtual instructions are never exchanged between client and server. - // Instead, the required accounts and arguments for instructions known to each actor are - // exchanged to allow independent and local construction. - // - // Client and server are expected to fully validate the intent. Proofs will - // be provided for any parameter requiring one. Signatures should only be - // generated after approval. - // - // This RPC is not a traditional streaming endpoint. It bundles two unary calls - // to enable DB-level transaction semantics. - // - // The high-level happy path flow for the RPC is as follows: - // 1. Client initiates a stream and sends SubmitIntentRequest.SubmitActions - // 2. Server validates the intent, its actions and metadata - // 3a. If there are transactions or virtual instructions requiring the user's signature, - // then server returns SubmitIntentResponse.ServerParameters - // 3b. Otherwise, server returns SubmitIntentResponse.Success and closes the - // stream - // 4. For each transaction or virtual instruction requiring the user's signature, the client - // locally constructs it, performs validation and collects the signature - // 5. Client sends SubmitIntentRequest.SubmitSignatures with the signature - // list generated from 4 - // 6. Server validates all signatures are submitted and are the expected values - // using locally constructed transactions or virtual instructions. - // 7. Server returns SubmitIntentResponse.Success and closes the stream - // In the error case: - // * Server will return SubmitIntentResponse.Error and close the stream - // * Client will close the stream - rpc SubmitIntent(stream SubmitIntentRequest) returns (stream SubmitIntentResponse); - // GetIntentMetadata gets basic metadata on an intent. It can also be used - // to fetch the status of submitted intents. Metadata exists only for intents - // that have been successfully submitted. - rpc GetIntentMetadata(GetIntentMetadataRequest) returns (GetIntentMetadataResponse); - // GetPrivacyUpgradeStatus gets the status of a private transaction and the - // ability to upgrade it to permanent privacy. - rpc GetPrivacyUpgradeStatus(GetPrivacyUpgradeStatusRequest) returns (GetPrivacyUpgradeStatusResponse); - // GetPrioritizedIntentsForPrivacyUpgrade allows clients to get private - // intent actions that can be upgraded in a secure and verifiable manner. - rpc GetPrioritizedIntentsForPrivacyUpgrade(GetPrioritizedIntentsForPrivacyUpgradeRequest) returns (GetPrioritizedIntentsForPrivacyUpgradeResponse); - // GetLimits gets limits for money moving intents for an owner account in an - // identity-aware manner - rpc GetLimits(GetLimitsRequest) returns (GetLimitsResponse); - // CanWithdrawToAccount provides hints to clients for submitting withdraw intents. - // The RPC indicates if a withdrawal is possible, and how it should be performed. - rpc CanWithdrawToAccount(CanWithdrawToAccountRequest) returns (CanWithdrawToAccountResponse); - // Airdrop airdrops Kin to the requesting account - rpc Airdrop(AirdropRequest) returns (AirdropResponse); - // Swap performs an on-chain swap. The high-level flow mirrors SubmitIntent - // closely. However, due to the time-sensitive nature and unreliability of - // swaps, they do not fit within the broader intent system. This results in - // a few key differences: - // * Transactions are submitted on a best-effort basis outside of the Code - // Sequencer within the RPC handler - // * Balance changes are applied after the transaction has finalized - // * Transactions use recent blockhashes over a nonce - // SubmitIntent also operates on VM virtual instructions, whereas Swap uses - // Solana transactions. - // - // The transaction will have the following instruction format: - // 1. ComputeBudget::SetComputeUnitLimit - // 2. ComputeBudget::SetComputeUnitPrice - // 3. SwapValidator::PreSwap - // 4. Dynamic swap instruction - // 5. SwapValidator::PostSwap - // - // Note: Currently limited to swapping USDC to Kin. - // Note: Kin is deposited into the token account derived from the VM deposit PDA of the owner account. - rpc Swap(stream SwapRequest) returns (stream SwapResponse); - // DeclareFiatOnrampPurchaseAttempt is called whenever a user attempts to use a fiat - // onramp to purchase crypto for use in Code. - rpc DeclareFiatOnrampPurchaseAttempt(DeclareFiatOnrampPurchaseAttemptRequest) returns (DeclareFiatOnrampPurchaseAttemptResponse); -} -// -// Request and Response Definitions -// -message SubmitIntentRequest { - oneof request { - SubmitActions submit_actions = 1; - SubmitSignatures submit_signatures = 2; - } - message SubmitActions { - // The globally unique client generated intent ID. Use the original intent - // ID when operating on actions that mutate the intent. - common.v1.IntentId id = 1; - - // The verified owner account public key - common.v1.SolanaAccountId owner = 2; - // Additional metadata that describes the high-level intention - Metadata metadata = 3; - // The set of all ordered actions required to fulfill the intent - repeated Action actions = 4 ; - // The signature is of serialize(SubmitActions) without this field set using the - // private key of the owner account. This provides an authentication mechanism - // to the RPC. - common.v1.Signature signature = 5; - // Device token for antispam measures against fake devices - common.v1.DeviceToken device_token = 6; - } - message SubmitSignatures { - // The set of all signatures for each transaction or virtual instruction requiring - // signature from the authority accounts. - // - // The signature for a transaction is for the marshalled transaction. - // The signature for a virtual instruction is the hash of the marshalled instruction. - repeated common.v1.Signature signatures = 1 ; - } -} -message SubmitIntentResponse { - oneof response { - ServerParameters server_parameters = 1; - Success success = 2; - Error error = 3; - } - message ServerParameters { - // The set of all server paremeters required to fill missing transaction - // or virtual instruction details. Server guarantees to provide a message - // for each client action in an order consistent with the received action - // list. - repeated ServerParameter server_parameters = 1 ; - } - message Success { - Code code = 1; - enum Code { - // The intent was successfully created and is now scheduled. - OK = 0; - } - // todo: Revisit if we need side-effects. Clients are effecitively doing - // local simulation now with the privacy solution. - } - message Error { - Code code = 1; - enum Code { - // Denied by a guard (spam, money laundering, etc) - DENIED = 0; - // The intent is invalid. - INVALID_INTENT = 1; - // There is an issue with provided signatures. - SIGNATURE_ERROR = 2; - // Server detected client has stale state. - STALE_STATE = 3; - } - repeated ErrorDetails error_details = 2; - } -} -message GetIntentMetadataRequest { - // The intent ID to query - common.v1.IntentId intent_id = 1; - // The verified owner account public key when not signing with the rendezvous - // key. Only owner accounts involved in the intent can access the metadata. - common.v1.SolanaAccountId owner = 2; - // The signature is of serialize(GetIntentStatusRequest) without this field set - // using the private key of the rendezvous or owner account. This provides an - // authentication mechanism to the RPC. - common.v1.Signature signature = 3; -} -message GetIntentMetadataResponse { - Result result = 1; - enum Result { - OK = 0; - NOT_FOUND = 1; - } - Metadata metadata = 2; -} -message GetPrivacyUpgradeStatusRequest { - // The intent ID - common.v1.IntentId intent_id = 1; - // The action ID for private transfer - uint32 action_id = 2; -} -message GetPrivacyUpgradeStatusResponse { - Result result = 1; - enum Result { - OK = 0; - // The provided intent ID doesn't exist - INTENT_NOT_FOUND = 1; - // The provided action ID doesn't exist - ACTION_NOT_FOUND = 2; - // The provided action doesn't map to a private transfer - INVALID_ACTION = 3; - } - Status status = 2; - enum Status { - UNKNOWN = 0; - // The action for the temporary private transfer was submitted and - // finalized. The opportunity to upgrade was missed. - TEMPORARY_ACTION_FINALIZED = 1; - // The next block of private transfers hasn't been created, so there - // is no proof available. Wait and try again later. - WAITING_FOR_NEXT_BLOCK = 2; - // The action can be upgraded to permanent privacy - READY_FOR_UPGRADE = 3; - // The action has already been upgraded - ALREADY_UPGRADED = 4; - } -} -message GetPrioritizedIntentsForPrivacyUpgradeRequest { - // The owner account to query against for upgradeable intents. - common.v1.SolanaAccountId owner = 1; - // The maximum number of intents to return in the response. Default is 10. - uint32 limit = 2; - // The signature is of serialize(GetPrioritizedIntentsForPrivacyUpgradeRequest) - // without this field set using the private key of the owner account. This - // provides an authentication mechanism to the RPC. - common.v1.Signature signature = 3; -} -message GetPrioritizedIntentsForPrivacyUpgradeResponse { - Result result = 1; - enum Result { - OK = 0; - NOT_FOUND = 1; - } - // Ordered from highest to lowest priority - repeated UpgradeableIntent items = 2 ; -} -message GetLimitsRequest { - // The owner account whose limits will be calculated. Any other owner accounts - // linked with the same identity of the owner will also be applied. - common.v1.SolanaAccountId owner = 1; - // The signature is of serialize(GetLimitsRequest) without this field set - // using the private key of the owner account. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 2; - // All transactions starting at this time will be incorporated into the consumed - // limit calculation. Clients should set this to the start of the current day in - // the client's current time zone (because server has no knowledge of this atm). - google.protobuf.Timestamp consumed_since = 3; -} -message GetLimitsResponse { - Result result = 1; - enum Result { - OK = 0; - } - // Send limits keyed by currency - map send_limits_by_currency = 2; - // Deposit limits - DepositLimit deposit_limit = 3; - // Micro payment limits keyed by currency - map micro_payment_limits_by_currency = 4; - // Buy module limits keyed by currency - map buy_module_limits_by_currency = 5; -} -message CanWithdrawToAccountRequest { - common.v1.SolanaAccountId account = 1; -} -message CanWithdrawToAccountResponse { - // Metadata so the client knows how to withdraw to the account. Server cannot - // provide precalculated addresses in this response to maintain non-custodial - // status. - AccountType account_type = 2; - enum AccountType { - Unknown = 0; // Server cannot determine - TokenAccount = 1; // Client uses the address as is in SubmitIntent - OwnerAccount = 2; // Client locally derives the ATA to use in SubmitIntent - } - // Server-controlled flag to indicate if the account can be withdrawn to. - // There are several reasons server may deny it, including: - // - Wrong type of Code account - // - Not wanting to subsidize the creation of an ATA - // - Unsupported external account type (eg. token account but of the wrong mint) - // This is guaranteed to be false when account_type = Unknown. - bool is_valid_payment_destination = 1; - // Token account requires initialization before the withdrawal can occur. - // Server has chosen not to subsidize the fees. The response is guaranteed - // to have set is_valid_payment_destination = false in this case. - bool requires_initialization = 3; -} -message AirdropRequest { - // The type of airdrop to claim - AirdropType airdrop_type = 1 ; - // The owner account to airdrop Kin to - common.v1.SolanaAccountId owner = 2; - // The signature is of serialize(AirdropRequest) without this field set - // using the private key of the owner account. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 3; -} -message AirdropResponse { - Result result = 1; - enum Result { - OK = 0; - // Airdrops are unavailable - UNAVAILABLE = 1; - // The airdrop has already been claimed by the owner - ALREADY_CLAIMED = 2; - } - // Exchange data for the amount of Kin airdropped when successful - ExchangeData exchange_data = 2; -} -message SwapRequest { - oneof request { - Initiate initiate = 1; - SubmitSignature submit_signature = 2; - } - message Initiate { - // The verified owner account public key - common.v1.SolanaAccountId owner = 1; - // The user authority account that will sign to authorize the swap. Ideally, - // this is an account derived off the owner account that is solely responsible - // for swapping. - common.v1.SolanaAccountId swap_authority = 2; - // Maximum amount to swap from the source mint, in quarks. If value is set to zero, - // the entire amount will be swapped. - uint64 limit = 3; - // Whether the client wants the RPC to wait for blockchain status. If false, - // then the RPC will return Success when the swap is submitted to the blockchain. - // Otherwise, the RPC will observe and report back the status of the transaction. - bool wait_for_blockchain_status = 4; - // The signature is of serialize(Initiate) without this field set using the - // private key of the owner account. This provides an authentication mechanism - // to the RPC. - common.v1.Signature signature = 5; - } - message SubmitSignature { - // The signature for the locally constructed swap transaction - common.v1.Signature signature = 1; - } -} -message SwapResponse { - oneof response { - ServerParameters server_parameters = 1; - Success success = 2; - Error error = 3; - } - message ServerParameters { - // Subisdizer account that will be paying for the swap - common.v1.SolanaAccountId payer = 1; - // Recent blockhash - common.v1.Blockhash recent_blockhash = 2; - // Compute unit limit provided to the ComputeBudget::SetComputeUnitLimit - // instruction. If the value is 0, then the instruction can be omitted. - uint32 compute_unit_limit = 3; - // Compute unit price provided in the ComputeBudget::SetComputeUnitPrice - // instruction. If the value is 0, then the instruction can be omitted. - uint64 compute_unit_price = 4; - // On-chain program that will be performing the swap - common.v1.SolanaAccountId swap_program = 5; - // Accounts provided to the swap instruction - repeated common.v1.InstructionAccount swap_ixn_accounts = 6 ; - // Instruction data for the swap instruction - bytes swap_ixn_data = 7 ; - // Maximum quarks that will be sent out of the source account after - // executing the swap. If not, the validation instruction will cause - // the transaction to fail. - uint64 max_to_send = 8; - // Minimum quarks that will be received into the destination account - // after executing the swap. If not, the validation instruction will - // cause the transaction to fail. - uint64 min_to_receive = 9; - // Nonce to use in swap validator state account PDA - common.v1.SolanaAccountId nonce = 10; - } - message Success { - Code code = 1; - enum Code { - // The swap was submitted to the blockchain. - SWAP_SUBMITTED = 0; - // The swap was finalized on the blockchain. - SWAP_FINALIZED = 1; - } - } - message Error { - Code code = 1; - enum Code { - // Denied by a guard (spam, money laundering, etc) - DENIED = 0; - // There is an issue with the provided signature. - SIGNATURE_ERROR = 2; - // The swap failed server-side validation - INVALID_SWAP = 3; - // The submitted swap transaction failed. Attempt the swap again. - SWAP_FAILED = 4; - } - repeated ErrorDetails error_details = 2; - } -} -message DeclareFiatOnrampPurchaseAttemptRequest { - // The owner account invoking the buy module - common.v1.SolanaAccountId owner = 1; - // The amount being purchased - ExchangeDataWithoutRate purchase_amount = 2; - // A nonce value unique to the purchase. If it's included in a memo for the - // transaction for the deposit to the owner, then purchase_amount will be used - // for display values. Otherwise, the amount will be inferred from the transaction. - common.v1.UUID nonce = 3; - // The signature is of serialize(DeclareFiatOnrampPurchaseAttemptRequest) without - // this field set using the private key of the owner account. This provides an - // authentication mechanism to the RPC. - common.v1.Signature signature = 4; -} -message DeclareFiatOnrampPurchaseAttemptResponse { - Result result = 1; - enum Result { - OK = 0; - // The owner account is not valid (ie. it isn't a Code account) - INVALID_OWNER = 1; - // The currency isn't supported - UNSUPPORTED_CURRENCY = 2; - // The amount specified exceeds limits - AMOUNT_EXCEEDS_MAXIMUM = 3; - } -} -// -// Metadata definitions -// -// Metadata describes the high-level details of an intent -message Metadata { - oneof type { - OpenAccountsMetadata open_accounts = 1; - SendPrivatePaymentMetadata send_private_payment = 2; - ReceivePaymentsPrivatelyMetadata receive_payments_privately = 3; - UpgradePrivacyMetadata upgrade_privacy = 4; - SendPublicPaymentMetadata send_public_payment = 6; - ReceivePaymentsPubliclyMetadata receive_payments_publicly = 7; - EstablishRelationshipMetadata establish_relationship = 8; - } - reserved 5; // Deprecated MigrateToPrivacy2022Metadata -} -// ExtendedPaymentMetadata is additional metadata that can be used for custom -// payment types -message ExtendedPaymentMetadata { - google.protobuf.Any value = 1; -; -} -// Open a set of accounts. Currently, clients should only use this for new users -// to open all required accounts up front (buckets, incoming, and outgoing). -// -// Action Spec: -// -// for account in [PRIMARY, TEMPORARY_INCOMING, TEMPORARY_OUTGOING, BUCKET_1_KIN, ... , BUCKET_1_000_000_KIN] -// actions.push_back(OpenAccountAction(account)) -message OpenAccountsMetadata { - // Nothing is currently required -} -// Sends a payment to a destination account with initial temporary privacy. Clients -// should also reorganize their bucket accounts and rotate their temporary outgoing -// account. -// -// Action Spec (In Person Cash Payment or Withdrawal or Tip): -// -// actions = [ -// // Section 1: Transfer ExchangeData.Quarks from BUCKET_X_KIN accounts to TEMPORARY_OUTGOING account with reogranizations -// -// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize), -// TemporaryPrivacyTransferAction(BUCKET_X_KIN, TEMPORARY_OUTGOING[index], multiple * bucketSize), -// ..., -// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize), -// TemporaryPrivacyTransferAction(BUCKET_X_KIN, TEMPORARY_OUTGOING[index], multiple * bucketSize), -// -// // Section 2: Rotate TEMPORARY_OUTGOING account -// -// // Below must appear last in this exact order -// NoPrivacyWithdrawAction(TEMPORARY_OUTGOING[index], destination, ExchangeData.Quarks), -// OpenAccountAction(TEMPORARY_OUTGOING[index + 1]), -// ] -// -// Action Spec (Remote Send): -// -// actions = [ -// // Section 1: Open REMOTE_SEND_GIFT_CARD account -// -// OpenAccountAction(REMOTE_SEND_GIFT_CARD), -// -// // Section 2: Transfer ExchangeData.Quarks from BUCKET_X_KIN accounts to TEMPORARY_OUTGOING account with reogranizations -// -// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize), -// TemporaryPrivacyTransferAction(BUCKET_X_KIN, TEMPORARY_OUTGOING[index], multiple * bucketSize), -// ..., -// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize), -// TemporaryPrivacyTransferAction(BUCKET_X_KIN, TEMPORARY_OUTGOING[index], multiple * bucketSize), -// -// // Section 3: Rotate TEMPORARY_OUTGOING account -// -// // Below must appear last in this exact order -// NoPrivacyWithdrawAction(TEMPORARY_OUTGOING[index], REMOTE_SEND_GIFT_CARD, ExchangeData.Quarks), -// OpenAccountAction(TEMPORARY_OUTGOING[index + 1]), -// -// // Section 4: Close REMOTE_SEND_GIFT_CARD if not redeemed after period of time -// -// todo: We need a new mechanism that doesn't rely on the deprecated CloseDormantAccountAction -// -// Action Spec (Micro Payment): -// -// actions = [ -// // Section 1: Transfer ExchangeData.Quarks from BUCKET_X_KIN accounts to TEMPORARY_OUTGOING account with reogranizations -// -// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize), -// TemporaryPrivacyTransferAction(BUCKET_X_KIN, TEMPORARY_OUTGOING[index], multiple * bucketSize), -// ..., -// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize), -// TemporaryPrivacyTransferAction(BUCKET_X_KIN, TEMPORARY_OUTGOING[index], multiple * bucketSize), -// -// // Section 2: Fee payments -// -// // Hard-coded Code $0.01 USD fee to a dynamic fee account -// FeePayment(TEMPORARY_OUTGOING[index], codeFeeAccount, $0.01 USD of Kin), -// -// // Additional fees, exactly as specified in the original payment request -// FeePayment(TEMPORARY_OUTGOING[index], additionalFeeAccount0, additionalFeeQuarks0), -// ... -// FeePayment(TEMPORARY_OUTGOING[index], additionalFeeAccountN, additionalFeeQuarksN), -// -// // Section 3: Rotate TEMPORARY_OUTGOING account -// -// // Below must appear last in this exact order -// NoPrivacyWithdrawAction(TEMPORARY_OUTGOING[index], destination, ExchangeData.Quarks - $0.01 USD of Kin - additionalFeeQuarks0 - ... - additionalFeeQuarksN), -// OpenAccountAction(TEMPORARY_OUTGOING[index + 1]), -// ] -message SendPrivatePaymentMetadata { - // The destination token account to send funds to - common.v1.SolanaAccountId destination = 1; - // The exchange data of total funds being sent to the destination - ExchangeData exchange_data = 2; - // Is the payment a withdrawal? For destinations that are not Code temporary - // accounts, this must be set to true. - bool is_withdrawal = 3; - // Is the payment for a remote send? - bool is_remote_send = 4; - // Is the payment for a tip? - bool is_tip = 5; - // If is_tip is true, the user being tipped - TippedUser tipped_user = 6; -} -// Send a payment to a destination account publicly. -// -// Action Spec: -// -// source = PRIMARY or RELATIONSHIP -// actions = [NoPrivacyTransferAction(source, destination, ExchangeData.Quarks)] -message SendPublicPaymentMetadata { - // The primary or relatinship account where funds will be sent from. The primary - // account is assumed if this field is not set for backwards compatibility with - // old clients. - common.v1.SolanaAccountId source = 4; - // The destination token account to send funds to. This cannot be a Code - // temporary account. - common.v1.SolanaAccountId destination = 1; - // The exchange data of total funds being sent to the destination - ExchangeData exchange_data = 2; - // Is the payment a withdrawal? Currently, this is always true. - bool is_withdrawal = 3; - ExtendedPaymentMetadata extended_metadata = 5; -} -// Receive funds into an organizer with initial temporary privacy. Clients should -// also reorganize their bucket accounts and rotate their temporary incoming account -// as applicable. Only accounts owned and derived by a user's 12 words should operate -// as a source in this intent type to guarantee privacy upgradeability. -// -// Action Spec (Payment): -// -// actions = [ -// // Section 1: Transfer Quarks from TEMPORARY_INCOMING account to BUCKET_X_KIN accounts with reorganizations -// -// TemporaryPrivacyTransferAction(TEMPORARY_INCOMING[index], BUCKET_X_KIN, multiple * bucketSize), -// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize), -// ..., -// TemporaryPrivacyTransferAction(TEMPORARY_INCOMING[index], BUCKET_X_KIN, multiple * bucketSize), -// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize), -// -// // Section 2: Rotate TEMPORARY_INCOMING account -// -// // Below must appear last in this exact order -// OpenAccountAction(TEMPORARY_INCOMING[index + 1]) -// ] -// -// Action Spec (Deposit): -// -// source = PRIMARY or RELATIONSHIP -// actions = [ -// TemporaryPrivacyTransferAction(source, BUCKET_X_KIN, multiple * bucketSize), -// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize), -// ..., -// TemporaryPrivacyTransferAction(source, BUCKET_X_KIN, multiple * bucketSize), -// TemporaryPrivacyExchangeAction(BUCKET_X_KIN, BUCKET_X_KIN, multiple * bucketSize), -// ] -message ReceivePaymentsPrivatelyMetadata { - // The temporary incoming, primary or relationship account to receive funds from - common.v1.SolanaAccountId source = 1; - // The exact amount of Kin in quarks being received - uint64 quarks = 2; - // Is the receipt of funds from a deposit? If true, the source account must - // be a primary or relationship account. Otherwise, it must be from a temporary - // incoming account. - bool is_deposit = 3; -} -// Receive funds into a user-owned account publicly. All use cases of this intent -// close the account, so all funds must be moved. Use this intent to receive payments -// from an account not owned by a user's 12 words into a temporary incoming account, -// which will guarantee privacy upgradeability. -// -// Action Spec (Remote Send): -// -// actions = [NoPrivacyWithdrawAction(REMOTE_SEND_GIFT_CARD, TEMPORARY_INCOMING[latest_index], quarks)] -message ReceivePaymentsPubliclyMetadata { - // The remote send gift card to receive funds from - common.v1.SolanaAccountId source = 1; - // The exact amount of Kin in quarks being received - uint64 quarks = 2; - // Is the receipt of funds from a remote send gift card? Currently, this is - // the only use case for this intent and validation enforces the flag to true. - bool is_remote_send = 3; - // If is_remote_send is true, is the gift card being voided? The user owner - // account's 12 words that issued the gift card may only set this flag to true. - // Functionally, this doesn't affect the intent, but rather if we decide to show - // it in a user-friendly payment history. - bool is_issuer_voiding_gift_card = 4; - // If is_remote_send is true, the original exchange data that was provided as - // part of creating the gift card account. This is purely a server-provided value. - // SubmitIntent will disallow this being set. - ExchangeData exchange_data = 5; -} -// Upgrade existing private virtual instructions from temporary to permanent privacy. -message UpgradePrivacyMetadata { - // Nothing is currently required -} -// Establishes a long-lived private relationship between a user and another -// entity. -// -// Prereqs: -// - OpenAccounts intent has been submitted -// -// Action spec: -// -// actions = [OpenAccountAction(RELATIONSHIP)] -message EstablishRelationshipMetadata { - common.v1.Relationship relationship = 1; -} -// -// Action Definitions -// -// Action is a well-defined, ordered and small set of transactions or virtual instructions -// for a unit of work that the client wants to perform on the blockchain. Clients provide -// parameters known to them in the action. -message Action { - // The ID of this action, which is unique within an intent. It must match - // the index of the action's location in the SubmitAction's actions field. - uint32 id = 1; - // The type of action to perform. - oneof type { - OpenAccountAction open_account = 2; - NoPrivacyTransferAction no_privacy_transfer = 5; - NoPrivacyWithdrawAction no_privacy_withdraw = 6; - TemporaryPrivacyTransferAction temporary_privacy_transfer = 7; - TemporaryPrivacyExchangeAction temporary_privacy_exchange = 8; - PermanentPrivacyUpgradeAction permanent_privacy_upgrade = 9; - FeePaymentAction fee_payment = 10; - } - reserved 3; // Deprecated CloseEmptyAccountAction - reserved 4; // Deprecated CloseDormantAccountAction -} -// Virtual Instruction 1 -// Instructions: -// 1. system::AdvanceNonce -// 2. cvm::SystemTimelockInit -// Client Signature Required: No -message OpenAccountAction { - // The type of account, which will dictate its intended use - common.v1.AccountType account_type = 1; - // The owner of the account. For accounts liked to a user's 12 words, this is - // the verified parent owner account public key. All other account types should - // set this to the authority value. - common.v1.SolanaAccountId owner = 2; - // The index used to for accounts that are derived from owner - uint64 index = 3; - // The public key of the private key that has authority over the opened token account - common.v1.SolanaAccountId authority = 4; - // The token account being opened - common.v1.SolanaAccountId token = 5; - // The signature is of serialize(OpenAccountAction) without this field set - // using the private key of the authority account. This provides a proof - // of authorization to link authority to owner. - common.v1.Signature authority_signature = 6; -} -// Virtual Instruction 1 -// Instructions: -// 1. system::AdvanceNonce -// 2. memo::Memo -// 3. timelock::TransferWithAuthority (source -> destination) -// Client Signature Required: Yes -message NoPrivacyTransferAction { - // The public key of the private key that has authority over source - common.v1.SolanaAccountId authority = 1; - // The source account where funds are transferred from - common.v1.SolanaAccountId source = 2; - // The destination account where funds are transferred to - common.v1.SolanaAccountId destination = 3; - // The Kin quark amount to transfer - uint64 amount = 4; -} -// Virtual Instruction 1 -// Instructions: -// 1. system::AdvanceNonce -// 2. memo::Memo -// 3. timelock::RevokeLockWithAuthority -// 4. timelock::DeactivateLock -// 5. timelock::Withdraw (source -> destination) -// 6. timelock::CloseAccounts -// Client Signature Required: Yes -message NoPrivacyWithdrawAction { - // The public key of the private key that has authority over source - common.v1.SolanaAccountId authority = 1; - // The source account where funds are transferred from - common.v1.SolanaAccountId source = 2; - // The destination account where funds are transferred to - common.v1.SolanaAccountId destination = 3; - // The intended Kin quark amount to withdraw - uint64 amount = 4; - - // Whether the account is closed afterwards. This is always true, since there - // are no current se cases to leave it open. - bool should_close = 5; -} -// Virtual Instruction 1 -// Instructions: -// 1. system::AdvanceNonce -// 2. memo::Memo -// 3. splitter::TransferWithCommitment (treasury -> destination) -// Client Signature Required: No -// -// Virtual Instruction 2 -// Instructions: -// 1. system::AdvanceNonce -// 2. memo::Memo -// 3. timelock::TransferWithAuthority (source -> commitment) -// Client Signature Required: Yes -message TemporaryPrivacyTransferAction { - // The public key of the private key that has authority over source - common.v1.SolanaAccountId authority = 1; - // The source account where funds are transferred from - common.v1.SolanaAccountId source = 2; - // The destination account where funds are transferred to - common.v1.SolanaAccountId destination = 3; - // The Kin quark amount to transfer - uint64 amount = 4; -} -// Virtual Instruction 1 -// Instructions: -// 1. system::AdvanceNonce -// 2. memo::Memo -// 3. splitter::TransferWithCommitment (treasury -> destination) -// Client Signature Required: No -// -// Virtual Instruction 2 -// Instructions: -// 1. system::AdvanceNonce -// 2. memo::Memo -// 3. timelock::TransferWithAuthority (source -> commitment) -// Client Signature Required: Yes -message TemporaryPrivacyExchangeAction { - // The public key of the private key that has authority over source - common.v1.SolanaAccountId authority = 1; - // The source account where funds are exchanged from - common.v1.SolanaAccountId source = 2; - // The destination account where funds are exchanged to - common.v1.SolanaAccountId destination = 3; - // The Kin quark amount to exchange - uint64 amount = 4; -} -// Virtual Instruction 1 -// Instructions: -// 1. system::AdvanceNonce -// 2. memo::Memo -// 3. timelock::TransferWithAuthority (source -> different commitment) -// Client Signature Required: Yes -message PermanentPrivacyUpgradeAction { - // The action ID of the temporary private transfer or exchange to upgrade - uint32 action_id = 1; -} -// Virtual Instruction 1 -// Instructions: -// 1. system::AdvanceNonce -// 2. memo::Memo -// 3. timelock::TransferWithAuthority (source -> fee account) -// Client Signature Required: Yes -// -// Note: This is exactly a NoPrivacyTransferAction, but with specialized metadata -// for fees. -message FeePaymentAction { - // The type of fee being operated on - FeeType type = 4; - enum FeeType { - CODE = 0; // Hardcoded $0.01 USD fee to a dynamic fee account specified by server - THIRD_PARTY = 1; // Third party fee specified at time of payment request - } - // The public key of the private key that has authority over source - common.v1.SolanaAccountId authority = 1; - // The source account where funds are transferred from - common.v1.SolanaAccountId source = 2; - // The Kin quark amount to transfer - uint64 amount = 3; - // The destination where the fee payment is being made for fees outside of - // Code. - common.v1.SolanaAccountId destination = 5; -} -// -// Server Parameter Definitions -// -// ServerParameter are a set of parameters known and returned by server that -// enables clients to complete transaction construction. Any necessary proofs, -// which are required to be locally verifiable, are also provided to ensure -// safe use in the event of a malicious server. -message ServerParameter { - // The action the server parameters belong to - uint32 action_id = 1; - // The set of nonces used for the action. Server will only provide values - // for transactions requiring client signatures. - repeated NoncedTransactionMetadata nonces = 2 ; - // The type of server parameter which maps to the type of action requested - oneof type { - OpenAccountServerParameter open_account = 3; - NoPrivacyTransferServerParameter no_privacy_transfer = 6; - NoPrivacyWithdrawServerParameter no_privacy_withdraw = 7; - TemporaryPrivacyTransferServerParameter temporary_privacy_transfer = 8; - TemporaryPrivacyExchangeServerParameter temporary_privacy_exchange = 9; - PermanentPrivacyUpgradeServerParameter permanent_privacy_upgrade = 10; - FeePaymentServerParameter fee_payment = 11; - } - reserved 4; // Deprecated CloseEmptyAccountServerParameter - reserved 5; // Deprecated CloseDormantAccountServerParameter -} -// For transactions, the nonce is a standard nonce on Solana -// For virtual instructions, the nonce is a virtual nonce on the Code VM -message NoncedTransactionMetadata { - // The nonce account to use in the system::AdvanceNonce instruction - common.v1.SolanaAccountId nonce = 1; - // The blockhash to set in the transaction or virtual instruction - common.v1.Blockhash blockhash = 2; -} -message OpenAccountServerParameter { - // There are no transactions requiring client signatures -} -message NoPrivacyTransferServerParameter { - // There are no action-specific server parameters -} -message NoPrivacyWithdrawServerParameter { - // There are no action-specific server parameters -} -message TemporaryPrivacyTransferServerParameter { - // The treasury that will be used to split payments and provide a level of privacy - common.v1.SolanaAccountId treasury = 1; - // A recent root server observed from the treasury - common.v1.Hash recent_root = 2; -} -message TemporaryPrivacyExchangeServerParameter { - // The treasury that will be used to split payments and provide a level of privacy - common.v1.SolanaAccountId treasury = 1; - // A recent root server observed from the treasury - common.v1.Hash recent_root = 2; -} -message PermanentPrivacyUpgradeServerParameter { - // The new commitment that is being paid - common.v1.SolanaAccountId new_commitment = 1; - // The new commitment account's transcript. This is purely needed by client - // to validate merkle_root with commitment PDA logic. - common.v1.Hash new_commitment_transcript = 2; - // The new commitment account's destination. This is purely needed by client - // to validate merkle_root with commitment PDA logic. - common.v1.SolanaAccountId new_commitment_destination = 3; - // The new commitment account's payment amount. This is purely needed by client - // to validate merkle_root with commitment PDA logic. - uint64 new_commitment_amount = 4; - // The merkle root, which was the recent root used in the new commitment account - common.v1.Hash merkle_root = 5; - // The merkle proof that validates the original commitment occurred prior to - // the new commitment server is asking client to pay - repeated common.v1.Hash merkle_proof = 6 ; -} -message FeePaymentServerParameter { - // The destination account where Code fee payments should be sent. This will - // only be set when the corresponding FeePaymentAction Type is CODE. - common.v1.SolanaAccountId code_destination = 1; -} -// -// Structured Error Definitions -// -message ErrorDetails { - oneof type { - ReasonStringErrorDetails reason_string = 1; - InvalidSignatureErrorDetails invalid_signature = 2; - DeniedErrorDetails denied = 3; - } -} -message ReasonStringErrorDetails { - // Human readable string indicating the failure. - string reason = 1 ; -} -message InvalidSignatureErrorDetails { - // The action whose signature mismatched - uint32 action_id = 1; - oneof expected_blob { - // The transaction the server expected to have signed. - common.v1.Transaction expected_transaction = 2; - // The virtual ixn hash the server expected to have signed. - common.v1.Hash expected_vixn_hash = 4; - } - // The signature that was provided by the client. - common.v1.Signature provided_signature = 3; -} -message DeniedErrorDetails { - Code code = 1; - enum Code { - // Reason code not yet defined - UNSPECIFIED = 0; - // Phone number has exceeded its free account allocation - TOO_MANY_FREE_ACCOUNTS_FOR_PHONE_NUMBER = 1; - // Device has exceeded its free account allocation - TOO_MANY_FREE_ACCOUNTS_FOR_DEVICE = 2; - // The country associated with the phone number with the account is not - // supported (eg. it is on the sanctioned list). - UNSUPPORTED_COUNTRY = 3; - // The device is not supported (eg. it fails device attestation checks) - UNSUPPORTED_DEVICE = 4; - } - // Human readable string indicating the failure. - string reason = 2 ; -} -// -// Other Model Definitions -// -// UpgradeableIntent is an intent whose actions can be upgraded. -message UpgradeableIntent { - // The intent ID - common.v1.IntentId id = 1; - // The set of private actions that can be upgraded - repeated UpgradeablePrivateAction actions = 2 ; - message UpgradeablePrivateAction { - // The blob that was hashed and signed by the client for a virtual instruction. - // Clients *MUST* use the source and destination accounts in the timelock::TransferWithAuthority - // instruction to validate all fields provided by server by locally computing the expected - // addresses. - common.v1.Transaction transaction_blob = 1; - // The client's signature for the virtual instruction. Clients MUST use this to - // locally validate the virtual instruction blob provided by server. - common.v1.Signature client_signature = 2; - // The action ID of this virtual instruction - uint32 action_id = 3; - // The source account's type, which hints how to efficiently derive source - common.v1.AccountType source_account_type = 4; -; - // The source account's derivation index, which hints how to efficiently derive source - uint64 source_derivation_index = 5; - // The original destination account that was paid by the treasury - common.v1.SolanaAccountId original_destination = 6; - // The original quark amount for the action - uint64 original_amount = 7; - // The treasury used for this the private action - common.v1.SolanaAccountId treasury = 8; - // The recent root observed at the time of intent creation for this private action - common.v1.Hash recent_root = 9; - } -} -// ExchangeData defines an amount of Kin with currency exchange data -message ExchangeData { - // ISO 4217 alpha-3 currency code. - string currency = 1; - // The agreed upon exchange rate. This might not be the same as the - // actual exchange rate at the time of intent or fund transfer. - double exchange_rate = 2; - // The agreed upon transfer amount in the currency the payment was made - // in. - double native_amount = 3; - // The exact amount of quarks to send. This will be used as the source of - // truth for validating transaction transfer amounts. - uint64 quarks = 4; -} -message ExchangeDataWithoutRate { - // ISO 4217 alpha-3 currency code. - string currency = 1; - // The agreed upon transfer amount in the currency the payment was made - // in. - double native_amount = 2; -} -message AdditionalFeePayment { - // Destination Kin token account where the fee payment will be made - common.v1.SolanaAccountId destination = 1; - // Fee percentage, in basis points, of the total quark amount of a payment. - uint32 fee_bps = 2 ; -} -message SendLimit { - // Remaining limit to apply on the next transaction - float next_transaction = 1; - // Maximum allowed on a per-transaction basis - float max_per_transaction = 2; - // Maximum allowed on a per-day basis - float max_per_day = 3; -} -message DepositLimit { - // Maximum quarks that may be deposited at any time. Server will guarantee - // this threshold will be below enforced dollar value limits, while also - // ensuring sufficient funds are available for a full organizer that supports - // max payment sends. Total dollar value limits may be spread across many deposits. - uint64 max_quarks = 1; -} -message MicroPaymentLimit { - // Maximum native amount that can be applied per micro payment transaction - float max_per_transaction = 1; - // Minimum native amount that can be applied per micro payment transaction - float min_per_transaction = 2; -} -message BuyModuleLimit { - // Minimum amount that can be purchased through the buy module - float min_per_transaction = 1; - // Maximum amount that can be purchased through the buy module - float max_per_transaction = 2; -} -message TippedUser { - Platform platform = 1 ; - enum Platform { - UNKNOWN = 0; - TWITTER = 1; - } - string username = 2 ; -} -message Cursor { - bytes value = 1 ; -} -enum AirdropType { - UNKNOWN = 0; - // Reward for giving someone else their first Kin - GIVE_FIRST_KIN = 1; - // Airdrop for getting a user started with first Kin balance - GET_FIRST_KIN = 2; -} diff --git a/definitions/flipchat-vm/protos/src/main/proto/user/v1/code_identity_service.proto b/definitions/flipchat-vm/protos/src/main/proto/user/v1/code_identity_service.proto deleted file mode 100644 index 4c71a3ae1..000000000 --- a/definitions/flipchat-vm/protos/src/main/proto/user/v1/code_identity_service.proto +++ /dev/null @@ -1,294 +0,0 @@ -syntax = "proto3"; -package code.user.v1; -option go_package = "github.com/code-payments/code-protobuf-api/generated/go/user/v1;user"; -option java_package = "com.codeinc.gen.user.v1"; -option objc_class_prefix = "CPBUserV1"; -import "common/v1/code_model.proto"; -import "phone/v1/code_phone_verification_service.proto"; -import "transaction/v2/code_transaction_service.proto"; - -service Identity { - // LinkAccount links an owner account to the user identified and authenticated - // by a one-time use token. - // - // Notably, this RPC has the following side effects: - // * A new user is automatically created if one doesn't exist. - // * Server will create a new data container for at least every unique - // owner account linked to the user. - rpc LinkAccount(LinkAccountRequest) returns (LinkAccountResponse); - // UnlinkAccount removes links from an owner account. It will NOT remove - // existing associations between users, owner accounts and identifying - // features. - // - // The following associations will remain intact to ensure owner accounts - // can continue to be used with a consistent login experience: - // * the user continues to be associated to existing owner accounts and - // identifying features - // - // Client can continue mainting their current login session. Their current - // user and data container will remain the same. - // - // The call is guaranteed to be idempotent. It will not fail if the link is - // already removed by either a previous call to this RPC or by a more recent - // call to LinkAccount. A failure will only occur if the link between a user - // and the owner accout or identifying feature never existed. - rpc UnlinkAccount(UnlinkAccountRequest) returns (UnlinkAccountResponse); - // GetUser gets user information given a user identifier and an owner account. - rpc GetUser(GetUserRequest) returns (GetUserResponse); - // UpdatePreferences updates user preferences. - rpc UpdatePreferences(UpdatePreferencesRequest) returns (UpdatePreferencesResponse); - // LoginToThirdPartyApp logs a user into a third party app for a given intent - // ID. If the original request requires payment, then SubmitIntent must be called. - rpc LoginToThirdPartyApp(LoginToThirdPartyAppRequest) returns (LoginToThirdPartyAppResponse); - // GetLoginForThirdPartyApp gets a login for a third party app from an existing - // request. This endpoint supports all paths where login is possible (login on payment, - // raw login, etc.). - rpc GetLoginForThirdPartyApp(GetLoginForThirdPartyAppRequest) returns (GetLoginForThirdPartyAppResponse); - // GetTwitterUser gets Twitter user information - // - // Note 1: This RPC will only return results for Twitter users that have - // accounts linked with Code. - // - // Note 2: This RPC is heavily cached, and may not reflect real-time Twitter - // information. - rpc GetTwitterUser(GetTwitterUserRequest) returns (GetTwitterUserResponse); -} -message LinkAccountRequest { - // The public key of the owner account that will be linked to a user. - common.v1.SolanaAccountId owner_account_id = 1; - // The signature is of serialize(LinkAccountRequest) without this field set - // using the private key of owner_account_id. This validates that the client - // actually owns the account. - common.v1.Signature signature = 2; - // A one-time use token that identifies and authenticates the user. - oneof token { - // A token received after successfully verifying a phone number via a - // SMS code using the phone verification service. - phone.v1.PhoneLinkingToken phone = 3; - } -} -message LinkAccountResponse { - Result result = 1; - enum Result { - OK = 0; - // The provided token is invalid. A token may be invalid for a number of - // reasons including: it's already been used, has been modified by the - // client or has expired. - INVALID_TOKEN = 1; - // The client is rate limited (eg. by IP, user ID, etc). The client should - // retry at a later time. - RATE_LIMITED = 2; - } - // The user that was linked to the owner account - User user = 2; - // The data container where the user can store a copy of their data - common.v1.DataContainerId data_container_id = 3; - // Field 4 is the deprecated kin_token_account_details - reserved 4; - // Metadata about the user based for the instance of their view - oneof metadata { - // Metadata that corresponds to a phone-based identifying feature. - PhoneMetadata phone = 5; - } -} -message UnlinkAccountRequest { - // The public key of the owner account that will be unliked. - common.v1.SolanaAccountId owner_account_id = 1; - // The signature is of serialize(UnlinkAccountRequest) without this field set - // using the private key of owner_account_id. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 2; - oneof identifying_feature { - // The phone number associated with the owner account. - common.v1.PhoneNumber phone_number = 4; - } -} -message UnlinkAccountResponse { - Result result = 1; - enum Result { - OK = 0; - // The client attempted to unlink an owner account or identifying feature - // that never had a valid association. - NEVER_ASSOCIATED = 1; - } -} -message GetUserRequest { - // The public key of the owner account that signed this request message. - common.v1.SolanaAccountId owner_account_id = 1; - // The signature is of serialize(GetUserRequest) without this field set - // using the private key of owner_account_id. This provides an authentication - // mechanism to the RPC. - common.v1.Signature signature = 2; - // The user's indentifying feature, which maps to an instance of a view. - oneof identifying_feature { - common.v1.PhoneNumber phone_number = 3; - } -} -message GetUserResponse { - Result result = 1; - enum Result { - OK = 0; - // The user doesn't exist - NOT_FOUND = 1; - // The user is no longer invited - NOT_INVITED = 2; - // The user exists, but at least one of their timelock accounts is unlocked - UNLOCKED_TIMELOCK_ACCOUNT = 3; - } - // The user associated with the identifier - User user = 2; - // The data container where the user can store a copy of their data - common.v1.DataContainerId data_container_id = 3; - // Field 4 is the deprecated kin_token_account_details - reserved 4; - // Metadata about the user based for the instance of their view - oneof metadata { - // Metadata that corresponds to a phone-based identifying feature. - PhoneMetadata phone = 5; - } - // Whether client internal flags are enabled for this user - bool enable_internal_flags = 6; - // Set of which airdrops the user is eligible to receive - repeated transaction.v2.AirdropType eligible_airdrops = 7; - // Wether the buy module is enabled for this user - bool enable_buy_module = 8; -} -message UpdatePreferencesRequest { - // The public key of the owner account that signed this request message. - common.v1.SolanaAccountId owner_account_id = 1; - // The data container for the copy of the contact list being added to. - common.v1.DataContainerId container_id = 2; - // The signature is of serialize(UpdatePreferencesRequest) without this field set - // using the private key of owner_account_id. - common.v1.Signature signature = 3; - // The user's locale, which is used for server-side localization of things like - // chat messages, pushes, etc. If no locale is set, or the provided locale isn't - // supported, then English is used as the default fallback. - // - // Note: This is required since it's the only preference. In the future, we'll add - // optional fields, where the subset of fields provided will be the ones that - // are updated. - common.v1.Locale locale = 4; -} -message UpdatePreferencesResponse { - Result result = 1; - enum Result { - OK = 0; - // The provided locale couldn't be parsed or recognized and is invalid. - INVALID_LOCALE = 1; - } -} -message LoginToThirdPartyAppRequest { - // The intent ID identifying the instance of the login flow. - common.v1.IntentId intent_id = 1; - // The relationship authority account logging in. - common.v1.SolanaAccountId user_id = 2; - // Signature of this message using the user private key, which authenticates - // the user. - common.v1.Signature signature = 3; -} -message LoginToThirdPartyAppResponse { - Result result = 1; - enum Result { - // This supports idempotency. The same login with the same user will result - // in OK. - OK = 0; - // There is no request for the provided intent ID. - REQUEST_NOT_FOUND = 1; - // The request requires a payment. Call SubmitIntent instead. - PAYMENT_REQUIRED = 2; - // The request exists, but doesn't support login. - LOGIN_NOT_SUPPORTED = 3; - // A login with a different user already exists - DIFFERENT_LOGIN_EXISTS = 4; - // The provided account is not valid for login. It must be a relationship - // account with the correct identifier specified in the original request. - INVALID_ACCOUNT = 5; - } -} -message GetLoginForThirdPartyAppRequest { - // The intent ID identifying the instance of the login flow. - common.v1.IntentId intent_id = 1; - // Owner account owned by the third party used in domain verification. - common.v1.SolanaAccountId verifier = 2; - // Signature of this message using the verifier private key, which in addition - // to domain verification, authenticates the third party. - common.v1.Signature signature = 3; -} -message GetLoginForThirdPartyAppResponse { - Result result = 1; - enum Result { - OK = 0; - // There is no request for the provided intent ID. - REQUEST_NOT_FOUND = 1; - // The request exists, but doesn't support login. - LOGIN_NOT_SUPPORTED = 2; - // The intent supports login, but it hasn't been submitted. There is no - // logged in user yet. - NO_USER_LOGGED_IN = 3; - } - // The relationship authority account that logged in. - common.v1.SolanaAccountId user_id = 2; -} -message GetTwitterUserRequest { - oneof query { - // The Twitter username to query against - string username = 1 ; - // The tip address to query against - common.v1.SolanaAccountId tip_address = 2; - } -} -message GetTwitterUserResponse { - Result result = 1; - enum Result { - OK = 0; - // The Twitter user doesn't exist or isn't linked with a Code account - NOT_FOUND = 1; - } - TwitterUser twitter_user = 2; -} -// User is the highest order of a form of identity within Code. -// -// Note: Users outside Code are modelled as relationship accounts -message User { - // The user's ID - common.v1.UserId id = 1; - // The identifying features that are associated with the user - View view = 2; -} -// View is a well-defined set of identifying features. It is contrained to having -// exactly one feature set at a time, for now. -message View { - // The phone number associated with a user. - // - // Note: This field is mandatory as of right now, since it's the only one - // supported to date. - common.v1.PhoneNumber phone_number = 1; -} -message PhoneMetadata { - // State that determines whether a phone number is linked to the owner - // account. A phone number is linked if we can treat it as an alias. - // This is notably different from association, which answers the question - // of whether the number was linked at any point in time. - bool is_linked = 1; -} -message TwitterUser { - // Public key for a token account where tips are routed - common.v1.SolanaAccountId tip_address = 1; - // The user's username on Twitter - string username = 2 ; - // The user's friendly name on Twitter - string name = 3 ; - // URL to the user's Twitter profile picture - string profile_pic_url = 4 ; - // The type of Twitter verification associated with the user - VerifiedType verified_type = 5; - enum VerifiedType { - NONE = 0; - BLUE = 1; - BUSINESS = 2; - GOVERNMENT = 3; - } - // The number of followers the user has on Twitter - uint32 follower_count = 6; -} diff --git a/definitions/flipchat/models/.gitignore b/definitions/flipchat/models/.gitignore deleted file mode 100644 index 9f2a07880..000000000 --- a/definitions/flipchat/models/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -build/ -.gradle/ diff --git a/definitions/flipchat/models/build.gradle.kts b/definitions/flipchat/models/build.gradle.kts deleted file mode 100644 index 553a6531e..000000000 --- a/definitions/flipchat/models/build.gradle.kts +++ /dev/null @@ -1,90 +0,0 @@ -import org.apache.tools.ant.taskdefs.condition.Os -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - id(Plugins.android_library) - id(Plugins.kotlin_android) - id("com.google.protobuf") -} - -val archSuffix = if (Os.isFamily(Os.FAMILY_MAC)) ":osx-x86_64" else "" - -version = "0.0.1" -group = "com.codeinc.fc.gen" - -dependencies { - protobuf(project(":definitions:flipchat:protos")) - - implementation(Libs.grpc_protobuf_lite) - implementation(Libs.grpc_stub) - - // Kotlin Generation - implementation(Libs.grpc_kotlin) - implementation(Libs.protobuf_kotlin_lite) - implementation(Libs.kotlinx_coroutines_core) -} - -kotlin { - jvmToolchain(Versions.java.toInt()) -} - -android { - namespace = "${Gradle.codeNamespace}.defs.fc.models" - compileSdk = Android.compileSdkVersion - defaultConfig { - minSdk = Android.minSdkVersion - testInstrumentationRunner = Android.testInstrumentationRunner - } -} - -kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(Versions.java)) - } - - compilerOptions { - jvmTarget.set(JvmTarget.fromTarget(Versions.java)) - optIn.addAll( - "kotlin.time.ExperimentalTime", - "kotlin.ExperimentalUnsignedTypes", - "kotlin.RequiresOptIn" - ) - } -} - -protobuf { - protoc { - artifact = "com.google.protobuf:protoc:${Versions.protobuf}$archSuffix" - } - plugins { - create("java") { - artifact = "io.grpc:protoc-gen-grpc-java:${Versions.grpc}" - } - create("grpc") { - artifact = "io.grpc:protoc-gen-grpc-java:${Versions.grpc}" - } - create("grpckt") { - artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar" - } - } - generateProtoTasks { - all().forEach { - it.plugins { - create("java") { - option("lite") - } - create("grpc") { - option("lite") - } - create("grpckt") { - option("lite") - } - } - it.builtins { - create("kotlin") { - option("lite") - } - } - } - } -} diff --git a/definitions/flipchat/protos/.gitignore b/definitions/flipchat/protos/.gitignore deleted file mode 100644 index 9f2a07880..000000000 --- a/definitions/flipchat/protos/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -build/ -.gradle/ diff --git a/definitions/flipchat/protos/build.gradle.kts b/definitions/flipchat/protos/build.gradle.kts deleted file mode 100644 index a42e252c7..000000000 --- a/definitions/flipchat/protos/build.gradle.kts +++ /dev/null @@ -1,11 +0,0 @@ -// todo: maybe use variants / configurations to do both stub & stub-lite here - -// Note: We use the java-library plugin to get the protos into the artifact for this subproject -// because there doesn't seem to be an better way. -plugins { - `java-library` -} - -java { - sourceSets.getByName("main").resources.srcDir("src/main/proto") -} diff --git a/definitions/flipchat/protos/src/main/proto/account/v1/account_service.proto b/definitions/flipchat/protos/src/main/proto/account/v1/account_service.proto deleted file mode 100644 index 2861a5420..000000000 --- a/definitions/flipchat/protos/src/main/proto/account/v1/account_service.proto +++ /dev/null @@ -1,147 +0,0 @@ -syntax = "proto3"; -package flipchat.account.v1; -option go_package = "github.com/code-payments/flipchat-protobuf-api/generated/go/account/v1;acountpb"; -option java_package = "com.codeinc.flipchat.gen.account.v1"; -option objc_class_prefix = "FCPBAccountV1"; -import "common/v1/common.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/timestamp.proto"; - -service Account { - // Register registers a new user, bound to the provided PublicKey. - // If the PublicKey is already in use, the previous user account is returned. - rpc Register(RegisterRequest) returns (RegisterResponse); - // Login retrieves the UserId (and in the future, potentially other information) - // required for 'recovering' an account. - rpc Login(LoginRequest) returns (LoginResponse); - // AuthorizePublicKey authorizes an additional PublicKey to an account. - rpc AuthorizePublicKey(AuthorizePublicKeyRequest) returns (AuthorizePublicKeyResponse); - // RevokePublicKey revokes a public key from an account. - // - // There must be at least one public key per account. For now, any authorized public key - // may revoke another public key, but this may change in the future. - rpc RevokePublicKey(RevokePublicKeyRequest) returns (RevokePublicKeyResponse); - // GetPaymentDestination gets the payment destination for a UserId - rpc GetPaymentDestination(GetPaymentDestinationRequest) returns (GetPaymentDestinationResponse); - // GetUserFlags gets user-specific flags - rpc GetUserFlags(GetUserFlagsRequest) returns (GetUserFlagsResponse); -} -message RegisterRequest { - // PublicKey the public key that is authorized to perform actions on the - // registered users behalf. - common.v1.PublicKey public_key = 1; - // Signature of this message (without the signature), using the provided keypaid. - common.v1.Signature signature = 2; - // Deprecated: New account creation flow requires using profile service after IAP - string display_name = 3 ; -} -message RegisterResponse { - Result result = 1; - enum Result { - OK = 0; - INVALID_SIGNATURE = 1; - INVALID_DISPLAY_NAME = 2; - DENIED = 3; - } - // Error reason contains the reason for the error, if the - // result > 1. This allows for server to impose moderation restrictions - // on user provided fields. - string error_reason = 2; - // The UserId associated with the account. - common.v1.UserId user_id = 3; -} -message LoginRequest { - // Timestamp is the timestamp the request was generated. - // - // The server may reject the request if the timestamp is too far off - // the current (server) time. This is to prevent replay attacks. - google.protobuf.Timestamp timestamp = 1; - common.v1.Auth auth = 2; -} -message LoginResponse { - Result result = 1; - enum Result { - OK = 0; - INVALID_TIMESTAMP = 1; - DENIED = 2; - } - // UserId is the user associated with the PubKey/Auth. - common.v1.UserId user_id = 2; -} -message AuthorizePublicKeyRequest { - // UserId to bound the new public key to. - common.v1.UserId user_id = 1; - // PublicKey of the account to be added. - common.v1.PublicKey public_key = 2; - // Signature of this message, not including auth or signature, using the - // new public key. - common.v1.Signature signature = 3; - common.v1.Auth auth = 4; -} -message AuthorizePublicKeyResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - } -} -message RevokePublicKeyRequest { - // UserId to remove the public key from. - common.v1.UserId user_id = 1; - // PublicKey to remove. - common.v1.PublicKey public_key = 2; - common.v1.Auth auth = 4; -} -message RevokePublicKeyResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - LAST_PUB_KEY = 2; - } -} -message GetPaymentDestinationRequest { - // UserId to get the payment destination from. - common.v1.UserId user_id = 1; -} -message GetPaymentDestinationResponse { - Result result = 1; - enum Result { - OK = 0; - NOT_FOUND = 1; - } - // Payment destination for the UserId. - common.v1.PublicKey payment_destination = 2; -} -message GetUserFlagsRequest { - // UserId to get user flags for. - common.v1.UserId user_id = 1; - common.v1.Auth auth = 2; -} -message GetUserFlagsResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - } - UserFlags user_flags = 2; -} -message UserFlags { - // Is this user associated with a Flipchat staff member? - bool is_staff = 1; - // The fee payment amount for starting a new group - common.v1.PaymentAmount start_group_fee = 2; - // The destination account where fees should be paid to - common.v1.PublicKey fee_destination = 3; - // Is this a fully registered account using IAP for account creation? - bool is_registered_account = 4; - // Can this user call NotifyIsTyping at all? - bool can_send_is_typing_notifications = 5; - // Can this user call NotifyIsTyping in chats where they are a listener? - bool can_send_is_typing_notifications_as_listener = 6; - // Interval between calling NotifyIsTyping - google.protobuf.Duration is_typing_notification_interval = 7; - // Client-side timeout for when they haven't seen an IsTyping event from a user. - // After this timeout has elapsed, client should assume the user has stopped typing. - google.protobuf.Duration is_typing_notification_timeout = 8; -} diff --git a/definitions/flipchat/protos/src/main/proto/badge/v1/badge_service.proto b/definitions/flipchat/protos/src/main/proto/badge/v1/badge_service.proto deleted file mode 100644 index 3c4d45c8e..000000000 --- a/definitions/flipchat/protos/src/main/proto/badge/v1/badge_service.proto +++ /dev/null @@ -1,21 +0,0 @@ -syntax = "proto3"; -package flipchat.badge.v1; -option go_package = "github.com/code-payments/flipchat-protobuf-api/generated/go/badge/v1;badgepb"; -option java_package = "com.codeinc.flipchat.gen.badge.v1"; -option objc_class_prefix = "FPBBadgeV1"; -import "common/v1/common.proto"; - -service Badge { - // ResetBadgeCount resets an owner account's app icon badge count back to zero - rpc ResetBadgeCount(ResetBadgeCountRequest) returns (ResetBadgeCountResponse); -} -message ResetBadgeCountRequest { - common.v1.UserId user_id = 1; - common.v1.Auth auth = 2; -} -message ResetBadgeCountResponse { - Result result = 1; - enum Result { - OK = 0; - } -} diff --git a/definitions/flipchat/protos/src/main/proto/chat/v1/chat_service.proto b/definitions/flipchat/protos/src/main/proto/chat/v1/chat_service.proto deleted file mode 100644 index c95fe26bc..000000000 --- a/definitions/flipchat/protos/src/main/proto/chat/v1/chat_service.proto +++ /dev/null @@ -1,633 +0,0 @@ -syntax = "proto3"; -package flipchat.chat.v1; -option go_package = "github.com/code-payments/flipchat-protobuf-api/generated/go/chat/v1;chatpb"; -option java_package = "com.codeinc.flipchat.gen.chat.v1"; -option objc_class_prefix = "FCPBChatV1"; -import "common/v1/common.proto"; -import "messaging/v1/model.proto"; -import "profile/v1/model.proto"; -import "google/protobuf/timestamp.proto"; - -service Chat { - // StreamChatEvents streams all chat events for the requesting user. - // - // Chat events will include any update to a chat, including: - // 1. Metadata changes. - // 2. Membership changes. - // 3. Latest messages. - // - // The server will optionally filter out some events depending on load - // and chat type. For example, Broadcast chats may not receive latest - // messages. - // - // Clients should use GetMessages to backfill in any historical messages - // for a chat. It should be sufficient to rely on ChatEvents for some types - // of chats, but using StreamMessages provides a guarentee of message events - // for all chats. - rpc StreamChatEvents(stream StreamChatEventsRequest) returns (stream StreamChatEventsResponse); - // GetChats gets the set of chats for an owner account using a paged API. - // This RPC is aware of all identities tied to the owner account. - rpc GetChats(GetChatsRequest) returns (GetChatsResponse); - // GetChat returns the metadata for a specific chat. - rpc GetChat(GetChatRequest) returns (GetChatResponse); - // StartChat starts a chat. The RPC call is idempotent and will use existing - // chats whenever applicable within the context of message routing. - rpc StartChat(StartChatRequest) returns (StartChatResponse); - // JoinChat joins a given chat. - rpc JoinChat(JoinChatRequest) returns (JoinChatResponse); - // LeaveChat leaves a given chat. - rpc LeaveChat(LeaveChatRequest) returns (LeaveChatResponse); - // OpenChat opens a chat up for messaging across all members - rpc OpenChat(OpenChatRequest) returns (OpenChatResponse); - // CloseChat closes a chat up for messaging to just the chat owner - rpc CloseChat(CloseChatRequest) returns (CloseChatResponse); - // CheckDisplayName checks whether a chat display name passes moderation - rpc CheckDisplayName(CheckDisplayNameRequest) returns (CheckDisplayNameResponse); - // SetDisplayName sets a chat's display name. If the display name isn't allowed, - // then a set of alternate suggestions may be provided - rpc SetDisplayName(SetDisplayNameRequest) returns (SetDisplayNameResponse); - // SetDescription sets a chat's description - rpc SetDescription(SetDescriptionRequest) returns (SetDescriptionResponse); - // SetCoverCharge sets a chat's cover charge - // - // Deprecated: Use SetMessagingFee instead - rpc SetCoverCharge(SetCoverChargeRequest) returns (SetCoverChargeResponse); - // SetMessagingFee sets a chat's messaging fee - rpc SetMessagingFee(SetMessagingFeeRequest) returns (SetMessagingFeeResponse); - // GetMemberUpdates gets member updates for a given chat - rpc GetMemberUpdates(GetMemberUpdatesRequest) returns (GetMemberUpdatesResponse); - // PromoteUser promotes a user to an elevated permission state - rpc PromoteUser(PromoteUserRequest) returns (PromoteUserResponse); - // DemoteUser demotes a user to a lower permission state - rpc DemoteUser(DemoteUserRequest) returns (DemoteUserResponse); - // RemoveUser removes a user from a chat - rpc RemoveUser(RemoveUserRequest) returns (RemoveUserResponse); - // MuteUser mutes a user in the chat and removes their ability to send messages - rpc MuteUser(MuteUserRequest) returns (MuteUserResponse); - // MuteChat mutes a chat and disables push notifications - rpc MuteChat(MuteChatRequest) returns (MuteChatResponse); - // UnmuteChat unmutes a chat and enables push notifications - rpc UnmuteChat(UnmuteChatRequest) returns (UnmuteChatResponse); - // ReportUser reports a user for a given message - // - // todo: might belong in a different service long-term - rpc ReportUser(ReportUserRequest) returns (ReportUserResponse); -} -message StreamChatEventsRequest { - oneof type { - Params params = 1; - common.v1.ClientPong pong = 2; - } - message Params { - common.v1.Auth auth = 1; - // ts contains the time for stream open. - // - // It is used primarily as a nonce for auth. Server may reject - // timestamps that are too far in the future or past. - google.protobuf.Timestamp ts = 2; - } -} -message StreamChatEventsResponse { - oneof type { - common.v1.ServerPing ping = 1; - StreamError error = 2; - EventBatch events = 3; - } - message StreamError { - Code code = 1; - enum Code { - DENIED = 0; - } - } - message EventBatch { - repeated ChatUpdate updates = 1 ; - } - // ChatUpdate contains a set of updates for a given chat id. - // - // Only the relevant fields will be set on update. On initial - // stream open, all fields will be set, however. - message ChatUpdate { - common.v1.ChatId chat_id = 1; - // Metadata contains the latest (full) chat metadata. - // - // Deprecated: Use metadata_updates instead. For backwards compatibility - // this will only contain full metadata refreshes. - Metadata metadata = 2; - // MetadataUpdate contains updates to a chat metadata - repeated MetadataUpdate metadata_updates = 7; - // MemberUpdate contains an update to the membership set. - // - // Deprecated: Use member_updates instead. For backwards compatibility - // this will only contain full member refreshes. - MemberUpdate member_update = 3; - // MemberUpdate contains updates to the membership set. - repeated MemberUpdate member_updates = 8; - // Message contains the last known message of the chat. - messaging.v1.Message last_message = 4; - // Relevant update to a chat member's pointer state, where 'relevant' means - // "relevant to UI updates". For example, when a user has read the latest - // message. - // - // Note: Updates now go through the message stream, but may be rediverted here - // in the future - messaging.v1.PointerUpdate pointer = 5; - // IsTyping indicates whether or not someone is typing in the group. - // - // Note: Updates now go through the message stream, but may be rediverted here - // in the future - messaging.v1.IsTyping is_typing = 6; - } -} -message GetChatsRequest { - common.v1.QueryOptions query_options = 1; - common.v1.Auth auth = 2; -} -message GetChatsResponse { - Result result = 1; - enum Result { - OK = 0; - } - repeated Metadata chats = 2 ; -} -message GetChatRequest { - oneof identifier { - common.v1.ChatId chat_id = 1; - uint64 room_number = 2; - } - bool exclude_members = 9; - // Auth is an optional field that authenticates the call, which - // can be used to fill out extra information in the Metadata. - common.v1.Auth auth = 10; -} -message GetChatResponse { - Result result = 1; - enum Result { - OK = 0; - NOT_FOUND = 1; - } - // Metadata is the chat metadata, if result == OK. - // - // The contents of the metadata may change whether or not the - // call was authenticated. - Metadata metadata = 2; - // Members contains the chat members, if result == OK and were requested. - repeated Member members = 3; -} -message StartChatRequest { - oneof parameters { - StartTwoWayChatParameters two_way_chat = 1; - StartGroupChatParameters group_chat = 2; - } - // StartTwoWayChatParameters contains the parameters required to start - // or recover a two way chat between the caller and the specified 'other_user'. - // - // The 'other_user' is currently the 'tip_address', normally retrieved from - // user.Identity.GetTwitterUser(username). - message StartTwoWayChatParameters { - // The account id of the user the caller wishes to chat with. - common.v1.UserId other_user_id = 1; - } - message StartGroupChatParameters { - // A set of users (not including self) to initially set in the group chat. - repeated common.v1.UserId users = 1 ; - // Optional display name for the group chat - string display_name = 2 ; - // Optional payment for creating the group. It's up to server to decide - // if the user is allowed to create a group without payment. - common.v1.IntentId payment_intent = 3; - } - common.v1.Auth auth = 10; -} -message StartChatResponse { - Result result = 1; - enum Result { - OK = 0; - // DENIED indicates the caller is not allowed to start/join the chat. - DENIED = 1; - // USER_NOT_FOUND indicates that (one of) the target user's was not found. - USER_NOT_FOUND = 2; - } - // The chat to use, if result == OK. - Metadata chat = 2; - // Members contains the chat members, if result == OK. - repeated Member members = 3; -} -message StartGroupChatPaymentMetadata { - // The user creating the group chat, who will be the initial owner - common.v1.UserId user_id = 1; -} -message JoinChatRequest { - oneof identifier { - common.v1.ChatId chat_id = 1; - uint64 room_id = 2; - } - // Does the user want to join without the ability to send messages in the chat? - // If so, then payment_intent is not required? Otherwise, it is. - bool without_send_permission = 8; - // The payment for joining a chat, which is required for sending messages in - // the chat. - // - // Note: The chat owner can always bypass payment. - common.v1.IntentId payment_intent = 9; - common.v1.Auth auth = 10; -} -message JoinChatResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - } - // The chat metadata, if join was successful. - Metadata metadata = 2; - // The members of the chat, if join was successful. - repeated Member members = 3; -} -message JoinChatPaymentMetadata { - // The user joining the chat - common.v1.UserId user_id = 1; - // The chat that the user is joining - common.v1.ChatId chat_id = 2; -} -message LeaveChatRequest { - common.v1.ChatId chat_id = 1; - common.v1.Auth auth = 2; -} -message LeaveChatResponse { - Result result = 1; - enum Result { - OK = 0; - } -} -message OpenChatRequest { - // The chat that is being opened - common.v1.ChatId chat_id = 1; - common.v1.Auth auth = 2; -} -message OpenChatResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - } -} -message CloseChatRequest { - /// The chat that is being closed - common.v1.ChatId chat_id = 1; - common.v1.Auth auth = 2; -} -message CloseChatResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - } -} -message CheckDisplayNameRequest { - string display_name = 1 ; -} -message CheckDisplayNameResponse { - Result result = 1; - enum Result { - OK = 0; - } - bool is_allowed = 2; -} -message SetDisplayNameRequest { - common.v1.ChatId chat_id = 1; - string display_name = 2 ; - common.v1.Auth auth = 3; -} -message SetDisplayNameResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - CANT_SET = 2; - } - repeated string alternate_suggestions = 2 ; -} -message SetDescriptionRequest { - common.v1.ChatId chat_id = 1; - string description = 2 ; - common.v1.Auth auth = 3; -} -message SetDescriptionResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - CANT_SET = 2; - } -} -message SetCoverChargeRequest { - common.v1.ChatId chat_id = 1; - common.v1.PaymentAmount cover_charge = 2; - common.v1.Auth auth = 3; -} -message SetCoverChargeResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - CANT_SET = 2; - } -} -message SetMessagingFeeRequest { - common.v1.ChatId chat_id = 1; - common.v1.PaymentAmount messaging_fee = 2; - common.v1.Auth auth = 3; -} -message SetMessagingFeeResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - CANT_SET = 2; - } -} -message GetMemberUpdatesRequest { - common.v1.ChatId chat_id = 1; - // If not provided, a full refresh is performed. Server may also choose - // to compact updates into a full or individual refresh. - common.v1.PagingToken paging_token = 2; - // Auth is an optional field that authenticates the call, which - // can be used to fill out extra information. - common.v1.Auth auth = 3; -} -message GetMemberUpdatesResponse { - Result result = 1; - enum Result { - OK = 0; - NOT_FOUND = 1; - } - repeated MemberUpdate updates = 2 ; -} -message PromoteUserRequest { - common.v1.ChatId chat_id = 1; - common.v1.UserId user_id = 2; - // Enables send permissions when value is true - bool enable_send_permission = 3; - common.v1.Auth auth = 100; -} -message PromoteUserResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - NOT_REGISTERED = 2; - } -} -message DemoteUserRequest { - common.v1.ChatId chat_id = 1; - common.v1.UserId user_id = 2; - // Disables send permissions when value is true - bool disable_send_permission = 3; - common.v1.Auth auth = 100; -} -message DemoteUserResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - } -} -message RemoveUserRequest{ - common.v1.ChatId chat_id = 1; - common.v1.UserId user_id = 2; - common.v1.Auth auth = 3; -} -message RemoveUserResponse{ - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - } -} -message MuteUserRequest{ - common.v1.ChatId chat_id = 1; - common.v1.UserId user_id = 2; - common.v1.Auth auth = 3; -} -message MuteUserResponse{ - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - } -} -message MuteChatRequest { - common.v1.ChatId chat_id = 1; - common.v1.Auth auth = 2; -} -message MuteChatResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - } -} -message UnmuteChatRequest { - common.v1.ChatId chat_id = 1; - common.v1.Auth auth = 2; -} -message UnmuteChatResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - } -} -message ReportUserRequest{ - common.v1.UserId user_id = 1; - messaging.v1.MessageId message_id = 2; - common.v1.Auth auth = 3; -} -message ReportUserResponse{ - Result result = 1; - enum Result { - OK = 0; - } -} -message Metadata { - common.v1.ChatId chat_id = 1; - // The type of chat - ChatType type = 2 ; - enum ChatType { - UNKNOWN = 0; - TWO_WAY = 1; - GROUP = 2; - } - // The chat display name - string display_name = 3 ; - // If non-zero, the room number associated with the chat. - uint64 room_number = 4; - // Are push notifications enabled for this chat (from the perspective of the caller)? - bool is_push_enabled = 5; - // Can the user disable push notifications for this chat using MuteChat? - bool can_disable_push = 6; - // Number of (estimated) unread message (from the perspective of the caller). - uint32 num_unread = 7; - // If there are more unread messages than indicated by num_unread. If this is - // true, client should show num_unread+ as the unread count. - bool has_more_unread = 11; - // Owner is the owner/creator of the chat. - // - // This is a super priviledge role, in which there can only be one. - // This role is displayed as a 'host' currently. - common.v1.UserId owner = 8; - // If present, the fee that must be paid to send a message as a non-regular - // chat member. - // - // This replaces the legacy cover charge mechanic, which is deprecated - common.v1.PaymentAmount messaging_fee = 9; - // The timestamp of the last activity in this chat - google.protobuf.Timestamp last_activity = 10; - // The status as to whether the room is open or closed. This may be - // omitted for chats where it doesn't apply. If not provided, it's - // safe to assume the chat is open indefinitely until otherwise provided. - OpenStatus open_status = 12; - // Chat description - string description = 13 ; -} -// todo: In the future, we may add additional fields like open/closed until a timestamp, etc. -// For backwards compatibility, client can always refer to is_currently_open for whether -// a room is open right now or not for the purposes of sending messages. -// todo: A better name for this -message OpenStatus { - bool is_currently_open = 1; -} -message MetadataUpdate { - oneof kind { - FullRefresh full_refresh = 1; - UnreadCountChanged unread_count_changed = 2; - DisplayNameChanged display_name_changed = 3; - MessagingFeeChanged messaging_fee_changed = 4; - LastActivityChanged last_activity_changed = 5; - OpenStatusChanged open_status_changed = 6; - DescriptionChanged description_changed = 7; - } - // Refreshes the entire chat metadata - message FullRefresh { - Metadata metadata = 1; - } - // New message in the chat has generated a new unread count - message UnreadCountChanged { - // Number of (estimated) unread message - uint32 num_unread = 1; - // If there are more unread messages than indicated by num_unread. - // If this is true, client should show num_unread+ as the unread count. - bool has_more_unread = 2; - } - // The chat display name has been updated to a new value - message DisplayNameChanged { - string new_display_name = 1 ; - } - // The chat messaging fee has been updated to a new value - message MessagingFeeChanged { - common.v1.PaymentAmount new_messaging_fee = 1; - } - // The last activity timestamp has changed to a newer value - message LastActivityChanged { - google.protobuf.Timestamp new_last_activity = 1; - } - // The open status has changed to a newer value - message OpenStatusChanged { - OpenStatus new_open_status = 1; - } - message DescriptionChanged { - string new_description = 1 ; - } -} -message Member { - common.v1.UserId user_id = 1; - // The chat member's identity/profile information. - // - // It is a light weight version of the users full profile, which - // can be retrieved from the Profile service. - MemberIdentity identity = 2; - // Chat message state for this member. - // - // If set, the list may contain DELIVERED and READ pointers. SENT pointers - // are only shared between the sender and server, to indicate persistence. - // - // The server may wish to omit all pointers in various types of group chats - // or as relief valves. - repeated messaging.v1.Pointer pointers = 3 ; - // If the member is the caller (where applicable), will be set to true. - bool is_self = 4; - // Does the chat member have permission to perform moderation actions in - // the chat? - bool has_moderator_permission = 5; - // Has the chat member been muted by a moderator? If so, they cannot send - // messages, even if they paid for the permission. - bool is_muted = 6; - // Does the chat member have permission to send messages in the chat? If - // not, the user is considered to be a spectator or listener. Otherwise, - // they are a speaker. - bool has_send_permission = 7; -} -message MemberIdentity { - // If present, the display name of the user. - string display_name = 1 ; - // If present, the URL of the users profile pic. - string profile_pic_url = 2 ; - // The linked social profiles of the user - repeated profile.v1.SocialProfile social_profiles = 3 ; -} -message MemberUpdate { - oneof kind { - FullRefresh full_refresh = 1; - IndividualRefresh individual_refresh = 2; - Joined joined = 3; - Left left = 4; - Removed removed = 5; - Muted muted = 6; - Promoted promoted = 7; - Demoted demoted = 8; - IdentityChanged identity_changed = 9; - } - common.v1.PagingToken paging_token = 1000; - // Refreshes the state of the entire chat membership - message FullRefresh { - repeated Member members = 1 ; - } - // Refreshes the state of an individual member in the chat - message IndividualRefresh { - Member member = 1; - } - // Member joined the chat via the JoinChat RPC - message Joined { - Member member = 1; - } - // Member left the chat via the LeaveChat RPC - message Left { - common.v1.UserId member = 1; - } - // Member was removed from the chat via the RemoveUser RPC - message Removed { - common.v1.UserId member = 1; - common.v1.UserId removed_by = 2; - } - // Member was muted in the chat via the MuteUser RPC - message Muted { - common.v1.UserId member = 1; - common.v1.UserId muted_by = 2; - } - // Member was promoted in the chat via the PromoteUser RPC - message Promoted { - common.v1.UserId member = 1; - common.v1.UserId promoted_by = 2; - bool send_permission_enabled = 3; - } - // Member was demoted in the chat via the DemoteUser RPC - message Demoted { - common.v1.UserId member = 1; - common.v1.UserId demoted_by = 2; - bool send_permission_disabled = 3; - } - // Member identity has changed - message IdentityChanged { - common.v1.UserId member = 1; - MemberIdentity new_identity = 2; - } -} diff --git a/definitions/flipchat/protos/src/main/proto/common/v1/common.proto b/definitions/flipchat/protos/src/main/proto/common/v1/common.proto deleted file mode 100644 index 618f3bddf..000000000 --- a/definitions/flipchat/protos/src/main/proto/common/v1/common.proto +++ /dev/null @@ -1,95 +0,0 @@ -syntax = "proto3"; -package flipchat.common.v1; -option go_package = "github.com/code-payments/flipchat-protobuf-api/generated/go/common/v1;commonpb"; -option java_package = "com.codeinc.flipchat.gen.common.v1"; -option objc_class_prefix = "FPBCommonV1"; -import "google/protobuf/duration.proto"; -import "google/protobuf/timestamp.proto"; - -// Auth provides an authentication information for RPCs/messages. -// -// Currently, only a single form is supported, but it may be useful in -// the future to rely on session tokens instead. -message Auth { - oneof kind { - // KeyPair uses pub key cryptography to verify. - KeyPair key_pair = 1; - } - // KeyPair uses a keypair to verify a message. - // - // The signature should be of the encapsulating proto message, - // _without_ the Auth section being set. - message KeyPair { - PublicKey pub_key = 1; - Signature signature = 2; - } -} -message UserId { - bytes value = 1 ; -} -message ChatId { - // Sufficient space is left for a consistent hash value, though other types - // of values may be used. - bytes value = 1 ; -} -// AppInstallId is a unque ID tied to a client app installation. It does not -// identify a device. Value should remain private and not be shared across -// installs. -message AppInstallId { - string value = 1 ; -} -// Locale is a user locale consisting of a combination of language, script and region -message Locale { - string value = 1; -} -message PublicKey { - bytes value = 1 ; -} -message IntentId { - bytes value = 1 ; -} -message Signature { - bytes value = 1 ; -} -message PaymentAmount { - uint64 quarks = 1; -} -message ServerPing { - // Timestamp the ping was sent on the stream, for client to get a sense - // of potential network latency - google.protobuf.Timestamp timestamp = 1; - // The delay server will apply before sending the next ping - google.protobuf.Duration ping_delay = 2; -} -message ClientPong { - // Timestamp the Pong was sent on the stream, for server to get a sense - // of potential network latency - google.protobuf.Timestamp timestamp = 1; -} -message PagingToken { - // Value contains a value of an identifier of the collection in common. - // - // For example, GetChats uses the ChatId.Value, where GetMessages uses MessageId.Value - // as the contents. It does _not_ contain the serialized ChatId or MessageId. - bytes value = 1 ; -} -message QueryOptions { - // PageSize limits the maximum page size of a response. - // - // Server may choose to return less items. If empty, server - // may select an arbitrary page size. - int64 page_size = 1; - // PagingToken is token that can be extracted from the identifier of a collection. - PagingToken paging_token = 2; - // Order is the order of elements, if applicable. - Order order = 3; - enum Order { - ASC = 0; - DESC = 1; - } -} -enum Platform { - UNKNOWN = 0; - APPLE = 1; - GOOGLE = 2; -} diff --git a/definitions/flipchat/protos/src/main/proto/iap/v1/iap_service.proto b/definitions/flipchat/protos/src/main/proto/iap/v1/iap_service.proto deleted file mode 100644 index f8fd43423..000000000 --- a/definitions/flipchat/protos/src/main/proto/iap/v1/iap_service.proto +++ /dev/null @@ -1,27 +0,0 @@ -syntax = "proto3"; -package flipchat.iap.v1; -option go_package = "github.com/code-payments/flipchat-protobuf-api/generated/go/iap/v1;iappb"; -option java_package = "com.codeinc.flipchat.gen.iap.v1"; -option objc_class_prefix = "FPBIapV1"; -import "common/v1/common.proto"; - -service Iap { - // OnPurchaseCompleted is called when an IAP has been completed - rpc OnPurchaseCompleted(OnPurchaseCompletedRequest) returns (OnPurchaseCompletedResponse); -} -message OnPurchaseCompletedRequest { - common.v1.Platform platform = 1; - Receipt receipt = 2; - common.v1.Auth auth = 3; -} -message OnPurchaseCompletedResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - INVALID_RECEIPT = 2; // Returned if the receipt is invalid, or not in a completed payment state - } -} -message Receipt { - string value = 1 ; -} diff --git a/definitions/flipchat/protos/src/main/proto/messaging/v1/messaging_service.proto b/definitions/flipchat/protos/src/main/proto/messaging/v1/messaging_service.proto deleted file mode 100644 index 68ab5076e..000000000 --- a/definitions/flipchat/protos/src/main/proto/messaging/v1/messaging_service.proto +++ /dev/null @@ -1,151 +0,0 @@ -syntax = "proto3"; -package flipchat.messaging.v1; -option go_package = "github.com/code-payments/flipchat-protobuf-api/generated/go/messaging/v1;messagingpb"; -option java_package = "com.codeinc.flipchat.gen.messaging.v1"; -option objc_class_prefix = "FCPBMessagingV1"; -import "common/v1/common.proto"; -import "messaging/v1/model.proto"; - -service Messaging { - // StreamMessages streams all messages/message states (eg. pointers, typing, etc) - // for the requested chat. - rpc StreamMessages(stream StreamMessagesRequest) returns (stream StreamMessagesResponse); - // GetMessage gets a single message in a chat - rpc GetMessage(GetMessageRequest) returns (GetMessageResponse); - // GetMessages gets the set of messages for a chat using a paged and batched APIs - rpc GetMessages(GetMessagesRequest) returns (GetMessagesResponse); - // SendMessage sends a message to a chat. - rpc SendMessage(SendMessageRequest) returns (SendMessageResponse); - // AdvancePointer advances a pointer in message history for a chat member. - rpc AdvancePointer(AdvancePointerRequest) returns (AdvancePointerResponse); - // NotifyIsTypingRequest notifies a chat that the sending member is typing. - // - // These requests are transient, and may be dropped at any point. - rpc NotifyIsTyping(NotifyIsTypingRequest) returns (NotifyIsTypingResponse); -} -message StreamMessagesRequest { - oneof type { - Params params = 1; - common.v1.ClientPong pong = 2; - } - message Params { - common.v1.ChatId chat_id = 1; - // Deprecated: stream flushes are no longer supported - oneof resume { - MessageId last_known_message_id = 2; - bool latest_only = 3; - } - common.v1.Auth auth = 4; - } -} -message StreamMessagesResponse { - oneof type { - common.v1.ServerPing ping = 1; - StreamError error = 2; - MessageBatch messages = 3; - PointerUpdateBatch pointer_updates = 4; - IsTypingBatch is_typing_notifications = 5; - } - message StreamError { - Code code = 1; - enum Code { - DENIED = 0; - } - } -} -message GetMessageRequest { - common.v1.ChatId chat_id = 1; - MessageId message_id = 2; - common.v1.Auth auth = 3; -} -message GetMessageResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - NOT_FOUND = 2; - } - Message message = 2; -} -message GetMessagesRequest { - common.v1.ChatId chat_id = 1; - // If not set, defaults to an ascending query option without a page token and server-defined limit - oneof query { - common.v1.QueryOptions options = 2; - MessageIdBatch message_ids = 3; - } - common.v1.Auth auth = 5; -} -message GetMessagesResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - } - repeated Message messages = 2 ; -} -message SendMessageRequest { - common.v1.ChatId chat_id = 1; - // Allowed content types that can be sent by client: - // - TextContent - // - ReactionContent - // - ReplyContent - // - TipContent - // - DeleteMessageContent - // - ReviewContent - repeated Content content = 2 ; - common.v1.Auth auth = 3; - // Intent ID for message contents that require a payment - common.v1.IntentId payment_intent = 4; -} -message SendMessageResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - } - // The chat message that was sent if the RPC was succesful, which includes - // server-side metadata like the generated message ID and official timestamp - Message message = 2; -} -message SendMessageAsListenerPaymentMetadata { - // The chat where the message is being sent - common.v1.ChatId chat_id = 1; - // The user sending the message - common.v1.UserId user_id = 2; -} -message SendTipMessagePaymentMetadata { - // The chat where the message is being tipped - common.v1.ChatId chat_id = 1; - // The message that is being tipped - MessageId message_id = 2; - // The user sending the tip - common.v1.UserId tipper_id = 3; -} -message AdvancePointerRequest { - common.v1.ChatId chat_id = 1; - Pointer pointer = 2; - common.v1.Auth auth = 3; -} -message AdvancePointerResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - MESSAGE_NOT_FOUND = 2; - } -} -message NotifyIsTypingRequest { - common.v1.ChatId chat_id = 1; - // Deprecated: Use typing_state instead - bool is_typing = 2; - TypingState typing_state = 4; - common.v1.Auth auth = 3; -} -message NotifyIsTypingResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - } -} diff --git a/definitions/flipchat/protos/src/main/proto/messaging/v1/model.proto b/definitions/flipchat/protos/src/main/proto/messaging/v1/model.proto deleted file mode 100644 index 552025b7c..000000000 --- a/definitions/flipchat/protos/src/main/proto/messaging/v1/model.proto +++ /dev/null @@ -1,152 +0,0 @@ -syntax = "proto3"; -package flipchat.messaging.v1; -option go_package = "github.com/code-payments/flipchat-protobuf-api/generated/go/messaging/v1;messagingpb"; -option java_package = "com.codeinc.flipchat.gen.messaging.v1"; -option objc_class_prefix = "FCPBMessagingV1"; -import "common/v1/common.proto"; -import "google/protobuf/timestamp.proto"; - -message MessageId { - // A lexicographically sortable ID that can be used to sort source of - // chat history. - bytes value = 1 ; -} -message MessageIdBatch { - repeated MessageId message_ids = 1 ; -} -// A message in a chat -message Message { - // Globally unique ID for this message - MessageId message_id = 1; - // The chat member that sent the message. For NOTIFICATION chats, this field - // is omitted since the chat has exactly 1 member. - common.v1.UserId sender_id = 2; - // Message content, which is currently guaranteed to have exactly one item. - repeated Content content = 3 ; - // Timestamp this message was generated at. This value is also encoded in - // any time-based UUID message IDs. - google.protobuf.Timestamp ts = 4; - // If sender_id is provided, were they off stage at the time of sending - // this message - bool was_sender_off_stage = 5; -} -message MessageBatch { - repeated Message messages = 1 ; -} -// Pointer in a chat indicating a user's message history state in a chat. -message Pointer { - // The type of pointer indicates which user's message history state can be - // inferred from the pointer value. It is also possible to infer cross-pointer - // state. For example, if a chat member has a READ pointer for a message with - // ID N, then the DELIVERED pointer must be at least N. - Type type = 1 ; - enum Type { - UNKNOWN = 0; - SENT = 1; // Always inferred by OK result in SendMessageResponse or message presence in a chat - DELIVERED = 2; - READ = 3; - } - // Everything at or before this message ID is considered to have the state - // inferred by the type of pointer. - MessageId value = 2; -} -message PointerUpdate { - common.v1.UserId member = 1; - messaging.v1.Pointer pointer = 2; -} -message PointerUpdateBatch { - repeated PointerUpdate pointer_updates = 1 ; -} -message IsTyping { - common.v1.UserId user_id = 1; - // Deprecated: Use typing_state instead - bool is_typing = 2; - TypingState typing_state = 3; -} -message IsTypingBatch { - repeated IsTyping is_typing_notifications = 1 ; -} -enum TypingState { - UNKNOWN_TYPING_STATE = 0; - STARTED_TYPING = 1; - STILL_TYPING = 2; - STOPPED_TYPING = 3; - TYPING_TIMED_OUT = 4; -} -// Content for a chat message -message Content { - oneof type { - TextContent text = 1; - LocalizedAnnouncementContent localized_announcement = 2; - ReactionContent reaction = 5; - ReplyContent reply = 6; - TipContent tip = 7; - DeleteMessageContent deleted = 8; - ReviewContent review = 9; - ActionableAnnouncementContent actionable_announcement = 10; - } - reserved 3; // ExchangeDataContent - reserved 4; // NaclBoxEncryptedContent -} -// Raw text content -message TextContent { - string text = 1 ; -} -// LocalizedAnnouncementContent content is an annoucement that is either a -// localization key that should be translated on client, or a server-side -// translated piece of text. -message LocalizedAnnouncementContent { - string key_or_text = 1 ; - // todo: define arguments list - reserved 2; -} -// ActionableAnnouncementContent is like LocalizedAnnouncementContent, but -// contains additional metadata for actions -message ActionableAnnouncementContent { - string key_or_text = 1 ; - // todo: define arguments list - reserved 2; - // An action that can be taken by a user - Action action = 3; - message Action { - oneof type { - - ShareRoomLink share_room_link = 1; - } - // Displays a button to share a link to a room - message ShareRoomLink { - } - } -} -// Emoji reaction to another message -message ReactionContent { - // The message ID of the message this reaction is associated with - MessageId original_message_id = 1 ; - // The emoji or reaction symbol - string emoji = 2 ; -} -// Text reply of another message -message ReplyContent { - // The message ID of the message this reply is referencing - MessageId original_message_id = 1 ; - // The reply text, which can be handled similarly to TextContent - string reply_text = 2 ; -} -message TipContent { - // The message ID of the message this tip is referencing - MessageId original_message_id = 1 ; - // The amount tipped for the message - common.v1.PaymentAmount tip_amount = 2; -} -message DeleteMessageContent { - // The message ID of the message that was deleted - MessageId original_message_id = 1 ; -} -message ReviewContent { - // The message ID of the message that is being reviewed. Currently, only - // off stage messages can be reviewed - MessageId original_message_id = 1 ; - // Whether the message has been approved. In the event of multiple reviews, - // the first message in the message log takes priority. - bool is_approved = 2; -} diff --git a/definitions/flipchat/protos/src/main/proto/profile/v1/model.proto b/definitions/flipchat/protos/src/main/proto/profile/v1/model.proto deleted file mode 100644 index 762047a8a..000000000 --- a/definitions/flipchat/protos/src/main/proto/profile/v1/model.proto +++ /dev/null @@ -1,39 +0,0 @@ -syntax = "proto3"; -package flipchat.profile.v1; -option go_package = "github.com/code-payments/flipchat-protobuf-api/generated/go/profile/v1;profilepb"; -option java_package = "com.codeinc.flipchat.gen.profile.v1"; -option objc_class_prefix = "FCPBProfileV1"; - -message UserProfile { - // Display name is the display name of the user (if found). - string display_name = 1 ; - // Social profiles are links to external social accounts - repeated SocialProfile social_profiles = 2 ; -} -message SocialProfile { - oneof type { - XProfile x = 1; - } -} -message XProfile { - // The user's ID on X - string id = 1 ; - // The user's username on X - string username = 2 ; - // The user's friendly name on X - string name = 3 ; - // The user's description on X - string description = 4 ; - // URL to the user's X profile picture - string profile_pic_url = 5 ; - // The type of X verification associated with the user - VerifiedType verified_type = 6; - enum VerifiedType { - NONE = 0; - BLUE = 1; - BUSINESS = 2; - GOVERNMENT = 3; - } - // The number of followers the user has on X - uint32 follower_count = 7; -} diff --git a/definitions/flipchat/protos/src/main/proto/profile/v1/profile_service.proto b/definitions/flipchat/protos/src/main/proto/profile/v1/profile_service.proto deleted file mode 100644 index 9eca31946..000000000 --- a/definitions/flipchat/protos/src/main/proto/profile/v1/profile_service.proto +++ /dev/null @@ -1,80 +0,0 @@ -syntax = "proto3"; -package flipchat.profile.v1; -option go_package = "github.com/code-payments/flipchat-protobuf-api/generated/go/profile/v1;profilepb"; -option java_package = "com.codeinc.flipchat.gen.profile.v1"; -option objc_class_prefix = "FCPBProfileV1"; -import "common/v1/common.proto"; -import "profile/v1/model.proto"; - -service Profile { - rpc GetProfile(GetProfileRequest) returns (GetProfileResponse); - rpc SetDisplayName(SetDisplayNameRequest) returns (SetDisplayNameResponse); - // LinkSocialAccount links a social account to a user - rpc LinkSocialAccount(LinkSocialAccountRequest) returns (LinkSocialAccountResponse); - // UnlinkSocialAccount removes a social account link from a user - rpc UnlinkSocialAccount(UnlinkSocialAccountRequest) returns (UnlinkSocialAccountResponse); -} -message GetProfileRequest { - common.v1.UserId user_id = 1; -} -message GetProfileResponse { - Result result = 1; - enum Result { - OK = 0; - NOT_FOUND = 1; - } - // UserProfile, if found. - // - // Some fields may or may not be set, depending on the scope of request - // in the future. - UserProfile user_profile = 2; -} -message SetDisplayNameRequest { - // DisplayName is the new name to set. - string display_name = 1 ; - common.v1.Auth auth = 10; -} -message SetDisplayNameResponse { - Result result = 1; - enum Result { - OK = 0; - INVALID_DISPLAY_NAME = 1; - DENIED = 2; - } -} -message LinkSocialAccountRequest { - LinkingToken linking_token = 1; - message LinkingToken { - oneof type { - XLinkingToken x = 1; - } - message XLinkingToken { - // X access token from the OAuth 2.0 flow - string access_token = 1; - } - } - common.v1.Auth auth = 10; -} -message LinkSocialAccountResponse { - Result result = 1; - enum Result { - OK = 0; - INVALID_LINKING_TOKEN = 1; - EXISTING_LINK = 2; - DENIED = 3; - } - SocialProfile social_profile = 2; -} -message UnlinkSocialAccountRequest { - oneof social_identifier { - string x_user_id = 1 ; - } - common.v1.Auth auth = 10; -} -message UnlinkSocialAccountResponse { - Result result = 1; - enum Result { - OK = 0; - DENIED = 1; - } -} diff --git a/definitions/flipchat/protos/src/main/proto/push/v1/push_service.proto b/definitions/flipchat/protos/src/main/proto/push/v1/push_service.proto deleted file mode 100644 index 76e983f6d..000000000 --- a/definitions/flipchat/protos/src/main/proto/push/v1/push_service.proto +++ /dev/null @@ -1,58 +0,0 @@ -syntax = "proto3"; -package flipchat.push.v1; -option go_package = "github.com/code-payments/flipchat-protobuf-api/generated/go/push/v1;pushpb"; -option java_package = "com.codeinc.flipchat.gen.push.v1"; -option objc_class_prefix = "FPBPushV1"; -import "common/v1/common.proto"; - -service Push { - // AddToken adds a push token associated with a user. - rpc AddToken(AddTokenRequest) returns (AddTokenResponse); - // DeleteToken removes a specific push token from a user. - // - // Deprecated: Use DeleteTokens intead - rpc DeleteToken(DeleteTokenRequest) returns (DeleteTokenResponse); - // DeleteTokens removes all push tokens within an app install for a user - rpc DeleteTokens(DeleteTokensRequest) returns (DeleteTokensResponse); -} -enum TokenType { - UNKNOWN = 0; - // FCM registration token for an Android device - FCM_ANDROID = 1; - // FCM registration token or an iOS device - FCM_APNS = 2; -} -message AddTokenRequest { - TokenType token_type = 1; - string push_token = 2 ; - common.v1.AppInstallId app_install = 3; - common.v1.Auth auth = 4; -} -message AddTokenResponse { - Result result = 1; - enum Result { - OK = 0; - INVALID_PUSH_TOKEN = 1; - } -} -message DeleteTokenRequest { - TokenType token_type = 1; - string push_token = 2 ; - common.v1.Auth auth = 3; -} -message DeleteTokenResponse { - Result result = 1; - enum Result { - OK = 0; - } -} -message DeleteTokensRequest { - common.v1.AppInstallId app_install = 1; - common.v1.Auth auth = 2; -} -message DeleteTokensResponse { - Result result = 1; - enum Result { - OK = 0; - } -} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index da65431be..0b54ff445 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -16,18 +16,6 @@ default_platform(:android) platform :android do - desc "Runs all the tests for Code" - lane :code_tests do - gradle(task: "generateEmojiList") - gradle(task: "apps:codeApp:testDebug") - end - - desc "Runs all the tests for Flipchat" - lane :flipchat_tests do - gradle(task: "generateEmojiList") - gradle(task: "apps:flipchatApp:testDebug") - end - desc "Runs all the tests for Flipcash" lane :flipcash_tests do gradle( @@ -38,86 +26,6 @@ platform :android do ) end - desc "Build and Deploy a new version of Code to the Google Play" - lane :deploy_code do - #puts "Patch version for this build will be " + ENV["BUILD_NUMBER"] - gradle( - task: "clean apps:codeApp:bundle", #"clean app:bundleRelease", - build_type: "release", - properties: { - #"versionPatch" => ENV["BUILD_NUMBER"], - "android.injected.signing.store.file" => "key/key", - "android.injected.signing.store.password" => ENV["STORE_PASSWORD"], - "android.injected.signing.key.alias" => ENV["KEY_ALIAS"], - "android.injected.signing.key.password" => ENV["KEY_PASSWORD"] - } - ) - - validate_play_store_json_key( - json_key: ENV["SERVICE_ACCOUNT_KEY_JSON"] - ) - - upload_to_play_store( - track: ENV["PLAYSTORE_TRACK"], -# release_status: ENV["RELEASE_STATUS"], - aab: Actions.lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH], - skip_upload_apk: true, - skip_upload_changelogs: true, - skip_upload_images: true, - package_name: ENV["PACKAGE_NAME"], - mapping: Actions.lane_context[SharedValues::GRADLE_MAPPING_TXT_OUTPUT_PATH] - # mapping: mapping_file_exists() ? Actions.lane_context[SharedValues::GRADLE_MAPPING_TXT_OUTPUT_PATH] : nil - ) - end - - - desc "Build and Deploy a new version of Flipchat to the Google Play Store" - lane :deploy_flipchat do - #puts "Patch version for this build will be " + ENV["BUILD_NUMBER"] - gradle( - task: "generateEmojiList" - ) - - gradle( - task: "clean apps:flipchatApp:bundle", #"clean app:bundleRelease", - build_type: "release", - properties: { - #"versionPatch" => ENV["BUILD_NUMBER"], - "android.injected.signing.store.file" => "key/key", - "android.injected.signing.store.password" => ENV["STORE_PASSWORD"], - "android.injected.signing.key.alias" => ENV["KEY_ALIAS"], - "android.injected.signing.key.password" => ENV["KEY_PASSWORD"] - } - ) - - bundletool( - ks_path: "key/key", - ks_password: ENV["STORE_PASSWORD"], - ks_key_alias: ENV["KEY_ALIAS"], - ks_key_alias_password: ENV["KEY_PASSWORD"], - bundletool_version: '1.10.0', - aab_path: Actions.lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH], - apk_output_path: "apps/flipchatApp/build/outputs/apk/release", - verbose: true, - ) - - validate_play_store_json_key( - json_key: ENV["SERVICE_ACCOUNT_KEY_JSON"] - ) - - upload_to_play_store( - track: ENV["PLAYSTORE_TRACK"], - release_status: ENV["RELEASE_STATUS"], - aab: Actions.lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH], - skip_upload_apk: true, - skip_upload_changelogs: true, - skip_upload_images: true, - package_name: ENV["PACKAGE_NAME"], - mapping: Actions.lane_context[SharedValues::GRADLE_MAPPING_TXT_OUTPUT_PATH] - # mapping: mapping_file_exists() ? Actions.lane_context[SharedValues::GRADLE_MAPPING_TXT_OUTPUT_PATH] : nil - ) - end - desc "Build a new version of Flipcash" lane :build_flipcash do gradle( diff --git a/libs/models-ktx/.gitignore b/libs/models-ktx/.gitignore deleted file mode 100644 index 9f2a07880..000000000 --- a/libs/models-ktx/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -build/ -.gradle/ diff --git a/libs/models-ktx/build.gradle.kts b/libs/models-ktx/build.gradle.kts deleted file mode 100644 index 1306be492..000000000 --- a/libs/models-ktx/build.gradle.kts +++ /dev/null @@ -1,26 +0,0 @@ -plugins { - alias(libs.plugins.flipcash.android.library) - id("org.jetbrains.kotlin.plugin.parcelize") -} - -android { - namespace = "${Gradle.codeNamespace}.libs.models" -} - -dependencies { - implementation(project(":libs:encryption:base58")) - implementation(project(":libs:encryption:ed25519")) - implementation(project(":libs:encryption:keys")) - implementation(project(":libs:encryption:utils")) - implementation(project(":libs:crypto:kin")) - implementation(project(":libs:currency")) - api(project(":libs:models")) - implementation(libs.bundles.kotlinx.serialization) - - implementation(libs.androidx.room.runtime) - implementation(libs.androidx.room.ktx) - - implementation(libs.sodium.bindings) - - implementation(project(":definitions:code:models")) -} diff --git a/libs/models-ktx/src/main/kotlin/com/getcode/model/EncryptedData.kt b/libs/models-ktx/src/main/kotlin/com/getcode/model/EncryptedData.kt deleted file mode 100644 index d3dc9d474..000000000 --- a/libs/models-ktx/src/main/kotlin/com/getcode/model/EncryptedData.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.getcode.model - -import com.getcode.ed25519.Ed25519.KeyPair - -fun EncryptedData.decryptMessageUsingNaClBox(keyPair: KeyPair): String? { - val encryptionKey = keyPair.encryptionPrivateKey ?: return null - - val data = encryptedData.boxOpen( - privateKey = encryptionKey, - publicKey = peerPublicKey, - nonce = nonce, - ).getOrNull() ?: return null - - return String(data.toByteArray()) -} \ No newline at end of file diff --git a/libs/models-ktx/src/main/kotlin/com/getcode/model/Sodium.kt b/libs/models-ktx/src/main/kotlin/com/getcode/model/Sodium.kt deleted file mode 100644 index 447a5fd55..000000000 --- a/libs/models-ktx/src/main/kotlin/com/getcode/model/Sodium.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.getcode.model - -import android.util.Base64 -import com.getcode.ed25519.Ed25519 -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.solana.keys.PrivateKey -import com.getcode.solana.keys.PublicKey -import com.ionspin.kotlin.crypto.box.Box -import com.ionspin.kotlin.crypto.secretbox.SecretBox -import com.ionspin.kotlin.crypto.signature.Signature - -/** Some cryptographic function require the private - * key to be formatted this way to work correctly. - * A good example of this would Sodium and the box - * `seal` and `open` functions. -*/ -val KeyPair.encryptionPrivateKey: PrivateKey? - get() { - if (seed.isNullOrEmpty()) return null - - val bytes = Base64.decode(seed, Base64.DEFAULT) + publicKeyBytes - return PrivateKey(bytes.toList()) - } - -val PrivateKey.curvePrivate: Result - get() { - val localBytes = bytes - - val result = runCatching { Signature.ed25519SkToCurve25519(localBytes.map { it.toUByte() }.toUByteArray()) } - - if (result.isFailure) { - return Result.failure(SodiumError.ConversionToCurveFailed(result.exceptionOrNull())) - } - return Result.success(result.getOrNull()!!.toPublicKey()) - } - -val PublicKey.curvePublic: Result - get() { - val localBytes = bytes - - val result = runCatching { Signature.ed25519PkToCurve25519(localBytes.map { it.toUByte() }.toUByteArray()) } - if (result.isFailure) { - return Result.failure(SodiumError.ConversionToCurveFailed(result.exceptionOrNull())) - } - return Result.success(result.getOrNull()!!.toPublicKey()) - } - -fun PublicKey.Companion.shared(publicKey: PublicKey, privateKey: PublicKey): Result { - val pubKeyBytes = publicKey.bytes.map { it.toUByte() }.toUByteArray() - val secretKeyBytes = privateKey.bytes.map { it.toUByte() }.toUByteArray() - val result = runCatching { Box.beforeNM(pubKeyBytes, secretKeyBytes) } - - if (result.isFailure) { - return Result.failure(SodiumError.SharedKeyFailed(result.exceptionOrNull())) - } - return Result.success(result.getOrNull()!!.toPublicKey()) -} - -fun PublicKey.Companion.fromUbytes(bytes: List): PublicKey { - return PublicKey(bytes.map { it.toByte() }) -} - -fun String.boxSeal(privateKey: PrivateKey, publicKey: PublicKey, nonce: List): Result> { - val publicCurve = publicKey.curvePublic.getOrThrow() - val privateCurve = privateKey.curvePrivate.getOrThrow() - - val sharedKey = PublicKey.shared(publicCurve, privateCurve).getOrThrow() - val nonceU = nonce.map { it.toUByte() }.toUByteArray() - val message = toByteArray().map { it.toUByte() }.toUByteArray() - - val encrypted = runCatching { - SecretBox.easy( - key = sharedKey.bytes.map { it.toUByte() }.toUByteArray(), - message = message, - nonce = nonceU - ) - } - - if (encrypted.isFailure) { - return Result.failure(SodiumError.EncryptionFailed(encrypted.exceptionOrNull())) - } - - return Result.success(encrypted.getOrNull()!!.map { it.toByte() }) -} - -fun List.boxOpen(privateKey: PrivateKey, publicKey: PublicKey, nonce: List): Result> { - val publicCurve = publicKey.curvePublic.getOrThrow() - val privateCurve = privateKey.curvePrivate.getOrThrow() - - val sharedKey = PublicKey.shared(publicCurve, privateCurve).getOrThrow() - .bytes.map { it.toUByte() }.toUByteArray() - - val nonceU = nonce.map { it.toUByte() }.toUByteArray() - val cipher = map { it.toUByte() }.toUByteArray() - - val decrypted = runCatching { SecretBox.openEasy(cipher, nonceU, sharedKey) } - - if (decrypted.isFailure) { - return Result.failure(SodiumError.DecryptionFailed(decrypted.exceptionOrNull())) - } - - return Result.success(decrypted.getOrNull()!!.map { it.toByte() }) -} - -sealed class SodiumError { - data class ConversionToCurveFailed(val root: Throwable? = null): Throwable(cause = root) - data class SharedKeyFailed(val root: Throwable? = null): Throwable(cause = root) - data class EncryptionFailed(val root: Throwable? = null): Throwable(cause = root) - data class DecryptionFailed(val root: Throwable? = null): Throwable(cause = root) -} \ No newline at end of file diff --git a/libs/models-ktx/src/main/kotlin/com/getcode/model/Title.kt b/libs/models-ktx/src/main/kotlin/com/getcode/model/Title.kt deleted file mode 100644 index 1dcd4e618..000000000 --- a/libs/models-ktx/src/main/kotlin/com/getcode/model/Title.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.getcode.model - -import com.codeinc.gen.chat.v1.ChatService -import com.getcode.model.chat.Title -import com.getcode.model.chat.Title.Domain -import com.getcode.model.chat.Title.Localized - -operator fun Title.Companion.invoke(proto: ChatService.ChatMetadata): Title? { - return when (proto.titleCase) { - ChatService.ChatMetadata.TitleCase.LOCALIZED -> Localized(proto.localized.keyOrText) - ChatService.ChatMetadata.TitleCase.DOMAIN -> Domain(proto.domain.value) - ChatService.ChatMetadata.TitleCase.TITLE_NOT_SET -> null - else -> null - } -} \ No newline at end of file diff --git a/libs/requests/.gitignore b/libs/requests/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/libs/requests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/libs/requests/build.gradle.kts b/libs/requests/build.gradle.kts deleted file mode 100644 index 25270026b..000000000 --- a/libs/requests/build.gradle.kts +++ /dev/null @@ -1,27 +0,0 @@ -plugins { - alias(libs.plugins.flipcash.android.library.compose) -} - -android { - namespace = "${Gradle.codeNamespace}.libs.requests" -} - -dependencies { - implementation(project(":services:legacy-shared")) - implementation(project(":libs:currency")) - implementation(project(":libs:logging")) - implementation(project(":libs:models")) - implementation(project(":libs:encryption:base58")) - implementation(project(":libs:encryption:keys")) - implementation(project(":libs:encryption:mnemonic")) - implementation(project(":libs:encryption:utils")) - implementation(project(":ui:resources")) - - implementation(libs.bundles.kotlinx.serialization) - implementation(libs.kotlinx.datetime) - implementation(libs.bundles.hilt) - - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.junit) - androidTestImplementation(libs.androidx.test.runner) -} diff --git a/libs/requests/src/main/kotlin/com/getcode/domain/BillController.kt b/libs/requests/src/main/kotlin/com/getcode/domain/BillController.kt deleted file mode 100644 index 2d3816e26..000000000 --- a/libs/requests/src/main/kotlin/com/getcode/domain/BillController.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.getcode.domain - -import com.getcode.models.BillState -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class BillController @Inject constructor( - -) { - private val _state = MutableStateFlow(BillState.Default) - val state: StateFlow - get() = _state - - fun update(function: (BillState) -> BillState) { - _state.update(function) - } - - fun reset() { - _state.update { BillState.Default } - } -} \ No newline at end of file diff --git a/libs/requests/src/main/kotlin/com/getcode/models/BillState.kt b/libs/requests/src/main/kotlin/com/getcode/models/BillState.kt deleted file mode 100644 index ce002e531..000000000 --- a/libs/requests/src/main/kotlin/com/getcode/models/BillState.kt +++ /dev/null @@ -1,289 +0,0 @@ -package com.getcode.models - -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import com.getcode.extensions.formatted -import com.getcode.libs.requests.R -import com.getcode.model.ID -import com.getcode.model.Kin -import com.getcode.model.KinAmount -import com.getcode.model.Rate -import com.getcode.model.SocialUser -import com.getcode.model.TwitterUser -import com.getcode.services.model.CodePayload -import com.getcode.services.model.ExtendedMetadata -import com.getcode.solana.keys.PublicKey - -data class BillState( - val bill: Bill?, - val showToast: Boolean, - val toast: BillToast?, - val valuation: Valuation?, - val privatePaymentConfirmation: PrivatePaymentConfirmation?, - val publicPaymentConfirmation: PublicPaymentConfirmation?, - val messageTipPaymentConfirmation: MessageTipPaymentConfirmation?, - val loginConfirmation: LoginConfirmation?, - val socialUserPaymentConfirmation: SocialUserPaymentConfirmation?, - val primaryAction: Action?, - val secondaryAction: Action?, -) { - val hideBillButtons: Boolean - get() = primaryAction == null && secondaryAction == null - - val canSwipeToDismiss: Boolean - get() = bill?.canSwipeToDismiss ?: false - - companion object { - val Default = BillState( - bill = null, - showToast = false, - toast = null, - valuation = null, - privatePaymentConfirmation = null, - publicPaymentConfirmation = null, - messageTipPaymentConfirmation = null, - loginConfirmation = null, - socialUserPaymentConfirmation = null, - primaryAction = null, - secondaryAction = null, - ) - } - - sealed interface Action { - - val label: String? - @Composable get - val asset: Painter - @Composable get - - val action: () -> Unit - - data class Send(override val action: () -> Unit): Action { - override val label: String - @Composable get() = stringResource(R.string.action_send) - override val asset: Painter - @Composable get() = painterResource(id = R.drawable.ic_remote_send) - } - - data class Share(override val action: () -> Unit): Action { - override val label: String - @Composable get() = stringResource(R.string.action_shareAsURL) - - override val asset: Painter - @Composable get() = painterResource(id = R.drawable.ic_remote_send) - } - - data class Cancel(override val action: () -> Unit): Action { - override val label: String? - @Composable get() = null - - override val asset: Painter - @Composable get() = painterResource(R.drawable.ic_bill_close) - } - - data class Done(override val action: () -> Unit): Action { - override val label: String - @Composable get() = stringResource(R.string.action_done) - - override val asset: Painter - @Composable get() = painterResource(R.drawable.ic_check_white) - } - } -} - -sealed interface Bill { - val didReceive: Boolean - val amount: KinAmount - val data: List - - enum class Kind { - cash, remote, firstKin, referral, tip - } - - val canSwipeToDismiss: Boolean - get() = when (this) { - is Cash -> true - is Login -> false - is Payment -> false - is Tip -> true - } - - val canFlip: Boolean - - val metadata: Metadata - get() { - return when (this) { - is Cash -> Metadata( - kinAmount = amount, - data = data - ) - - is Payment -> Metadata( - kinAmount = amount, - data = payload.codeData.toList(), - request = request, - ) - - is Login -> Metadata( - kinAmount = amount, - data = payload.codeData.toList(), - request = request, - ) - - is Tip -> Metadata( - kinAmount = amount, - data = data - ) - } - } - - data class Cash( - override val amount: KinAmount, - override val didReceive: Boolean = false, - override val data: List = emptyList(), - val kind: Kind = Kind.cash, - ) : Bill { - override val canFlip: Boolean = false - } - - data class Payment( - override val amount: KinAmount, - val payload: CodePayload, - val request: DeepLinkRequest? = null - ) : Bill { - override val didReceive: Boolean = false - override val data: List = payload.codeData.toList() - override val canFlip: Boolean = false - } - - data class Login( - override val amount: KinAmount, - val payload: CodePayload, - val request: DeepLinkRequest? = null - ) : Bill { - override val didReceive: Boolean = false - override val data: List = payload.codeData.toList() - override val canFlip: Boolean = false - } - - data class Tip( - val payload: CodePayload, - val request: DeepLinkRequest? = null, - override val canFlip: Boolean = false - ) : Bill { - override val amount: KinAmount = KinAmount.newInstance(Kin.fromKin(0), Rate.oneToOne) - override val didReceive: Boolean = false - override val data: List = payload.codeData.toList() - } -} - -val Bill.amountFloored: KinAmount - get() = amount.copy(kin = amount.kin.toKinTruncating()) - -sealed interface Valuation -data class PaymentValuation(val amount: KinAmount): Valuation - -data class BillToast( - val amount: KinAmount, - val isDeposit: Boolean, -) { - val formattedAmount: String - @Composable get() = StringBuilder() - .append(if (isDeposit) "+" else "-") - .append(amount.formatted()) - .toString() -} - -sealed class Confirmation( - open val showScrim: Boolean = false, - open val state: ConfirmationState, - open val cancellable: Boolean = false, -) - -data class PrivatePaymentConfirmation( - override val state: ConfirmationState, - val payload: CodePayload, - val requestedAmount: KinAmount, - val localAmount: KinAmount, - override val showScrim: Boolean = false, - override val cancellable: Boolean = false, -): Confirmation(showScrim, state) - -data class LoginConfirmation( - override val state: ConfirmationState, - val payload: CodePayload, - val domain: com.getcode.model.Domain, - override val showScrim: Boolean = false, - override val cancellable: Boolean = false, -): Confirmation(showScrim, state) - - -data class PublicPaymentConfirmation( - override val state: ConfirmationState, - val amount: KinAmount, - val destination: PublicKey, - val metadata: ExtendedMetadata, - override val showScrim: Boolean = true, - override val cancellable: Boolean = true, -): Confirmation(showScrim, state) - -data class MessageTipPaymentConfirmation( - override val state: ConfirmationState, - val destination: PublicKey, - val metadata: ExtendedMetadata, - override val showScrim: Boolean = true, - override val cancellable: Boolean = true, -): Confirmation(showScrim, state) - -data class SocialUserPaymentConfirmation( - override val state: ConfirmationState, - val amount: KinAmount, - val payload: CodePayload?, - val metadata: SocialUser, - val isPrivate: Boolean = false, - override val showScrim: Boolean = false, - override val cancellable: Boolean = false, -): Confirmation(showScrim, state) { - val imageUrl: String? - get() { - return when (metadata) { - is TwitterUser -> { - metadata.imageUrlSanitized - } - - else -> null - } - } - - val followerCountFormatted: String? - get() { - return when (metadata) { - is TwitterUser -> { - when { - metadata.followerCount > 1000 -> { - val decimal = metadata.followerCount.toDouble() / 1_000 - val formattedString = String.format("%.1fK", decimal) - formattedString - } - else -> metadata.followerCount.toString() - } - } - else -> null - } - } -} - - -sealed interface ConfirmationState { - data object AwaitingConfirmation : ConfirmationState - data object Sending : ConfirmationState - data object Sent : ConfirmationState -} - -data class Metadata( - val kinAmount: KinAmount, - val data: List, - val request: DeepLinkRequest? = null -) \ No newline at end of file diff --git a/libs/requests/src/main/kotlin/com/getcode/models/PaymentRequest.kt b/libs/requests/src/main/kotlin/com/getcode/models/PaymentRequest.kt deleted file mode 100644 index 1c2598821..000000000 --- a/libs/requests/src/main/kotlin/com/getcode/models/PaymentRequest.kt +++ /dev/null @@ -1,219 +0,0 @@ -package com.getcode.models - -import android.net.Uri -import com.getcode.model.CurrencyCode -import com.getcode.utils.ErrorUtils -import com.getcode.utils.decodeBase64 -import com.getcode.vendor.Base58 -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.decodeFromJsonElement -import timber.log.Timber - - -data class DeepLinkRequest( - val mode: Mode, - val clientSecret: List, - val paymentRequest: PaymentRequest? = null, - val loginRequest: LoginRequest? = null, - val tipRequest: TipRequest? = null, - val imageRequest: ImageRequest? = null, - val successUrl: String?, - val cancelUrl: String?, - - ) { - @Serializable - enum class Mode { - @SerialName("payment") Payment, - @SerialName("donation") Donation, - @SerialName("login") Login, - @SerialName("tip") Tip, - @SerialName("internal:image") Image, - } - - companion object { - fun fromImage(uri: Uri): DeepLinkRequest { - return DeepLinkRequest( - mode = Mode.Image, - clientSecret = emptyList(), - imageRequest = ImageRequest(uri), - cancelUrl = null, - successUrl = null, - ) - } - - fun fromTipCardUsername(platform: String, username: String): DeepLinkRequest { - return DeepLinkRequest( - mode = Mode.Tip, - clientSecret = emptyList(), - tipRequest = TipRequest( - platformName = platform, - username = username - ), - cancelUrl = null, - successUrl = null, - ) - } - - fun from(data: ByteArray?): DeepLinkRequest? { - data ?: return null - val container = runCatching { - val string = data.decodeBase64().decodeToString() - Json.decodeFromString(string) - }.getOrNull() ?: return null - - val mode = container.decode("mode") ?: return null - Timber.d("mode=$mode") - val secret = container.decode("clientSecret") ?: return null - val clientSecret = Base58.decode(secret) - if (clientSecret.size != 11) { - Timber.e("Invalid client secret") - return null - } - - val (successUrl, cancelUrl) = container.decode("confirmParams") - ?: ConfirmParams(null, null) - - - val baseRequest = DeepLinkRequest( - mode = mode, - clientSecret = clientSecret.toList(), - successUrl = successUrl?.url, - cancelUrl = cancelUrl?.url, - ) - - when (mode) { - Mode.Payment, - Mode.Donation -> { - val currencyCode = container.decode("currency") { CurrencyCode.tryValueOf(it) } - val amount = container.decode("amount") ?: return null - val destinationString = container.decode("destination") ?: return null - - if (currencyCode == null) { - Timber.e("Invalid currency code") - return null - } - - val destination = runCatching { com.getcode.solana.keys.PublicKey.fromBase58(destinationString) } - .getOrNull() - if (destination == null) { - ErrorUtils.handleError(Throwable()) - Timber.e("Invalid destination address") - return null - } - - // optional fees - val fees = container.decode>("fees").orEmpty() - - val fiat = com.getcode.model.Fiat(currency = currencyCode, amount = amount) - - Timber.d("fiat=${fiat.amount}, fees=$fees") - return baseRequest.copy( - paymentRequest = PaymentRequest( - fiat = fiat, - destination = destination, - fees = fees, - ) - ) - } - Mode.Login -> { - val loginContainer = container.decode("login") - ?: return null - - val verifier = runCatching { com.getcode.solana.keys.PublicKey.fromBase58(loginContainer.verifier.orEmpty()) } - .getOrNull() - - if (verifier == null) { - ErrorUtils.handleError(Throwable()) - Timber.e("Invalid verifier address") - return null - } - - val domain = com.getcode.model.Domain.from(loginContainer.domain) ?: return null - - return baseRequest.copy( - loginRequest = LoginRequest( - verifier = verifier, - domain = domain - ) - ) - } - Mode.Tip -> { - val platform = container.decode("platform") ?: return null - - return baseRequest.copy( - tipRequest = TipRequest(platform.name, platform.username) - ) - } - - Mode.Image -> { - return baseRequest - } - } - } - } -} -data class PaymentRequest( - val fiat: com.getcode.model.Fiat, - val destination: com.getcode.solana.keys.PublicKey, - val fees: List, -) - -data class LoginRequest( - val verifier: com.getcode.solana.keys.PublicKey, - val domain: com.getcode.model.Domain, -) - -data class TipRequest( - val platformName: String, - val username: String, -) - -data class ImageRequest( - val uri: Uri -) - -private inline fun JsonObject.decode(key: String): T? { - return runCatching { Json.decodeFromJsonElement(getValue(key)) } - .onFailure { Timber.e("failed to decode $key from result") } - .getOrElse { - runCatching { Json.decodeFromString(getValue(key).toString()) } - .getOrNull() - } -} - -private inline fun JsonObject.decode(key: String, map: (T) -> R): R? { - return decode(key)?.let { map(it) } -} - -@Serializable -private data class LoginKeys( - val verifier: String? = null, - val domain: String? = null, - val clientSecret: String? = null, -) - -@Serializable -private data class PlatformKeys( - val name: String, - val username: String, -) - -@Serializable -private data class ConfirmParams( - @SerialName("success") val success: UrlHolder?, - @SerialName("cancel") val cancel: UrlHolder? -) - -@Serializable -private data class UrlHolder( - @SerialName("url") val url: String?, -) - -@Serializable -data class ProvidedFee( - val destination: String, - val basisPoints: Int -) diff --git a/services/code/.gitignore b/services/code/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/services/code/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/services/code/build.gradle.kts b/services/code/build.gradle.kts deleted file mode 100644 index 1aa732d0a..000000000 --- a/services/code/build.gradle.kts +++ /dev/null @@ -1,129 +0,0 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - id(Plugins.android_library) - id(Plugins.kotlin_android) - id(Plugins.kotlin_kapt) - id(Plugins.kotlin_serialization) -} - -android { - namespace = "${Gradle.codeNamespace}.services" - compileSdk = Android.compileSdkVersion - defaultConfig { - minSdk = Android.minSdkVersion - testInstrumentationRunner = Android.testInstrumentationRunner - - consumerProguardFiles("consumer-rules.pro") - - buildConfigField("String", "VERSION_NAME", "\"${Packaging.Code.versionName}\"") - buildConfigField("Boolean", "NOTIFY_ERRORS", "false") - buildConfigField( - "String", - "GOOGLE_CLOUD_PROJECT_NUMBER", - "\"${tryReadProperty(rootProject.rootDir, "GOOGLE_CLOUD_PROJECT_NUMBER", "-1L")}\"" - ) - - buildConfigField( - "String", - "FINGERPRINT_API_KEY", - "\"${tryReadProperty(rootProject.rootDir, "FINGERPRINT_API_KEY")}\"" - ) - - javaCompileOptions { - annotationProcessorOptions { - arguments += mapOf("room.schemaLocation" to "$projectDir/schemas") - } - } - } - - buildFeatures { - buildConfig = true - } -} - -kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(Versions.java)) - } - - compilerOptions { - jvmTarget.set(JvmTarget.fromTarget(Versions.java)) - optIn.addAll( - "kotlin.time.ExperimentalTime", - "kotlin.ExperimentalUnsignedTypes", - "kotlin.RequiresOptIn" - ) - } -} - -dependencies { - implementation(project(":definitions:code:models")) - api(project(":services:legacy-shared")) - api(project(":libs:crypto:solana")) - api(project(":libs:currency")) - api(project(":libs:analytics")) - api(project(":libs:datetime")) - api(project(":libs:crypto:kin")) - api(project(":libs:crypto:solana")) - api(project(":libs:currency")) - api(project(":libs:encryption:base58")) - api(project(":libs:encryption:ed25519")) - api(project(":libs:encryption:hmac")) - api(project(":libs:encryption:keys")) - api(project(":libs:encryption:mnemonic")) - api(project(":libs:encryption:sha256")) - api(project(":libs:encryption:sha512")) - api(project(":libs:encryption:utils")) - api(project(":libs:logging")) - api(project(":libs:models-ktx")) - api(project(":libs:network:exchange")) - api(project(":libs:network:connectivity:public")) - - implementation(project(":libs:locale:public")) - implementation(project(":ui:resources")) - - implementation(Libs.rxjava) - implementation(Libs.kotlinx_coroutines_core) - implementation(Libs.kotlinx_serialization_json) - implementation(Libs.inject) - - implementation(Libs.grpc_android) - implementation(Libs.grpc_okhttp) - implementation(Libs.grpc_kotlin) - implementation(Libs.androidx_lifecycle_runtime) - implementation(Libs.androidx_room_runtime) - implementation(Libs.androidx_room_ktx) - implementation(Libs.androidx_room_rxjava3) - implementation(Libs.androidx_room_paging) - implementation(Libs.okhttp) - implementation(Libs.mixpanel) - - implementation(platform(Libs.firebase_bom)) - implementation(Libs.firebase_crashlytics) - implementation(Libs.firebase_installations) - implementation(Libs.firebase_perf) - implementation(Libs.firebase_messaging) - - implementation(Libs.play_integrity) - - implementation(Libs.androidx_paging_runtime) - - kapt(Libs.androidx_room_compiler) - implementation(Libs.sqlcipher) - - implementation(Libs.fingerprint_pro) - - implementation(Libs.lib_phone_number_google) - - androidTestImplementation(Libs.androidx_junit) - androidTestImplementation(Libs.junit) - androidTestImplementation(Libs.androidx_test_runner) - - implementation(Libs.hilt) - kapt(Libs.hilt_android_compiler) - kapt(Libs.hilt_compiler) - - implementation(Libs.timber) - implementation(Libs.bugsnag) -} diff --git a/services/code/consumer-rules.pro b/services/code/consumer-rules.pro deleted file mode 100644 index 5f642a47c..000000000 --- a/services/code/consumer-rules.pro +++ /dev/null @@ -1,6 +0,0 @@ -# Needed to keep generic signatures --keepattributes Signature - --keepclasseswithmembernames class * { - native ; -} \ No newline at end of file diff --git a/services/code/schemas/com.getcode.db.CodeAppDatabase/1.json b/services/code/schemas/com.getcode.db.CodeAppDatabase/1.json deleted file mode 100644 index 74dfded2f..000000000 --- a/services/code/schemas/com.getcode.db.CodeAppDatabase/1.json +++ /dev/null @@ -1,280 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 1, - "identityHash": "28c0ca6c58dad65912cec88c2914c371", - "entities": [ - { - "tableName": "HistoricalTransaction", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `id` TEXT NOT NULL, `status` TEXT, `kin` REAL NOT NULL, `source` TEXT, `destination` TEXT, `outgoing` INTEGER NOT NULL, `date` INTEGER NOT NULL, `transactionRateFx` REAL, `transactionRateCurrency` TEXT, `transactionSignature` TEXT NOT NULL, `isExternal` INTEGER NOT NULL, `isPending` INTEGER NOT NULL, `statusText` TEXT NOT NULL, `region` TEXT)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "kin", - "columnName": "kin", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "source", - "columnName": "source", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "destination", - "columnName": "destination", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "outgoing", - "columnName": "outgoing", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "transactionRateFx", - "columnName": "transactionRateFx", - "affinity": "REAL", - "notNull": false - }, - { - "fieldPath": "transactionRateCurrency", - "columnName": "transactionRateCurrency", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "transactionSignature", - "columnName": "transactionSignature", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "isExternal", - "columnName": "isExternal", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isPending", - "columnName": "isPending", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "statusText", - "columnName": "statusText", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "region", - "columnName": "region", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "uid" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "CurrencyRate", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rate", - "columnName": "rate", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "FaqItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `question` TEXT NOT NULL, `answer` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "question", - "columnName": "question", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "answer", - "columnName": "answer", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "uid" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '28c0ca6c58dad65912cec88c2914c371')" - ] - } -} \ No newline at end of file diff --git a/services/code/schemas/com.getcode.db.CodeAppDatabase/10.json b/services/code/schemas/com.getcode.db.CodeAppDatabase/10.json deleted file mode 100644 index a089215e8..000000000 --- a/services/code/schemas/com.getcode.db.CodeAppDatabase/10.json +++ /dev/null @@ -1,384 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 10, - "identityHash": "f2095500e8b757e790d9d45cc1820bb3", - "entities": [ - { - "tableName": "CurrencyRate", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rate", - "columnName": "rate", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "FaqItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `question` TEXT NOT NULL, `answer` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "question", - "columnName": "question", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "answer", - "columnName": "answer", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "uid" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "GiftCard", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `entropy` TEXT NOT NULL, `amount` INTEGER NOT NULL, `date` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "entropy", - "columnName": "entropy", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "exchangeData", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fiat` REAL NOT NULL, `currency` TEXT NOT NULL, `synced_at` INTEGER NOT NULL, PRIMARY KEY(`currency`))", - "fields": [ - { - "fieldPath": "fx", - "columnName": "fiat", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "currency", - "columnName": "currency", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "synced", - "columnName": "synced_at", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "currency" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `cursorBase58` TEXT NOT NULL, `tipAmount` TEXT NOT NULL, `createdByUser` INTEGER NOT NULL, `hasRevealedIdentity` INTEGER NOT NULL, `user` TEXT, `userImage` TEXT, `lastActivity` INTEGER, PRIMARY KEY(`messageIdBase58`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "cursorBase58", - "columnName": "cursorBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "tipAmount", - "columnName": "tipAmount", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "createdByUser", - "columnName": "createdByUser", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "hasRevealedIdentity", - "columnName": "hasRevealedIdentity", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "user", - "columnName": "user", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "userImage", - "columnName": "userImage", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `cursorBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `dateMillis` INTEGER NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "cursorBase58", - "columnName": "cursorBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages_remote_keys", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `prevCursorBase58` TEXT, `nextCursorBase58` TEXT, PRIMARY KEY(`messageIdBase58`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "prevCursorBase58", - "columnName": "prevCursorBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "nextCursorBase58", - "columnName": "nextCursorBase58", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f2095500e8b757e790d9d45cc1820bb3')" - ] - } -} \ No newline at end of file diff --git a/services/code/schemas/com.getcode.db.CodeAppDatabase/11.json b/services/code/schemas/com.getcode.db.CodeAppDatabase/11.json deleted file mode 100644 index fd95ebfe9..000000000 --- a/services/code/schemas/com.getcode.db.CodeAppDatabase/11.json +++ /dev/null @@ -1,391 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 11, - "identityHash": "b4d80f3920a1e7dfde1384c88d8642be", - "entities": [ - { - "tableName": "CurrencyRate", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rate", - "columnName": "rate", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "FaqItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `question` TEXT NOT NULL, `answer` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "question", - "columnName": "question", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "answer", - "columnName": "answer", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "uid" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "GiftCard", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `entropy` TEXT NOT NULL, `amount` INTEGER NOT NULL, `date` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "entropy", - "columnName": "entropy", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "exchangeData", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fiat` REAL NOT NULL, `currency` TEXT NOT NULL, `synced_at` INTEGER NOT NULL, PRIMARY KEY(`currency`))", - "fields": [ - { - "fieldPath": "fx", - "columnName": "fiat", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "currency", - "columnName": "currency", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "synced", - "columnName": "synced_at", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "currency" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `cursorBase58` TEXT NOT NULL, `tipAmount` TEXT NOT NULL, `createdByUser` INTEGER NOT NULL, `hasRevealedIdentity` INTEGER NOT NULL, `user` TEXT, `userImage` TEXT, `lastActivity` INTEGER, PRIMARY KEY(`messageIdBase58`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "cursorBase58", - "columnName": "cursorBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "tipAmount", - "columnName": "tipAmount", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "createdByUser", - "columnName": "createdByUser", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "hasRevealedIdentity", - "columnName": "hasRevealedIdentity", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "user", - "columnName": "user", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "userImage", - "columnName": "userImage", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `cursorBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `dateMillis` INTEGER NOT NULL, `content` TEXT NOT NULL, `status` TEXT NOT NULL DEFAULT 'Unknown', PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "cursorBase58", - "columnName": "cursorBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "'Unknown'" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages_remote_keys", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `prevCursorBase58` TEXT, `nextCursorBase58` TEXT, PRIMARY KEY(`messageIdBase58`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "prevCursorBase58", - "columnName": "prevCursorBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "nextCursorBase58", - "columnName": "nextCursorBase58", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b4d80f3920a1e7dfde1384c88d8642be')" - ] - } -} \ No newline at end of file diff --git a/services/code/schemas/com.getcode.db.CodeAppDatabase/12.json b/services/code/schemas/com.getcode.db.CodeAppDatabase/12.json deleted file mode 100644 index f591866f4..000000000 --- a/services/code/schemas/com.getcode.db.CodeAppDatabase/12.json +++ /dev/null @@ -1,406 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 12, - "identityHash": "1ea54cdb1965364f9997b8591154effc", - "entities": [ - { - "tableName": "CurrencyRate", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rate", - "columnName": "rate", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "FaqItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `question` TEXT NOT NULL, `answer` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "question", - "columnName": "question", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "answer", - "columnName": "answer", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "uid" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "GiftCard", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `entropy` TEXT NOT NULL, `amount` INTEGER NOT NULL, `date` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "entropy", - "columnName": "entropy", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "exchangeData", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fiat` REAL NOT NULL, `currency` TEXT NOT NULL, `synced_at` INTEGER NOT NULL, PRIMARY KEY(`currency`))", - "fields": [ - { - "fieldPath": "fx", - "columnName": "fiat", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "currency", - "columnName": "currency", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "synced", - "columnName": "synced_at", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "currency" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `title` TEXT NOT NULL DEFAULT 'Tip Chat', `hasRevealedIdentity` INTEGER NOT NULL, `user` TEXT, `userImage` TEXT, `lastActivity` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "'Tip Chat'" - }, - { - "fieldPath": "hasRevealedIdentity", - "columnName": "hasRevealedIdentity", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "user", - "columnName": "user", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "userImage", - "columnName": "userImage", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `cursorBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `dateMillis` INTEGER NOT NULL, `content` TEXT NOT NULL, `status` TEXT NOT NULL DEFAULT 'Unknown', PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "cursorBase58", - "columnName": "cursorBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "'Unknown'" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages_remote_keys", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `prevCursorBase58` TEXT, `nextCursorBase58` TEXT, PRIMARY KEY(`messageIdBase58`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "prevCursorBase58", - "columnName": "prevCursorBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "nextCursorBase58", - "columnName": "nextCursorBase58", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversation_intent_id_mapping", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `intentIdBase58` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "intentIdBase58", - "columnName": "intentIdBase58", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1ea54cdb1965364f9997b8591154effc')" - ] - } -} \ No newline at end of file diff --git a/services/code/schemas/com.getcode.db.CodeAppDatabase/13.json b/services/code/schemas/com.getcode.db.CodeAppDatabase/13.json deleted file mode 100644 index 2e4482b31..000000000 --- a/services/code/schemas/com.getcode.db.CodeAppDatabase/13.json +++ /dev/null @@ -1,395 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 13, - "identityHash": "546bcf41a12f9c2319a18de48d8b02a3", - "entities": [ - { - "tableName": "CurrencyRate", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rate", - "columnName": "rate", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "FaqItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `question` TEXT NOT NULL, `answer` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "question", - "columnName": "question", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "answer", - "columnName": "answer", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "uid" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "GiftCard", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `entropy` TEXT NOT NULL, `amount` INTEGER NOT NULL, `date` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "entropy", - "columnName": "entropy", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "exchangeData", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fiat` REAL NOT NULL, `currency` TEXT NOT NULL, `synced_at` INTEGER NOT NULL, PRIMARY KEY(`currency`))", - "fields": [ - { - "fieldPath": "fx", - "columnName": "fiat", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "currency", - "columnName": "currency", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "synced", - "columnName": "synced_at", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "currency" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `title` TEXT NOT NULL DEFAULT 'Tip Chat', `hasRevealedIdentity` INTEGER NOT NULL, `user` TEXT, `userImage` TEXT, `lastActivity` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "'Tip Chat'" - }, - { - "fieldPath": "hasRevealedIdentity", - "columnName": "hasRevealedIdentity", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "user", - "columnName": "user", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "userImage", - "columnName": "userImage", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `cursorBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `dateMillis` INTEGER NOT NULL, `status` TEXT NOT NULL DEFAULT 'Unknown', PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "cursorBase58", - "columnName": "cursorBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "'Unknown'" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversation_intent_id_mapping", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `intentIdBase58` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "intentIdBase58", - "columnName": "intentIdBase58", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "message_contents", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`messageIdBase58`, `content`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58", - "content" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '546bcf41a12f9c2319a18de48d8b02a3')" - ] - } -} \ No newline at end of file diff --git a/services/code/schemas/com.getcode.db.CodeAppDatabase/14.json b/services/code/schemas/com.getcode.db.CodeAppDatabase/14.json deleted file mode 100644 index ba1f0c78f..000000000 --- a/services/code/schemas/com.getcode.db.CodeAppDatabase/14.json +++ /dev/null @@ -1,421 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 14, - "identityHash": "f86a832650ec3ffe8592e140677befd9", - "entities": [ - { - "tableName": "CurrencyRate", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rate", - "columnName": "rate", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "FaqItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `question` TEXT NOT NULL, `answer` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "question", - "columnName": "question", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "answer", - "columnName": "answer", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "uid" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "GiftCard", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `entropy` TEXT NOT NULL, `amount` INTEGER NOT NULL, `date` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "entropy", - "columnName": "entropy", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "exchangeData", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fiat` REAL NOT NULL, `currency` TEXT NOT NULL, `synced_at` INTEGER NOT NULL, PRIMARY KEY(`currency`))", - "fields": [ - { - "fieldPath": "fx", - "columnName": "fiat", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "currency", - "columnName": "currency", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "synced", - "columnName": "synced_at", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "currency" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `title` TEXT NOT NULL DEFAULT 'Tip Chat', `hasRevealedIdentity` INTEGER NOT NULL, `user` TEXT, `userImage` TEXT, `lastActivity` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "'Tip Chat'" - }, - { - "fieldPath": "hasRevealedIdentity", - "columnName": "hasRevealedIdentity", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "user", - "columnName": "user", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "userImage", - "columnName": "userImage", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `cursorBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `dateMillis` INTEGER NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "cursorBase58", - "columnName": "cursorBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversation_intent_id_mapping", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `intentIdBase58` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "intentIdBase58", - "columnName": "intentIdBase58", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "message_contents", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`messageIdBase58`, `content`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58", - "content" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f86a832650ec3ffe8592e140677befd9')" - ] - } -} \ No newline at end of file diff --git a/services/code/schemas/com.getcode.db.CodeAppDatabase/15.json b/services/code/schemas/com.getcode.db.CodeAppDatabase/15.json deleted file mode 100644 index 59426632b..000000000 --- a/services/code/schemas/com.getcode.db.CodeAppDatabase/15.json +++ /dev/null @@ -1,415 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 15, - "identityHash": "d1e86dc40a4ec7600e32782a3c070ab1", - "entities": [ - { - "tableName": "CurrencyRate", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rate", - "columnName": "rate", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "FaqItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `question` TEXT NOT NULL, `answer` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "question", - "columnName": "question", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "answer", - "columnName": "answer", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "uid" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "GiftCard", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `entropy` TEXT NOT NULL, `amount` INTEGER NOT NULL, `date` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "entropy", - "columnName": "entropy", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "exchangeData", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fiat` REAL NOT NULL, `currency` TEXT NOT NULL, `synced_at` INTEGER NOT NULL, PRIMARY KEY(`currency`))", - "fields": [ - { - "fieldPath": "fx", - "columnName": "fiat", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "currency", - "columnName": "currency", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "synced", - "columnName": "synced_at", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "currency" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `title` TEXT, `hasRevealedIdentity` INTEGER NOT NULL, `members` TEXT NOT NULL DEFAULT '', `lastActivity` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "hasRevealedIdentity", - "columnName": "hasRevealedIdentity", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "members", - "columnName": "members", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `cursorBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `dateMillis` INTEGER NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "cursorBase58", - "columnName": "cursorBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversation_intent_id_mapping", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `intentIdBase58` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "intentIdBase58", - "columnName": "intentIdBase58", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "message_contents", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`messageIdBase58`, `content`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58", - "content" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd1e86dc40a4ec7600e32782a3c070ab1')" - ] - } -} \ No newline at end of file diff --git a/services/code/schemas/com.getcode.db.CodeAppDatabase/16.json b/services/code/schemas/com.getcode.db.CodeAppDatabase/16.json deleted file mode 100644 index fbef9c245..000000000 --- a/services/code/schemas/com.getcode.db.CodeAppDatabase/16.json +++ /dev/null @@ -1,246 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 16, - "identityHash": "74448cf3624e83c81490293ea858aaaf", - "entities": [ - { - "tableName": "CurrencyRate", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rate", - "columnName": "rate", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "FaqItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `question` TEXT NOT NULL, `answer` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "question", - "columnName": "question", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "answer", - "columnName": "answer", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "uid" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "GiftCard", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `entropy` TEXT NOT NULL, `amount` INTEGER NOT NULL, `date` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "entropy", - "columnName": "entropy", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "exchangeData", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fiat` REAL NOT NULL, `currency` TEXT NOT NULL, `synced_at` INTEGER NOT NULL, PRIMARY KEY(`currency`))", - "fields": [ - { - "fieldPath": "fx", - "columnName": "fiat", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "currency", - "columnName": "currency", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "synced", - "columnName": "synced_at", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "currency" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '74448cf3624e83c81490293ea858aaaf')" - ] - } -} \ No newline at end of file diff --git a/services/code/schemas/com.getcode.db.CodeAppDatabase/2.json b/services/code/schemas/com.getcode.db.CodeAppDatabase/2.json deleted file mode 100644 index eb4c480d6..000000000 --- a/services/code/schemas/com.getcode.db.CodeAppDatabase/2.json +++ /dev/null @@ -1,306 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 2, - "identityHash": "a10f864550ffa4783ce248e1e1d12ec5", - "entities": [ - { - "tableName": "HistoricalTransaction", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `id` TEXT NOT NULL, `status` TEXT, `kin` REAL NOT NULL, `source` TEXT, `destination` TEXT, `outgoing` INTEGER NOT NULL, `date` INTEGER NOT NULL, `transactionRateFx` REAL, `transactionRateCurrency` TEXT, `transactionSignature` TEXT NOT NULL, `isExternal` INTEGER NOT NULL, `isPending` INTEGER NOT NULL, `statusText` TEXT NOT NULL, `region` TEXT)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "kin", - "columnName": "kin", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "source", - "columnName": "source", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "destination", - "columnName": "destination", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "outgoing", - "columnName": "outgoing", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "transactionRateFx", - "columnName": "transactionRateFx", - "affinity": "REAL", - "notNull": false - }, - { - "fieldPath": "transactionRateCurrency", - "columnName": "transactionRateCurrency", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "transactionSignature", - "columnName": "transactionSignature", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "isExternal", - "columnName": "isExternal", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isPending", - "columnName": "isPending", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "statusText", - "columnName": "statusText", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "region", - "columnName": "region", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "uid" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "CurrencyRate", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rate", - "columnName": "rate", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "FaqItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `question` TEXT NOT NULL, `answer` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "question", - "columnName": "question", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "answer", - "columnName": "answer", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "uid" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "SendLimit", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `limit` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "limit", - "columnName": "limit", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a10f864550ffa4783ce248e1e1d12ec5')" - ] - } -} \ No newline at end of file diff --git a/services/code/schemas/com.getcode.db.CodeAppDatabase/3.json b/services/code/schemas/com.getcode.db.CodeAppDatabase/3.json deleted file mode 100644 index 409c1cf02..000000000 --- a/services/code/schemas/com.getcode.db.CodeAppDatabase/3.json +++ /dev/null @@ -1,276 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 3, - "identityHash": "28a3c122418f57fa1b1f3b48cf99ee4c", - "entities": [ - { - "tableName": "HistoricalTransaction", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `id` TEXT NOT NULL, `paymentType` TEXT NOT NULL, `date` INTEGER NOT NULL, `transactionRateFx` REAL, `transactionRateCurrency` TEXT, `transactionAmountQuarks` INTEGER NOT NULL, `nativeAmount` REAL NOT NULL, `isDeposit` INTEGER NOT NULL, `isWithdrawal` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "paymentType", - "columnName": "paymentType", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "transactionRateFx", - "columnName": "transactionRateFx", - "affinity": "REAL", - "notNull": false - }, - { - "fieldPath": "transactionRateCurrency", - "columnName": "transactionRateCurrency", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "transactionAmountQuarks", - "columnName": "transactionAmountQuarks", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "nativeAmount", - "columnName": "nativeAmount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "isDeposit", - "columnName": "isDeposit", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isWithdrawal", - "columnName": "isWithdrawal", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "uid" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "CurrencyRate", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rate", - "columnName": "rate", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "FaqItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `question` TEXT NOT NULL, `answer` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "question", - "columnName": "question", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "answer", - "columnName": "answer", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "uid" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "SendLimit", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `limit` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "limit", - "columnName": "limit", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '28a3c122418f57fa1b1f3b48cf99ee4c')" - ] - } -} \ No newline at end of file diff --git a/services/code/schemas/com.getcode.db.CodeAppDatabase/4.json b/services/code/schemas/com.getcode.db.CodeAppDatabase/4.json deleted file mode 100644 index b74d1fa71..000000000 --- a/services/code/schemas/com.getcode.db.CodeAppDatabase/4.json +++ /dev/null @@ -1,326 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 4, - "identityHash": "af1d779e2591926367dd73501e3872f2", - "entities": [ - { - "tableName": "HistoricalTransaction", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `id` TEXT NOT NULL, `paymentType` TEXT NOT NULL, `date` INTEGER NOT NULL, `transactionRateFx` REAL, `transactionRateCurrency` TEXT, `transactionAmountQuarks` INTEGER NOT NULL, `nativeAmount` REAL NOT NULL, `isDeposit` INTEGER NOT NULL, `isWithdrawal` INTEGER NOT NULL, `isRemoteSend` INTEGER NOT NULL, `isReturned` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "paymentType", - "columnName": "paymentType", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "transactionRateFx", - "columnName": "transactionRateFx", - "affinity": "REAL", - "notNull": false - }, - { - "fieldPath": "transactionRateCurrency", - "columnName": "transactionRateCurrency", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "transactionAmountQuarks", - "columnName": "transactionAmountQuarks", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "nativeAmount", - "columnName": "nativeAmount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "isDeposit", - "columnName": "isDeposit", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isWithdrawal", - "columnName": "isWithdrawal", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isRemoteSend", - "columnName": "isRemoteSend", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isReturned", - "columnName": "isReturned", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "uid" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "CurrencyRate", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rate", - "columnName": "rate", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "FaqItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `question` TEXT NOT NULL, `answer` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "question", - "columnName": "question", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "answer", - "columnName": "answer", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "uid" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "SendLimit", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `limit` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "limit", - "columnName": "limit", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "GiftCard", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `entropy` TEXT NOT NULL, `amount` INTEGER NOT NULL, `date` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "entropy", - "columnName": "entropy", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'af1d779e2591926367dd73501e3872f2')" - ] - } -} \ No newline at end of file diff --git a/services/code/schemas/com.getcode.db.CodeAppDatabase/5.json b/services/code/schemas/com.getcode.db.CodeAppDatabase/5.json deleted file mode 100644 index f22b1d365..000000000 --- a/services/code/schemas/com.getcode.db.CodeAppDatabase/5.json +++ /dev/null @@ -1,332 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 5, - "identityHash": "088d5512ad3f058e8d3b1c237a3ddde0", - "entities": [ - { - "tableName": "HistoricalTransaction", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `id` TEXT NOT NULL, `paymentType` TEXT NOT NULL, `date` INTEGER NOT NULL, `transactionRateFx` REAL, `transactionRateCurrency` TEXT, `transactionAmountQuarks` INTEGER NOT NULL, `nativeAmount` REAL NOT NULL, `isDeposit` INTEGER NOT NULL, `isWithdrawal` INTEGER NOT NULL, `isRemoteSend` INTEGER NOT NULL, `isReturned` INTEGER NOT NULL, `isBonus` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "paymentType", - "columnName": "paymentType", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "transactionRateFx", - "columnName": "transactionRateFx", - "affinity": "REAL", - "notNull": false - }, - { - "fieldPath": "transactionRateCurrency", - "columnName": "transactionRateCurrency", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "transactionAmountQuarks", - "columnName": "transactionAmountQuarks", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "nativeAmount", - "columnName": "nativeAmount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "isDeposit", - "columnName": "isDeposit", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isWithdrawal", - "columnName": "isWithdrawal", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isRemoteSend", - "columnName": "isRemoteSend", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isReturned", - "columnName": "isReturned", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isBonus", - "columnName": "isBonus", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "uid" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "CurrencyRate", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rate", - "columnName": "rate", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "FaqItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `question` TEXT NOT NULL, `answer` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "question", - "columnName": "question", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "answer", - "columnName": "answer", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "uid" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "SendLimit", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `limit` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "limit", - "columnName": "limit", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "GiftCard", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `entropy` TEXT NOT NULL, `amount` INTEGER NOT NULL, `date` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "entropy", - "columnName": "entropy", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '088d5512ad3f058e8d3b1c237a3ddde0')" - ] - } -} \ No newline at end of file diff --git a/services/code/schemas/com.getcode.db.CodeAppDatabase/6.json b/services/code/schemas/com.getcode.db.CodeAppDatabase/6.json deleted file mode 100644 index 2b5e78cbe..000000000 --- a/services/code/schemas/com.getcode.db.CodeAppDatabase/6.json +++ /dev/null @@ -1,332 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 6, - "identityHash": "6688e4ddc89eb429b1f8268a4c075248", - "entities": [ - { - "tableName": "HistoricalTransaction", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `id` TEXT NOT NULL, `paymentType` TEXT NOT NULL, `date` INTEGER NOT NULL, `transactionRateFx` REAL, `transactionRateCurrency` TEXT, `transactionAmountQuarks` INTEGER NOT NULL, `nativeAmount` REAL NOT NULL, `isDeposit` INTEGER NOT NULL, `isWithdrawal` INTEGER NOT NULL, `isRemoteSend` INTEGER NOT NULL, `isReturned` INTEGER NOT NULL, `airdropType` TEXT)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "paymentType", - "columnName": "paymentType", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "transactionRateFx", - "columnName": "transactionRateFx", - "affinity": "REAL", - "notNull": false - }, - { - "fieldPath": "transactionRateCurrency", - "columnName": "transactionRateCurrency", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "transactionAmountQuarks", - "columnName": "transactionAmountQuarks", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "nativeAmount", - "columnName": "nativeAmount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "isDeposit", - "columnName": "isDeposit", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isWithdrawal", - "columnName": "isWithdrawal", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isRemoteSend", - "columnName": "isRemoteSend", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isReturned", - "columnName": "isReturned", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "airdropType", - "columnName": "airdropType", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "uid" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "CurrencyRate", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rate", - "columnName": "rate", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "FaqItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `question` TEXT NOT NULL, `answer` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "question", - "columnName": "question", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "answer", - "columnName": "answer", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "uid" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "SendLimit", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `limit` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "limit", - "columnName": "limit", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "GiftCard", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `entropy` TEXT NOT NULL, `amount` INTEGER NOT NULL, `date` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "entropy", - "columnName": "entropy", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6688e4ddc89eb429b1f8268a4c075248')" - ] - } -} \ No newline at end of file diff --git a/services/code/schemas/com.getcode.db.CodeAppDatabase/7.json b/services/code/schemas/com.getcode.db.CodeAppDatabase/7.json deleted file mode 100644 index 5a186fa59..000000000 --- a/services/code/schemas/com.getcode.db.CodeAppDatabase/7.json +++ /dev/null @@ -1,364 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 7, - "identityHash": "8fde6ca4b4b516c576d4a9740aef5742", - "entities": [ - { - "tableName": "HistoricalTransaction", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `id` TEXT NOT NULL, `paymentType` TEXT NOT NULL, `date` INTEGER NOT NULL, `transactionRateFx` REAL, `transactionRateCurrency` TEXT, `transactionAmountQuarks` INTEGER NOT NULL, `nativeAmount` REAL NOT NULL, `isDeposit` INTEGER NOT NULL, `isWithdrawal` INTEGER NOT NULL, `isRemoteSend` INTEGER NOT NULL, `isReturned` INTEGER NOT NULL, `airdropType` TEXT)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "paymentType", - "columnName": "paymentType", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "transactionRateFx", - "columnName": "transactionRateFx", - "affinity": "REAL", - "notNull": false - }, - { - "fieldPath": "transactionRateCurrency", - "columnName": "transactionRateCurrency", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "transactionAmountQuarks", - "columnName": "transactionAmountQuarks", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "nativeAmount", - "columnName": "nativeAmount", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "isDeposit", - "columnName": "isDeposit", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isWithdrawal", - "columnName": "isWithdrawal", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isRemoteSend", - "columnName": "isRemoteSend", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isReturned", - "columnName": "isReturned", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "airdropType", - "columnName": "airdropType", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "uid" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "CurrencyRate", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rate", - "columnName": "rate", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "FaqItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `question` TEXT NOT NULL, `answer` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "question", - "columnName": "question", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "answer", - "columnName": "answer", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "uid" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "SendLimit", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `limit` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "limit", - "columnName": "limit", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "GiftCard", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `entropy` TEXT NOT NULL, `amount` INTEGER NOT NULL, `date` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "entropy", - "columnName": "entropy", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "exchangeData", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fiat` REAL NOT NULL, `currency` TEXT NOT NULL, `synced_at` INTEGER NOT NULL, PRIMARY KEY(`currency`))", - "fields": [ - { - "fieldPath": "fx", - "columnName": "fiat", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "currency", - "columnName": "currency", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "synced", - "columnName": "synced_at", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "currency" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8fde6ca4b4b516c576d4a9740aef5742')" - ] - } -} \ No newline at end of file diff --git a/services/code/schemas/com.getcode.db.CodeAppDatabase/8.json b/services/code/schemas/com.getcode.db.CodeAppDatabase/8.json deleted file mode 100644 index ed12ef725..000000000 --- a/services/code/schemas/com.getcode.db.CodeAppDatabase/8.json +++ /dev/null @@ -1,272 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 8, - "identityHash": "45c39753eadbdaeddb35d2e002424dac", - "entities": [ - { - "tableName": "CurrencyRate", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rate", - "columnName": "rate", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "FaqItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `question` TEXT NOT NULL, `answer` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "question", - "columnName": "question", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "answer", - "columnName": "answer", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "uid" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "SendLimit", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `limit` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "limit", - "columnName": "limit", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "GiftCard", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `entropy` TEXT NOT NULL, `amount` INTEGER NOT NULL, `date` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "entropy", - "columnName": "entropy", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "exchangeData", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fiat` REAL NOT NULL, `currency` TEXT NOT NULL, `synced_at` INTEGER NOT NULL, PRIMARY KEY(`currency`))", - "fields": [ - { - "fieldPath": "fx", - "columnName": "fiat", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "currency", - "columnName": "currency", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "synced", - "columnName": "synced_at", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "currency" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '45c39753eadbdaeddb35d2e002424dac')" - ] - } -} \ No newline at end of file diff --git a/services/code/schemas/com.getcode.db.CodeAppDatabase/9.json b/services/code/schemas/com.getcode.db.CodeAppDatabase/9.json deleted file mode 100644 index 81b67fecb..000000000 --- a/services/code/schemas/com.getcode.db.CodeAppDatabase/9.json +++ /dev/null @@ -1,246 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 9, - "identityHash": "74448cf3624e83c81490293ea858aaaf", - "entities": [ - { - "tableName": "CurrencyRate", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rate", - "columnName": "rate", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "FaqItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `question` TEXT NOT NULL, `answer` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "question", - "columnName": "question", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "answer", - "columnName": "answer", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "uid" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "GiftCard", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `entropy` TEXT NOT NULL, `amount` INTEGER NOT NULL, `date` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "entropy", - "columnName": "entropy", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "exchangeData", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fiat` REAL NOT NULL, `currency` TEXT NOT NULL, `synced_at` INTEGER NOT NULL, PRIMARY KEY(`currency`))", - "fields": [ - { - "fieldPath": "fx", - "columnName": "fiat", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "currency", - "columnName": "currency", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "synced", - "columnName": "synced_at", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "currency" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '74448cf3624e83c81490293ea858aaaf')" - ] - } -} \ No newline at end of file diff --git a/services/code/schemas/com.kin.code.db.AppDatabase/1.json b/services/code/schemas/com.kin.code.db.AppDatabase/1.json deleted file mode 100644 index 74dfded2f..000000000 --- a/services/code/schemas/com.kin.code.db.AppDatabase/1.json +++ /dev/null @@ -1,280 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 1, - "identityHash": "28c0ca6c58dad65912cec88c2914c371", - "entities": [ - { - "tableName": "HistoricalTransaction", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `id` TEXT NOT NULL, `status` TEXT, `kin` REAL NOT NULL, `source` TEXT, `destination` TEXT, `outgoing` INTEGER NOT NULL, `date` INTEGER NOT NULL, `transactionRateFx` REAL, `transactionRateCurrency` TEXT, `transactionSignature` TEXT NOT NULL, `isExternal` INTEGER NOT NULL, `isPending` INTEGER NOT NULL, `statusText` TEXT NOT NULL, `region` TEXT)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "kin", - "columnName": "kin", - "affinity": "REAL", - "notNull": true - }, - { - "fieldPath": "source", - "columnName": "source", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "destination", - "columnName": "destination", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "outgoing", - "columnName": "outgoing", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "date", - "columnName": "date", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "transactionRateFx", - "columnName": "transactionRateFx", - "affinity": "REAL", - "notNull": false - }, - { - "fieldPath": "transactionRateCurrency", - "columnName": "transactionRateCurrency", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "transactionSignature", - "columnName": "transactionSignature", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "isExternal", - "columnName": "isExternal", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "isPending", - "columnName": "isPending", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "statusText", - "columnName": "statusText", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "region", - "columnName": "region", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "uid" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "CurrencyRate", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `rate` REAL NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "rate", - "columnName": "rate", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "FaqItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT, `question` TEXT NOT NULL, `answer` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "question", - "columnName": "question", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "answer", - "columnName": "answer", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "uid" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "key" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '28c0ca6c58dad65912cec88c2914c371')" - ] - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/codeScanner/KikEncodingTest.kt b/services/code/src/androidTest/java/com/getcode/codeScanner/KikEncodingTest.kt deleted file mode 100644 index 8a794fdca..000000000 --- a/services/code/src/androidTest/java/com/getcode/codeScanner/KikEncodingTest.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.getcode.codeScanner - -import com.getcode.utils.toByteList -import junit.framework.Assert.assertEquals -import org.junit.Test - -class KikEncodingTest { - @Test - fun kikEncoding() { - val encoded: List = CodeScanner.Encode( - listOf( - 0, 64, 75, 76, 0, 0, 0, 0, 0, 61, 86, 30, 96, 221, 64, 70, 137, 136, 106, 154 - ) - .toByteList() - .toByteArray() - ) - .toList() - - val expectedEncoded: List = - listOf( - 166, 113, 97, 198, 249, 29, 149, 39, 234, 219, 180, 240, 41, 2, 0, 0, 64, 75, 76, - 0, 0, 0, 0, 0, 61, 86, 30, 96, 221, 64, 70, 137, 136, 106, 154 - ) - .toByteList() - - assertEquals(expectedEncoded, encoded) - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/codeScanner/PayloadEncodingTest.kt b/services/code/src/androidTest/java/com/getcode/codeScanner/PayloadEncodingTest.kt deleted file mode 100644 index 495f0fdbf..000000000 --- a/services/code/src/androidTest/java/com/getcode/codeScanner/PayloadEncodingTest.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.getcode.codeScanner - -import com.getcode.services.model.CodePayload -import com.getcode.model.Kin -import com.getcode.services.model.Kind -import junit.framework.Assert.assertEquals -import org.junit.Test - -class PayloadEncodingTest { - @Test - fun payloadEncoding() { - val encodedData = listOf( - 0x00, 0x40, 0x4B, 0x4C, 0x00, 0x0, 0x00, 0x00, 0x00, 0x01, - 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11 - ) - val nonceData = listOf( - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11 - ) - val payload = CodePayload( - kind = Kind.Cash, - value = Kin.fromKin(50), - nonce = nonceData - ) - - val encoded = payload.encode() - val decoded = CodePayload.Companion.fromList(encoded) - - // -------------------------------------------------------- - - assertEquals(Kind.Cash.value, decoded.kind.value) - assertEquals(50, decoded.kin?.toKin()?.toInt()) - assertEquals(nonceData, decoded.nonce) - - assertEquals(encodedData, encoded) - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/mocks/SolanaTransaction.kt b/services/code/src/androidTest/java/com/getcode/mocks/SolanaTransaction.kt deleted file mode 100644 index 6b6ea6c10..000000000 --- a/services/code/src/androidTest/java/com/getcode/mocks/SolanaTransaction.kt +++ /dev/null @@ -1,127 +0,0 @@ -package com.getcode.mocks - -import com.getcode.solana.SolanaTransaction -import com.getcode.utils.decodeBase64 - -object SolanaTransaction { - /// Mock Timelock Create Account Transaction - /// - /// - Instruction 1: Advance Nonce (No Change) - /// - /// - Instruction 2: Initialize Timelock - /// - Accounts - /// - Timelock: 4B6DvoEJugGBrKedasVvT2n5GykbtsVUFknsz12FWEv9 - /// - Vault: EQDNoJMxbAWr81XFM1TykpFuJuK5CjxmJLyP45S95wR8 - /// - VaultOwner: G9zksyBhzGzFDPjfF333HEXCWKstU8Go4JvUChBNBLf7 - /// - Mint: kinXdEcpDQeHPEuQnqmUgtYykqKGVFq6CeVX5iAHJq6 - /// - Time Authority: codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR - /// - Payer: codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR - /// - /// - Instruction 3: Activate Timelock - /// - Arguments - /// - Timelock Bump: 254 - /// - Unlock Duration: 1209600 - /// - Accounts - /// - Timelock: 4B6DvoEJugGBrKedasVvT2n5GykbtsVUFknsz12FWEv9 - /// - Time Authority: codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR - /// - Vault Owner: G9zksyBhzGzFDPjfF333HEXCWKstU8Go4JvUChBNBLf7 - /// - Payer: codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR - /// - fun mockTimelockCreateAccount(): Pair = - transaction("AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAHCwksDha4qmHvDLlGQXd2cjb/PDR7UoWkLijNmnwnO1nuN8DXVqgdQXIsX+LDS9MUe8jvjg/Ff6Vj6VZaNxer98K8yIAO97UcHAd2xm4zfG8AtfQDMyT8/7QC0Sen9vq/lgSaWxmcYQR245MI8/QpznC4B3qZptwxn5SBWI9Bizr8PKg85sywGzAYembl67Ega1GZSC7hiY7u3Yz/akHwTAoGp9UXGSxWjuCKhF9z0peIzwNcMUWyGrNE2AYuqUAAAAan1RcZLFxRIYzJTD1K8X9Y2u4Im6H9ROPb2YoAAAAACzM4oKssyEHVsBS8ajz3VikYdLMZyVF9m7+p5OlmHvkG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADT/Zr02LPzD6xb5Nf2x4+R0n7wWJyKAfah6AyAlWXFeo9TwTAtFbaDGnTWWclU2+wNsZXm1C/+ztSBss80USmAIKAwEFAAQEAAAACgoJAgMEBwAACAkGEK+vbR8NmJvtgK8bAAAAAAA=") - - - /// Mock Timelock Transfer Transaction - /// - /// - Instruction 1: Advance Nonce (No Change) - /// - /// - Instruction 2: Memo (No Change) - /// - /// - Instruction 3: Transfer With Authority - /// - Arguments - /// - Timelock Bump: 254 - /// - Amount: 100,000 - /// - Accounts - /// - Timelock: 4B6DvoEJugGBrKedasVvT2n5GykbtsVUFknsz12FWEv9 - /// - Vault: EQDNoJMxbAWr81XFM1TykpFuJuK5CjxmJLyP45S95wR8 - /// - Vault Owner: G9zksyBhzGzFDPjfF333HEXCWKstU8Go4JvUChBNBLf7 - /// - Time Authority: codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR - /// - Destination: 67ziVAtk8djEKbwNtFhUPrkiEi8RdYaq4GXkpHzHd2Nq - /// - Payer: codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR - /// - fun mockTimelockTransfer(): Pair = - transaction("AqWNqWCdgbzlWTVZQB7+iBg52O9A8107s/pfQ/Z2FndWizwNXukZioklvScCgQTZFr2f3eg4ojfEvpiZqwm9+wAiQGg5UsZEf/DjuHrnZr7YxHl0dIZexmPtmpgOdI69G7YVGSk2rE3sLk+65GeFUoDhpq7tzxP9W6nWzI5/5HQHAgEGCwksDha4qmHvDLlGQXd2cjb/PDR7UoWkLijNmnwnO1nuu7Xnafr2nnC0//MZTieqGWg8ygCot6SYJVjyndZCxGoaDXxcmpoifit1bsGjzYQ/vWUcn2k/tEUOjKQmrm5eKRu4eednzoBrNKpxRazVlEwA0hfWw9AnV3fGaPVtKli874Pwj2BfKXqwWqg0L7RsCDBgdqH2i6hMyNuRTnhIR1sGp9UXGSxWjuCKhF9z0peIzwNcMUWyGrNE2AYuqUAAAAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCp58J2lG1wb0LqCFPN9+Fla+HyGz75GCqQaZxuRH8yyfEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVKU1D4XciC1hSlVnJ4iilt3x6rq9CmBniISTL07vagDT/Zr02LPzD6xb5Nf2x4+R0n7wWJyKAfah6AyAlWXFf4ixn10XVBBNgH+xMmt/5cXJ6W7m1U9liIi4o8EvBHfAMIAwQFAAQEAAAACQAsWlRBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT0KCAcCAQADAAYIEUSA3sCBRUel/0ANAwAAAAAA") - - - /// Mock SPL Transfer Transaction - /// - /// - Instruction 1: Advance Nonce - /// - /// - Instruction 2: Memo - /// - /// - Instruction 3: Token Transfer - /// - Arguments: - /// - Amount: 100,000 - /// - Accounts: - /// - Source: HFMeSarShcvgKARwwMTS6WafuRzhW1BPRsWzo3WEa4FS - /// - Destination: FmoyfcoDYya27XtJcZUKtnXRn6RKNA8yrApkf88DcKvj - /// - fun mockSimpleTransfer(): Pair = - transaction( - "A1tAcqFQvsLAwkzzR6IyioVR7RanubupIBmSTJLmVRgehHpCXA4vw1iydd/nXGRM2MFkcOO486sPb" + - "Y/t5YkcoQ+HczaWQofwtUOGMXOaitdnW4QV2IrNouP7OekZ5X/nrVTaMNsRldU4hDKv4TpBW5ZtUu" + - "MxNj+K0hYaiOhe0bwAe03tCXMy5w2tn22FQRD98vyOk9lllvhOiNvrz2MkQBrKyGZkvFx+GxzIr2J" + - "jKp2ZcLiYQAabpKmmqae3WbejDAMBBAkJLA4WuKph7wy5RkF3dnI2/zw0e1KFpC4ozZp8JztZ7gu8" + - "C2nMEidsVQv64veR8KGN+uSVB8t3QXls8pS1g3hpYe7aeLzQPu4s5+1/zFV3d0sA+QjBW17I1Gz2k" + - "SOyqk7bfkolDNcGZ49pe8RELDOdW6UGljTmveo3XvKO0SSYDPFod1RaA9JNk/lmcAnsZEkxtqcuUg" + - "nkYIlTqbFPislJBqfVFxksVo7gioRfc9KXiM8DXDFFshqzRNgGLqlAAAAFSlNQ+F3IgtYUpVZyeIo" + - "pbd8eq6vQpgZ4iEky9O72oAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpAAAAAAAAAAAA" + - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB9c5VBOZKF0Rkn4tPQFamDoUjF8oa04karh4ZnmDORpgMIA" + - "wEFAAQEAAAABgAsWlRBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT0HAw" + - "QDAgkDoIYBAAAAAAA=" - ) - - - fun mockCloseDormantAccount(): Pair = - transaction("Ag83S4kpkDdVCkjHoQMbY1PFlHQuN4iNc0HvKRY4GAYtS2fJg2sD0pTswcXX80wQ0l1LNz" + - "fEPMFq1L65zh3ZqAMalMR4K8eVSOb2026zqdo2y+jtsqXZUwLXo5u7kpxcQdesl0RyvYFW5TtfOahNZEmt" + - "130Bqr7JW52XByUWOMUFAgEFCwksDha4qmHvDLlGQXd2cjb/PDR7UoWkLijNmnwnO1nuymNnIuP7iSQN87" + - "RAiXXExCM/rcMSDk1ufucz1QBgrMsQi2SY08yHh0QVR56YXP/W3hU8rVeYaNQ3Pou62KeUaMXihwH1Rl/j" + - "ONnj6X35HTZ5IaCcgRGCf5ejrg2iOClK2CjKDyXQ3p6u/9OZeYr/MDJt3dfzYPPo50L/Nl89J5fiWbl2YM" + - "re3ew8o0M5ZWp069OdcB1BTXvdFHPVb8kEggan1RcZLFaO4IqEX3PSl4jPA1wxRbIas0TYBi6pQAAABt32" + - "4ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + - "VKU1D4XciC1hSlVnJ4iilt3x6rq9CmBniISTL07vagDT/Zr02LPzD6xb5Nf2x4+R0n7wWJyKAfah6AyAlW" + - "XFdklqEX6a61ZsH0qvimrsORM6Wqr1q/dAdLEVNifIlQSgYIAwIGAAQEAAAACQAsWlRBRUFBQUFBQUFBQU" + - "FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT0KBgQDAAAHCAnltTryqwjJkP8KAwQBAAkscCGscRyO" + - "Df8KBwQDAQUABwgJtxJGnJRtoSL/CgYEAwAABwgJq95e6SL6ygH/") - - fun mockCloseDormantAccount2(): Pair = - transaction("AkH0mv3Wb+chdEx7SDmLKBDu+MEJuAecPUhgHUV2uHPx6rF061sMmL055hGCGrW8iGnv4" + - "GPR7ozqV74Xc2g2cwHYt8SiG9X1Jyesj7INStyZ+5yY3OtB12RnOUEhcmGbju91ehu/W/O22Kz5kdpb7v" + - "kWzNEQIRiWopVHJy2IdFoKAgEFCwksDha4qmHvDLlGQXd2cjb/PDR7UoWkLijNmnwnO1nuKAvMOr2G9++" + - "j3FKHaYR45PV5PjUE7fNwU+O8l4ozKdELvAfBis4EiZ742UqJJmdRJAxuh5nsIdVfzf6/BQOMlkwWp+FX" + - "GEbr7ILKWA/w3YDZHN+ZR0WEK3Sf0BzuIUH2qEDphy9YNl6UtOVMq/2wT3QVO5YV9DY27Nq4MO3E+QnVQ" + - "/teS/yrCfsfzeH4V559kxgBVPs4O/8VZrj607/XuAan1RcZLFaO4IqEX3PSl4jPA1wxRbIas0TYBi6pQA" + - "AABt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + - "AAAAAAAVKU1D4XciC1hSlVnJ4iilt3x6rq9CmBniISTL07vagDT/Zs1d0xV/ZGv3T91QbiBCGO0SfEPaa" + - "w23Wl4xfukvGwvzIqUysvHSh1VVyp/E8ZQYV0j7Gi7rs422YnxbHgAYIAwIGAAQEAAAACQAsWlRBRUFBQ" + - "UFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT0KBgMEAAAHCAnltTryqwjJkPYKAwMBAA" + - "kscCGscRyODfYKBwMEAQUABwgJtxJGnJRtoSL2CgYDBAAABwgJq95e6SL6ygH2") - - - private fun transaction(base64: String): Pair { - return Pair(SolanaTransaction.fromList(base64.decodeBase64().toList())!!, base64) - } - - fun mockPrivateTransfer(): Pair { - return transaction("AqWNqWCdgbzlWTVZQB7+iBg52O9A8107s/pfQ/Z2FndWizwNXukZioklvScCgQTZFr2f3eg4ojfEvpiZqwm9+wAiQGg5UsZEf/DjuHrnZr7YxHl0dIZexmPtmpgOdI69G7YVGSk2rE3sLk+65GeFUoDhpq7tzxP9W6nWzI5/5HQHAgEGCwksDha4qmHvDLlGQXd2cjb/PDR7UoWkLijNmnwnO1nuu7Xnafr2nnC0//MZTieqGWg8ygCot6SYJVjyndZCxGoaDXxcmpoifit1bsGjzYQ/vWUcn2k/tEUOjKQmrm5eKRu4eednzoBrNKpxRazVlEwA0hfWw9AnV3fGaPVtKli874Pwj2BfKXqwWqg0L7RsCDBgdqH2i6hMyNuRTnhIR1sGp9UXGSxWjuCKhF9z0peIzwNcMUWyGrNE2AYuqUAAAAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCp58J2lG1wb0LqCFPN9+Fla+HyGz75GCqQaZxuRH8yyfEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVKU1D4XciC1hSlVnJ4iilt3x6rq9CmBniISTL07vagDT/Zr02LPzD6xb5Nf2x4+R0n7wWJyKAfah6AyAlWXFf4ixn10XVBBNgH+xMmt/5cXJ6W7m1U9liIi4o8EvBHfAMIAwQFAAQEAAAACQAsWlRBRUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT0KCAcCAQADAAYIEUSA3sCBRUel/0ANAwAAAAAA") - } - - fun mockCloseEmptyAccount(): Pair { - return transaction("AliNx0Qd/Yh3rxDOm7tP6Nk5F/kIqkBCgUxtiPXbPa4hI/lPvnFu2R1kOUPSVyXfukFkhWVmVMbAhPsC1julZQZgPnBJozuXsURdMLy8FyhML7D4H0v1fhCHQJMOoAORJO75IeFbekkBWFFZZK+TOhBApSCQK4uEdjv7lyhK7dEDAgEECgksDha4qmHvDLlGQXd2cjb/PDR7UoWkLijNmnwnO1nuAwAkc+jnVXSj6mVi3qDSxmtMOwdDpQZWwcrlIreF0/0B6uU3GHXrot2xFmpofLZ+RJ3k0x3D0yyzG7HtV5blAgszOKCrLMhB1bAUvGo891YpGHSzGclRfZu/qeTpZh75/IUvnzG1DltbSH1irqx2Cyh7SroxvAgiqc8rXoqDGHz/5wv1HyDU2Q6Ue0j8NoyFFNuy3gehalCj+lmXjCqUuQan1RcZLFaO4IqEX3PSl4jPA1wxRbIas0TYBi6pQAAABt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0/2a9Niz8w+sW+TX9sePkdJ+8FicigH2oegMgJVlxXnFoL9tCTU/3A8XPsFV4gd1BbGTdFQhiF05WPGERPmJIDCAMFBgAEBAAAAAkIBAIBAAMABwgRJyr/2g58Ti3/oIYBAAAAAAAJBgQCAAAHCAmr3l7pIvrKAf8=") - } - - fun mockSwapValidatorTransaction(): Pair { - return transaction("Ai8DDCmTiXNWVGjdVLVomTNKV30XWulDmprX+GmPRng14pzjRoak2VVn7FFBZpve9QTSSbKuYcWdvnPkJajBwgFjkpFwVm060lRYaIlhjFl0cYgrpzH5XhEFGhYtApuXXLqNLu01t9txR3BovWiOgv23P6MqHz8VVZhvDIyeg4oCAgENGg0MwfSFE4w1uhbhBNf6mRCF47iXCgmTTYxIwi+Fm5ehUMerqyiPefE9E7gBk7ZY5IJ4nLf772AVw55CzWTiiGY09m1htWy94fpmRKYOx+N0X34FeOmFa82zzf8INdpxP0cN1WYA2iMPIbXmYU8XE8vPErCWVHyyg3f4ZBHwrx/TTZc3mn4uFamT2biCB9ecpIC79npoR1q31BE+gla0ksBWojdnIjFnS5owqvspIyaTinDkoOAUytGwounm7qJ09ndeuIfsf9a7VhtUk213wo2gTsuYKP9CaiqioTcPfMZLfTCbEm355LXq/rWOjv6vEbgMFZk77iH3BuaRzKpOQ3Gn7YNis9dzD2gKH/e+jXm5yLDtUZ5PRhWSVzIyB3pTmrFH/+NdHxsK7E1CA4UuCPrQE3DSexU7p4casiZBmZfpyQ+0ezK8pEPmQbXwZ8Tz3O52/YACoT20v0iE4A7D1bTURbecZ+g+bRkN8K5AGjN//DXbGWjUuVu2F2xcmZuxcObLflaj6b9a9fKWYlPrwknHDfUtWTdyXfpdoH7SUukmAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGp9UXGSxcUSGMyUw9SvF/WNruCJuh/UTj29mKAAAAAAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpCzM4oKssyEHVsBS8ajz3VikYdLMZyVF9m7+p5OlmHvkOA2hfjpCQU+RYEhxm9adq7cdwaqEcgviqlSqPK3h5qRxSrqrcz1GI/DprRxhG35nwvF3uXW41WyV5PsZBM/cYf/gGBDqt+4KYGybpo9JbzQS27gYJPMzK6XfEbDqZnyCCl+VE/0RAahO3+mTCz/o+xuFd+JNeAMdBcN6u8BPzfbQ/+if11/ZKdMCbHylYed5LCas238ndUUsyGqezjOXoxvp6877brTo9ZfNqq8l0MbG75MLS9uDkfKYCA0UvXWEDBkZv5SEXMv/srbpyw5vnvIzlu8X3EmssQ5s6QAAAAAR51VvyMcBu7nTFbs5oFQf9sbLeo/SOUQKxzaJWvBOPDPDd/QTxayDMlt0ISimdJXij1pDlyvED9R/+yK/m0CfVRXzVknfVde5H5wl7mZGH6+R72ZyE7kEMwojrAgmA+wUXAAUCwFwVABcACQNuEAAAAAAAABkSAgEDBxIADQ4KBAgECwoFCQwGCLfdx4rPSX9xGBkPFAEDCgQHFhAYGBUYEQ8UCAQLCgUJDAYTJcEgmzNB1pyBBgEAAAARAGQAARAnAAAAAAAAhmBuAwAAAAAyAAAZBAIDBwAZn9W3ObOKdaH+ECcAAAAAAAA0/GkDAAAAAA==") - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/models/CodePayloadTests.kt b/services/code/src/androidTest/java/com/getcode/models/CodePayloadTests.kt deleted file mode 100644 index 63c0878be..000000000 --- a/services/code/src/androidTest/java/com/getcode/models/CodePayloadTests.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.getcode.models - -import com.getcode.services.model.CodePayload -import com.getcode.services.model.Kind -import com.getcode.services.model.payload.Username -import junit.framework.Assert -import org.junit.Test - -class CodePayloadTests { - - private val sampleTip = listOf( - 5, 0, 0, 0, 0, 103, 101, 116, 99, 111, 100, 101, 46, 52, 86, 113, 47, 114, 43, 88 - ) - - private val sampleTipDot = listOf( - 5, 0, 0, 0, 0, 97, 110, 100, 114, 111, 105, 100, 100, 101, 118, 49, 50, 51, 52, 46 - ) - - @Test - fun testEncodeUsername() { - val payload = CodePayload( - kind = Kind.Tip, - value = Username("getcode"), - ) - - val encoded = payload.encode() - Assert.assertEquals(sampleTip, encoded) - - } - - @Test - fun testEncodedSinglePad() { - val payload = CodePayload( - kind = Kind.Tip, - value = Username("androiddev1234"), - ) - - val encoded = payload.encode() - Assert.assertEquals(sampleTipDot, encoded) - } - - @Test - fun testEncodeUsernameTooLong() { - val payload0 = CodePayload( - kind = Kind.Tip, - value = Username("androiddev1234_"), // max length - ) - - val payload1 = CodePayload( - kind = Kind.Tip, - value = Username("androiddev1234__"), - ) - - val payload2 = CodePayload( - kind = Kind.Tip, - value = Username("androiddev1234__1412412412"), - ) - - val expected = payload0.encode() - - Assert.assertEquals(expected, payload1.encode()) - Assert.assertEquals(expected, payload2.encode()) - } - - @Test - fun testDecodeUsername() { - val payload = CodePayload.fromList(sampleTip) - Assert.assertEquals(payload.kind, Kind.Tip) - Assert.assertEquals(payload.value, Username("getcode")) - Assert.assertEquals(payload.nonce, emptyList()) - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/models/KinTests.kt b/services/code/src/androidTest/java/com/getcode/models/KinTests.kt deleted file mode 100644 index 2c7325012..000000000 --- a/services/code/src/androidTest/java/com/getcode/models/KinTests.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.getcode.models - -import com.getcode.model.Kin -import org.junit.Assert.* -import org.junit.Test - -class KinTests { - @Test - fun testInitWithKin() { - assertEquals(100_000, Kin.fromKin(kin = 1).quarks) - - assertEquals(133_334, Kin.fromKin(kin = 1 / 3.0 + 1).quarks) - assertEquals(166_667, Kin.fromKin(kin = 2 / 3.0 + 1).quarks) - - assertEquals(100_000, Kin.fromKin(kin = 1 / 3 + 1).quarks) - assertEquals(100_000, Kin.fromKin(kin = 2 / 3 + 1).quarks) - } - - @Test - fun testInitNegativeKin() { - assertThrows(IllegalStateException::class.java) { - Kin.fromKin(kin = -1) - } - assertThrows(IllegalStateException::class.java) { - Kin(-1) - } - - assertNotNull(Kin.fromKin(kin = -0)) - assertNotNull(Kin(-0)) - } - - @Test - fun testInitWithQuarks() { - assertEquals(100_000, Kin(100_000).quarks) - assertEquals(133_334, Kin(133_334).quarks) - assertEquals(166_667, Kin(166_667).quarks) - } - - @Test - fun testKinValues() { - assertEquals(1, Kin(100_000).toKinTruncatingLong()) - assertEquals(1, Kin(133_334).toKinTruncatingLong()) - assertEquals(1, Kin(166_667).toKinTruncatingLong()) - } - - /*fun testComparison() { - assertTrue(Kin(13) > Kin(12)) - assertTrue(Kin(11) < Kin(12)) - assertTrue(Kin(13) >= Kin(13)) - assertTrue(Kin(14) >= Kin(13)) - assertTrue(Kin(12) <= Kin(12)) - assertTrue(Kin(11) <= Kin(12)) - }*/ - - @Test - fun testTruncation() { - assertEquals(100_000, Kin.fromQuarks(quarks = 100_000).toKinTruncating().quarks) - assertEquals(100_000, Kin.fromQuarks(quarks = 133_334).toKinTruncating().quarks) - assertEquals(100_000, Kin.fromQuarks(quarks = 166_667).toKinTruncating().quarks) - } - - @Test - fun testFractional() { - assertEquals(0, Kin.fromQuarks(quarks = 100_000).fractionalQuarks().quarks) - assertEquals(33_334, Kin.fromQuarks(quarks = 133_334).fractionalQuarks().quarks) - assertEquals(66_667, Kin.fromQuarks(quarks = 166_667).fractionalQuarks().quarks) - } - - @Test - fun testInflation() { - assertEquals(100_000, Kin.fromQuarks(quarks = 99_999).inflating().quarks) - assertEquals(100_000, Kin.fromQuarks(quarks = 100_000).inflating().quarks) - assertEquals(200_000, Kin.fromQuarks(quarks = 133_334).inflating().quarks) - assertEquals(200_000, Kin.fromQuarks(quarks = 166_667).inflating().quarks) - } - - @Test - fun testMultiplication() { - assertEquals(Kin.fromKin(kin = 123_000), Kin.fromKin(kin = 123) * 1_000) - assertEquals(Kin.fromKin(kin = 123), Kin.fromKin(kin = 123) * 1) - assertEquals(Kin.fromKin(kin = 0), Kin.fromKin(kin = 123) * 0) - } - - @Test - fun testDivision() { - assertEquals(10_000.0, Kin.fromKin(kin = 100_000 / 10).toKin().toDouble(), 0.0) - assertEquals(12.0, Kin.fromKin(kin = 123 / 10).toKin().toDouble(), 0.0) - assertEquals(123.0, Kin.fromKin(kin = 123 / 1).toKin().toDouble(), 0.0) - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/models/intents/IntentDepositTest.kt b/services/code/src/androidTest/java/com/getcode/models/intents/IntentDepositTest.kt deleted file mode 100644 index 2b3bb8bc2..000000000 --- a/services/code/src/androidTest/java/com/getcode/models/intents/IntentDepositTest.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.getcode.models.intents - -import android.content.Context -import androidx.test.platform.app.InstrumentationRegistry -import com.getcode.crypt.MnemonicPhrase -import com.getcode.model.Kin -import com.getcode.model.intents.IntentDeposit -import com.getcode.model.intents.actions.ActionTransfer -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.SlotType -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotEquals -import org.junit.Before -import org.junit.Test - -class IntentDepositTest { - lateinit var context: Context - - private val mnemonic = MnemonicPhrase.newInstance( - words = "couple divorce usage surprise before range feature source bubble chunk spot away".split( - " " - ) - )!! - - @Before - fun setup() { - context = InstrumentationRegistry.getInstrumentation().context - } - - @Test - fun testReceiveSevenDollars() { - val organizer = Organizer.newInstance(mnemonic) - val amount = Kin.Companion.fromKin(1_000_000) - - organizer.setBalances( - mapOf(AccountType.Primary to amount) - ) - - val intent = IntentDeposit.newInstance( - source = AccountType.Primary, - organizer = organizer, - amount = amount - ) - - - assertNotEquals(intent.id, - com.getcode.solana.keys.PublicKey(ByteArray(com.getcode.solana.keys.LENGTH_32).toList()) - ) - - val resultTray = intent.resultTray - - // Ensure the funds have been moved out of the - // primary accounts and into the tray slots - assertEquals(0.0, resultTray.owner.partialBalance.toKinValueDouble(), 0.0) - - assertEquals(1_000_000.0, resultTray.availableBalance.toKinValueDouble(), 0.0) - - assertEquals(0.0, resultTray.owner.partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, resultTray.incoming.partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, resultTray.outgoing.partialBalance.toKinValueDouble(), 0.0) - - assertEquals(10.0, resultTray.slot(SlotType.Bucket1).partialBalance.toKinValueDouble(), 0.0) - assertEquals(90.0, resultTray.slot(SlotType.Bucket10).partialBalance.toKinValueDouble(), 0.0) - assertEquals(900.0, resultTray.slot(SlotType.Bucket100).partialBalance.toKinValueDouble(), 0.0) - assertEquals(9_000.0, resultTray.slot(SlotType.Bucket1k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(90_000.0, resultTray.slot(SlotType.Bucket10k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(900_000.0, resultTray.slot(SlotType.Bucket100k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, resultTray.slot(SlotType.Bucket1m).partialBalance.toKinValueDouble(), 0.0) - - /* - * Expected actions: - * - * K 1000000 (0) -> AB4w6m9nhQaagqpnu6TcsgE1Z34wXKwKSoxU6tGadAfN (tempPrivacyTransfer) - * K 1000000 (0) -> BEZasPLNZ5vsHH3SfdxeWuTD5uXm8pPUmbyrZkPJqQwr (tempPrivacyExchange) - * K 100000 (0) -> 7GpxPmL2sGqRq1ru4nKTPWPRruemat5BCReGmNLNXsRE (tempPrivacyExchange) - * K 10000 (0) -> 6upXkqkiY3GYBqm3wSReuAsaWxQSY1d67GuRxLhM74Va (tempPrivacyExchange) - * K 1000 (0) -> 6eqAKwBqtAQ28juRdc3429GoRpUTuu86gScJrDN6cqGQ (tempPrivacyExchange) - * K 100 (0) -> BbCteP1N7DiyShnEuCRGkNLeoYbr7v1d5deGDBbeu5Zg (tempPrivacyExchange) - * K 10 (0) -> J5fzggJmRyPwmAcJw7iVD9jG4q4xZyeELNEEjfhKxp4i (tempPrivacyExchange) - */ - - intent.getActions().forEachIndexed { index, action -> - assertEquals(index, action.id) - } - - assertEquals(7, intent.getActions().size) - - (intent.getAction(0) as ActionTransfer).let { action -> - assertEquals(ActionTransfer.Kind.TempPrivacyTransfer, action.kind) - assertEquals(1_000_000.0, action.amount.toKinValueDouble(), 0.0) - assertEquals(organizer.tray.owner.getCluster(), action.source) - assertEquals(organizer.tray.slots[6].getCluster().vaultPublicKey, action.destination) - } - - (intent.getAction(1) as ActionTransfer).let { action -> - assertEquals(ActionTransfer.Kind.TempPrivacyExchange, action.kind) - assertEquals(1_000_000.0, action.amount.toKinValueDouble(), 0.0) - assertEquals(organizer.tray.slots[6].getCluster(), action.source) - assertEquals(organizer.tray.slots[5].getCluster().vaultPublicKey, action.destination) - } - - (intent.getAction(2) as ActionTransfer).let { action -> - assertEquals(ActionTransfer.Kind.TempPrivacyExchange, action.kind) - assertEquals(100_000.0, action.amount.toKinValueDouble(), 0.0) - assertEquals(organizer.tray.slots[5].getCluster(), action.source) - assertEquals(organizer.tray.slots[4].getCluster().vaultPublicKey, action.destination) - } - - (intent.getAction(3) as ActionTransfer).let { action -> - assertEquals(ActionTransfer.Kind.TempPrivacyExchange, action.kind) - assertEquals(10_000.0, action.amount.toKinValueDouble(), 0.0) - assertEquals(organizer.tray.slots[4].getCluster(), action.source) - assertEquals(organizer.tray.slots[3].getCluster().vaultPublicKey, action.destination) - } - - (intent.getAction(4) as ActionTransfer).let { action -> - assertEquals(ActionTransfer.Kind.TempPrivacyExchange, action.kind) - assertEquals(1_000.0, action.amount.toKinValueDouble(), 0.0) - assertEquals(organizer.tray.slots[3].getCluster(), action.source) - assertEquals(organizer.tray.slots[2].getCluster().vaultPublicKey, action.destination) - } - - (intent.getAction(5) as ActionTransfer).let { action -> - assertEquals(ActionTransfer.Kind.TempPrivacyExchange, action.kind) - assertEquals(100.0, action.amount.toKinValueDouble(), 0.0) - assertEquals(organizer.tray.slots[2].getCluster(), action.source) - assertEquals(organizer.tray.slots[1].getCluster().vaultPublicKey, action.destination) - } - - (intent.getAction(6) as ActionTransfer).let { action -> - assertEquals(ActionTransfer.Kind.TempPrivacyExchange, action.kind) - assertEquals(10.0, action.amount.toKinValueDouble(), 0.0) - assertEquals(organizer.tray.slots[1].getCluster(), action.source) - assertEquals(organizer.tray.slots[0].getCluster().vaultPublicKey, action.destination) - } - - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/models/intents/IntentPrivateTransferTest.kt b/services/code/src/androidTest/java/com/getcode/models/intents/IntentPrivateTransferTest.kt deleted file mode 100644 index 091c834fe..000000000 --- a/services/code/src/androidTest/java/com/getcode/models/intents/IntentPrivateTransferTest.kt +++ /dev/null @@ -1,170 +0,0 @@ -package com.getcode.models.intents - -import android.content.Context -import androidx.test.platform.app.InstrumentationRegistry -import com.getcode.crypt.MnemonicPhrase -import com.getcode.model.CurrencyCode -import com.getcode.model.Kin -import com.getcode.model.KinAmount -import com.getcode.model.fromFiatAmount -import com.getcode.model.generate -import com.getcode.model.intents.IntentPrivateTransfer -import com.getcode.model.intents.actions.ActionOpenAccount -import com.getcode.model.intents.actions.ActionTransfer -import com.getcode.model.intents.actions.ActionWithdraw -import com.getcode.model.toPublicKey -import com.getcode.solana.keys.Key32.Companion.mock -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.SlotType -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test - -class IntentPrivateTransferTest { - lateinit var context: Context - - private val mnemonic = MnemonicPhrase.newInstance( - words = "couple divorce usage surprise before range feature source bubble chunk spot away".split( - " " - ) - )!! - - @Before - fun setup() { - context = InstrumentationRegistry.getInstrumentation().context - } - - @Test - fun testSendFiveDollars() { - val organizer = Organizer.newInstance(mnemonic) - - organizer.setBalances( - mapOf(AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(1_000_000)) - ) - - val destination: PublicKey = mock - val amount = KinAmount.fromFiatAmount( - fiat = 5.00, - fx = 0.00001, - currencyCode = CurrencyCode.USD - ) - - val rendezvous = PublicKey.generate() - - val intent = IntentPrivateTransfer.newInstance( - rendezvousKey = rendezvous, - organizer = organizer, - destination = destination, - amount = amount, - isWithdrawal = false, - fee = Kin.fromKin(0), - additionalFees = emptyList(), - metadata = null, - ) - - assertEquals(rendezvous, intent.id) - - val resultTray = intent.resultTray - - // Ensure outgoing is incremented - assertEquals(organizer.tray.outgoing.getCluster().index, resultTray.outgoing.getCluster().index - 1) - - // The outgoing account has been rotated so we need to ensure - // the previous outgoing account has the correct balance and - // the new account is empty - assertEquals(0.0, resultTray.outgoing.partialBalance.toKinValueDouble(), 0.0) - - assertEquals(500_000.0, organizer.tray.availableBalance.toKinValueDouble() - intent.resultTray.availableBalance.toKinValueDouble(), 0.0) - assertEquals(10.0, resultTray.slot(SlotType.Bucket1).partialBalance.toKinValueDouble(), 0.0) - assertEquals(90.0, resultTray.slot(SlotType.Bucket10).partialBalance.toKinValueDouble(), 0.0) - assertEquals(900.0, resultTray.slot(SlotType.Bucket100).partialBalance.toKinValueDouble(), 0.0) - assertEquals(9_000.0, resultTray.slot(SlotType.Bucket1k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(90_000.0, resultTray.slot(SlotType.Bucket10k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(400_000.0, resultTray.slot(SlotType.Bucket100k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, resultTray.slot(SlotType.Bucket1m).partialBalance.toKinValueDouble(), 0.0) - - /* - * Expected actions: - * - * K 1000000 (0) -> BEZasPLNZ5vsHH3SfdxeWuTD5uXm8pPUmbyrZkPJqQwr (tempPrivacyExchange) - * K 100000 (0) -> 7GpxPmL2sGqRq1ru4nKTPWPRruemat5BCReGmNLNXsRE (tempPrivacyExchange) - * K 500000 (0) -> Gfuc6w9vPwoGKtRwEv7YJtxGWtR4knLGoMoT5Hu1eS6A (tempPrivacyTransfer) - * K 500000 (0) -> EBDRoayCDDUvDgCimta45ajQeXbexv7aKqJubruqpyvu (noPrivacyWithdraw) - * K 10000 (0) -> 6upXkqkiY3GYBqm3wSReuAsaWxQSY1d67GuRxLhM74Va (tempPrivacyExchange) - * K 1000 (0) -> 6eqAKwBqtAQ28juRdc3429GoRpUTuu86gScJrDN6cqGQ (tempPrivacyExchange) - * K 100 (0) -> BbCteP1N7DiyShnEuCRGkNLeoYbr7v1d5deGDBbeu5Zg (tempPrivacyExchange) - * K 10 (0) -> J5fzggJmRyPwmAcJw7iVD9jG4q4xZyeELNEEjfhKxp4i (tempPrivacyExchange) - * Close Dormant -> Gfuc6w9vPwoGKtRwEv7YJtxGWtR4knLGoMoT5Hu1eS6A sending to 8DDrALtni72M6FnCiTMToEssMHDEH3KRP1nhA6svQDxp (closeDormantAccount) - * Open -> 48FpNcnDn5kdFPRjsm5dzr7iXSVBPuF2GcKYbyyrQFD6 (openAccount) - */ - - // Ensure all actions have indexes - intent.getActions().forEachIndexed { index, action -> - assertEquals(index, action.id) - } - - assertEquals(10, intent.getActions().size) - - (intent.getAction(0) as ActionTransfer).let { action -> - assertEquals(ActionTransfer.Kind.TempPrivacyExchange, action.kind) - assertEquals(1_000_000.0, action.amount.toKinValueDouble(), 0.0) - assertEquals(organizer.tray.slots[5].getCluster().vaultPublicKey, action.destination) - } - - (intent.getAction(1) as ActionTransfer).let { action -> - assertEquals(ActionTransfer.Kind.TempPrivacyExchange, action.kind) - assertEquals(100_000.0, action.amount.toKinValueDouble(), 0.0) - assertEquals(organizer.tray.slots[4].getCluster().vaultPublicKey, action.destination) - } - - (intent.getAction(2) as ActionTransfer).let { action -> - assertEquals(ActionTransfer.Kind.TempPrivacyTransfer, action.kind) - assertEquals(500_000.0, action.amount.toKinValueDouble(), 0.0) - assertEquals(organizer.tray.outgoing.getCluster().vaultPublicKey, action.destination) - } - - (intent.getAction(3) as ActionTransfer).let { action -> - assertEquals(ActionTransfer.Kind.TempPrivacyExchange, action.kind) - assertEquals(10_000.0, action.amount.toKinValueDouble(), 0.0) - assertEquals(organizer.tray.slots[3].getCluster().vaultPublicKey, action.destination) - } - - (intent.getAction(4) as ActionTransfer).let { action -> - assertEquals(ActionTransfer.Kind.TempPrivacyExchange, action.kind) - assertEquals(1_000.0, action.amount.toKinValueDouble(), 0.0) - assertEquals(organizer.tray.slots[2].getCluster().vaultPublicKey, action.destination) - } - - (intent.getAction(5) as ActionTransfer).let { action -> - assertEquals(ActionTransfer.Kind.TempPrivacyExchange, action.kind) - assertEquals(100.0, action.amount.toKinValueDouble(), 0.0) - assertEquals(organizer.tray.slots[1].getCluster().vaultPublicKey, action.destination) - } - - (intent.getAction(6) as ActionTransfer).let { action -> - assertEquals(ActionTransfer.Kind.TempPrivacyExchange, action.kind) - assertEquals(10.0, action.amount.toKinValueDouble(), 0.0) - assertEquals(organizer.tray.slots[0].getCluster().vaultPublicKey, action.destination) - } - - (intent.getAction(7) as ActionWithdraw).let { action -> - assertEquals(ActionWithdraw.Kind.NoPrivacyWithdraw(Kin.fromKin(500_000)), action.kind) - assertEquals(destination, action.destination) - } - - (intent.getAction(8) as ActionOpenAccount).let { action -> - assertEquals(AccountType.Outgoing, action.type) - assertEquals(resultTray.owner.getCluster().authority.keyPair.publicKeyBytes.toPublicKey(), action.owner) - assertEquals(resultTray.outgoing.getCluster(), action.accountCluster) - } - - (intent.getAction(9) as ActionWithdraw).let { action -> - assertEquals(ActionWithdraw.Kind.CloseDormantAccount(AccountType.Outgoing), action.kind) - assertEquals(resultTray.outgoing.getCluster(), action.cluster) - assertEquals(organizer.tray.owner.getCluster().vaultPublicKey, action.destination) - } - - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/models/intents/IntentPublicTransferTest.kt b/services/code/src/androidTest/java/com/getcode/models/intents/IntentPublicTransferTest.kt deleted file mode 100644 index 12015975f..000000000 --- a/services/code/src/androidTest/java/com/getcode/models/intents/IntentPublicTransferTest.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.getcode.models.intents - -import android.content.Context -import androidx.test.platform.app.InstrumentationRegistry -import com.getcode.crypt.MnemonicPhrase -import com.getcode.model.CurrencyCode -import com.getcode.model.Kin -import com.getcode.model.KinAmount -import com.getcode.model.fromFiatAmount -import com.getcode.model.intents.IntentPublicTransfer -import com.getcode.model.intents.actions.ActionTransfer -import com.getcode.solana.keys.Key32.Companion.mock -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.SlotType -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test - -class IntentPublicTransferTest { - lateinit var context: Context - - private val mnemonic = MnemonicPhrase.newInstance( - words = "couple divorce usage surprise before range feature source bubble chunk spot away".split( - " " - ) - )!! - - @Before - fun setup() { - context = InstrumentationRegistry.getInstrumentation().context - } - - @Test - fun testSendFiveDollars() { - val organizer = Organizer.newInstance(mnemonic) - - organizer.setBalances( - mapOf(AccountType.Primary to Kin.fromKin(1_000_000)) - ) - - val destination: PublicKey = mock - val amount = KinAmount.fromFiatAmount( - fiat = 10.00, - fx = 0.00001, - currencyCode = CurrencyCode.USD - ) - - val intent = IntentPublicTransfer.newInstance( - organizer = organizer, - destination = IntentPublicTransfer.Destination.External(destination), - amount = amount, - source = AccountType.Primary - ) - - val resultTray = intent.resultTray - - // Ensure outgoing is NOT incremented - assertEquals(resultTray.outgoing.getCluster().index, organizer.tray.outgoing.getCluster().index) - assertEquals(organizer.tray.slotsBalance, intent.resultTray.slotsBalance) - - assertEquals(0.0, resultTray.slot(SlotType.Bucket1).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, resultTray.slot(SlotType.Bucket10).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, resultTray.slot(SlotType.Bucket100).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, resultTray.slot(SlotType.Bucket1k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, resultTray.slot(SlotType.Bucket10k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, resultTray.slot(SlotType.Bucket100k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, resultTray.slot(SlotType.Bucket1m).partialBalance.toKinValueDouble(), 0.0) - - // Ensure all actions have indexes - intent.getActions().forEachIndexed { index, action -> - assertEquals(index, action.id) - } - - assertEquals(1, intent.getActions().size) - - intent.getAction(0).let { action -> - action as ActionTransfer - assertEquals(ActionTransfer.Kind.NoPrivacyTransfer, action.kind) - assertEquals(destination, action.destination) - } - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/models/intents/IntentReceiveTest.kt b/services/code/src/androidTest/java/com/getcode/models/intents/IntentReceiveTest.kt deleted file mode 100644 index 08d82609b..000000000 --- a/services/code/src/androidTest/java/com/getcode/models/intents/IntentReceiveTest.kt +++ /dev/null @@ -1,188 +0,0 @@ -package com.getcode.models.intents - -import android.content.Context -import androidx.test.platform.app.InstrumentationRegistry -import com.getcode.crypt.MnemonicPhrase -import com.getcode.model.Kin -import com.getcode.model.intents.IntentReceive -import com.getcode.model.intents.actions.ActionCloseEmptyAccount -import com.getcode.model.intents.actions.ActionOpenAccount -import com.getcode.model.intents.actions.ActionTransfer -import com.getcode.model.intents.actions.ActionWithdraw -import com.getcode.model.toPublicKey -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.SlotType -import org.junit.Assert -import org.junit.Before -import org.junit.Test - -class IntentReceiveTest { - lateinit var context: Context - - private val mnemonic = MnemonicPhrase.newInstance( - words = "couple divorce usage surprise before range feature source bubble chunk spot away".split( - " " - ) - )!! - - @Before - fun setup() { - context = InstrumentationRegistry.getInstrumentation().context - } - - - @Test - fun testReceiveSevenDollars() { - val organizer = Organizer.newInstance(mnemonic) - val amount = Kin.Companion.fromKin(1_000_000) - - organizer.setBalances( - mapOf(AccountType.Incoming to amount) - ) - - val previousIncoming = organizer.tray.incoming - - val intent = IntentReceive.newInstance( - context = context, - organizer = organizer, - amount = amount - ) - - Assert.assertNotEquals(intent.id, - com.getcode.solana.keys.PublicKey(ByteArray(com.getcode.solana.keys.LENGTH_32).toList()) - ) - - val resultTray = intent.resultTray - - // Ensure incoming is incremented - Assert.assertEquals(organizer.tray.incoming.getCluster().index, resultTray.incoming.getCluster().index - 1) - - // The incoming account has been rotated so we need to ensure - // the previous incoming account has the correct balance and - // the new account is empty - Assert.assertEquals(0.0, resultTray.incoming.partialBalance.toKinValueDouble(), 0.0) - - Assert.assertEquals(1_000_000.0, resultTray.availableBalance.toKinValueDouble(), 0.0) - - Assert.assertEquals(0.0, resultTray.owner.partialBalance.toKinValueDouble(), 0.0) - Assert.assertEquals(0.0, resultTray.incoming.partialBalance.toKinValueDouble(), 0.0) - Assert.assertEquals(0.0, resultTray.outgoing.partialBalance.toKinValueDouble(), 0.0) - - Assert.assertEquals(10.0, resultTray.slot(SlotType.Bucket1).partialBalance.toKinValueDouble(), 0.0) - Assert.assertEquals(90.0, resultTray.slot(SlotType.Bucket10).partialBalance.toKinValueDouble(), 0.0) - Assert.assertEquals(900.0, resultTray.slot(SlotType.Bucket100).partialBalance.toKinValueDouble(), 0.0) - Assert.assertEquals(9_000.0, resultTray.slot(SlotType.Bucket1k).partialBalance.toKinValueDouble(), 0.0) - Assert.assertEquals(90_000.0, resultTray.slot(SlotType.Bucket10k).partialBalance.toKinValueDouble(), 0.0) - Assert.assertEquals(900_000.0, resultTray.slot(SlotType.Bucket100k).partialBalance.toKinValueDouble(), 0.0) - Assert.assertEquals(0.0, resultTray.slot(SlotType.Bucket1m).partialBalance.toKinValueDouble(), 0.0) - - /* - * Expected actions: - * - * K 1000000 (0) -> AB4w6m9nhQaagqpnu6TcsgE1Z34wXKwKSoxU6tGadAfN (tempPrivacyTransfer) - * K 1000000 (0) -> BEZasPLNZ5vsHH3SfdxeWuTD5uXm8pPUmbyrZkPJqQwr (tempPrivacyExchange) - * K 100000 (0) -> 7GpxPmL2sGqRq1ru4nKTPWPRruemat5BCReGmNLNXsRE (tempPrivacyExchange) - * K 10000 (0) -> 6upXkqkiY3GYBqm3wSReuAsaWxQSY1d67GuRxLhM74Va (tempPrivacyExchange) - * K 1000 (0) -> 6eqAKwBqtAQ28juRdc3429GoRpUTuu86gScJrDN6cqGQ (tempPrivacyExchange) - * K 100 (0) -> BbCteP1N7DiyShnEuCRGkNLeoYbr7v1d5deGDBbeu5Zg (tempPrivacyExchange) - * K 10 (0) -> J5fzggJmRyPwmAcJw7iVD9jG4q4xZyeELNEEjfhKxp4i (tempPrivacyExchange) - * Close Empty -> G2JXRCvg2PVXHd9veJ5MCcR38723tcuz52Mtw7uE4QK8 (closeEmptyAccount) - * Open -> 7YHmKkV675HNVMpUeFgWssyQZxxFoFG9gRu6RtR2KEfg (openAccount) - * Close Dormant -> 7YHmKkV675HNVMpUeFgWssyQZxxFoFG9gRu6RtR2KEfg sending to 8DDrALtni72M6FnCiTMToEssMHDEH3KRP1nhA6svQDxp (closeDormantAccount) - */ - - intent.getActions().forEachIndexed { index, action -> - Assert.assertEquals(index, action.id) - } - - Assert.assertEquals(10, intent.getActions().size) - - (intent.getAction(0) as ActionTransfer).let { action -> - Assert.assertEquals(ActionTransfer.Kind.TempPrivacyTransfer, action.kind) - Assert.assertEquals(1_000_000.0, action.amount.toKinValueDouble(), 0.0) - Assert.assertEquals(organizer.tray.incoming.getCluster(), action.source) - Assert.assertEquals( - organizer.tray.slots[6].getCluster().vaultPublicKey, - action.destination - ) - } - - (intent.getAction(1) as ActionTransfer).let { action -> - Assert.assertEquals(ActionTransfer.Kind.TempPrivacyExchange, action.kind) - Assert.assertEquals(1_000_000.0, action.amount.toKinValueDouble(), 0.0) - Assert.assertEquals(organizer.tray.slots[6].getCluster(), action.source) - Assert.assertEquals( - organizer.tray.slots[5].getCluster().vaultPublicKey, - action.destination - ) - } - - (intent.getAction(2) as ActionTransfer).let { action -> - Assert.assertEquals(ActionTransfer.Kind.TempPrivacyExchange, action.kind) - Assert.assertEquals(100_000.0, action.amount.toKinValueDouble(), 0.0) - Assert.assertEquals(organizer.tray.slots[5].getCluster(), action.source) - Assert.assertEquals( - organizer.tray.slots[4].getCluster().vaultPublicKey, - action.destination - ) - } - - (intent.getAction(3) as ActionTransfer).let { action -> - Assert.assertEquals(ActionTransfer.Kind.TempPrivacyExchange, action.kind) - Assert.assertEquals(10_000.0, action.amount.toKinValueDouble(), 0.0) - Assert.assertEquals(organizer.tray.slots[4].getCluster(), action.source) - Assert.assertEquals( - organizer.tray.slots[3].getCluster().vaultPublicKey, - action.destination - ) - } - - (intent.getAction(4) as ActionTransfer).let { action -> - Assert.assertEquals(ActionTransfer.Kind.TempPrivacyExchange, action.kind) - Assert.assertEquals(1_000.0, action.amount.toKinValueDouble(), 0.0) - Assert.assertEquals(organizer.tray.slots[3].getCluster(), action.source) - Assert.assertEquals( - organizer.tray.slots[2].getCluster().vaultPublicKey, - action.destination - ) - } - - (intent.getAction(5) as ActionTransfer).let { action -> - Assert.assertEquals(ActionTransfer.Kind.TempPrivacyExchange, action.kind) - Assert.assertEquals(100.0, action.amount.toKinValueDouble(), 0.0) - Assert.assertEquals(organizer.tray.slots[2].getCluster(), action.source) - Assert.assertEquals( - organizer.tray.slots[1].getCluster().vaultPublicKey, - action.destination - ) - } - - (intent.getAction(6) as ActionTransfer).let { action -> - Assert.assertEquals(ActionTransfer.Kind.TempPrivacyExchange, action.kind) - Assert.assertEquals(10.0, action.amount.toKinValueDouble(), 0.0) - Assert.assertEquals(organizer.tray.slots[1].getCluster(), action.source) - Assert.assertEquals( - organizer.tray.slots[0].getCluster().vaultPublicKey, - action.destination - ) - } - - (intent.getAction(7) as ActionCloseEmptyAccount).let { action -> - Assert.assertEquals(AccountType.Incoming, action.type) - Assert.assertEquals(previousIncoming.getCluster(), action.cluster) - } - - (intent.getAction(8) as ActionOpenAccount).let { action -> - Assert.assertEquals(AccountType.Incoming, action.type) - Assert.assertEquals(resultTray.owner.getCluster().authority.keyPair.publicKeyBytes.toPublicKey(), action.owner) - Assert.assertEquals(resultTray.incoming.getCluster(), action.accountCluster) - } - - (intent.getAction(9) as ActionWithdraw).let { action -> - Assert.assertEquals(ActionWithdraw.Kind.CloseDormantAccount(AccountType.Incoming), action.kind) - Assert.assertEquals(resultTray.incoming.getCluster(), action.cluster) - Assert.assertEquals(resultTray.owner.getCluster().vaultPublicKey, action.destination) - } - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/models/intents/actions/ActionTest.kt b/services/code/src/androidTest/java/com/getcode/models/intents/actions/ActionTest.kt deleted file mode 100644 index 1a669df10..000000000 --- a/services/code/src/androidTest/java/com/getcode/models/intents/actions/ActionTest.kt +++ /dev/null @@ -1,304 +0,0 @@ -package com.getcode.models.intents.actions - -import android.content.Context -import androidx.test.platform.app.InstrumentationRegistry -import com.getcode.model.Kin -import com.getcode.model.intents.ServerParameter -import com.getcode.model.intents.actions.* -import com.getcode.model.toPublicKey -import com.getcode.solana.keys.Key32.Companion.mock -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.SlotType -import com.getcode.utils.decodeBase58 -import org.junit.Assert.* -import org.junit.Before -import org.junit.Test - -class ActionTest { - lateinit var context: Context - lateinit var organizer: Organizer - - private val mnemonic = com.getcode.crypt.MnemonicPhrase.newInstance( - words = "couple divorce usage surprise before range feature source bubble chunk spot away".split( - " " - ) - )!! - - private val nonce = com.getcode.solana.keys.PublicKey.fromBase58(base58 = "JDwJWHij1E75GVAAcMUPkwDgC598wRdF4a7d76QX895S") - private val blockhash = com.getcode.solana.keys.PublicKey.fromBase58(base58 = "BXLEqnSJxMHvEJQHRMSbsFQGDpBn891BpQo828xejbi1") - private val treasury = com.getcode.solana.keys.PublicKey.fromBase58(base58 = "Ddk7k7zMMWsp8fZB12wqbiADdXKQFWfwUUsxSo73JaQ9") - private val recentRoot = com.getcode.solana.keys.PublicKey.fromBase58(base58 = "2sDAFcEZkLd3mbm6SaZhifctkyB4NWsp94GMnfDs1BfR") - - @Before - fun setup() { - context = InstrumentationRegistry.getInstrumentation().context - organizer = Organizer.newInstance(mnemonic) - } - - @Test - fun testSignatureProvidedByCloseDormantAccount() { - val action = ActionWithdraw.newInstance( - kind = ActionWithdraw.Kind.CloseDormantAccount(AccountType.Incoming), - cluster = organizer.tray.outgoing.getCluster(), - destination = mock - ) - - assertEquals(0, action.signatures().size) - - action.serverParameter = basicConfig - - assertEquals(1, action.signatures().size) - } - - @Test - fun testSignatureProvidedByNoPrivacyWithdraw() { - val action = ActionWithdraw.newInstance( - kind = ActionWithdraw.Kind.NoPrivacyWithdraw(Kin.Companion.fromKin(10)), - cluster = organizer.tray.outgoing.getCluster(), - destination = mock - ) - - assertEquals(0, action.signatures().size) - - action.serverParameter = basicConfig - - assertEquals(1, action.signatures().size) - } - - @Test - fun testSignatureProvidedByTempPrivacyTransfer() { - val action = ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyTransfer, - intentId = mock, - amount = Kin.fromKin(1), - source = organizer.tray.cluster(AccountType.Bucket(SlotType.Bucket1)), - destination = mock - ) - - assertEquals(0, action.signatures().size) - - action.serverParameter = ServerParameter( - actionId = 0, - parameter = ServerParameter.Parameter.TempPrivacy( - treasury = treasury, - recentRoot = recentRoot - ), - configs = listOf( - ServerParameter.Config( - nonce = nonce, - blockhash = blockhash - ) - ) - ) - - assertEquals(1, action.signatures().size) - } - - @Test - fun testSignatureProvidedByTempPrivacyExchange() { - val action = ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyExchange, - intentId = mock, - amount = Kin.fromKin(1), - source = organizer.tray.cluster(AccountType.Bucket(SlotType.Bucket1)), - destination = mock - ) - - assertEquals(0, action.signatures().size) - - action.serverParameter = tempPrivacy - - assertEquals(1, action.signatures().size) - } - - @Test - fun testSignatureProvidedByNoPrivacyTransfer() { - val action = ActionTransfer.newInstance( - kind = ActionTransfer.Kind.NoPrivacyTransfer, - intentId = mock, - amount = Kin.fromKin(1), - source = organizer.tray.cluster(AccountType.Bucket(SlotType.Bucket1)), - destination = mock - ) - - assertEquals(0, action.signatures().size) - - action.serverParameter = tempPrivacy - - assertEquals(1, action.signatures().size) - } - - @Test - fun testSignatureProvidedByClosedEmptyAccount() { - val action = ActionCloseEmptyAccount.newInstance( - type = AccountType.Incoming, - cluster = organizer.tray.incoming.getCluster() - ) - - assertEquals(0, action.signatures().size) - - action.serverParameter = basicConfig - - assertEquals(1, action.signatures().size) - } - - @Test - fun testSignatureProvidedByPrivacyUpgrade() { - val action = ActionPrivacyUpgrade.newInstance( - source = organizer.tray.incoming.getCluster(), - originalActionID = 0, - originalCommitmentStateAccount = leaf, - originalAmount = Kin.fromKin(1), - originalNonce = nonce, - originalRecentBlockhash = blockhash, - treasury = treasury - ) - - assertThrows(ActionPrivacyUpgradeException.MissingServerParameterException::class.java) { - action.signatures().isEmpty() - } - - action.serverParameter = privacyUpgrade - // Only validates that signatures are provided - // and merkle proof is valid but doesn't verify - // any other server parameters - - assertEquals(action.signatures().size, 1) - } - - - @Test - fun testNoSignatureProvidedByOpenAccount() { - val action = ActionOpenAccount.newInstance( - owner = organizer.ownerKeyPair.publicKeyBytes.toPublicKey(), - type = AccountType.Outgoing, - accountCluster = organizer.tray.outgoing.getCluster() - ) - - assertEquals(0, action.signatures().size) - - action.serverParameter = basicConfig - - assertEquals(0, action.signatures().size) - } - - private val basicConfig: ServerParameter = ServerParameter( - actionId = 0, - parameter = null, - configs = listOf( - ServerParameter.Config( - nonce = com.getcode.solana.keys.PublicKey.fromBase58(base58 = "JDwJWHij1E75GVAAcMUPkwDgC598wRdF4a7d76QX895S"), - blockhash = com.getcode.solana.keys.PublicKey.fromBase58(base58 = "BXLEqnSJxMHvEJQHRMSbsFQGDpBn891BpQo828xejbi1") - ) - ) - ) - - private val tempPrivacy: ServerParameter = ServerParameter( - actionId = 0, - parameter = ServerParameter.Parameter.TempPrivacy( - treasury = com.getcode.solana.keys.PublicKey.fromBase58(base58 = "Ddk7k7zMMWsp8fZB12wqbiADdXKQFWfwUUsxSo73JaQ9"), - recentRoot = com.getcode.solana.keys.PublicKey.fromBase58(base58 = "2sDAFcEZkLd3mbm6SaZhifctkyB4NWsp94GMnfDs1BfR") - ), - configs = listOf( - ServerParameter.Config( - nonce = com.getcode.solana.keys.PublicKey.fromBase58(base58 = "JDwJWHij1E75GVAAcMUPkwDgC598wRdF4a7d76QX895S"), - blockhash = com.getcode.solana.keys.PublicKey.fromBase58(base58 = "BXLEqnSJxMHvEJQHRMSbsFQGDpBn891BpQo828xejbi1") - ) - ) - ) - - private val leaf = com.getcode.solana.keys.PublicKey( - "2ocuvgy8ETZp9WDaEy4rpYz2QyeZ7JAiEvXKbW5rKcd4".decodeBase58().toList() - ) - private val root = com.getcode.solana.keys.Hash( - "9EuLAJgnMpEq8wmQUFTNxgYJG2FkAPAGCUhrNK447Uox".decodeBase58().toList() - ) - private val proof = listOf( - "4DEt3CHLarXBy74hiJf5t74HmKfTw5DeLK2nzTLFv3Pq", - "73uNXKLpHkTgc9ubvyRXTGaNUh19TUx8M9bN4PNTn544", - "2QH34Bqm89sadRqpz1U5M3Cd34xxNLnTHdxzn4LA3EKU", - "AaySpzaCsgyTVVgUA9bNTNC6sGsws7sTYNyz2oFAe1gT", - "BFHDwqjAPupY4PoJn5Lvx7t9mQrXy6iGnTS7NuRyrEav", - "BFy4XgE4j8NW5PxzDMkH7FWXZcMuFb9zoVSBqdjxm21A", - "B5Lvj9Zdrynu2DGjYXGmxKxRbvmVFtYoGLyqrzZFymXo", - "GEspB8aMfyV4Hmtt7fGmFXsZ5QWrbBUSfPeDT3dXS7gG", - "FuVPGmTwWZayoWt4th2dv8X9xEmRrLvqTbdAXXfRi6Ei", - "CbTZ7BrcBUsmGEjUqxrjkvDkKNRRJHrjF9tTb3mLmMWb", - "GEoigbUN6rsrrpRdNi5rJgX2YDXmE6gDsLYevSchzcg4", - "Gb2zXSV9vxhPkem6PrW45rPiEy9dbJ9nFg7ixQEV4JYh", - "Bb2r8JJdExSAasR38yuTJu2XHRZEGHRxCR71MrJ6nW1z", - "5zTGsTA9vmzGYwVYeD3MDcehybca93prZRdjVqRzZQ6y", - "BbH8JeD3emXYkNw3DvLERM3hMPXhgCEqcU132hSo2uH7", - "F9re8k2sX2BVGX8WqRBGyiZ2aPvvRj4s62jmtgM73hmT", - "DGFU6XD6eYi3GtVAwYBP4d2DUYv1BGiquijQH6HXLLi4", - "8TaNzgiEAP4VoXkBjb1toiZ9fw84RhqezdYt3RhNXR3u", - "BnF8qb2kYZxFHtmqWircb1Di33XTQc8TV17oFwi1tZ4u", - "BBNfGrQ7cKBcZgQmqCgw45s9QLkx41qcTjYwrn7tAtoM", - "Dkf4Fpukx558idi6XwnEx9aAu8GLDzYUC3eN7hQQxPsJ", - "72BxCoqc9cnQvqEZmqzLcZH7VyMBjJFj3R47D8gpV8WL", - "BSVw2t3RwN4ab9Zpd68pwLqwHVecgHvacZB28QgNQ3L", - "7YvGe21SSF93mZoyPsgVF6dD78YWCkVwSff9a4EdE3aj", - "9wZwjy3V8827XZeeE4CxZgXU5WsRGCfcRYHkaPtB7QGb", - "GQb7NMsiEfwVWxuLgn7Tev1KEZSs4ASayUiULjACtxNv", - "Wpb4nF5rc9GpSbWXPUNwA31bemJp61HexerhUx97H8B", - "HwXMPKHXQoBQkM533yqatDbaY3HLDapUWVGVWUv6366a", - "Wom44oATBqSD7SZpBwHRmkXhKV4qsC7SnneGTLKhvdN", - "3xmn2hQdDSKN1ompFNh6AwnQBucWK7Z8mJPyXJzTpLb4", - "HQ5WDTtCvL14aa16UZJStZVVCTcoYbiUayzzBFm8e97r", - "Af6F2xzEKyjuiy7wjukatK9BzW42K7vXekkZq9C7W3K", - "ECS1Mcvt2pYJxYkMDNAp4sNjQq4SadsL3KeJx59ATLbo", - "5B2C3uLH4TvrrriZgo5UbfQwDVbeqqtd7NPwaujfipAd", - "DLd8jX9r1o57SaBgnqzextfBHb7aSGdL98t3EzhiKXjC", - "2cz8R5HYZXs5PKhXXXr562go49A4d5gor42Khejz8K2A", - "B8ucJEMrosxPoSnnBgMbkcmsGWoqusaaheATT8UFa7AF", - "D7JCQL3FMGkoqfvvP9TBzzirTeNQDHbUYAxA9Di6pkQm", - "7fiWLao84havAULW9y5mRDpAqCDSFp2hRmDfN7xQWdWk", - "9SCyBETC2xV1yk44voyQ7MED4SSURpNsuu6MNY12KndN", - "FQKnR4ngeSet5UfWyzXsf7RFU4QN83T9C5xSJWcHcZVX", - "DzN3txNccgTDG58PWfgkx9wBuVPShTp1VTrPyHkvnbpq", - "4cCa2Zen5gn83AJmAgn3mZE3NodVaYaMZM4UykQcTXyy", - "7XDMh3UsVHVomz4MmsSPcKtEkSwxGA4S5ypUb5t2Dvmv", - "6VoqDc2CWeg158GDuQTeerh1VWHRFbcjrKo2VeiiXAD7", - "5uGA5QbthCBe5QiY3BwaqfnwwfJgVahZc8WHPFXwBV2m", - "AAV7TTewgfmN6FHW3oV3ad3Q6KKcdbP6ijt7SUThD2bN", - "AD2GHkMgEmtRFWi6HLpZeHKAeRAZUnGPF7h4mVbeUj7o", - "By3ScwWYWdZAcFXo6V68cRH6kSbdqtUMgNVdnSNevgb5", - "C2RCxm7ZSd9A2fXEvJguBYNK6oUdVrMSCHB3k2JxBKek", - "FHvwmz1bJqm6SfeBLriNhSh8wuwEJc4KDxi2PpVfQtqg", - "R29HU9mCjih74wRWLbW3nXLcUAbEJAqKUrpXENSbziS", - "AvNJa3vqawjfrGmqKPybWQa4V42uyiigUgchT4gHt5Gf", - "CozYPv4cdVeRb5QWCrtuVJk3f2rHJeAFD9ZtQjHLqyyW", - "BeLz3Yqv2ikvQkmH2mxXYGHgvo93Ns77hStxSjuA8UsZ", - "H4tEGNHHcCLbA963wtzJxsZVuEdrpQwrBBnmWcBAhChs", - "CUmSAsfdvnJTUkWLA8chums2ffyveiRNkNVu3t6as6N7", - "62djLqbyFJz3iJiY4NkWvio46dMfP291cWfZ44wdeDgE", - "4JjYv9v3z2YoBrMCsmXE6abpm5bNTKwuJp99rjoTdmPF", - "JB7wk6DDiYKjzasvHPpySd2aCjh1UX5adD5eZgazwEUM", - "3fCTfZwwMipFUpvbXbcDBBtwuo23hmuTJMYaVECWwNTP", - "CZ94cA7JHBb4a8mqN9xEJquPNX1TxKqL3cBQms6yfgTr", - "8PxPosHkG5Q6VBnhiimJaH88yPYt6ZDp4szMBfaVTLun", - ).map { com.getcode.solana.keys.PublicKey(it.decodeBase58().toList()) } - - val privacyUpgrade = ServerParameter( - actionId = 0, - parameter = ServerParameter.Parameter.PermanentPrivacyUpgrade( - newCommitment = mock, - newCommitmentTranscript = mock, - newCommitmentDestination = mock, - newCommitmentAmount = Kin.fromKin(1), - merkleRoot = root, - merkleProof = proof - ), - configs = listOf( - ServerParameter.Config( - nonce = com.getcode.solana.keys.PublicKey( - "JDwJWHij1E75GVAAcMUPkwDgC598wRdF4a7d76QX895S".decodeBase58().toList() - ), - blockhash = com.getcode.solana.keys.PublicKey( - "BXLEqnSJxMHvEJQHRMSbsFQGDpBn891BpQo828xejbi1".decodeBase58().toList() - ) - ) - ) - ) -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/solana/builder/TransactionBuilder_Test.kt b/services/code/src/androidTest/java/com/getcode/solana/builder/TransactionBuilder_Test.kt deleted file mode 100644 index 20b13360e..000000000 --- a/services/code/src/androidTest/java/com/getcode/solana/builder/TransactionBuilder_Test.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.getcode.solana.builder - -import com.getcode.mocks.SolanaTransaction -import com.getcode.model.Kin -import com.getcode.model.extensions.newInstance -import com.getcode.model.intents.actions.ActionType.Companion.kreIndex -import com.getcode.utils.decodeBase58 -import junit.framework.Assert -import org.junit.Test - -class TransactionBuilder_Test { - - @Test - fun testCloseDormantAccountTransaction() { - val (transaction, _) = SolanaTransaction.mockCloseDormantAccount() - - val authority = - com.getcode.solana.keys.PublicKey( - "Ed3GWPEdMiRXDMf7jU46fRwBF7n6ZZFGN3vH1dYAgME2".decodeBase58().toList() - ) - val destination = - com.getcode.solana.keys.PublicKey( - "GEaVZeZ52Jn8xHPy4VKaXsHQ34E6pwfJGuYh8EsYQi6M".decodeBase58().toList() - ) - val nonce = - com.getcode.solana.keys.PublicKey( - "27aoaJKNVtqKXRKQeMdKrtPMqAzcyYH5PGEgQ8x88TMH".decodeBase58().toList() - ) - val blockhash = - com.getcode.solana.keys.PublicKey( - "7mezFVdzzwHfAxXCDo1gSdRTZE8WwQP9sHbAnPjS3AJD".decodeBase58().toList() - ) - - val derivedAccounts = com.getcode.solana.keys.TimelockDerivedAccounts.newInstance(authority) - - val builtTransaction = TransactionBuilder.closeDormantAccount( - authority = authority, - timelockDerivedAccounts = derivedAccounts, - destination = destination, - nonce = nonce, - recentBlockhash = blockhash, - kreIndex = kreIndex - ) - - // Remove the signatures before comparison - val transactionNoSignatures = - com.getcode.solana.SolanaTransaction( - transaction.message, - listOf(com.getcode.solana.keys.Signature.zero, com.getcode.solana.keys.Signature.zero) - ) - - Assert.assertEquals(transactionNoSignatures.encode(), builtTransaction.encode()) - } - - @Test - fun testTransferTransaction() { - val (transaction, _) = SolanaTransaction.mockPrivateTransfer() - - val authority = - com.getcode.solana.keys.PublicKey( - "Ddk7k7zMMWsp8fZB12wqbiADdXKQFWfwUUsxSo73JaQ9".decodeBase58().toList() - ) - val destination = - com.getcode.solana.keys.PublicKey( - "2sDAFcEZkLd3mbm6SaZhifctkyB4NWsp94GMnfDs1BfR".decodeBase58().toList() - ) - val nonce = - com.getcode.solana.keys.PublicKey( - "H7y8REaqickypzCfke3onJVKbbp8ELmaccFYeLZzJ2Wn".decodeBase58().toList() - ) - val blockhash = - com.getcode.solana.keys.PublicKey( - "HjD8boPVb9pBVMQBdSzUMTt1HKTonwPsC3RibtXw44pK".decodeBase58().toList() - ) - - val derivedAccounts = com.getcode.solana.keys.TimelockDerivedAccounts.newInstance(owner = authority) - - val builtTransaction = TransactionBuilder.transfer( - timelockDerivedAccounts = derivedAccounts, - destination = destination, - amount = Kin.Companion.fromKin(2), - nonce = nonce, - recentBlockhash = blockhash, - kreIndex = kreIndex - ) - - // Remove the signatures before comparison - val transactionNoSignatures = - com.getcode.solana.SolanaTransaction( - transaction.message, - listOf(com.getcode.solana.keys.Signature.zero, com.getcode.solana.keys.Signature.zero) - ) - - Assert.assertEquals(transactionNoSignatures.encode(), builtTransaction.encode()) - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/solana/encoding/AgoraMemo_Test.kt b/services/code/src/androidTest/java/com/getcode/solana/encoding/AgoraMemo_Test.kt deleted file mode 100644 index 3e80c04bf..000000000 --- a/services/code/src/androidTest/java/com/getcode/solana/encoding/AgoraMemo_Test.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.getcode.solana.encoding - -import com.getcode.solana.AgoraMemo -import com.getcode.solana.MagicByte.Companion.default -import com.getcode.solana.TransferType -import junit.framework.Assert.assertEquals -import org.junit.Test -import org.kin.sdk.base.tools.toByteArray -import java.util.* - -class AgoraMemo_Test { - @Test - fun testEncodingSpecificData() { - val memo = AgoraMemo( - magicByte = default, - version = 2, - transferType = TransferType.p2p, - appIndex = 10, - bytes = listOf(0xAE.toByte(), 0xFD.toByte()) - ) - val decoded = AgoraMemo.newInstance(memo.encode().toList()) - assertEquals(memo.bytes, decoded.bytes) - } - - @Test - fun testEncodingValidDataLessThanMax() { - val memo = AgoraMemo( - magicByte = default, - version = 2, - transferType = TransferType.p2p, - appIndex = 10, - bytes = UUID.randomUUID().toByteArray().toList() - ) - val decoded = AgoraMemo.newInstance(memo.encode().toList()) - assertEquals(memo.bytes, decoded.bytes) - } - - - @Test - fun testEncodingValidDataLargerThanMax() { - val memo = AgoraMemo( - magicByte = default, - version = 2, - transferType = TransferType.p2p, - appIndex = 10, - bytes = UUID.randomUUID().toByteArray().toList() + UUID.randomUUID().toByteArray() - .toList() - ) - val decoded = AgoraMemo.newInstance(memo.encode().toList()) - assertEquals(memo.bytes, decoded.bytes.subList(0, 28)) - } - - - @Test - fun testEncodingappIndexValidRange() { - val appIndexes = listOf(0, 10_000, 65_535) - val foreignKeyBytes = - UUID.randomUUID().toByteArray().toList() + UUID.randomUUID().toByteArray().toList() - - appIndexes.forEach { appIndex -> - val memo = AgoraMemo( - magicByte = default, - version = 7, - transferType = TransferType.p2p, - appIndex = appIndex, - bytes = foreignKeyBytes - ) - val decoded = AgoraMemo.newInstance(memo.encode().toList()) - assertEquals(memo.bytes, decoded.bytes.subList(0, 28)) - - assertEquals(decoded.magicByte, default) - assertEquals(decoded.version, 7) - assertEquals(decoded.transferType, TransferType.p2p) - assertEquals(decoded.appIndex, appIndex) - assertEquals(decoded.bytes, memo.bytes) - } - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/solana/encoding/MessageEncoding_Test.kt b/services/code/src/androidTest/java/com/getcode/solana/encoding/MessageEncoding_Test.kt deleted file mode 100644 index ed7a6d651..000000000 --- a/services/code/src/androidTest/java/com/getcode/solana/encoding/MessageEncoding_Test.kt +++ /dev/null @@ -1,169 +0,0 @@ -package com.getcode.solana.encoding - -import com.getcode.solana.Message -import com.getcode.solana.MessageHeader -import com.getcode.utils.toByteList -import junit.framework.Assert.assertEquals -import org.junit.Test - -class MessageEncoding_Test { - @Test - fun messageEncoding() { - val headers = listOf( - MessageHeader(3, 1, 4) - ) - val hashes = listOf( - com.getcode.solana.keys.Hash( - listOf( - 101, 116, 70, 63, 191, 201, 150, 31, 115, 132, 250, 155, 18, 171, 135, 213, - 132, 223, 128, 23, 142, 119, 126, 47, 204, 233, 8, 136, 95, 8, 43, 153 - ).toByteList() - ) - ) - - val accounts = listOf( - listOf( - com.getcode.solana.keys.AccountMeta.payer( - com.getcode.solana.keys.PublicKey( - listOf( - 9, 44, 14, 22, 184, 170, 97, 239, 12, 185, 70, 65, 119, 118, 114, - 54, 255, 60, 52, 123, 82, 133, 164, 46, 40, 205, 154, 124, 39, 59, - 89, 238 - ).toByteList() - ) - ), - com.getcode.solana.keys.AccountMeta.writable( - com.getcode.solana.keys.PublicKey( - listOf( - 11, 188, 11, 147, 54, 247, 148, 189, 25, 169, 205, 216, 208, 118, - 234, 39, 168, 247, 199, 188, 172, 150, 159, 153, 178, 201, 247, 3, - 224, 207, 185, 87 - ).toByteList() - ), - signer = true - ), - com.getcode.solana.keys.AccountMeta.readonly( - com.getcode.solana.keys.PublicKey( - listOf( - 57, 88, 174, 113, 57, 253, 24, 181, 197, 67, 143, 220, 53, 132, 20, - 11, 230, 5, 94, 238, 153, 242, 28, 230, 123, 135, 54, 69, 200, 188, - 103, 6 - ).toByteList() - ), - signer = true - ), - com.getcode.solana.keys.AccountMeta.writable( - com.getcode.solana.keys.PublicKey( - listOf( - 188, 72, 197, 118, 240, 244, 192, 109, 13, 22, 88, 163, 208, 174, - 63, 174, 11, 201, 26, 143, 48, 48, 193, 21, 163, 215, 115, 44, 201, - 210, 236, 159 - ).toByteList() - ), - signer = false - ), - com.getcode.solana.keys.AccountMeta.writable( - com.getcode.solana.keys.PublicKey( - listOf( - 249, 177, 29, 97, 248, 176, 59, 124, 27, 171, 138, 129, 191, 135, - 7, 35, 191, 253, 252, 164, 96, 202, 198, 204, 68, 206, 100, 147, 68, - 207, 164, 239 - ).toByteList() - ), - signer = false - ), - com.getcode.solana.keys.AccountMeta.readonly( - com.getcode.solana.keys.PublicKey( - listOf( - 6, 167, 213, 23, 25, 44, 86, 142, 224, 138, 132, 95, 115, 210, 151, - 136, 207, 3, 92, 49, 69, 178, 26, 179, 68, 216, 6, 46, 169, 64, 0, 0 - ).toByteList() - ), - signer = false - ), - com.getcode.solana.keys.AccountMeta.readonly( - com.getcode.solana.keys.PublicKey( - listOf( - 5, 74, 83, 80, 248, 93, 200, 130, 214, 20, 165, 86, 114, 120, 138, - 41, 109, 223, 30, 171, 171, 208, 166, 6, 120, 136, 73, 50, 244, 238, - 246, 160 - ).toByteList() - ), - signer = false - ), - com.getcode.solana.keys.AccountMeta.readonly( - com.getcode.solana.keys.PublicKey( - listOf( - 6, 221, 246, 225, 215, 101, 161, 147, 217, 203, 225, 70, 206, 235, - 121, 172, 28, 180, 133, 237, 95, 91, 55, 145, 58, 140, 245, 133, - 126, 255, 0, 169 - ).toByteList() - ), - signer = false - ), - com.getcode.solana.keys.AccountMeta.readonly( - com.getcode.solana.keys.PublicKey( - listOf( - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ).toByteList() - ), - signer = false - ), - ) - ) - - val inputs = listOf( - listOf( - 3, 1, 4, 9, 9, 44, 14, 22, 184, 170, 97, 239, 12, 185, 70, 65, 119, 118, 114, 54, - 255, 60, 52, 123, 82, 133, 164, 46, 40, 205, 154, 124, 39, 59, 89, 238, 11, 188, - 11, 147, 54, 247, 148, 189, 25, 169, 205, 216, 208, 118, 234, 39, 168, 247, 199, - 188, 172, 150, 159, 153, 178, 201, 247, 3, 224, 207, 185, 87, 57, 88, 174, 113, - 57, 253, 24, 181, 197, 67, 143, 220, 53, 132, 20, 11, 230, 5, 94, 238, 153, 242, - 28, 230, 123, 135, 54, 69, 200, 188, 103, 6, 188, 72, 197, 118, 240, 244, 192, 109, - 13, 22, 88, 163, 208, 174, 63, 174, 11, 201, 26, 143, 48, 48, 193, 21, 163, 215, - 115, 44, 201, 210, 236, 159, 249, 177, 29, 97, 248, 176, 59, 124, 27, 171, 138, 129, - 191, 135, 7, 35, 191, 253, 252, 164, 96, 202, 198, 204, 68, 206, 100, 147, 68, 207, - 164, 239, 6, 167, 213, 23, 25, 44, 86, 142, 224, 138, 132, 95, 115, 210, 151, 136, - 207, 3, 92, 49, 69, 178, 26, 179, 68, 216, 6, 46, 169, 64, 0, 0, 5, 74, 83, 80, 248, - 93, 200, 130, 214, 20, 165, 86, 114, 120, 138, 41, 109, 223, 30, 171, 171, 208, 166, - 6, 120, 136, 73, 50, 244, 238, 246, 160, 6, 221, 246, 225, 215, 101, 161, 147, 217, - 203, 225, 70, 206, 235, 121, 172, 28, 180, 133, 237, 95, 91, 55, 145, 58, 140, 245, - 133, 126, 255, 0, 169, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 101, 116, 70, 63, 191, 201, 150, 31, 115, 132, - 250, 155, 18, 171, 135, 213, 132, 223, 128, 23, 142, 119, 126, 47, 204, 233, 8, 136, - 95, 8, 43, 153, 3, 8, 3, 1, 5, 0, 4, 4, 0, 0, 0, 6, 0, 44, 90, 84, 65, 69, 65, 65, - 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, - 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 61, 7, 3, 4, 3, 2, - 9, 3, 64, 75, 76, 0, 0, 0, 0, 0 - ).toByteList(), - ) - - // -------------------------------------------------------- - - val messages = - listOf( - Message.newInstance(inputs[0]) - ) - - // -------------------------------------------------------- - headers.forEachIndexed { i, header -> - assertEquals(header.requiredSignatures, messages[i]?.header?.requiredSignatures) - assertEquals(header.readOnlySigners, messages[i]?.header?.readOnlySigners) - assertEquals(header.readOnly, messages[i]?.header?.readOnly) - } - hashes.forEachIndexed { i, hash -> - assertEquals(hash.bytes, messages[i]?.recentBlockhash?.bytes) - } - - accounts.forEachIndexed { i1, account -> - account.forEachIndexed { i2, item -> - assertEquals(item.publicKey.bytes, messages[i1]?.accounts?.get(i2)?.publicKey?.bytes) - assertEquals(item.isSigner, messages[i1]?.accounts?.get(i2)?.isSigner) - assertEquals(item.isWritable, messages[i1]?.accounts?.get(i2)?.isWritable) - assertEquals(item.isPayer, messages[i1]?.accounts?.get(i2)?.isPayer) - assertEquals(item.isProgram, messages[i1]?.accounts?.get(i2)?.isProgram) - } - } - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/solana/encoding/Message_Test.kt b/services/code/src/androidTest/java/com/getcode/solana/encoding/Message_Test.kt deleted file mode 100644 index 07f5bb5dc..000000000 --- a/services/code/src/androidTest/java/com/getcode/solana/encoding/Message_Test.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.getcode.solana.encoding - - -import com.getcode.ed25519.Ed25519 -import com.getcode.solana.keys.AccountMeta -import com.getcode.solana.Instruction -import com.getcode.solana.Message -import com.getcode.solana.MessageHeader -import junit.framework.Assert -import org.junit.Test - -class Message_Test { - @Test - fun testMessageHeader() { - val header = MessageHeader( - requiredSignatures = 2, - readOnlySigners = 1, - readOnly = 3 - ) - val data = header.encode() - val decodedHeader = MessageHeader.fromList(data.toList()) - Assert.assertEquals(2, decodedHeader.requiredSignatures) - Assert.assertEquals(1, decodedHeader.readOnlySigners) - Assert.assertEquals(3, decodedHeader.readOnly) - } - - - @Test - fun testMessageEncodeDecodeCycle() { - fun randomPublicKey() = - com.getcode.solana.keys.PublicKey(Ed25519.createKeyPair().publicKeyBytes.toList()) - - val program = randomPublicKey() - val program2 = randomPublicKey() - - val accounts = listOf( - AccountMeta.payer(publicKey = randomPublicKey()), - AccountMeta.writable(publicKey = randomPublicKey()), - AccountMeta.readonly(publicKey = randomPublicKey()) - ) - - val accounts2 = listOf( - AccountMeta.writable(publicKey = randomPublicKey()), - AccountMeta.readonly(publicKey = randomPublicKey()), - AccountMeta.writable(publicKey = randomPublicKey()), - AccountMeta.readonly(publicKey = randomPublicKey()) - ) - - val instructions = listOf( - Instruction( - program = program, - accounts = accounts, - data = listOf(85, 73, 81, 94, 90, 23, 54, 12) - ), - Instruction( - program = program2, - accounts = accounts2, - data = listOf(81, 77, 95, 71, 86, 13, 34, 17) - ), - ) - - val blockhash = com.getcode.solana.keys.Hash(randomPublicKey().bytes) - - val allAccounts = mutableListOf( - AccountMeta.readonly(program), - AccountMeta.readonly(program2) - ) - allAccounts.addAll(accounts) - allAccounts.addAll(accounts2) - - - val message = Message.newInstance( - accounts = allAccounts, - recentBlockhash = blockhash, - instructions = instructions - ) - - val data = message.encode() - val decodedMessage = Message.newInstance(data.toList()) - - Assert.assertEquals(message.header, decodedMessage?.header) - Assert.assertEquals(allAccounts.sorted(), decodedMessage?.accounts) - Assert.assertEquals(blockhash, decodedMessage?.recentBlockhash) - Assert.assertEquals(instructions, decodedMessage?.instructions) - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/solana/encoding/ShortVec_Test.kt b/services/code/src/androidTest/java/com/getcode/solana/encoding/ShortVec_Test.kt deleted file mode 100644 index 1581940f2..000000000 --- a/services/code/src/androidTest/java/com/getcode/solana/encoding/ShortVec_Test.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.getcode.solana.encoding - -import com.getcode.solana.ShortVec -import org.junit.Assert -import org.junit.Test - -class ShortVec_Test { - @Test - fun testEncode() { - var encoded = ShortVec.encodeLen(0) - Assert.assertEquals(listOf(0), encoded) - - encoded = ShortVec.encodeLen(5) - Assert.assertEquals(listOf(5), encoded) - - encoded = ShortVec.encodeLen(0x7f) - Assert.assertEquals(listOf(0x7f), encoded) - - encoded = ShortVec.encodeLen(0x80) - Assert.assertEquals(listOf(0x80.toByte(), 0x01), encoded) - - encoded = ShortVec.encodeLen(0xff) - Assert.assertEquals(listOf(0xff.toByte(), 0x01), encoded) - - encoded = ShortVec.encodeLen(0x100) - Assert.assertEquals(listOf(0x80.toByte(), 0x02), encoded) - - encoded = ShortVec.encodeLen(0x7fff) - Assert.assertEquals(listOf(0xff.toByte(), 0xff.toByte(), 0x01), encoded) - } - - @Test - fun testEncodeComponents() { - val components = listOf( - listOf(1, 2, 3, 4), - listOf(5, 6, 7, 8), - listOf(9, 8, 7, 6), - listOf(4, 3, 2, 1), - ) - - val data = ShortVec.encodeList(components) - - Assert.assertEquals(17, data.size) - Assert.assertEquals(4, data[0].toInt()) - - val (length, remaining) = ShortVec.decodeLen(data) - - Assert.assertEquals(4, length) - Assert.assertEquals(components[0], remaining.subList(0, 4)) - Assert.assertEquals(components[1], remaining.subList(4, 8)) - Assert.assertEquals(components[2], remaining.subList(8, 12)) - Assert.assertEquals(components[3], remaining.subList(12, 16)) - } - - @Test - fun testDecode() { - var decoded = ShortVec.decodeLen(listOf(0)) - Assert.assertEquals(0, decoded.first) - - decoded = ShortVec.decodeLen(listOf(5)) - Assert.assertEquals(5, decoded.first) - - decoded = ShortVec.decodeLen(listOf(0x7f)) - Assert.assertEquals(0x7f, decoded.first) - - decoded = ShortVec.decodeLen(listOf(0x80.toByte(), 0x01)) - Assert.assertEquals(0x80, decoded.first) - - decoded = ShortVec.decodeLen(listOf(0xff.toByte(), 0x01)) - Assert.assertEquals(0xff, decoded.first) - - decoded = ShortVec.decodeLen(listOf(0x80.toByte(), 0x02)) - Assert.assertEquals(0x100, decoded.first) - - decoded = ShortVec.decodeLen(listOf(0xff.toByte(), 0xff.toByte(), 0x01)) - Assert.assertEquals(0x7fff, decoded.first) - - decoded = ShortVec.decodeLen(listOf(0x80.toByte(), 0x80.toByte(), 0x80.toByte(), 0x01)) - Assert.assertEquals(0x200000, decoded.first) - } - - @Test - fun testValidity() { - for (i in 0 until 255) { - val input = ShortVec.encodeLen(i) - val actual = ShortVec.decodeLen(input) - Assert.assertEquals(actual.first, i) - } - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/solana/encoding/ShortVec_Test2.kt b/services/code/src/androidTest/java/com/getcode/solana/encoding/ShortVec_Test2.kt deleted file mode 100644 index 0c4c315ce..000000000 --- a/services/code/src/androidTest/java/com/getcode/solana/encoding/ShortVec_Test2.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.getcode.solana.encoding - -import com.getcode.solana.ShortVec -import com.getcode.utils.toByteList -import junit.framework.Assert.assertEquals -import org.junit.Test - -class ShortVecTest { - @Test - fun testEncode() { - var encoded = ShortVec.encodeLen(0) - assertEquals(listOf(0).toByteList(), encoded) - - encoded = ShortVec.encodeLen(5) - assertEquals(listOf(5).toByteList(), encoded) - - encoded = ShortVec.encodeLen(0x7f) - assertEquals(listOf(0x7f).toByteList(), encoded) - - encoded = ShortVec.encodeLen(0x80) - assertEquals(listOf(0x80, 0x01).toByteList(), encoded) - - encoded = ShortVec.encodeLen(0xff) - assertEquals(listOf(0xff, 0x01).toByteList(), encoded) - - encoded = ShortVec.encodeLen(0x100) - assertEquals(listOf(0x80, 0x02).toByteList(), encoded) - - encoded = ShortVec.encodeLen(0x7fff) - assertEquals(listOf(0xff, 0xff, 0x01).toByteList(), encoded) - } - - @Test - fun testEncodeComponents() { - val components = listOf( - listOf(1, 2, 3, 4).toByteList(), - listOf(5, 6, 7, 8).toByteList(), - listOf(9, 8, 7, 6).toByteList(), - listOf(4, 3, 2, 1).toByteList(), - ) - - val data = ShortVec.encodeList(components) - - assertEquals(17, data.size) - assertEquals(4, data[0]) - - val (length, remaining) = ShortVec.decodeLen(data) - - assertEquals(length, 4) - assertEquals(components[0], remaining.subList(0, 4)) - assertEquals(components[1], remaining.subList(4, 8)) - assertEquals(components[2], remaining.subList(8, 12)) - assertEquals(components[3], remaining.subList(12, 16)) - } - - @Test - fun testDecode() { - var decoded = ShortVec.decodeLen(listOf(0)).first - assertEquals(0, decoded) - - decoded = ShortVec.decodeLen(listOf(5).toByteList()).first - assertEquals(5, decoded) - - decoded = ShortVec.decodeLen(listOf(0x7f).toByteList()).first - assertEquals(0x7f, decoded) - - decoded = ShortVec.decodeLen(listOf(0x80, 0x01).toByteList()).first - assertEquals(0x80, decoded) - - decoded = ShortVec.decodeLen(listOf(0xff, 0x01).toByteList()).first - assertEquals(0xff, decoded) - - decoded = ShortVec.decodeLen(listOf(0x80, 0x02).toByteList()).first - assertEquals(0x100, decoded) - - decoded = ShortVec.decodeLen(listOf(0xff, 0xff, 0x01).toByteList()).first - assertEquals(0x7fff, decoded) - - decoded = ShortVec.decodeLen(listOf(0x80, 0x80, 0x80, 0x01).toByteList()).first - assertEquals(0x200000, decoded) - } - - @Test - fun testValidity() { - for (i in 0..1000) { - val input = ShortVec.encodeLen(i) - val actual = ShortVec.decodeLen(input) - assertEquals(i, actual.first) - } - } - - @Test - fun testCrossImplementation() { - listOf( - Pair(0x0, listOf(0x0).toByteList()), - Pair(0x7f, listOf(0x7f).toByteList()), - Pair(0x80, listOf(0x80, 0x01).toByteList()), - Pair(0xff, listOf(0xff, 0x01).toByteList()), - Pair(0x100, listOf(0x80, 0x02).toByteList()), - Pair(0x7fff, listOf(0xff, 0xff, 0x01).toByteList()), - Pair(0xffff, listOf(0xff, 0xff, 0x03).toByteList()), - ).forEach { item -> - val output = ShortVec.encodeLen(item.first) - assertEquals(item.second.size, output.size) - assertEquals(item.second, output) - } - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/SwapValidatorTests.kt b/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/SwapValidatorTests.kt deleted file mode 100644 index 09d8309a0..000000000 --- a/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/SwapValidatorTests.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.mocks.SolanaTransaction -import com.getcode.solana.keys.base58 -import com.getcode.utils.DataSlice.byteToUnsignedInt -import org.junit.Assert -import org.junit.Test - -class SwapValidatorTests { - @Test - fun testDecodePreSwap() { - val (transaction, _) = SolanaTransaction.mockSwapValidatorTransaction() - val rawInstruction = transaction.message.instructions[2] - val instruction = SwapValidatorProgram_PreSwap.newInstance(rawInstruction) - - Assert.assertEquals( - "4Zk9E4HaVBJKnukv2nV8aZQ1ZhqJCs74GaTyXBmPXBN6", - instruction.preSwapState.base58(), - ) - Assert.assertEquals( - "6SLCvHRtnB1UJJN7RmHKq6aJ6Ugo5aQEswQ9f9wgybxy", - instruction.user.base58() - ) - Assert.assertEquals( - "5nNBW1KhzHVbR4NMPLYPRYj3UN5vgiw5GrtpdK6eGoce", - instruction.source.base58() - ) - Assert.assertEquals( - "9Rgx4kjnYZBbeXXgbbYLT2FfgzrNHFUShDtp8dpHHjd2", - instruction.destination.base58(), - ) - Assert.assertEquals( - "2uZYLABYpqCAqE2PHa1nzpVRpy3aB8fUv293y6MQxm1Z", - instruction.nonce.base58() - ) - Assert.assertEquals( - "swapBMF2EzkHSn9NDwaSFWMtGC7ZsgzApQv9NSkeUeU", - instruction.payer.base58() - ) - Assert.assertEquals(12, instruction.remainingAccounts.count()) - } - - @Test - fun testDecodePostSwap() { - val (transaction, _) = SolanaTransaction.mockSwapValidatorTransaction() - val rawTransaction = transaction.message.instructions[4] - val instruction = SwapValidatorProgram_PostSwap.newInstance(instruction = rawTransaction) - - Assert.assertEquals(254, instruction.stateBump.byteToUnsignedInt()) - Assert.assertEquals(10000, instruction.maxToSend) - Assert.assertEquals(57277492, instruction.minToReceive) - Assert.assertEquals( - "4Zk9E4HaVBJKnukv2nV8aZQ1ZhqJCs74GaTyXBmPXBN6", - instruction.preSwapState.base58(), - ) - Assert.assertEquals( - "5nNBW1KhzHVbR4NMPLYPRYj3UN5vgiw5GrtpdK6eGoce", - instruction.source.base58() - ) - Assert.assertEquals( - "9Rgx4kjnYZBbeXXgbbYLT2FfgzrNHFUShDtp8dpHHjd2", instruction.destination.base58(), - ) - Assert.assertEquals( - "swapBMF2EzkHSn9NDwaSFWMtGC7ZsgzApQv9NSkeUeU", - instruction.payer.base58() - ) - } - - @Test - fun testDecodeComputeUnitLimit() { - val (transaction, _) = SolanaTransaction.mockSwapValidatorTransaction() - val rawInstruction = transaction.message.instructions[0] - val instruction = - ComputeBudgetProgram_SetComputeUnitLimit.newInstance(instruction = rawInstruction) - Assert.assertEquals(1400000, instruction.limit) - } - - @Test - fun testDecodeComputeUnitPrice() { - val (transaction, _) = SolanaTransaction.mockSwapValidatorTransaction() - val rawInstruction = transaction.message.instructions[1] - val instruction = - ComputeBudgetProgram_SetComputeUnitPrice.newInstance(instruction = rawInstruction) - - Assert.assertEquals(4206, instruction.microLamports) - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_BurnDustWithAuthority_Test.kt b/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_BurnDustWithAuthority_Test.kt deleted file mode 100644 index 2221227e2..000000000 --- a/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_BurnDustWithAuthority_Test.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.mocks.SolanaTransaction -import com.getcode.solana.keys.base58 -import com.getcode.utils.DataSlice.byteToUnsignedInt -import junit.framework.Assert.assertEquals -import org.junit.Test - -class TimelockProgram_BurnDustWithAuthority_Test { - - @Test - fun testDecode() { - val (transaction, _) = SolanaTransaction.mockCloseEmptyAccount() - val rawInstruction = transaction.message.instructions[1] - val instruction = TimelockProgram_BurnDustWithAuthority.newInstance(rawInstruction) - - assertEquals("HzjXkhAQTEffQfXVwCW3yYJ6RbbJToXEDjnfaFZg7e9R", instruction.timelock.base58()) - assertEquals("8V9ioABwqNLsidtRdSWqjJZPxqzKCh6vVqxZWxoSVMb", instruction.vault.base58()) - assertEquals("CiMF8M1VD8HYbWHoX3BhKk4XDcLgzpvz4QJsdULWU84", instruction.vaultOwner.base58()) - assertEquals("codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR", instruction.timeAuthority.base58()) - assertEquals("kinXdEcpDQeHPEuQnqmUgtYykqKGVFq6CeVX5iAHJq6", instruction.mint.base58()) - assertEquals("codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR", instruction.payer.base58()) - assertEquals(255, instruction.bump.byteToUnsignedInt()) - assertEquals(1.0, instruction.maxAmount.toKinValueDouble(), 0.0) - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_CloseAccounts_Test.kt b/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_CloseAccounts_Test.kt deleted file mode 100644 index c38170053..000000000 --- a/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_CloseAccounts_Test.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.mocks.SolanaTransaction -import com.getcode.solana.keys.base58 -import com.getcode.utils.DataSlice.byteToUnsignedInt -import junit.framework.Assert.assertEquals -import org.junit.Test - -class TimelockProgram_CloseAccounts_Test { - - @Test - fun testDecode() { - val (transaction, _) = SolanaTransaction.mockCloseDormantAccount() - val rawInstruction = transaction.message.instructions[5] - val instruction = TimelockProgram_CloseAccounts.newInstance(rawInstruction) - - assertEquals("FYo8wnNMXhQNy2pV4cC35ZXspBQ3TaERGKDkzwBvGM4r", instruction.timelock.base58()) - assertEquals("EKTfBuyKkhPcvzM7rzKVNCxfj5qeiUwVYLtSrB5XQZ4d", instruction.vault.base58()) - assertEquals("codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR", instruction.closeAuthority.base58()) - assertEquals("codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR", instruction.payer.base58()) - assertEquals(255, instruction.bump.byteToUnsignedInt()) - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_DeactivateLock_Test.kt b/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_DeactivateLock_Test.kt deleted file mode 100644 index 19e6d4601..000000000 --- a/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_DeactivateLock_Test.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.mocks.SolanaTransaction -import com.getcode.solana.keys.base58 -import com.getcode.utils.DataSlice.byteToUnsignedInt -import junit.framework.Assert.assertEquals -import org.junit.Test - -class TimelockProgram_DeactivateLock_Test { - - @Test - fun testDecode() { - val (transaction, _) = SolanaTransaction.mockCloseDormantAccount() - val rawInstruction = transaction.message.instructions[3] - val instruction = TimelockProgram_DeactivateLock.newInstance(rawInstruction) - - assertEquals("FYo8wnNMXhQNy2pV4cC35ZXspBQ3TaERGKDkzwBvGM4r", instruction.timelock.base58()) - assertEquals("Ed3GWPEdMiRXDMf7jU46fRwBF7n6ZZFGN3vH1dYAgME2", instruction.vaultOwner.base58()) - assertEquals("codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR", instruction.payer.base58()) - assertEquals(255, instruction.bump.byteToUnsignedInt()) - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_Initialize_Test.kt b/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_Initialize_Test.kt deleted file mode 100644 index 6c628bdc6..000000000 --- a/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_Initialize_Test.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.mocks.SolanaTransaction -import com.getcode.solana.keys.base58 -import junit.framework.Assert.assertEquals -import org.junit.Test - -class TimelockProgram_Initialize_Test { - - @Test - fun testDecode() { - val (transaction, _) = SolanaTransaction.mockTimelockCreateAccount() - val rawInstruction = transaction.message.instructions[1] - val instruction = TimelockProgram_Initialize.newInstance(rawInstruction) - - assertEquals("11111111111111111111111111111111", instruction.nonce.base58()) - assertEquals("DhvyJ6DsJTUsuhCTy8UzBj4r4nREadG6Cx4HCyiGPQJ1", instruction.timelock.base58()) - assertEquals("Jy9M4nEwwfeiteamfJ3BN75p45e4tJEaR3xcYh1NtB5", instruction.vault.base58()) - assertEquals("55nFdnZsTaQUEcRiT4CRuTKCduvpDPWf5VKaPpup6Pus", instruction.vaultOwner.base58()) - assertEquals("kinXdEcpDQeHPEuQnqmUgtYykqKGVFq6CeVX5iAHJq6", instruction.mint.base58()) - assertEquals("codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR", instruction.timeAuthority.base58()) - assertEquals("codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR", instruction.payer.base58()) - assertEquals(1814400, instruction.lockout) - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_RevokeLockWithAuthority_Test.kt b/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_RevokeLockWithAuthority_Test.kt deleted file mode 100644 index 10cb9d499..000000000 --- a/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_RevokeLockWithAuthority_Test.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.mocks.SolanaTransaction -import com.getcode.solana.keys.base58 -import com.getcode.utils.DataSlice.byteToUnsignedInt -import junit.framework.Assert.assertEquals -import org.junit.Test - -class TimelockProgram_RevokeLockWithAuthority_Test { - - @Test - fun testDecode() { - val (transaction, _) = SolanaTransaction.mockCloseDormantAccount() - val rawInstruction = transaction.message.instructions[2] - val instruction = TimelockProgram_RevokeLockWithAuthority.newInstance(rawInstruction) - - assertEquals("FYo8wnNMXhQNy2pV4cC35ZXspBQ3TaERGKDkzwBvGM4r", instruction.timelock.base58()) - assertEquals("EKTfBuyKkhPcvzM7rzKVNCxfj5qeiUwVYLtSrB5XQZ4d", instruction.vault.base58()) - assertEquals("codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR", instruction.closeAuthority.base58()) - assertEquals("codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR", instruction.payer.base58()) - assertEquals(255, instruction.bump.byteToUnsignedInt()) - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_TransferWithAuthority_Test.kt b/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_TransferWithAuthority_Test.kt deleted file mode 100644 index e07f35f73..000000000 --- a/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_TransferWithAuthority_Test.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.mocks.SolanaTransaction -import com.getcode.model.Kin -import com.getcode.solana.keys.base58 -import com.getcode.utils.DataSlice.byteToUnsignedInt -import junit.framework.Assert.assertEquals -import org.junit.Test - -class TimelockProgram_TransferWithAuthority_Test { - - @Test - fun testDecode() { - val (transaction, _) = SolanaTransaction.mockTimelockTransfer() - val rawInstruction = transaction.message.instructions[2] - val instruction = TimelockProgram_TransferWithAuthority.newInstance(rawInstruction) - - assertEquals("GbhARQ2W8qVgFxE9jSAGTAqeaUuBrczWBd9VvtT5u4MW", instruction.timelock.base58()) - assertEquals("2khXZy3LDvTxf5VcdgLip11ip4FjTr1vUdq2ATLeQE7r", instruction.vault.base58()) - assertEquals("Ddk7k7zMMWsp8fZB12wqbiADdXKQFWfwUUsxSo73JaQ9", instruction.vaultOwner.base58()) - assertEquals("codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR", instruction.timeAuthority.base58()) - assertEquals("2sDAFcEZkLd3mbm6SaZhifctkyB4NWsp94GMnfDs1BfR", instruction.destination.base58()) - assertEquals("codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR", instruction.payer.base58()) - assertEquals(255, instruction.bump.byteToUnsignedInt()) - assertEquals(Kin.fromKin(2), instruction.kin) - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_Withdraw_Test.kt b/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_Withdraw_Test.kt deleted file mode 100644 index c607e45c0..000000000 --- a/services/code/src/androidTest/java/com/getcode/solana/instructions/programs/TimelockProgram_Withdraw_Test.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.getcode.solana.instructions.programs - -import com.getcode.mocks.SolanaTransaction -import com.getcode.solana.keys.base58 -import com.getcode.utils.DataSlice.byteToUnsignedInt -import junit.framework.Assert.assertEquals -import org.junit.Test - -class TimelockProgram_Withdraw_Test { - - @Test - fun testDecode() { - val (transaction, _) = SolanaTransaction.mockCloseDormantAccount() - val rawInstruction = transaction.message.instructions[4] - val instruction = TimelockProgram_Withdraw.newInstance(rawInstruction) - - assertEquals("FYo8wnNMXhQNy2pV4cC35ZXspBQ3TaERGKDkzwBvGM4r", instruction.timelock.base58()) - assertEquals("EKTfBuyKkhPcvzM7rzKVNCxfj5qeiUwVYLtSrB5XQZ4d", instruction.vault.base58()) - assertEquals("Ed3GWPEdMiRXDMf7jU46fRwBF7n6ZZFGN3vH1dYAgME2", instruction.vaultOwner.base58()) - assertEquals("GEaVZeZ52Jn8xHPy4VKaXsHQ34E6pwfJGuYh8EsYQi6M", instruction.destination.base58()) - assertEquals("codeHy87wGD5oMRLG75qKqsSi1vWE3oxNyYmXo5F9YR", instruction.payer.base58()) - assertEquals(255, instruction.bump.byteToUnsignedInt()) - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/solana/keys/MerkleProofTest.kt b/services/code/src/androidTest/java/com/getcode/solana/keys/MerkleProofTest.kt deleted file mode 100644 index b851b1612..000000000 --- a/services/code/src/androidTest/java/com/getcode/solana/keys/MerkleProofTest.kt +++ /dev/null @@ -1,174 +0,0 @@ -package com.getcode.solana.keys - -import com.getcode.utils.decodeBase58 -import org.junit.Test -import org.kin.sdk.base.models.toUTF8Bytes - -fun String.decodeHex(): ByteArray { - check(length % 2 == 0) { "Must have an even length" } - - val byteIterator = chunkedSequence(2) - .map { it.toInt(16).toByte() } - .iterator() - - return ByteArray(length / 2) { byteIterator.next() } -} - -class MerkleProofServerTest { - private val root = com.getcode.solana.keys.Hash( - "1d92df473ed3fd6326f7ee570ec34547a42a487a7500366ee8ce3bd2e3f5c99c".decodeHex() - .toList() - ) - - private val proof = - listOf( - "d103cfb5e499c566904787533afbdec56f95492d67fc00e2c0d0161ba99653f1", - "1fe3bed0007741bcb18e6a55d0a1b4742182c2a8a4ca67fe39c8d2f34492d02c", - "858921767bcad0ecb97bab67588a0c0a3e07098c68918fb47f1cd389ceb532a5", - "689311a4b926352c5abd99b68ad505a8bc52b9d38a8e8222a69fe31743459e84", - "349384c18d4631d050d1e6654566f368b03fab67e19e91bf564ee449e70679af", - "0081045413c64a2bceef711c88c83a474dd45281a5c3802cb19c64297ee2abcd", - "0d55a20d88a8a3b6ec1bdc0a2917ab8bd6073e2c6b4b7fbe150099bbb9e3cd08", - "696f022c109b9e4d517b46211d122588a3c8a8484c16fa9ce85b8adf042fbe20", - "5162aaf0959532c29243ed986e7db0b670efe182a3a233859c50d160333a0e64", - "c217e4ae5aba97363aae942bc514b73fb3ec3b568ba7502755538ae244c05438", - "07c3d35566546b2515053df639707588ac3170ed3b14cc46c4db0651a6160542", - "f4bc1133f8c2cb9cd9e08cabfe06c16ee60a03b832401d4c02c587c22bd2e9f4", - "1a17b1e27114c2f1f16fa898557ed0f8546e00cf9cc1dd8a07781d8bafbadba5", - "18ea423c80045847f939c0e57c6d6255d4cc7ed4c72f2c5528cc122fac687733", - "cd097bb2b70eabc6538d44d1583c0f2712b5a6ff16d3d7f9c22455cf0d786f47", - "be2ff6be7e99eca6736741b87cb131950f14496bd4eb8061a17a95f45b6fd9e8", - ).map { com.getcode.solana.keys.Hash(it.decodeHex().toList()) } - - - @Test - fun testValidProof() { - val originalCommitment = "leaf0".toUTF8Bytes() - assert(originalCommitment.verifyContained(root, proof)) - } - - @Test - fun testInvalidCommitment() { - val originalCommitment = "leaf1".toUTF8Bytes() - assert(!originalCommitment.verifyContained(root, proof)) - } - - @Test - fun testSingleInvalidNode() { - val originalCommitment = "leaf0".toUTF8Bytes() - - proof.forEachIndexed { index, _ -> - val modifiedProof = proof.toMutableList() - - val nodeBytes = modifiedProof[index].bytes.toMutableList() - nodeBytes[31] = 0xFF.toByte() // Modify the last byte of every node - modifiedProof[index] = com.getcode.solana.keys.Hash(nodeBytes) - - assert(!originalCommitment.verifyContained(root, modifiedProof)) - } - } -} - -class MerkleProofClientTests { - private val leaf = PublicKey( - "2ocuvgy8ETZp9WDaEy4rpYz2QyeZ7JAiEvXKbW5rKcd4".decodeBase58().toList() - ) - private val root = com.getcode.solana.keys.Hash( - "9EuLAJgnMpEq8wmQUFTNxgYJG2FkAPAGCUhrNK447Uox".decodeBase58().toList() - ) - - private val proof = - listOf( - "4DEt3CHLarXBy74hiJf5t74HmKfTw5DeLK2nzTLFv3Pq", - "73uNXKLpHkTgc9ubvyRXTGaNUh19TUx8M9bN4PNTn544", - "2QH34Bqm89sadRqpz1U5M3Cd34xxNLnTHdxzn4LA3EKU", - "AaySpzaCsgyTVVgUA9bNTNC6sGsws7sTYNyz2oFAe1gT", - "BFHDwqjAPupY4PoJn5Lvx7t9mQrXy6iGnTS7NuRyrEav", - "BFy4XgE4j8NW5PxzDMkH7FWXZcMuFb9zoVSBqdjxm21A", - "B5Lvj9Zdrynu2DGjYXGmxKxRbvmVFtYoGLyqrzZFymXo", - "GEspB8aMfyV4Hmtt7fGmFXsZ5QWrbBUSfPeDT3dXS7gG", - "FuVPGmTwWZayoWt4th2dv8X9xEmRrLvqTbdAXXfRi6Ei", - "CbTZ7BrcBUsmGEjUqxrjkvDkKNRRJHrjF9tTb3mLmMWb", - "GEoigbUN6rsrrpRdNi5rJgX2YDXmE6gDsLYevSchzcg4", - "Gb2zXSV9vxhPkem6PrW45rPiEy9dbJ9nFg7ixQEV4JYh", - "Bb2r8JJdExSAasR38yuTJu2XHRZEGHRxCR71MrJ6nW1z", - "5zTGsTA9vmzGYwVYeD3MDcehybca93prZRdjVqRzZQ6y", - "BbH8JeD3emXYkNw3DvLERM3hMPXhgCEqcU132hSo2uH7", - "F9re8k2sX2BVGX8WqRBGyiZ2aPvvRj4s62jmtgM73hmT", - "DGFU6XD6eYi3GtVAwYBP4d2DUYv1BGiquijQH6HXLLi4", - "8TaNzgiEAP4VoXkBjb1toiZ9fw84RhqezdYt3RhNXR3u", - "BnF8qb2kYZxFHtmqWircb1Di33XTQc8TV17oFwi1tZ4u", - "BBNfGrQ7cKBcZgQmqCgw45s9QLkx41qcTjYwrn7tAtoM", - "Dkf4Fpukx558idi6XwnEx9aAu8GLDzYUC3eN7hQQxPsJ", - "72BxCoqc9cnQvqEZmqzLcZH7VyMBjJFj3R47D8gpV8WL", - "BSVw2t3RwN4ab9Zpd68pwLqwHVecgHvacZB28QgNQ3L", - "7YvGe21SSF93mZoyPsgVF6dD78YWCkVwSff9a4EdE3aj", - "9wZwjy3V8827XZeeE4CxZgXU5WsRGCfcRYHkaPtB7QGb", - "GQb7NMsiEfwVWxuLgn7Tev1KEZSs4ASayUiULjACtxNv", - "Wpb4nF5rc9GpSbWXPUNwA31bemJp61HexerhUx97H8B", - "HwXMPKHXQoBQkM533yqatDbaY3HLDapUWVGVWUv6366a", - "Wom44oATBqSD7SZpBwHRmkXhKV4qsC7SnneGTLKhvdN", - "3xmn2hQdDSKN1ompFNh6AwnQBucWK7Z8mJPyXJzTpLb4", - "HQ5WDTtCvL14aa16UZJStZVVCTcoYbiUayzzBFm8e97r", - "Af6F2xzEKyjuiy7wjukatK9BzW42K7vXekkZq9C7W3K", - "ECS1Mcvt2pYJxYkMDNAp4sNjQq4SadsL3KeJx59ATLbo", - "5B2C3uLH4TvrrriZgo5UbfQwDVbeqqtd7NPwaujfipAd", - "DLd8jX9r1o57SaBgnqzextfBHb7aSGdL98t3EzhiKXjC", - "2cz8R5HYZXs5PKhXXXr562go49A4d5gor42Khejz8K2A", - "B8ucJEMrosxPoSnnBgMbkcmsGWoqusaaheATT8UFa7AF", - "D7JCQL3FMGkoqfvvP9TBzzirTeNQDHbUYAxA9Di6pkQm", - "7fiWLao84havAULW9y5mRDpAqCDSFp2hRmDfN7xQWdWk", - "9SCyBETC2xV1yk44voyQ7MED4SSURpNsuu6MNY12KndN", - "FQKnR4ngeSet5UfWyzXsf7RFU4QN83T9C5xSJWcHcZVX", - "DzN3txNccgTDG58PWfgkx9wBuVPShTp1VTrPyHkvnbpq", - "4cCa2Zen5gn83AJmAgn3mZE3NodVaYaMZM4UykQcTXyy", - "7XDMh3UsVHVomz4MmsSPcKtEkSwxGA4S5ypUb5t2Dvmv", - "6VoqDc2CWeg158GDuQTeerh1VWHRFbcjrKo2VeiiXAD7", - "5uGA5QbthCBe5QiY3BwaqfnwwfJgVahZc8WHPFXwBV2m", - "AAV7TTewgfmN6FHW3oV3ad3Q6KKcdbP6ijt7SUThD2bN", - "AD2GHkMgEmtRFWi6HLpZeHKAeRAZUnGPF7h4mVbeUj7o", - "By3ScwWYWdZAcFXo6V68cRH6kSbdqtUMgNVdnSNevgb5", - "C2RCxm7ZSd9A2fXEvJguBYNK6oUdVrMSCHB3k2JxBKek", - "FHvwmz1bJqm6SfeBLriNhSh8wuwEJc4KDxi2PpVfQtqg", - "R29HU9mCjih74wRWLbW3nXLcUAbEJAqKUrpXENSbziS", - "AvNJa3vqawjfrGmqKPybWQa4V42uyiigUgchT4gHt5Gf", - "CozYPv4cdVeRb5QWCrtuVJk3f2rHJeAFD9ZtQjHLqyyW", - "BeLz3Yqv2ikvQkmH2mxXYGHgvo93Ns77hStxSjuA8UsZ", - "H4tEGNHHcCLbA963wtzJxsZVuEdrpQwrBBnmWcBAhChs", - "CUmSAsfdvnJTUkWLA8chums2ffyveiRNkNVu3t6as6N7", - "62djLqbyFJz3iJiY4NkWvio46dMfP291cWfZ44wdeDgE", - "4JjYv9v3z2YoBrMCsmXE6abpm5bNTKwuJp99rjoTdmPF", - "JB7wk6DDiYKjzasvHPpySd2aCjh1UX5adD5eZgazwEUM", - "3fCTfZwwMipFUpvbXbcDBBtwuo23hmuTJMYaVECWwNTP", - "CZ94cA7JHBb4a8mqN9xEJquPNX1TxKqL3cBQms6yfgTr", - "8PxPosHkG5Q6VBnhiimJaH88yPYt6ZDp4szMBfaVTLun", - ).map { PublicKey(it.decodeBase58().toList()) } - - @Test - fun testValidProof() { - assert(leaf.verifyContained(root,proof)) - } - - @Test - fun testInvalidProof() { - val modifiedBytes = leaf.bytes.toMutableList() - modifiedBytes[31] = 0xFF.toByte() - val modifiedLeaf = PublicKey(modifiedBytes) - - assert(!modifiedLeaf.verifyContained(root, proof)) - } - - @Test - fun testSingleInvalidNode() { - proof.forEachIndexed { index, _ -> - val modifiedProof = proof.toMutableList() - - val nodeBytes = modifiedProof[index].bytes.toMutableList() - nodeBytes[31] = 0xFF.toByte() // Modify the last byte of every node - modifiedProof[index] = PublicKey(nodeBytes) - - assert(!leaf.verifyContained(root, modifiedProof)) - } - } - -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/solana/keys/ProgramDerivedAccountTest.kt b/services/code/src/androidTest/java/com/getcode/solana/keys/ProgramDerivedAccountTest.kt deleted file mode 100644 index 52d41c620..000000000 --- a/services/code/src/androidTest/java/com/getcode/solana/keys/ProgramDerivedAccountTest.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.getcode.solana.keys - -import com.getcode.model.Kin -import com.getcode.model.extensions.newInstance -import com.getcode.utils.decodeBase58 -import org.junit.Assert.assertEquals -import org.junit.Test - -class ProgramDerivedAccountTest { - @Test - fun testTimelockDerivation() { - val owner = PublicKey.fromBase58("BuAprBZugjXG6QRbRQN8QKF8EzbW5SigkDuyR9KtqN5z") - val derivedAccounts = TimelockDerivedAccounts.newInstance(owner) - - assertEquals(owner.base58(), derivedAccounts.owner.base58()) - assertEquals("7Ema8Z4gAUWegampp2AuX4cvaTRy3VMwJUq8LMJshQTV", derivedAccounts.state.publicKey.base58()) - assertEquals(254, derivedAccounts.state.bump) - assertEquals("3538bYdWoRXUgBbyAyvG3Zemmawh75nmCQEvWc9DfKFR", derivedAccounts.vault.publicKey.base58()) - assertEquals(255, derivedAccounts.vault.bump) - } - - @Test - fun testLegacyTimelockDerivation() { - val owner = PublicKey.fromBase58(base58 = "8XfsstyiyT4rCY8ydYthXLisgPHHZFXVtJbcRSsebkWo") - val derivedAccounts = TimelockDerivedAccounts.newInstance(owner = owner, legacy = true) - - assertEquals(owner.base58(), derivedAccounts.owner.base58()) - assertEquals("BsJs1qFrhJU6QZp3yniAkLfECA898a8yTxbJhVsY9rW2", derivedAccounts.state.publicKey.base58()) - assertEquals(254, derivedAccounts.state.bump) - assertEquals("Aqo1xaEUQqtVLcz2Q6sL5u2YwMaAJygTDeSWf7nEEWWN", derivedAccounts.vault.publicKey.base58()) - assertEquals(250, derivedAccounts.vault.bump) - } - - @Test - fun testCommitmentDerivation() { - val treasury = PublicKey.fromBase58("3HR2k4etyHtBgHCAisRQ5mAU1x3GxWSgmm1bHsNzvZKS") - val destination = PublicKey.fromBase58("A1WsiTaL6fPei2xcqDPiVnRDvRwpCjne3votXZmrQe86") - val recentRoot = com.getcode.solana.keys.Hash( - "BvtnzMe2CSunpGoYnvK6YZut1Jg41yaPBDGdJToPQrqy".decodeBase58().toList() - ) - val transcript = com.getcode.solana.keys.Hash( - "91aPsVLa6xCcVfC9FozexaMK8TgKCUZMkj4k6yPy2q4S".decodeBase58().toList() - ) - - val derivedAccounts = SplitterCommitmentAccounts.newInstance( - treasury = treasury, - destination = destination, - recentRoot = recentRoot, - transcript = transcript, - amount = Kin.Companion.fromKin(1) - ) - - assertEquals(treasury, derivedAccounts.treasury) - assertEquals(destination, derivedAccounts.destination) - assertEquals(recentRoot, derivedAccounts.recentRoot) - assertEquals(transcript, derivedAccounts.transcript) - - assertEquals("4vF8wWhuUSPTmUWPRvNcB5aPNzDvjCYBhyizpG6VFNi6", derivedAccounts.state.publicKey.base58()) - assertEquals(247, derivedAccounts.state.bump) - assertEquals("7BXkxmuwH4GGm48gPWMWqHnLYX7NwrtGPUtfHKnhgMmZ", derivedAccounts.vault.publicKey.base58()) - assertEquals(254, derivedAccounts.vault.bump) - } - - @Test - fun testTranscriptHash() { - val transcript = SplitterTranscript( - intentId = PublicKey.fromBase58(base58 = "4roBdWPCqbuqr4YtPavfi7hTAMdH52RXMDgKhqQ4qvX6"), - actionId = 1, - amount = Kin.fromKin(40), - source = PublicKey.fromBase58(base58 = "GNVyMgwkFQvm3YLuJdEVW4xEoqDYnixVaxVYT59frGWW"), - destination = PublicKey.fromBase58(base58 = "Cia66LdCtvfJ6G5jjmLtNoFx5JvWr3uNv2iaFvmSS9gW"), - ) - - assertEquals("5Yh4E953ePoBWe8w78FgMqEjiNmtCQi2ct9BTc2shuLi", transcript.transcriptHash.base58()) - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/solana/keys/SodiumTests.kt b/services/code/src/androidTest/java/com/getcode/solana/keys/SodiumTests.kt deleted file mode 100644 index 9e0623807..000000000 --- a/services/code/src/androidTest/java/com/getcode/solana/keys/SodiumTests.kt +++ /dev/null @@ -1,196 +0,0 @@ -package com.getcode.solana.keys - -import com.getcode.ed25519.Ed25519 -import com.getcode.model.boxOpen -import com.getcode.model.boxSeal -import com.getcode.model.curvePrivate -import com.getcode.model.curvePublic -import com.getcode.model.encryptionPrivateKey -import com.getcode.model.shared -import com.getcode.model.toPublicKey -import com.getcode.util.testBlocking -import com.ionspin.kotlin.crypto.LibsodiumInitializer -import org.junit.Assert -import org.junit.Test - -class SodiumTests { - - @Test - fun testPrivateToCurve() { - testBlocking { - LibsodiumInitializer.initialize() - val privateKey = - com.getcode.solana.keys.PrivateKey(base58 = "4vXZTu7W8FKV2cNB7t2MTp8KXrWpJRCodzUPoyPy1MWZiZQqVVXUrycCdoagzPN6YE9w9pyTbZVzVw9iLDUT7adR") - - Assert.assertEquals( - "F197LA9gxNFgu6bwmHFuBJWU4yuA3wRsBDky9twjeoJr", - privateKey.curvePrivate.getOrNull()!!.base58(), - ) - } - } - - @Test - fun testPublicToCurve() { - testBlocking { - LibsodiumInitializer.initialize() - val publicKey = - PublicKey(base58 = "GV6Aow3jPRXFQiC36EGc1BabhFVY1mEwKPEuwZorGh3R") - - Assert.assertEquals( - "37asXhXd7c8vUNCxHHxAMMrAGPCpYrAtJ8L1fvu4rxzU", - publicKey.curvePublic.getOrNull()!!.base58(), - ) - } - } - - @Test - fun testSharedKey() { - testBlocking { - LibsodiumInitializer.initialize() - val privateKey1 = - com.getcode.solana.keys.PrivateKey(base58 = "2fJLfaTREkNBiDbB26dL4syDozhCEf2pNMorXvBf7593yC59d1kDFsXAA9cN63Bb5MDUgSeU5AhsfS2aTZQHoNyU") - val privateKey2 = - com.getcode.solana.keys.PrivateKey(base58 = "3GKRCGo814rSVa6XkFARZGq13Rb7DSGwF2c6SSRSzMfyQ3wuDAPoELzhsvH6r5A1PFACpFuesDaRHUEoL1PFAxRa") - - val publicKey1 = - PublicKey(base58 = "eMTkrsg1acVKyk8jp4b6JQM3TK2fSxwaZV3gZqCmxsp") - val publicKey2 = - PublicKey(base58 = "J1uvrtrg42Yw3zA7v7VK1wBahW8XkTLxqsnKksZab9wS") - - val privateCurve1 = privateKey1.curvePrivate.getOrNull()!! - val privateCurve2 = privateKey2.curvePrivate.getOrNull()!! - - val publicCurve1 = publicKey1.curvePublic.getOrNull()!! - val publicCurve2 = publicKey2.curvePublic.getOrNull()!! - - val shared1 = PublicKey.shared(publicCurve1, privateCurve2).getOrNull()!! - - Assert.assertEquals( - "GC1cihUsj3rBqqdzBmWkEejWuv6p3scxPqCEwUBUUdQq", - shared1.base58() - ) - - val shared2 = PublicKey.shared(publicCurve2, privateCurve1).getOrNull()!! - - Assert.assertEquals( - "GC1cihUsj3rBqqdzBmWkEejWuv6p3scxPqCEwUBUUdQq", - shared2.base58() - ) - } - } - - @Test - fun testRoundTrip() { - testBlocking { - LibsodiumInitializer.initialize() - val senderPrivate = - com.getcode.solana.keys.PrivateKey(base58 = "2tKSW5f1dag1pGzDSsM9yo32KSMNcTkBAvXEfZ1u2pcqkmo8oYcbtsnA8m9YVd8EUzVJeU5mvjFKjPQF2m4Xifg8") - val senderPublic = - PublicKey(base58 = "3hpSY5ibVa87dDLJhLdVAy7QVso2Edhr28ZEJmpDF7UQ") - - val receiverPrivate = - com.getcode.solana.keys.PrivateKey(base58 = "38EyWg6Eay5bhcZR465FD2agT2bf7BhyWNJJ64ypfdQGTb6mHU3an2f8pvWapSrE3j3hEFu1h7HYoa6eykAHUBJr") - val receiverPublic = - PublicKey(base58 = "6Hsb5k8UjjsowqXgRBr1BR3EKFPeYjA8Nn9prYDU24v6") - - val nonce = com.getcode.vendor.Base58.decode("Jc1X8GdaMmcRDRKiAaMZSRBDLZAFuf9xq").toList() - val expectedEncrypted = - com.getcode.vendor.Base58.decode("2eXsYDo1gcuYc1Nw7uUGZmJZrj2vu33TnrXve62HwzhyTggjjz").toList() - - val message = "super secret message" - - val encrypted = message.boxSeal( - privateKey = senderPrivate, - publicKey = receiverPublic, - nonce = nonce - ) - - Assert.assertEquals( - expectedEncrypted, - encrypted.getOrNull()!!, - ) - - val decrypted = encrypted.getOrNull()!!.boxOpen( - privateKey = receiverPrivate, - publicKey = senderPublic, - nonce = nonce, - ) - - Assert.assertEquals( - message, - String(decrypted.getOrNull()!!.toByteArray()) - ) - } - } - - @Test - fun testRoundTrip2() { - testBlocking { - LibsodiumInitializer.initialize() - val sender = Ed25519.createKeyPair(com.getcode.vendor.Base58.decode("BAjtXtzJzjMvF1qHicCQdyi4AC2y9tQMjVCSwNAY5jnz")) - val receiver = Ed25519.createKeyPair(com.getcode.vendor.Base58.decode("BWUXLs1epmgQwc6kf3VuWcX4bkwjiRjGDp3CYNcVDpVd")) - - val nonce = com.getcode.vendor.Base58.decode("Jc1X8GdaMmcRDRKiAaMZSRBDLZAFuf9xq").toList() - val expectedEncrypted = - com.getcode.vendor.Base58.decode("SZa3RhUVBNhuCT8ARoG5k7V7Ji6TtoJfX8JtpZEHyUzMe4EEb").toList() - - val message = "super secret message" - - val encrypted = message.boxSeal( - privateKey = sender.encryptionPrivateKey!!, - publicKey = receiver.publicKeyFromBytes, - nonce = nonce - ) - - Assert.assertEquals( - expectedEncrypted, - encrypted.getOrNull()!!, - ) - - val decrypted = encrypted.getOrNull()!!.boxOpen( - privateKey = receiver.encryptionPrivateKey!!, - publicKey = sender.publicKeyFromBytes, - nonce = nonce, - ) - - Assert.assertEquals( - message, - String(decrypted.getOrNull()!!.toByteArray()) - ) - } - } - - @Test - fun testDecryptRealBlockchainMessage() { - testBlocking { - LibsodiumInitializer.initialize() - val senderPublic = - PublicKey(base58 = "McS32C1q6Rv1odkEoR5g1xtFBN7TdbkLFvGeyvQtzLF") - val receiverKeyPair = - Ed25519.createKeyPair(com.getcode.vendor.Base58.decode("CADTR1JPf4KzQ9fuYJMRaaWbfshB8qSb38RpFzC8mtjq")) - - val nonce = com.getcode.vendor.Base58.decode("PjgJtLTPZmHGCqJ6Sj1X4ZN8wVbinW4nU").toList() - val encrypted = - com.getcode.vendor.Base58.decode("2BRs8n3fqqDUXVjEdup3d5zoxFALbvs6KcKnMCgpoJ6iafXjikwqbjnbehyha") - .toList() - - val expectedDecrypted = "Blockchain messaging is 🔥" - - val decrypted = encrypted.boxOpen( - privateKey = receiverKeyPair.encryptionPrivateKey!!, - publicKey = senderPublic, - nonce = nonce - ) - - decrypted.exceptionOrNull()?.printStackTrace() - - Assert.assertEquals( - expectedDecrypted, - String(decrypted.getOrNull()!!.toByteArray()) - ) - } - } -} - -val Ed25519.KeyPair.publicKeyFromBytes: PublicKey - get() = publicKeyBytes.toPublicKey() \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/solana/organizer/OrganizerTest.kt b/services/code/src/androidTest/java/com/getcode/solana/organizer/OrganizerTest.kt deleted file mode 100644 index a645a34dd..000000000 --- a/services/code/src/androidTest/java/com/getcode/solana/organizer/OrganizerTest.kt +++ /dev/null @@ -1,158 +0,0 @@ -package com.getcode.solana.organizer - -import android.content.Context -import androidx.test.platform.app.InstrumentationRegistry -import com.getcode.crypt.DerivePath -import com.getcode.crypt.DerivePath.Companion.primary -import com.getcode.crypt.DerivedKey -import com.getcode.crypt.MnemonicCache -import com.getcode.crypt.MnemonicPhrase -import com.getcode.model.AccountInfo -import com.getcode.model.Kin -import com.getcode.model.extensions.newInstance -import com.getcode.model.toPublicKey -import com.getcode.solana.keys.* -import org.junit.Assert.* -import org.junit.Before -import org.junit.Test - -class OrganizerTest { - lateinit var context: Context - - private val mnemonic = MnemonicPhrase.newInstance( - words = "couple divorce usage surprise before range feature source bubble chunk spot away".split( - " " - ) - )!! - - lateinit var owner: DerivedKey - - @Before - fun setup() { - context = InstrumentationRegistry.getInstrumentation().context - MnemonicCache.init(context) - owner = DerivedKey( - path = primary, - keyPair = mnemonic.getSolanaKeyPair() - ) - } - - @Test - fun testInit() { - fun derive(path: DerivePath): DerivedKey { - return DerivedKey.derive( - path, - mnemonic - ) - } - - val organizer = Organizer.newInstance( - mnemonic = mnemonic - ) - - assertEquals(organizer.tray.owner.getCluster(), AccountCluster.newInstance(authority = owner, kind = AccountCluster.Kind.Timelock)) - - assertEquals(7, organizer.tray.slots.size) - - assertEquals( - organizer.tray.incoming.getCluster(), - AccountCluster.newInstance( - authority = derive(DerivePath.getBucketIncoming(0)), - kind = AccountCluster.Kind.Timelock - ) - ) - - assertEquals( - organizer.tray.outgoing.getCluster(), - AccountCluster.newInstance( - authority = derive(DerivePath.getBucketOutgoing(0)), - kind = AccountCluster.Kind.Timelock - ) - ) - - assertEquals( - listOf( - AccountCluster.newInstance(authority = derive(Denomination.ones.derivationPath), kind = AccountCluster.Kind.Timelock), - AccountCluster.newInstance(authority = derive(Denomination.tens.derivationPath), kind = AccountCluster.Kind.Timelock), - AccountCluster.newInstance(authority = derive(Denomination.hundreds.derivationPath), kind = AccountCluster.Kind.Timelock), - AccountCluster.newInstance(authority = derive(Denomination.thousands.derivationPath), kind = AccountCluster.Kind.Timelock), - AccountCluster.newInstance(authority = derive(Denomination.tenThousands.derivationPath), kind = AccountCluster.Kind.Timelock), - AccountCluster.newInstance(authority = derive(Denomination.hundredThousands.derivationPath), kind = AccountCluster.Kind.Timelock), - AccountCluster.newInstance(authority = derive(Denomination.millions.derivationPath), kind = AccountCluster.Kind.Timelock) - ), - organizer.tray.slots.map { it.getCluster() } - ) - } - - @Test - fun testAllAccounts() { - val organizer = Organizer.newInstance( - mnemonic = mnemonic - ) - - val accounts = organizer.allAccounts() - - assertEquals(accounts.size, 10) - - assertEquals(1, accounts.filter { it.first == AccountType.Primary }.size) - assertEquals(1, accounts.filter { it.first == AccountType.Incoming }.size) - assertEquals(1, accounts.filter { it.first == AccountType.Outgoing }.size) - assertEquals(1, accounts.filter { it.first == AccountType.Bucket(SlotType.Bucket1) }.size) - assertEquals(1, accounts.filter { it.first == AccountType.Bucket(SlotType.Bucket10) }.size) - assertEquals(1, accounts.filter { it.first == AccountType.Bucket(SlotType.Bucket100) }.size) - assertEquals(1, accounts.filter { it.first == AccountType.Bucket(SlotType.Bucket1k) }.size) - assertEquals(1, accounts.filter { it.first == AccountType.Bucket(SlotType.Bucket10k) }.size) - assertEquals(1, accounts.filter { it.first == AccountType.Bucket(SlotType.Bucket100k) }.size) - assertEquals(1, accounts.filter { it.first == AccountType.Bucket(SlotType.Bucket1m) }.size) - } - - @Test - fun testAccountCluster() { - val cluster = AccountCluster.newInstance(authority = owner, kind = AccountCluster.Kind.Timelock) - val timelockAccounts = - TimelockDerivedAccounts.newInstance(owner = owner.keyPair.publicKeyBytes.toPublicKey()) - - assertEquals(cluster.authority, owner) - assertEquals(cluster.timelock, timelockAccounts) - } - - @Test - fun testUnlockedState() { - val organizer = Organizer.newInstance( - mnemonic = mnemonic - ) - - assertFalse(organizer.isUnuseable) - - AccountInfo.ManagementState.entries.forEach { state -> - organizer.setAccountInfo( - mapOf( - organizer.primaryVault - to - AccountInfo( - index = 0, - accountType = AccountType.Primary, - address = organizer.primaryVault, - owner = null, - authority = null, - balanceSource = AccountInfo.BalanceSource.Blockchain, - balance = Kin.Companion.fromKin(15), - managementState = state, - blockchainState = AccountInfo.BlockchainState.Exists, - claimState = AccountInfo.ClaimState.Unknown, - mustRotate = false, - originalKinAmount = null, - relationship = null, - createdAt = System.currentTimeMillis(), - ) - ) - ) - - if (state == AccountInfo.ManagementState.Locked || state == AccountInfo.ManagementState.None) { - assertFalse(organizer.isUnuseable) - } else { - assertTrue(organizer.isUnuseable) - } - } - } -} diff --git a/services/code/src/androidTest/java/com/getcode/solana/organizer/SlotTest.kt b/services/code/src/androidTest/java/com/getcode/solana/organizer/SlotTest.kt deleted file mode 100644 index 1014d9556..000000000 --- a/services/code/src/androidTest/java/com/getcode/solana/organizer/SlotTest.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.getcode.solana.organizer - -import android.content.Context -import androidx.test.platform.app.InstrumentationRegistry -import com.getcode.crypt.MnemonicPhrase -import com.getcode.model.Kin -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test - -class SlotTest { - lateinit var context: Context - - private val mnemonic = MnemonicPhrase.newInstance( - words = "couple divorce usage surprise before range feature source bubble chunk spot away".split( - " " - ) - )!! - - @Before - fun setup() { - context = InstrumentationRegistry.getInstrumentation().context - } - - @Test - fun testBillCount() { - val tray = Tray.newInstance(mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(1) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(2) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(3) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(4) * SlotType.Bucket1k.getBillValue(), - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(5) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(6) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(7) * SlotType.Bucket1m.getBillValue(), - ) - ) - - assertEquals(1, tray.slots[0].billCount()) - assertEquals(2, tray.slots[1].billCount()) - assertEquals(3, tray.slots[2].billCount()) - assertEquals(4, tray.slots[3].billCount()) - assertEquals(5, tray.slots[4].billCount()) - assertEquals(6, tray.slots[5].billCount()) - assertEquals(7, tray.slots[6].billCount()) - } - -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/solana/organizer/TrayTest.kt b/services/code/src/androidTest/java/com/getcode/solana/organizer/TrayTest.kt deleted file mode 100644 index 7fb9da5c0..000000000 --- a/services/code/src/androidTest/java/com/getcode/solana/organizer/TrayTest.kt +++ /dev/null @@ -1,1382 +0,0 @@ -package com.getcode.solana.organizer - -import android.content.Context -import androidx.test.platform.app.InstrumentationRegistry -import com.getcode.crypt.MnemonicPhrase -import com.getcode.model.Kin -import org.junit.Assert.assertEquals -import org.junit.Assert.assertThrows -import org.junit.Before -import org.junit.Test - -class TrayTest { - lateinit var context: Context - - private val mnemonic = MnemonicPhrase.newInstance( - words = "couple divorce usage surprise before range feature source bubble chunk spot away".split( - " " - ) - )!! - - - @Before - fun setup() { - context = InstrumentationRegistry.getInstrumentation().context - } - - @Test - fun testSlotsUp() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(1) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(2) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(3) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(4) * SlotType.Bucket1k.getBillValue(), - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(5) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(6) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(7) * SlotType.Bucket1m.getBillValue(), - ) - ) - - assertEquals(tray.slotUp(SlotType.Bucket1)?.type, SlotType.Bucket10) - assertEquals(tray.slotUp(SlotType.Bucket10)?.type, SlotType.Bucket100) - assertEquals(tray.slotUp(SlotType.Bucket100)?.type, SlotType.Bucket1k) - assertEquals(tray.slotUp(SlotType.Bucket1k)?.type, SlotType.Bucket10k) - assertEquals(tray.slotUp(SlotType.Bucket10k)?.type, SlotType.Bucket100k) - assertEquals(tray.slotUp(SlotType.Bucket100k)?.type, SlotType.Bucket1m) - assertEquals(tray.slotUp(SlotType.Bucket1m)?.type, null) - } - - @Test - fun testSlotsDown() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(1) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(2) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(3) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(4) * SlotType.Bucket1k.getBillValue(), - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(5) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(6) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(7) * SlotType.Bucket1m.getBillValue(), - ) - ) - - assertEquals(tray.slotDown(SlotType.Bucket1)?.type, null) - assertEquals(tray.slotDown(SlotType.Bucket10)?.type, SlotType.Bucket1) - assertEquals(tray.slotDown(SlotType.Bucket100)?.type, SlotType.Bucket10) - assertEquals(tray.slotDown(SlotType.Bucket1k)?.type, SlotType.Bucket100) - assertEquals(tray.slotDown(SlotType.Bucket10k)?.type, SlotType.Bucket1k) - assertEquals(tray.slotDown(SlotType.Bucket100k)?.type, SlotType.Bucket10k) - assertEquals(tray.slotDown(SlotType.Bucket1m)?.type, SlotType.Bucket100k) - } - - @Test - fun testSetBalances() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(1) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(2) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(3) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(4) * SlotType.Bucket1k.getBillValue(), - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(5) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(6) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(7) * SlotType.Bucket1m.getBillValue(), - ) - ) - - assertEquals(tray.slots.size, 7) - assertEquals(Kin.fromKin(1) * SlotType.Bucket1.getBillValue(), tray.slot(SlotType.Bucket1).partialBalance) - assertEquals(Kin.fromKin(2) * SlotType.Bucket10.getBillValue(), tray.slot(SlotType.Bucket10).partialBalance) - assertEquals(Kin.fromKin(3) * SlotType.Bucket100.getBillValue(), tray.slot(SlotType.Bucket100).partialBalance) - assertEquals(Kin.fromKin(4) * SlotType.Bucket1k.getBillValue(), tray.slot(SlotType.Bucket1k).partialBalance) - assertEquals(Kin.fromKin(5) * SlotType.Bucket10k.getBillValue(), tray.slot(SlotType.Bucket10k).partialBalance) - assertEquals(Kin.fromKin(6) * SlotType.Bucket100k.getBillValue(), tray.slot(SlotType.Bucket100k).partialBalance) - assertEquals(Kin.fromKin(7) * SlotType.Bucket1m.getBillValue(), tray.slot(SlotType.Bucket1m).partialBalance) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(7) * SlotType.Bucket10k.getBillValue(), - ) - ) - - assertEquals(Kin.fromKin(1) * SlotType.Bucket1.getBillValue(), tray.slot(SlotType.Bucket1).partialBalance) - assertEquals(Kin.fromKin(2) * SlotType.Bucket10.getBillValue(), tray.slot(SlotType.Bucket10).partialBalance) - assertEquals(Kin.fromKin(3) * SlotType.Bucket100.getBillValue(), tray.slot(SlotType.Bucket100).partialBalance) - assertEquals(Kin.fromKin(4) * SlotType.Bucket1k.getBillValue(), tray.slot(SlotType.Bucket1k).partialBalance) - assertEquals(Kin.fromKin(7) * SlotType.Bucket10k.getBillValue(), tray.slot(SlotType.Bucket10k).partialBalance) - assertEquals(Kin.fromKin(6) * SlotType.Bucket100k.getBillValue(), tray.slot(SlotType.Bucket100k).partialBalance) - assertEquals(Kin.fromKin(7) * SlotType.Bucket1m.getBillValue(), tray.slot(SlotType.Bucket1m).partialBalance) - } - - @Test - fun testSetPartialBalances() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(9) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(9) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(9) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(9) * SlotType.Bucket1k.getBillValue(), - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(9) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(9) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(9) * SlotType.Bucket1m.getBillValue(), - ) - ) - } - - @Test - fun testBalance() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(3) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(0) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(0) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(0) * SlotType.Bucket1k.getBillValue(), - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(0) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(0) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(0) * SlotType.Bucket1m.getBillValue(), - ) - ) - - assertEquals(3, tray.availableBalance.toKin().toInt()) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(0) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(0) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(0) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(4) * SlotType.Bucket1k.getBillValue(), - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(0) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(0) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(0) * SlotType.Bucket1m.getBillValue(), - ) - ) - - assertEquals(4_000, tray.availableBalance.toKin().toInt()) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(0) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(0) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(0) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(0) * SlotType.Bucket1k.getBillValue(), - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(0) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(0) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(5) * SlotType.Bucket1m.getBillValue(), - ) - ) - - assertEquals(5_000_000, tray.availableBalance.toKin().toInt()) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(1) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(2) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(3) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(4) * SlotType.Bucket1k.getBillValue(), - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(5) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(6) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(7) * SlotType.Bucket1m.getBillValue(), - ) - ) - - assertEquals(7_654_321, tray.availableBalance.toKin().toInt()) - } - - @Test - fun testExchangeLargeToSmallFull() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(1) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(1) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(1) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(1) * SlotType.Bucket1k.getBillValue(), - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(1) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(1) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(1) * SlotType.Bucket1m.getBillValue(), - ) - ) - - val exchanges = tray.exchangeLargeToSmall() - - assertEquals(6, exchanges.size) - - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket1m), to = AccountType.Bucket( - SlotType.Bucket100k), kin = Kin.fromKin(1_000_000)), exchanges[0]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket100k), to = AccountType.Bucket( - SlotType.Bucket10k), kin = Kin.fromKin(100_000)), exchanges[1]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10k), to = AccountType.Bucket( - SlotType.Bucket1k), kin = Kin.fromKin(10_000)), exchanges[2]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket1k), to = AccountType.Bucket( - SlotType.Bucket100), kin = Kin.fromKin(1_000)), exchanges[3]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket100), to = AccountType.Bucket( - SlotType.Bucket10), kin = Kin.fromKin(100)), exchanges[4]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10), to = AccountType.Bucket( - SlotType.Bucket1), kin = Kin.fromKin(10)), exchanges[5]) - - assertEquals(11.0, tray.slot(SlotType.Bucket1).partialBalance.toKinValueDouble(), 0.0) - assertEquals(100.0, tray.slot(SlotType.Bucket10).partialBalance.toKinValueDouble(), 0.0) - assertEquals(1_000.0, tray.slot(SlotType.Bucket100).partialBalance.toKinValueDouble(), 0.0) - assertEquals(10_000.0, tray.slot(SlotType.Bucket1k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(100_000.0, tray.slot(SlotType.Bucket10k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(1_000_000.0, tray.slot(SlotType.Bucket100k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.slot(SlotType.Bucket1m).partialBalance.toKinValueDouble(), 0.0) - } - - @Test - fun testExchangeLargeToSmallLargestBillOnly() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(0) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(0) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(0) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(0) * SlotType.Bucket1k.getBillValue(), - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(0) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(0) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(1) * SlotType.Bucket1m.getBillValue(), - ) - ) - - val exchanges = tray.exchangeLargeToSmall() - - assertEquals(exchanges.size, 6) - - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket1m), to = AccountType.Bucket( - SlotType.Bucket100k), kin = Kin.fromKin(1_000_000)), exchanges[0]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket100k), to = AccountType.Bucket( - SlotType.Bucket10k), kin = Kin.fromKin(100_000)), exchanges[1]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10k), to = AccountType.Bucket( - SlotType.Bucket1k), kin = Kin.fromKin(10_000)), exchanges[2]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket1k), to = AccountType.Bucket( - SlotType.Bucket100), kin = Kin.fromKin(1_000)), exchanges[3]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket100), to = AccountType.Bucket( - SlotType.Bucket10), kin = Kin.fromKin(100)), exchanges[4]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10), to = AccountType.Bucket( - SlotType.Bucket1), kin = Kin.fromKin(10)), exchanges[5]) - - assertEquals(10.0, tray.slot(SlotType.Bucket1).partialBalance.toKinValueDouble(), 0.0) - assertEquals(90.0, tray.slot(SlotType.Bucket10).partialBalance.toKinValueDouble(), 0.0) - assertEquals(900.0, tray.slot(SlotType.Bucket100).partialBalance.toKinValueDouble(), 0.0) - assertEquals(9_000.0, tray.slot(SlotType.Bucket1k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(90_000.0, tray.slot(SlotType.Bucket10k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(900_000.0, tray.slot(SlotType.Bucket100k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.slot(SlotType.Bucket1m).partialBalance.toKinValueDouble(), 0.0) - } - - @Test - fun testExchangeLargeToSmallLargestBillOverflowing() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(20) * SlotType.Bucket1m.getBillValue(), - ) - ) - - val exchanges = tray.exchangeLargeToSmall() - - assertEquals(exchanges.size, 6) - - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket1m), to = AccountType.Bucket( - SlotType.Bucket100k), kin = Kin.fromKin(1_000_000)), exchanges[0]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket100k), to = AccountType.Bucket( - SlotType.Bucket10k), kin = Kin.fromKin(100_000)), exchanges[1]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10k), to = AccountType.Bucket( - SlotType.Bucket1k), kin = Kin.fromKin(10_000)), exchanges[2]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket1k), to = AccountType.Bucket( - SlotType.Bucket100), kin = Kin.fromKin(1_000)), exchanges[3]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket100), to = AccountType.Bucket( - SlotType.Bucket10), kin = Kin.fromKin(100)), exchanges[4]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10), to = AccountType.Bucket( - SlotType.Bucket1), kin = Kin.fromKin(10)), exchanges[5]) - - assertEquals(10.0, tray.slot(SlotType.Bucket1).partialBalance.toKinValueDouble(), 0.0) - assertEquals(90.0, tray.slot(SlotType.Bucket10).partialBalance.toKinValueDouble(), 0.0) - assertEquals(900.0, tray.slot(SlotType.Bucket100).partialBalance.toKinValueDouble(), 0.0) - assertEquals(9_000.0, tray.slot(SlotType.Bucket1k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(90_000.0, tray.slot(SlotType.Bucket10k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(900_000.0, tray.slot(SlotType.Bucket100k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(19_000_000.0, tray.slot(SlotType.Bucket1m).partialBalance.toKinValueDouble(), 0.0) - } - - @Test - fun testExchangeSmallToLargeSmallestBillOnly() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(1_000_000), - ) - ) - - val exchanges = tray.exchangeSmallToLarge() - - assertEquals(15, exchanges.size) - - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket1), to = AccountType.Bucket( - SlotType.Bucket10), kin = Kin.fromKin(900_000)), exchanges[0]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket1), to = AccountType.Bucket( - SlotType.Bucket10), kin = Kin.fromKin(90_000)), exchanges[1]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket1), to = AccountType.Bucket( - SlotType.Bucket10), kin = Kin.fromKin(9_000)), exchanges[2]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket1), to = AccountType.Bucket( - SlotType.Bucket10), kin = Kin.fromKin(900)), exchanges[3]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket1), to = AccountType.Bucket( - SlotType.Bucket10), kin = Kin.fromKin(90)), exchanges[4]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10), to = AccountType.Bucket( - SlotType.Bucket100), kin = Kin.fromKin(900_000)), exchanges[5]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10), to = AccountType.Bucket( - SlotType.Bucket100), kin = Kin.fromKin(90_000)), exchanges[6]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10), to = AccountType.Bucket( - SlotType.Bucket100), kin = Kin.fromKin(9_000)), exchanges[7]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10), to = AccountType.Bucket( - SlotType.Bucket100), kin = Kin.fromKin(900)), exchanges[8]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket100), to = AccountType.Bucket( - SlotType.Bucket1k), kin = Kin.fromKin(900_000)), exchanges[9]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket100), to = AccountType.Bucket( - SlotType.Bucket1k), kin = Kin.fromKin(90_000)), exchanges[10]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket100), to = AccountType.Bucket( - SlotType.Bucket1k), kin = Kin.fromKin(9_000)), exchanges[11]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket1k), to = AccountType.Bucket( - SlotType.Bucket10k), kin = Kin.fromKin(900_000)), exchanges[12]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket1k), to = AccountType.Bucket( - SlotType.Bucket10k), kin = Kin.fromKin(90_000)), exchanges[13]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10k), to = AccountType.Bucket( - SlotType.Bucket100k), kin = Kin.fromKin(900_000)), exchanges[14]) - - assertEquals(10.0, tray.slot(SlotType.Bucket1).partialBalance.toKinValueDouble(), 0.0) - assertEquals(90.0, tray.slot(SlotType.Bucket10).partialBalance.toKinValueDouble(), 0.0) - assertEquals(900.0, tray.slot(SlotType.Bucket100).partialBalance.toKinValueDouble(), 0.0) - assertEquals(9_000.0, tray.slot(SlotType.Bucket1k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(90_000.0, tray.slot(SlotType.Bucket10k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(900_000.0, tray.slot(SlotType.Bucket100k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.slot(SlotType.Bucket1m).partialBalance.toKinValueDouble(), 0.0) - } - - @Test - fun testExchangeSmallToLarge1000() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(1_000_000), - ) - ) - - val exchanges = tray.exchangeSmallToLarge() - - assertEquals(3, exchanges.size) - - assertEquals( - InternalExchange( - from = AccountType.Bucket(SlotType.Bucket1k), - to = AccountType.Bucket(SlotType.Bucket10k), - kin = Kin.fromKin(900_000) - ), exchanges[0] - ) - assertEquals( - InternalExchange( - from = AccountType.Bucket(SlotType.Bucket1k), - to = AccountType.Bucket(SlotType.Bucket10k), - kin = Kin.fromKin(90_000) - ), exchanges[1] - ) - assertEquals( - InternalExchange( - from = AccountType.Bucket(SlotType.Bucket10k), - to = AccountType.Bucket(SlotType.Bucket100k), - kin = Kin.fromKin(900_000) - ), exchanges[2] - ) - - assertEquals(0.0, tray.slot(SlotType.Bucket1).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.slot(SlotType.Bucket10).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.slot(SlotType.Bucket100).partialBalance.toKinValueDouble(), 0.0) - assertEquals(10_000.0, tray.slot(SlotType.Bucket1k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(90_000.0, tray.slot(SlotType.Bucket10k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(900_000.0, tray.slot(SlotType.Bucket100k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.slot(SlotType.Bucket1m).partialBalance.toKinValueDouble(), 0.0) - } - - @Test - fun testRedistributeFromMiddle() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(1_000_000), - ) - ) - - val exchanges = tray.redistribute() - - assertEquals(5, exchanges.size) - - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10k), to = AccountType.Bucket( - SlotType.Bucket1k), kin = Kin.fromKin(10_000)), exchanges[0]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket1k), to = AccountType.Bucket( - SlotType.Bucket100), kin = Kin.fromKin(1_000)), exchanges[1]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket100), to = AccountType.Bucket( - SlotType.Bucket10), kin = Kin.fromKin(100)), exchanges[2]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10), to = AccountType.Bucket( - SlotType.Bucket1), kin = Kin.fromKin(10)), exchanges[3]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10k), to = AccountType.Bucket( - SlotType.Bucket100k), kin = Kin.fromKin(900_000)), exchanges[4]) - - assertEquals(10.0, tray.slot(SlotType.Bucket1).partialBalance.toKin().toDouble(), 0.0) - assertEquals(90.0, tray.slot(SlotType.Bucket10).partialBalance.toKin().toDouble(), 0.0) - assertEquals(900.0, tray.slot(SlotType.Bucket100).partialBalance.toKin().toDouble(), 0.0) - assertEquals(9_000.0, tray.slot(SlotType.Bucket1k).partialBalance.toKin().toDouble(), 0.0) - assertEquals(90_000.0, tray.slot(SlotType.Bucket10k).partialBalance.toKin().toDouble(), 0.0) - assertEquals(900_000.0, tray.slot(SlotType.Bucket100k).partialBalance.toKin().toDouble(), 0.0) - assertEquals(0.0, tray.slot(SlotType.Bucket1m).partialBalance.toKin().toDouble(), 0.0) - } - - @Test - fun testRedistributeFull() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(19) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(28) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(16) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(39) * SlotType.Bucket1k.getBillValue(), - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(42) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(17) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(1) * SlotType.Bucket1m.getBillValue(), - ) - ) - - - val exchanges = tray.redistribute() - - assertEquals(5, exchanges.size) - - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket1), to = AccountType.Bucket( - SlotType.Bucket10), kin = Kin.fromKin(10)), exchanges[0]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10), to = AccountType.Bucket( - SlotType.Bucket100), kin = Kin.fromKin(200)), exchanges[1]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket1k), to = AccountType.Bucket( - SlotType.Bucket10k), kin = Kin.fromKin(30_000)), exchanges[2]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10k), to = AccountType.Bucket( - SlotType.Bucket100k), kin = Kin.fromKin(300_000)), exchanges[3]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket100k), to = AccountType.Bucket( - SlotType.Bucket1m), kin = Kin.fromKin(1_000_000)), exchanges[4]) - - assertEquals(9.0, tray.slot(SlotType.Bucket1).partialBalance.toKinValueDouble(), 0.0) - assertEquals(90.0, tray.slot(SlotType.Bucket10).partialBalance.toKinValueDouble(), 0.0) - assertEquals(1_800.0, tray.slot(SlotType.Bucket100).partialBalance.toKinValueDouble(), 0.0) - assertEquals(9_000.0, tray.slot(SlotType.Bucket1k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(150_000.0, tray.slot(SlotType.Bucket10k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(1_000_000.0, tray.slot(SlotType.Bucket100k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(2_000_000.0, tray.slot(SlotType.Bucket1m).partialBalance.toKinValueDouble(), 0.0) - } - - @Test - fun testRedistributeFullWithGap() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(19) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(28) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(16) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(0) * SlotType.Bucket1k.getBillValue(), //gap - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(42) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(17) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(1) * SlotType.Bucket1m.getBillValue(), - ) - ) - - - val exchanges = tray.redistribute() - - assertEquals(5, exchanges.size) - - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10k), to = AccountType.Bucket( - SlotType.Bucket1k), kin = Kin.fromKin(10_000)), exchanges[0]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket1), to = AccountType.Bucket( - SlotType.Bucket10), kin = Kin.fromKin(10)), exchanges[1]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10), to = AccountType.Bucket( - SlotType.Bucket100), kin = Kin.fromKin(200)), exchanges[2]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10k), to = AccountType.Bucket( - SlotType.Bucket100k), kin = Kin.fromKin(300_000)), exchanges[3]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket100k), to = AccountType.Bucket( - SlotType.Bucket1m), kin = Kin.fromKin(1_000_000)), exchanges[4]) - - assertEquals( 9.0, tray.slot(SlotType.Bucket1).partialBalance.toKinValueDouble(), 0.0) - assertEquals( 90.0, tray.slot(SlotType.Bucket10).partialBalance.toKinValueDouble(), 0.0) - assertEquals( 1_800.0, tray.slot(SlotType.Bucket100).partialBalance.toKinValueDouble(), 0.0) - assertEquals( 10_000.0, tray.slot(SlotType.Bucket1k).partialBalance.toKinValueDouble(), 0.0) - assertEquals( 110_000.0, tray.slot(SlotType.Bucket10k).partialBalance.toKinValueDouble(), 0.0) - assertEquals( 1_000_000.0, tray.slot(SlotType.Bucket100k).partialBalance.toKinValueDouble(), 0.0) - assertEquals( 2_000_000.0, tray.slot(SlotType.Bucket1m).partialBalance.toKinValueDouble(), 0.0) - } - - @Test - fun testNormalize() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - val cases: List>> = - listOf( - Triple(SlotType.Bucket1k, 5000, listOf(5000)), - Triple(SlotType.Bucket1k, 500, listOf()), - Triple(SlotType.Bucket1k, 0, listOf()), - Triple(SlotType.Bucket100, 1200, listOf(900, 300)), - Triple(SlotType.Bucket100, 1050, listOf(900, 100)), - Triple(SlotType.Bucket1, 20, listOf(9, 9, 2)) - ) - - cases.forEach { triple -> - val (slotType, kin, expectation) = triple - - val amounts = mutableListOf() - tray.normalize(slotType = slotType, amount = Kin.fromKin(kin)) { iterationAmount -> - amounts.add(iterationAmount) - } - assertEquals(expectation.map { Kin.fromKin(it) }, amounts) - } - } - - @Test - fun testNormalizeLargest() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - val cases: List>> = - listOf( - Pair( - 1_489_725, - listOf( - 1_000_000, - 400_000, - 80_000, - 9_000, - 700, - 20, - 5, - ) - ), - Pair( - 10_893_257, - listOf( - 9_000_000, - 1_000_000, - 800_000, - 90_000, - 3_000, - 200, - 50, - 7, - ) - ), - Pair( - 500_000, - listOf( - 500_000, - ) - ), - Pair( - 950_204, - listOf( - 900_000, - 50_000, - 200, - 4, - ) - ), - Pair( - 30_852, listOf( - 30_000, - 800, - 50, - 2, - ) - ), - ) - - cases.forEach { pair -> - val (kin, expectation) = pair - - val amounts = mutableListOf() - tray.normalizeLargest(amount = Kin.fromKin(kin)) { iterationAmount -> - amounts.add(iterationAmount) - } - assertEquals(expectation.map { Kin.fromKin(it) }, amounts) - } - } - - @Test - fun testNaiveTransfer() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(10) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(9) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(19) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(8) * SlotType.Bucket1k.getBillValue(), - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(9) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(9) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(0) * SlotType.Bucket1m.getBillValue(), - ) - ) - - val exchanges = tray.transfer(amount = Kin.fromKin(9_000)) - - assertEquals(3, exchanges.size) - - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket1k), to = AccountType.Outgoing, kin = Kin.fromKin(8_000)), exchanges[0]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket100), to = AccountType.Outgoing, kin = Kin.fromKin(900)), exchanges[1]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket100), to = AccountType.Outgoing, kin = Kin.fromKin(100)), exchanges[2]) - - assertEquals(10, tray.slot(type = SlotType.Bucket1).partialBalance.toKinTruncatingLong()) - assertEquals(90, tray.slot(type = SlotType.Bucket10).partialBalance.toKinTruncatingLong()) - assertEquals(900, tray.slot(type = SlotType.Bucket100).partialBalance.toKinTruncatingLong()) - assertEquals(0, tray.slot(type = SlotType.Bucket1k).partialBalance.toKinTruncatingLong()) - assertEquals(90_000, tray.slot(type = SlotType.Bucket10k).partialBalance.toKinTruncatingLong()) - assertEquals(900_000, tray.slot(type = SlotType.Bucket100k).partialBalance.toKinTruncatingLong()) - assertEquals(0, tray.slot(type = SlotType.Bucket1m).partialBalance.toKinTruncatingLong()) - - assertEquals(991_000.0, tray.availableBalance.toKinValueDouble(), 0.0) - assertEquals(9_000.0, tray.outgoing.partialBalance.toKinValueDouble(), 0.0) - } - - @Test - fun testNaiveTransferInsufficientBalance() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - assertThrows(Tray.OrganizerException.InsufficientTrayBalanceException::class.java) { - tray.transfer(amount = Kin.fromKin(900)) - } - } - - - @Test - fun testDynamicWithdrawalStep1GreaterThan() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(1) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(1) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(1) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(1) * SlotType.Bucket1k.getBillValue(), - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(10) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(9) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(0) * SlotType.Bucket1m.getBillValue(), - ) - ) - - val step = tray.withdrawDynamicallyStep1(amount = Kin.fromKin(111)) - - assertEquals(0.0, step.remaining.toKinValueDouble(), 0.0) - assertEquals(3, step.index) - - val exchanges = step.exchanges - - assertEquals(exchanges.size, 3) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket1), to = AccountType.Outgoing, kin = Kin.fromKin(1)), exchanges[0]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket10), to = AccountType.Outgoing, kin = Kin.fromKin(10)), exchanges[1]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket100), to = AccountType.Outgoing, kin = Kin.fromKin(100)), exchanges[2]) - - assertEquals(0, tray.slot(type = SlotType.Bucket1).partialBalance.toKinTruncatingLong()) - assertEquals(0, tray.slot(type = SlotType.Bucket10).partialBalance.toKinTruncatingLong()) - assertEquals(0, tray.slot(type = SlotType.Bucket100).partialBalance.toKinTruncatingLong()) - assertEquals(1_000, tray.slot(type = SlotType.Bucket1k).partialBalance.toKinTruncatingLong()) - assertEquals(100_000, tray.slot(type = SlotType.Bucket10k).partialBalance.toKinTruncatingLong()) - assertEquals(900_000, tray.slot(type = SlotType.Bucket100k).partialBalance.toKinTruncatingLong()) - assertEquals(0, tray.slot(type = SlotType.Bucket1m).partialBalance.toKinTruncatingLong()) - } - - @Test - fun testDynamicWithdrawalStep1LessThan() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(1) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(1) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(1) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(1) * SlotType.Bucket1k.getBillValue(), - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(10) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(9) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(0) * SlotType.Bucket1m.getBillValue(), - ) - ) - - val step = tray.withdrawDynamicallyStep1(amount = Kin.fromKin(9_000)) - - assertEquals(7_889, step.remaining.toKinTruncatingLong()) - assertEquals(4, step.index) - - val exchanges = step.exchanges - - assertEquals(4, exchanges.size) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket1), to = AccountType.Outgoing, kin = Kin.fromKin(1)), exchanges[0]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket10), to = AccountType.Outgoing, kin = Kin.fromKin(10)), exchanges[1]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket100), to = AccountType.Outgoing, kin = Kin.fromKin(100)), exchanges[2]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket1k), to = AccountType.Outgoing, kin = Kin.fromKin(1_000)), exchanges[3]) - - assertEquals(0, tray.slot(type = SlotType.Bucket1).partialBalance.toKinTruncatingLong()) - assertEquals(0, tray.slot(type = SlotType.Bucket10).partialBalance.toKinTruncatingLong()) - assertEquals(0, tray.slot(type = SlotType.Bucket100).partialBalance.toKinTruncatingLong()) - assertEquals(0, tray.slot(type = SlotType.Bucket1k).partialBalance.toKinTruncatingLong()) - assertEquals(100_000, tray.slot(type = SlotType.Bucket10k).partialBalance.toKinTruncatingLong()) - assertEquals(900_000, tray.slot(type = SlotType.Bucket100k).partialBalance.toKinTruncatingLong()) - assertEquals(0, tray.slot(type = SlotType.Bucket1m).partialBalance.toKinTruncatingLong()) - } - - @Test - fun testDynamicWithdrawalStep2() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(1) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(1) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(1) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(1) * SlotType.Bucket1k.getBillValue(), - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(10) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(9) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(0) * SlotType.Bucket1m.getBillValue(), - ) - ) - - val step = tray.withdrawDynamicallyStep1(amount = Kin.fromKin(9_000)) - - assertEquals(7_889, step.remaining.toKinTruncatingLong()) - assertEquals(1_111, step.exchanges.map { it.kin }.reduce { acc, kin -> acc + kin }.toKinTruncatingLong()) - assertEquals(4, step.index) - - val finalExchanges = tray.withdrawDynamicallyStep2(step = step) - val exchanges = step.exchanges + finalExchanges - - assertEquals(12, exchanges.size) - - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket1), to = AccountType.Outgoing, kin = Kin.fromKin(1)), exchanges[0]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket10), to = AccountType.Outgoing, kin = Kin.fromKin(10)), exchanges[1]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket100), to = AccountType.Outgoing, kin = Kin.fromKin(100)), exchanges[2]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket1k), to = AccountType.Outgoing, kin = Kin.fromKin(1_000)), exchanges[3]) - - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10k), to = AccountType.Bucket( - SlotType.Bucket1k), kin = Kin.fromKin(10_000)), exchanges[4]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket1k), to = AccountType.Bucket( - SlotType.Bucket100), kin = Kin.fromKin(1_000)), exchanges[5]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket1k), to = AccountType.Outgoing, kin = Kin.fromKin(7_000)), exchanges[6]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket100), to = AccountType.Bucket( - SlotType.Bucket10), kin = Kin.fromKin(100)), exchanges[7]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket100), to = AccountType.Outgoing, kin = Kin.fromKin(800)), exchanges[8]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10), to = AccountType.Bucket( - SlotType.Bucket1), kin = Kin.fromKin(10)), exchanges[9]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket10), to = AccountType.Outgoing, kin = Kin.fromKin(80)), exchanges[10]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket1), to = AccountType.Outgoing, kin = Kin.fromKin(9)), exchanges[11]) - - assertEquals(1, tray.slot(type = SlotType.Bucket1).partialBalance.toKinTruncatingLong()) - assertEquals(10, tray.slot(type = SlotType.Bucket10).partialBalance.toKinTruncatingLong()) - assertEquals(100, tray.slot(type = SlotType.Bucket100).partialBalance.toKinTruncatingLong()) - assertEquals(2_000, tray.slot(type = SlotType.Bucket1k).partialBalance.toKinTruncatingLong()) - assertEquals(90_000, tray.slot(type = SlotType.Bucket10k).partialBalance.toKinTruncatingLong()) - assertEquals(900_000, tray.slot(type = SlotType.Bucket100k).partialBalance.toKinTruncatingLong()) - assertEquals(0, tray.slot(type = SlotType.Bucket1m).partialBalance.toKinTruncatingLong()) - } - - @Test - fun testDynamicWithdrawalStep2InvalidIndex() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - // Too low - val step1 = InternalDynamicStep( - remaining = Kin(0), - index = 0, - exchanges = listOf() - ) - val results1 = tray.withdrawDynamicallyStep2(step = step1) - assertEquals(listOf(), results1) - - // Too high - val step2 = InternalDynamicStep( - remaining = Kin(0), - index = 9, - exchanges = listOf() - ) - val results2 = tray.withdrawDynamicallyStep2(step = step2) - assertEquals(listOf(), results2) - } - - @Test - fun testDynamicWithdrawalStep2NoRemaining() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - val step = InternalDynamicStep( - remaining = Kin.fromKin(0), - index = 2, - exchanges = listOf() - ) - - val exchanges = tray.withdrawDynamicallyStep2(step = step) - - assertEquals(listOf(), exchanges) - } - - @Test - fun testDynamicWithdrawalAndRedistribute() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(1) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(1) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(1) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(1) * SlotType.Bucket1k.getBillValue(), - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(10) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(9) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(0) * SlotType.Bucket1m.getBillValue(), - ) - ) - - assertEquals(1_001_111.0, tray.availableBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.outgoing.partialBalance.toKinValueDouble(), 0.0) - - val exchanges = tray.transfer(amount = Kin.fromKin(9_000)) - - assertEquals(12, exchanges.size) - - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket1), to = AccountType.Outgoing, kin = Kin.fromKin(1)), exchanges[0]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket10), to = AccountType.Outgoing, kin = Kin.fromKin(10)), exchanges[1]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket100), to = AccountType.Outgoing, kin = Kin.fromKin(100)), exchanges[2]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket1k), to = AccountType.Outgoing, kin = Kin.fromKin(1_000)), exchanges[3]) - - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10k), to = AccountType.Bucket( - SlotType.Bucket1k), kin = Kin.fromKin(10_000)), exchanges[4]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket1k), to = AccountType.Bucket( - SlotType.Bucket100), kin = Kin.fromKin(1_000)), exchanges[5]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket1k), to = AccountType.Outgoing, kin = Kin.fromKin(7_000)), exchanges[6]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket100), to = AccountType.Bucket( - SlotType.Bucket10), kin = Kin.fromKin(100)), exchanges[7]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket100), to = AccountType.Outgoing, kin = Kin.fromKin(800)), exchanges[8]) - assertEquals( - InternalExchange(from = AccountType.Bucket(SlotType.Bucket10), to = AccountType.Bucket( - SlotType.Bucket1), kin = Kin.fromKin(10)), exchanges[9]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket10), to = AccountType.Outgoing, kin = Kin.fromKin(80)), exchanges[10]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket1), to = AccountType.Outgoing, kin = Kin.fromKin(9)), exchanges[11]) - - assertEquals(1, tray.slot(type = SlotType.Bucket1).partialBalance.toKinTruncatingLong()) - assertEquals(10, tray.slot(type = SlotType.Bucket10).partialBalance.toKinTruncatingLong()) - assertEquals(100, tray.slot(type = SlotType.Bucket100).partialBalance.toKinTruncatingLong()) - assertEquals(2_000, tray.slot(type = SlotType.Bucket1k).partialBalance.toKinTruncatingLong()) - assertEquals(90_000, tray.slot(type = SlotType.Bucket10k).partialBalance.toKinTruncatingLong()) - assertEquals(900_000, tray.slot(type = SlotType.Bucket100k).partialBalance.toKinTruncatingLong()) - assertEquals(0, tray.slot(type = SlotType.Bucket1m).partialBalance.toKinTruncatingLong()) - - assertEquals(992_111.0, tray.availableBalance.toKinValueDouble(), 0.0) - assertEquals(9_000, tray.outgoing.partialBalance.toKinTruncatingLong()) - - val redistributions = tray.redistribute() - - assertEquals(5, redistributions.size) - - assertEquals(redistributions[0], InternalExchange(from = AccountType.Bucket(SlotType.Bucket10k), to = AccountType.Bucket( - SlotType.Bucket1k), kin = Kin.fromKin(10_000)) - ) - assertEquals(redistributions[1], InternalExchange(from = AccountType.Bucket(SlotType.Bucket100k), to = AccountType.Bucket( - SlotType.Bucket10k), kin = Kin.fromKin(100_000)) - ) - assertEquals(redistributions[2], InternalExchange(from = AccountType.Bucket(SlotType.Bucket1k), to = AccountType.Bucket( - SlotType.Bucket100), kin = Kin.fromKin(1_000)) - ) - assertEquals(redistributions[3], InternalExchange(from = AccountType.Bucket(SlotType.Bucket100), to = AccountType.Bucket( - SlotType.Bucket10), kin = Kin.fromKin(100)) - ) - assertEquals(redistributions[4], InternalExchange(from = AccountType.Bucket(SlotType.Bucket10), to = AccountType.Bucket( - SlotType.Bucket1), kin = Kin.fromKin(10)) - ) - - assertEquals(11, tray.slot(type = SlotType.Bucket1).partialBalance.toKinTruncatingLong()) - assertEquals(100, tray.slot(type = SlotType.Bucket10).partialBalance.toKinTruncatingLong()) - assertEquals(1_000, tray.slot(type = SlotType.Bucket100).partialBalance.toKinTruncatingLong()) - assertEquals(11_000, tray.slot(type = SlotType.Bucket1k).partialBalance.toKinTruncatingLong()) - assertEquals(180_000, tray.slot(type = SlotType.Bucket10k).partialBalance.toKinTruncatingLong()) - assertEquals(800_000, tray.slot(type = SlotType.Bucket100k).partialBalance.toKinTruncatingLong()) - assertEquals(0, tray.slot(type = SlotType.Bucket1m).partialBalance.toKinTruncatingLong()) - - assertEquals(992_111, tray.availableBalance.toKinTruncatingLong()) - assertEquals(9_000, tray.outgoing.partialBalance.toKinTruncatingLong()) - } - - @Test - fun testDynamicWithdrawalExample1() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(13) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(15) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(10) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(5) * SlotType.Bucket1k.getBillValue(), - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(0) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(0) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(0) * SlotType.Bucket1m.getBillValue(), - ) - ) - - assertEquals(6_163.0, tray.availableBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.outgoing.partialBalance.toKinValueDouble(), 0.0) - - val exchanges = tray.transfer(amount = Kin.fromKin(6_000)) // Should use naive strategy (no exchanges below) - - assertEquals(3, exchanges.size) - - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket1k), to = AccountType.Outgoing, kin = Kin.fromKin(5_000)), exchanges[0]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket100), to = AccountType.Outgoing, kin = Kin.fromKin(900)), exchanges[1]) - assertEquals(InternalExchange(from = AccountType.Bucket(SlotType.Bucket100), to = AccountType.Outgoing, kin = Kin.fromKin(100)), exchanges[2]) - - assertEquals(13, tray.slot(type = SlotType.Bucket1).partialBalance.toKinTruncatingLong()) - assertEquals(150, tray.slot(type = SlotType.Bucket10).partialBalance.toKinTruncatingLong()) - assertEquals(0, tray.slot(type = SlotType.Bucket100).partialBalance.toKinTruncatingLong()) - assertEquals(0, tray.slot(type = SlotType.Bucket1k).partialBalance.toKinTruncatingLong()) - assertEquals(0, tray.slot(type = SlotType.Bucket10k).partialBalance.toKinTruncatingLong()) - assertEquals(0, tray.slot(type = SlotType.Bucket100k).partialBalance.toKinTruncatingLong()) - assertEquals(0, tray.slot(type = SlotType.Bucket1m).partialBalance.toKinTruncatingLong()) - - assertEquals(163.0, tray.availableBalance.toKinValueDouble(), 0.0) - assertEquals(6_000, tray.outgoing.partialBalance.toKinTruncatingLong()) - } - - @Test - fun testDynamicWithdrawalExample2() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - - tray.setBalances( - mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(0) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(4) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(1) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(9) * SlotType.Bucket1k.getBillValue(), - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(1) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(6) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(1) * SlotType.Bucket1m.getBillValue(), - ) - ) - - assertEquals(1_619_140.0, tray.availableBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.outgoing.partialBalance.toKinValueDouble(), 0.0) - - val exchanges = tray.transfer(amount = Kin.fromKin(359_804)) - - assertEquals(14, exchanges.size) - - assertEquals( - InternalExchange( - from = AccountType.Bucket(SlotType.Bucket10), - to = AccountType.Outgoing, - kin = Kin.fromKin(40) - ), - exchanges[0] - ) - assertEquals( - InternalExchange( - from = AccountType.Bucket(SlotType.Bucket100), - to = AccountType.Outgoing, - kin = Kin.fromKin(100) - ), - exchanges[1] - ) - assertEquals( - InternalExchange( - from = AccountType.Bucket(SlotType.Bucket1k), - to = AccountType.Outgoing, - kin = Kin.fromKin(9_000) - ), - exchanges[2] - ) - assertEquals( - InternalExchange( - from = AccountType.Bucket(SlotType.Bucket10k), - to = AccountType.Outgoing, - kin = Kin.fromKin(10_000) - ), - exchanges[3] - ) - assertEquals( - InternalExchange( - from = AccountType.Bucket(SlotType.Bucket100k), - to = AccountType.Outgoing, - kin = Kin.fromKin(300_000) - ), - exchanges[4] - ) - assertEquals( - InternalExchange( - from = AccountType.Bucket(SlotType.Bucket100k), - to = AccountType.Bucket(SlotType.Bucket10k), - kin = Kin.fromKin(100_000) - ), - exchanges[5] - ) - assertEquals( - InternalExchange( - from = AccountType.Bucket(SlotType.Bucket10k), - to = AccountType.Bucket(SlotType.Bucket1k), - kin = Kin.fromKin(10_000) - ), - exchanges[6] - ) - assertEquals( - InternalExchange( - from = AccountType.Bucket(SlotType.Bucket10k), - to = AccountType.Outgoing, - kin = Kin.fromKin(40_000) - ), - exchanges[7] - ) - assertEquals( - InternalExchange( - from = AccountType.Bucket(SlotType.Bucket1k), - to = AccountType.Bucket(SlotType.Bucket100), - kin = Kin.fromKin(1_000) - ), - exchanges[8] - ) - assertEquals( - InternalExchange( - from = AccountType.Bucket(SlotType.Bucket100), - to = AccountType.Bucket(SlotType.Bucket10), - kin = Kin.fromKin(100) - ), - exchanges[9] - ) - assertEquals( - InternalExchange( - from = AccountType.Bucket(SlotType.Bucket100), - to = AccountType.Outgoing, - kin = Kin.fromKin(600) - ), - exchanges[10] - ) - assertEquals( - InternalExchange( - from = AccountType.Bucket(SlotType.Bucket10), - to = AccountType.Bucket(SlotType.Bucket1), - kin = Kin.fromKin(10) - ), - exchanges[11] - ) - assertEquals( - InternalExchange( - from = AccountType.Bucket(SlotType.Bucket10), - to = AccountType.Outgoing, - kin = Kin.fromKin(60) - ), - exchanges[12] - ) - assertEquals( - InternalExchange( - from = AccountType.Bucket(SlotType.Bucket1), - to = AccountType.Outgoing, - kin = Kin.fromKin(4) - ), - exchanges[13] - ) - - assertEquals(6.0, tray.slot(SlotType.Bucket1).partialBalance.toKinValueDouble(), 0.0) - assertEquals(30.0, tray.slot(SlotType.Bucket10).partialBalance.toKinValueDouble(), 0.0) - assertEquals(300.0, tray.slot(SlotType.Bucket100).partialBalance.toKinValueDouble(), 0.0) - assertEquals(9_000.0, tray.slot(SlotType.Bucket1k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(50_000.0, tray.slot(SlotType.Bucket10k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(200_000.0, tray.slot(SlotType.Bucket100k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(1_000_000.0, tray.slot(SlotType.Bucket1m).partialBalance.toKinValueDouble(), 0.0) - - assertEquals(1_259_336.0, tray.availableBalance.toKinValueDouble(), 0.0) - assertEquals(359_804.0, tray.outgoing.partialBalance.toKinValueDouble(), 0.0) - } - - //@Test - fun testTransferAllPermutations() { - var count = 0 - - var tray = Tray.newInstance(context = context, mnemonic = mnemonic) - val cleanTray = tray.copy() - - var a: Int - var b: Int - var c: Int - var d: Int - var e: Int - var f: Int - var g: Int - - for (i in 1..1000) { - a = (i / 1) % 10 // <1s - b = (i / 10) % 10 // <1s - c = (i / 100) % 10 // ~16 sec - d = (i / 1000) % 10 // ~28 min - e = (i / 10000) % 10 // ~48 hours - f = (i / 100000) % 10 // Too long - g = (i / 1000000) % 10 // Too long - - val balances = mapOf( - AccountType.Bucket(SlotType.Bucket1) to Kin.fromKin(a) * SlotType.Bucket1.getBillValue(), - AccountType.Bucket(SlotType.Bucket10) to Kin.fromKin(b) * SlotType.Bucket10.getBillValue(), - AccountType.Bucket(SlotType.Bucket100) to Kin.fromKin(c) * SlotType.Bucket100.getBillValue(), - AccountType.Bucket(SlotType.Bucket1k) to Kin.fromKin(d) * SlotType.Bucket1k.getBillValue(), - AccountType.Bucket(SlotType.Bucket10k) to Kin.fromKin(e) * SlotType.Bucket10k.getBillValue(), - AccountType.Bucket(SlotType.Bucket100k) to Kin.fromKin(f) * SlotType.Bucket100k.getBillValue(), - AccountType.Bucket(SlotType.Bucket1m) to Kin.fromKin(g) * SlotType.Bucket1m.getBillValue(), - ) - - tray = cleanTray - tray.setBalances(balances) - - for (amount in 0 until tray.availableBalance.toKinTruncatingLong()) { - tray = cleanTray - tray.setBalances(balances) - - val toWithdraw = Kin.fromKin(amount + 1) - try { - count += 1 -// let exchanges = try tray.transfer(amount: toWithdraw) - val exchanges = tray.withdrawDynamically(amount = toWithdraw) - val total = exchanges.reduce { acc, exchange -> - if (exchange.to == AccountType.Outgoing) { - acc.copy(kin = acc.kin + exchange.kin.toKinTruncating()) - } else { - acc - } - } - assertEquals(total.kin.toKinTruncating(), toWithdraw.toKinTruncating()) - } catch(e: Exception) { - print("Error: ${e.message}") - print("Withdrawing: ${toWithdraw.toKinTruncatingLong()}") - } - } - } - print("Total invocation count: $count") - } - - @Test - fun testReceiveSingleSlot() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - val amount = Kin.fromKin(1_000_000) - - tray.increment(AccountType.Incoming, Kin.fromKin(1_000_000)) - val exchanges = tray.receive(AccountType.Incoming, Kin.fromKin(1_000_000)) - - assertEquals(1, exchanges.size) - assertEquals( - InternalExchange( - from = AccountType.Incoming, - to = AccountType.Bucket(SlotType.Bucket1m), - kin = amount - ), - exchanges[0] - ) - - assertEquals(0.0, tray.incoming.partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.slot(SlotType.Bucket1).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.slot(SlotType.Bucket10).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.slot(SlotType.Bucket100).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.slot(SlotType.Bucket1k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.slot(SlotType.Bucket10k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.slot(SlotType.Bucket100k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(1_000_000.0, tray.slot(SlotType.Bucket1m).partialBalance.toKinValueDouble(), 0.0) - } - - @Test - fun testReceiveAllSlots() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - val amount = Kin.fromKin(1_234_567) - - tray.increment(AccountType.Incoming, amount) - val exchanges = tray.receive(AccountType.Incoming, amount) - - assertEquals(7, exchanges.size) - - assertEquals(AccountType.Bucket(SlotType.Bucket1m), exchanges[0].to) - assertEquals(AccountType.Bucket(SlotType.Bucket100k), exchanges[1].to) - assertEquals(AccountType.Bucket(SlotType.Bucket10k), exchanges[2].to) - assertEquals(AccountType.Bucket(SlotType.Bucket1k), exchanges[3].to) - assertEquals(AccountType.Bucket(SlotType.Bucket100), exchanges[4].to) - assertEquals(AccountType.Bucket(SlotType.Bucket10), exchanges[5].to) - assertEquals(AccountType.Bucket(SlotType.Bucket1), exchanges[6].to) - - assertEquals(1_000_000.0, exchanges[0].kin.toKinValueDouble(), 0.0) - assertEquals(200_000.0, exchanges[1].kin.toKinValueDouble(), 0.0) - assertEquals(30_000.0, exchanges[2].kin.toKinValueDouble(), 0.0) - assertEquals(4_000.0, exchanges[3].kin.toKinValueDouble(), 0.0) - assertEquals(500.0, exchanges[4].kin.toKinValueDouble(), 0.0) - assertEquals(60.0, exchanges[5].kin.toKinValueDouble(), 0.0) - assertEquals(7.0, exchanges[6].kin.toKinValueDouble(), 0.0) - } - - @Test - fun testReceiveThreeSlots() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - val amount = Kin.fromKin(1_200_500) - - tray.increment(AccountType.Incoming, amount) - val exchanges = tray.receive(AccountType.Incoming, amount) - - assertEquals(3, exchanges.size) - - assertEquals(AccountType.Bucket(SlotType.Bucket1m), exchanges[0].to) - assertEquals(AccountType.Bucket(SlotType.Bucket100k), exchanges[1].to) - assertEquals(AccountType.Bucket(SlotType.Bucket100), exchanges[2].to) - - assertEquals(0.0, tray.incoming.partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.slot(SlotType.Bucket1).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.slot(SlotType.Bucket10).partialBalance.toKinValueDouble(), 0.0) - assertEquals(500.0, tray.slot(SlotType.Bucket100).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.slot(SlotType.Bucket1k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.slot(SlotType.Bucket10k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(200_000.0, tray.slot(SlotType.Bucket100k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(1_000_000.0, tray.slot(SlotType.Bucket1m).partialBalance.toKinValueDouble(), 0.0) - } - - @Test - fun testReceiveLargeAmounts() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - val amount = Kin.fromKin(95_800_173) - - tray.increment(AccountType.Incoming, amount) - val exchanges = tray.receive(AccountType.Incoming, amount) - - assertEquals(15, exchanges.size) - - for (i in 0..9) { - assertEquals( - InternalExchange( - from = AccountType.Incoming, - to = AccountType.Bucket(SlotType.Bucket1m), - kin = Kin.fromKin(9_000_000) - ), - exchanges[i] - ) - } - - assertEquals( - InternalExchange( - from = AccountType.Incoming, - to = AccountType.Bucket(SlotType.Bucket1m), - kin = Kin.fromKin(5_000_000) - ), - exchanges[10] - ) - assertEquals( - InternalExchange( - from = AccountType.Incoming, - to = AccountType.Bucket(SlotType.Bucket100k), - kin = Kin.fromKin(800_000) - ), - exchanges[11] - ) - assertEquals( - InternalExchange( - from = AccountType.Incoming, - to = AccountType.Bucket(SlotType.Bucket100), - kin = Kin.fromKin(100) - ), - exchanges[12] - ) - assertEquals( - InternalExchange( - from = AccountType.Incoming, - to = AccountType.Bucket(SlotType.Bucket10), - kin = Kin.fromKin(70) - ), - exchanges[13] - ) - assertEquals( - InternalExchange( - from = AccountType.Incoming, - to = AccountType.Bucket(SlotType.Bucket1), - kin = Kin.fromKin(3) - ), - exchanges[14] - ) - - assertEquals(0.0, tray.incoming.partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.slot(SlotType.Bucket1).partialBalance.toKinValueDouble(), 3.0) - assertEquals(0.0, tray.slot(SlotType.Bucket10).partialBalance.toKinValueDouble(), 70.0) - assertEquals(0.0, tray.slot(SlotType.Bucket100).partialBalance.toKinValueDouble(), 100.0) - assertEquals(0.0, tray.slot(SlotType.Bucket1k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.slot(SlotType.Bucket10k).partialBalance.toKinValueDouble(), 0.0) - assertEquals(0.0, tray.slot(SlotType.Bucket100k).partialBalance.toKinValueDouble(), 800_000.0) - assertEquals(0.0, tray.slot(SlotType.Bucket1m).partialBalance.toKinValueDouble(), 95_000_000.0) - } - - @Test - fun testReceiveInsufficientBalance() { - val tray = Tray.newInstance(context = context, mnemonic = mnemonic) - assertThrows(Tray.OrganizerException.InvalidSlotBalanceException::class.java) { - tray.receive(AccountType.Incoming, amount = Kin.fromKin(100)) - } - } -} \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/util/TestUtils.kt b/services/code/src/androidTest/java/com/getcode/util/TestUtils.kt deleted file mode 100644 index 912d0a430..000000000 --- a/services/code/src/androidTest/java/com/getcode/util/TestUtils.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.getcode.util - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.runBlocking -import kotlin.coroutines.Continuation -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.coroutines.startCoroutine - -fun testBlocking(block : suspend () -> Unit) { - val continuation = Continuation(EmptyCoroutineContext) { - //Do nothing - if (it.isFailure) { - throw it.exceptionOrNull()!! - } - } - block.startCoroutine(continuation) -} - -fun runTest(block: suspend (scope : CoroutineScope) -> Unit) = runBlocking { block(this) } \ No newline at end of file diff --git a/services/code/src/androidTest/java/com/getcode/util/UUIDTests.kt b/services/code/src/androidTest/java/com/getcode/util/UUIDTests.kt deleted file mode 100644 index 3c9023222..000000000 --- a/services/code/src/androidTest/java/com/getcode/util/UUIDTests.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.getcode.util - -import com.getcode.utils.blockchainMemo -import junit.framework.Assert.assertEquals -import org.junit.Test -import java.util.UUID - -class UUIDTests { - @Test - fun testMemo() { - val uuid = UUID.fromString("c24a3bf2-ad4f-4756-944e-81948ff10882") - val memo = uuid.blockchainMemo - - assertEquals( - "Gk2Yb7W6BypLsdRoJqMAqXHDoV2jT", - memo - ) - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/CodeServicesConfig.kt b/services/code/src/main/java/com/getcode/CodeServicesConfig.kt deleted file mode 100644 index 86f9bfc94..000000000 --- a/services/code/src/main/java/com/getcode/CodeServicesConfig.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.getcode - -import com.getcode.services.BuildConfig -import com.getcode.services.ChannelConfig -import kotlin.time.Duration -import kotlin.time.Duration.Companion.minutes - -internal data class CodeServicesConfig( - override val baseUrl: String = "api.codeinfra.net", - override val userAgent: String = "Code/Android/${BuildConfig.VERSION_NAME}", -): ChannelConfig diff --git a/services/code/src/main/java/com/getcode/analytics/AnalyticsService.kt b/services/code/src/main/java/com/getcode/analytics/AnalyticsService.kt deleted file mode 100644 index 819eaa4a5..000000000 --- a/services/code/src/main/java/com/getcode/analytics/AnalyticsService.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.getcode.analytics - -import com.getcode.libs.analytics.AnalyticsService -import com.getcode.services.model.AppSetting -import com.getcode.model.CurrencyCode -import com.getcode.model.Kin -import com.getcode.model.KinAmount -import com.getcode.solana.keys.PublicKey - -interface CodeAnalyticsService : AnalyticsService { - fun login(ownerPublicKey: String, autoCompleteCount: Int, inputChangeCount: Int) - fun logout() - fun createAccount(isSuccessful: Boolean, ownerPublicKey: String?) - fun billTimeoutReached( - kin: Kin, - currencyCode: CurrencyCode, - animation: CodeAnalyticsManager.BillPresentationStyle - ) - - fun billShown( - kin: Kin, - currencyCode: CurrencyCode, - animation: CodeAnalyticsManager.BillPresentationStyle - ) - - fun billHidden( - kin: Kin, - currencyCode: CurrencyCode, - animation: CodeAnalyticsManager.BillPresentationStyle - ) - - fun transfer(amount: KinAmount, successful: Boolean) - fun transferForRequest(amount: KinAmount, successful: Boolean) - fun transferForTip(amount: KinAmount, successful: Boolean) - fun remoteSendOutgoing(kin: Kin, currencyCode: CurrencyCode) - fun remoteSendIncoming(kin: Kin, currencyCode: CurrencyCode, isVoiding: Boolean) - fun recomputed(fxIn: Double, fxOut: Double) - fun grabStart() - fun grab(kin: Kin, currencyCode: CurrencyCode) - fun requestShown(amount: KinAmount) - fun requestHidden(amount: KinAmount) - fun cashLinkGrabStart() - fun cashLinkGrab(kin: Kin, currencyCode: CurrencyCode) - fun migration(amount: Kin) - fun upgradePrivacy(successful: Boolean, intentId: PublicKey, actionCount: Int) - fun onBillReceived() - fun withdrawal(amount: KinAmount, successful: Boolean) - - fun tipCardShown(username: String) - fun tipCardLinked() - - fun backgroundSwapInitiated() - - fun appSettingToggled(setting: AppSetting, value: Boolean) - - fun photoScanned(successful: Boolean, timeToScanInMillis: Long) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/analytics/CodeAnalyticsManager.kt b/services/code/src/main/java/com/getcode/analytics/CodeAnalyticsManager.kt deleted file mode 100644 index ad2a0400c..000000000 --- a/services/code/src/main/java/com/getcode/analytics/CodeAnalyticsManager.kt +++ /dev/null @@ -1,458 +0,0 @@ -package com.getcode.analytics - -import com.getcode.libs.analytics.AppAction -import com.getcode.libs.analytics.AppActionSource -import com.getcode.services.BuildConfig -import com.getcode.services.model.AppSetting -import com.getcode.model.Kin -import com.getcode.model.KinAmount -import com.getcode.services.model.PrefsBool -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.base58 -import com.google.firebase.ktx.Firebase -import com.google.firebase.perf.ktx.performance -import com.google.firebase.perf.metrics.Trace -import com.mixpanel.android.mpmetrics.MixpanelAPI -import org.json.JSONObject -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -sealed class Action : AppAction { - data object CreateAccount : Action() { - override val value: String = "Create Account" - } - - data object EnterPhone : Action() { - override val value: String = "Enter Phone" - } - - data object VerifyPhone : Action() { - override val value: String = "Verify Phone" - } - - data object ConfirmAccessKey : Action() { - override val value: String = "Confirm Access Key" - } - - data object CompletedOnboarding : Action() { - override val value: String = "Completed Onboarding" - } - - data object OpenConnectAccount : Action() { - override val value: String = "Open Connect X Screen" - } - - data object MessageCodeOnX : Action() { - override val value: String = "Message Code on X" - } - - override fun toString(): String { - return "Action: $value" - } -} - -sealed class ActionSource: AppActionSource { - data object AccessKeySaved: ActionSource() { - override val value: String = "Saved to Photos" - } - - data object AccessKeyWroteDown: ActionSource() { - override val value: String = "Wrote it Down" - } -} - -@Singleton -class CodeAnalyticsManager @Inject constructor( - private val mixpanelAPI: MixpanelAPI -) : CodeAnalyticsService { - private var grabStartMillis: Long = 0L - private var cashLinkGrabStartMillis: Long = 0L - - private var traceAppInit: Trace? = null - private var timeAppInit: Long? = null - - override fun onAppStart() { - timeAppInit = System.currentTimeMillis() - traceAppInit = Firebase.performance.newTrace("Init") - traceAppInit?.start() - } - - override fun onAppStarted() { - traceAppInit ?: return - traceAppInit?.stop() - traceAppInit = null - Timber.i("App init time: " + (System.currentTimeMillis() - (timeAppInit ?: 0))) - } - - override fun logout() { - track(Name.Logout) - } - - override fun login(ownerPublicKey: String, autoCompleteCount: Int, inputChangeCount: Int) { - track( - Name.Login, - Pair(Property.OwnerPublicKey, ownerPublicKey), - Pair(Property.AutoCompleteCount, autoCompleteCount.toString()), - Pair(Property.InputChangeCount, inputChangeCount.toString()), - ) - } - - override fun createAccount(isSuccessful: Boolean, ownerPublicKey: String?) { - track( - Name.CreateAccount, - Pair(Property.Result, isSuccessful.toString()), - Pair(Property.OwnerPublicKey, ownerPublicKey.orEmpty()) - ) - } - - override fun unintentionalLogout() { - track(Name.UnintentionalLogout) - } - - - override fun billTimeoutReached( - kin: Kin, - currencyCode: com.getcode.model.CurrencyCode, - animation: BillPresentationStyle - ) { - track( - Name.Bill, - Pair(Property.State, StringValue.TimedOut.value), - Pair(Property.Amount, kin.toKin().toInt().toString()), - Pair(Property.Currency, currencyCode.name), - Pair(Property.Animation, animation.value) - ) - } - - override fun billShown( - kin: Kin, - currencyCode: com.getcode.model.CurrencyCode, - animation: BillPresentationStyle - ) { - track( - Name.Bill, - Pair(Property.State, StringValue.Shown.value), - Pair(Property.Amount, kin.toKin().toInt().toString()), - Pair(Property.Currency, currencyCode.name), - Pair(Property.Animation, animation.value) - ) - } - - override fun billHidden( - kin: Kin, - currencyCode: com.getcode.model.CurrencyCode, - animation: BillPresentationStyle - ) { - track( - Name.Bill, - Pair(Property.State, StringValue.Hidden.value), - Pair(Property.Amount, kin.toKin().toInt().toString()), - Pair(Property.Currency, currencyCode.name), - Pair(Property.Animation, animation.value) - ) - } - - override fun transfer(amount: KinAmount, successful: Boolean) { - track( - Name.Transfer, - Property.State to if (successful) StringValue.Success.value else StringValue.Failure.value, - Property.Amount to amount.kin.toKin().toInt().toString(), - Property.Fiat to amount.fiat.toString(), - Property.Fx to amount.rate.fx.toString(), - Property.Currency to amount.rate.currency.name, - ) - } - - override fun transferForRequest(amount: KinAmount, successful: Boolean) { - track( - Name.RequestPayment, - Property.State to if (successful) StringValue.Success.value else StringValue.Failure.value, - Property.Amount to amount.kin.toKin().toInt().toString(), - Property.Fiat to amount.fiat.toString(), - Property.Fx to amount.rate.fx.toString(), - Property.Currency to amount.rate.currency.name, - ) - } - - override fun transferForTip(amount: KinAmount, successful: Boolean) { - track( - Name.Tip, - Property.State to if (successful) StringValue.Success.value else StringValue.Failure.value, - Property.Amount to amount.kin.toKin().toInt().toString(), - Property.Fiat to amount.fiat.toString(), - Property.Fx to amount.rate.fx.toString(), - Property.Currency to amount.rate.currency.name, - ) - } - - override fun remoteSendOutgoing(kin: Kin, currencyCode: com.getcode.model.CurrencyCode) { - track( - Name.RemoteSendOutgoing, - Property.Amount to kin.toKin().toInt().toString(), - Property.Currency to currencyCode.name - ) - } - - override fun remoteSendIncoming( - kin: Kin, - currencyCode: com.getcode.model.CurrencyCode, - isVoiding: Boolean - ) { - track( - Name.RemoteSendIncoming, - Property.VoidingSend to if (isVoiding) StringValue.Yes.value else StringValue.No.value, - Property.Amount to kin.toKin().toInt().toString(), - Property.Currency to currencyCode.name - ) - } - - override fun recomputed(fxIn: Double, fxOut: Double) { - val delta = ((fxOut / fxIn) - 1) * 100 - track( - Name.Recompute, - Property.PercentDelta to delta.toString() - ) - } - - override fun grabStart() { - grabStartMillis = System.currentTimeMillis() - } - - override fun grab(kin: Kin, currencyCode: com.getcode.model.CurrencyCode) { - if (grabStartMillis == 0L) return - val millisecondsToGrab = System.currentTimeMillis() - grabStartMillis - track( - Name.Grab, - Pair(Property.Amount, kin.toKin().toInt().toString()), - Pair(Property.Currency, currencyCode.name), - Pair(Property.GrabTime, String.format("%,.2f", millisecondsToGrab / 1000.0)) - ) - grabStartMillis = 0 - } - - override fun requestShown(amount: KinAmount) { - track( - Name.Request, - Property.State to StringValue.Shown.value, - Property.Amount to amount.kin.toKin().toInt().toString(), - Property.Fiat to amount.fiat.toString(), - Property.Currency to amount.rate.currency.name, - ) - } - - override fun requestHidden(amount: KinAmount) { - track( - Name.Request, - Property.State to StringValue.Hidden.value, - Property.Amount to amount.kin.toKin().toInt().toString(), - Property.Fiat to amount.fiat.toString(), - Property.Currency to amount.rate.currency.name, - ) - } - - override fun cashLinkGrabStart() { - cashLinkGrabStartMillis = System.currentTimeMillis() - } - - override fun cashLinkGrab(kin: Kin, currencyCode: com.getcode.model.CurrencyCode) { - if (cashLinkGrabStartMillis == 0L) return - val millisecondsToGrab = System.currentTimeMillis() - cashLinkGrabStartMillis - track( - Name.CashLinkGrab, - Pair(Property.Amount, kin.toKin().toInt().toString()), - Pair(Property.Currency, currencyCode.name), - Pair(Property.GrabTime, String.format("%,.2f", millisecondsToGrab / 1000.0)) - ) - cashLinkGrabStartMillis = 0 - } - - override fun migration(amount: Kin) { - track( - Name.PrivacyMigration, - Pair(Property.Amount, amount.toKin().toInt().toString()) - ) - } - - override fun upgradePrivacy(successful: Boolean, intentId: PublicKey, actionCount: Int) { - track( - Name.UpgradePrivacy, - Pair( - Property.State, - if (successful) StringValue.Success.value else StringValue.Failure.value - ), - Pair(Property.IntentId, intentId.base58()), - Pair(Property.ActionCount, actionCount.toString()) - ) - } - - override fun withdrawal(amount: KinAmount, successful: Boolean) { - track( - Name.Withdrawal, - Property.State to if (successful) StringValue.Success.value else StringValue.Failure.value, - Property.Amount to amount.kin.toKin().toInt().toString(), - Property.Fiat to amount.fiat.toString(), - Property.Fx to amount.rate.fx.toString(), - Property.Currency to amount.rate.currency.name, - ) - } - - override fun onBillReceived() { - Timber.i("Bill scanned. From start: " + (System.currentTimeMillis() - (timeAppInit ?: 0))) - } - - override fun tipCardShown(username: String) { - track( - Name.TipCard, - Property.State to StringValue.Shown.value, - Property.xUsername to username, - ) - } - - override fun tipCardLinked() { - track(Name.TipCardLinked) - } - - override fun backgroundSwapInitiated() { - track(Name.BackgroundSwap) - } - - override fun appSettingToggled(setting: AppSetting, value: Boolean) { - val name = when (setting) { - PrefsBool.CAMERA_START_BY_DEFAULT -> Name.AutoStartCamera - PrefsBool.REQUIRE_BIOMETRICS -> Name.RequireBiometrics - } - - track( - name, - Property.State to if (value) StringValue.Yes.value else StringValue.No.value, - ) - } - - override fun photoScanned(successful: Boolean, timeToScanInMillis: Long) { - track( - Name.PhotoScanned, - Property.Result to successful.toString(), - Property.Time to timeToScanInMillis.toInt().toString() - ) - } - - override fun action(action: AppAction, source: AppActionSource?) { - track( - name = action.value, - properties = source?.let { arrayOf(Property.Source.value to it.value) }.orEmpty() - ) - } - - private fun track(event: Name, vararg properties: Pair) { - track( - name = event.value, - properties = properties.map { it.first.value to it.second }.toTypedArray() - ) - } - - private fun track(name: String, vararg properties: Pair) { - if (BuildConfig.DEBUG) { - Timber.d("debug track $name, ${properties.map { "${it.first}, ${it.second}" }}") - return - } //no logging in debug - - val jsonObject = JSONObject() - properties.forEach { property -> - jsonObject.put(property.first, property.second) - } - mixpanelAPI.track(name, jsonObject) - } - - enum class Name(val value: String) { - //Account - Logout("Logout"), - Login("Login"), - CreateAccount("Create Account"), - UnintentionalLogout("Unintentional Logout"), - TipCardLinked("Tip Card Linked"), - - //Bill - Bill("Bill"), - Request("Request Card"), - TipCard("Tip Card"), - - //Transfer - Transfer("Transfer"), - RequestPayment("Request Payment"), - Tip("Tip"), - RemoteSendOutgoing("Remote Send Outgoing"), - RemoteSendIncoming("Remote Send Incoming"), - Grab("Grab"), - CashLinkGrab("Cash Link Grab"), - UpgradePrivacy("Upgrade Privacy"), - ClaimGetFreeKin("Claim Get Free Kin"), - PrivacyMigration("Privacy Migration"), - BackgroundSwap("Background Swap Initiated"), - Withdrawal("Withdrawal"), - PhotoScanned("Photo Scanned"), - - // Errors - ErrorRequest("Error Request"), - - Recompute("Recompute"), - - // App Settings - AutoStartCamera("Camera Auto Start"), - RequireBiometrics("Require Biometrics") - } - - enum class Property(val value: String) { - //Open - Screen("Screen"), - - // Account - OwnerPublicKey("Owner Public Key"), - AutoCompleteCount("Auto-complete count"), - InputChangeCount("Input change count"), - Result("Result"), - MillisecondsToConfirm("Milliseconds to confirm"), - GrabTime("Grab Time"), - Time("Time"), - - // Bill - State("State"), - Amount("Amount"), - Fiat("Fiat"), - Fx("Exchange Rate"), - Currency("Currency"), - Animation("Animation"), - Rendezvous("Rendezvous"), - xUsername("X Username"), - - // Validation - Type("Type"), - Error("Error"), - - // Privacy Upgrade - IntentId("Intent ID"), - ActionCount("Action Count"), - - // Remote Send - VoidingSend("Voiding Send"), - - PercentDelta("Percent Delta"), - - Source("Source"), - } - - enum class BillPresentationStyle(val value: String) { - Pop("Pop"), - Slide("Slide"), - } - - enum class StringValue(val value: String) { - Success("Success"), - Failure("Failure"), - Yes("Yes"), - No("No"), - Shown("Shown"), - Hidden("Hidden"), - TimedOut("Timed Out"), - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/annotations/CodeManagedChannel.kt b/services/code/src/main/java/com/getcode/annotations/CodeManagedChannel.kt deleted file mode 100644 index 9a6839090..000000000 --- a/services/code/src/main/java/com/getcode/annotations/CodeManagedChannel.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.getcode.annotations - -import javax.inject.Qualifier - -@Qualifier -@Target( - AnnotationTarget.FUNCTION, - AnnotationTarget.PROPERTY_GETTER, - AnnotationTarget.PROPERTY_SETTER, - AnnotationTarget.VALUE_PARAMETER, - AnnotationTarget.FIELD -) -annotation class CodeManagedChannel \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/db/CodeAppDatabase.kt b/services/code/src/main/java/com/getcode/db/CodeAppDatabase.kt deleted file mode 100644 index 1efd253cd..000000000 --- a/services/code/src/main/java/com/getcode/db/CodeAppDatabase.kt +++ /dev/null @@ -1,196 +0,0 @@ -package com.getcode.db - -import android.content.Context -import androidx.room.AutoMigration -import androidx.room.Database -import androidx.room.DeleteColumn -import androidx.room.DeleteTable -import androidx.room.RenameColumn -import androidx.room.Room -import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import androidx.room.migration.AutoMigrationSpec -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase -import com.getcode.model.* -import com.getcode.services.db.ClosableDatabase -import com.getcode.services.db.SharedConverters -import com.getcode.services.model.PrefBool -import com.getcode.services.model.PrefDouble -import com.getcode.services.model.PrefInt -import com.getcode.services.model.PrefString -import com.getcode.utils.TraceType -import com.getcode.utils.trace -import io.reactivex.rxjava3.core.BackpressureStrategy -import io.reactivex.rxjava3.subjects.BehaviorSubject -import org.kin.sdk.base.tools.subByteArray -import java.io.File - -@Database( - entities = [ - CurrencyRate::class, - FaqItem::class, - PrefInt::class, - PrefString::class, - PrefBool::class, - PrefDouble::class, - GiftCard::class, - ExchangeRate::class, - ], - autoMigrations = [ - AutoMigration(from = 7, to = 8, spec = CodeAppDatabase.Migration7To8::class), - AutoMigration(from = 8, to = 9, spec = CodeAppDatabase.Migration8To9::class), - AutoMigration(from = 10, to = 11, spec = CodeAppDatabase.Migration10To11::class), - AutoMigration(from = 11, to = 12, spec = CodeAppDatabase.Migration11To12::class), - AutoMigration(from = 12, to = 13, spec = CodeAppDatabase.Migration12To13::class), - AutoMigration(from = 13, to = 14, spec = CodeAppDatabase.Migration13To14::class), - AutoMigration(from = 14, to = 15, spec = CodeAppDatabase.Migration14To15::class), - AutoMigration(from = 15, to = 16, spec = CodeAppDatabase.Migration15To16::class), - ], - version = 16 -) -@TypeConverters(SharedConverters::class) -abstract class CodeAppDatabase : RoomDatabase(), ClosableDatabase { - abstract fun prefIntDao(): RxPrefIntDao - abstract fun prefStringDao(): RxPrefStringDao - abstract fun prefBoolDao(): RxPrefBoolDao - abstract fun prefDoubleDao(): RxPrefDoubleDao - abstract fun giftCardDao(): GiftCardDao - abstract fun exchangeDao(): ExchangeDao - - @DeleteTable(tableName = "HistoricalTransaction") - class Migration7To8 : AutoMigrationSpec - - @DeleteTable(tableName = "SendLimit") - class Migration8To9 : AutoMigrationSpec - - class Migration10To11 : Migration(10, 11), AutoMigrationSpec { - override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL("DROP TABLE messages") - } - } - - @RenameColumn.Entries( - RenameColumn( - tableName = "conversations", - fromColumnName = "messageIdBase58", - toColumnName = "idBase58" - ) - ) - @DeleteColumn.Entries( - DeleteColumn( - tableName = "conversations", - columnName = "cursorBase58" - ), - DeleteColumn( - tableName = "conversations", - columnName = "tipAmount" - ), - DeleteColumn( - tableName = "conversations", - columnName = "createdByUser" - ) - ) - class Migration11To12 : Migration(11, 12), AutoMigrationSpec { - override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL("DROP TABLE messages") - } - } - - @DeleteColumn.Entries( - DeleteColumn( - tableName = "messages", - columnName = "content" - ) - ) - @DeleteTable("messages_remote_keys") - class Migration12To13 : Migration(12, 13), AutoMigrationSpec { - override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL("DROP TABLE messages") - } - } - - @DeleteColumn.Entries( - DeleteColumn( - tableName = "messages", - columnName = "status" - ) - ) - class Migration13To14 : AutoMigrationSpec - - @DeleteColumn.Entries( - DeleteColumn( - tableName = "conversations", - columnName = "user" - ), - DeleteColumn( - tableName = "conversations", - columnName = "userImage" - ) - ) - class Migration14To15 : AutoMigrationSpec - - /** - * Chat removed from Code and moved to Flipchat - */ - @DeleteTable("conversations") - @DeleteTable("conversation_pointers") - @DeleteTable("messages") - @DeleteTable("conversation_intent_id_mapping") - @DeleteTable("message_contents") - class Migration15To16 : AutoMigrationSpec - - override fun closeDb() { - instance?.close() - instance = null - isInitSubject.onNext(false) - } - - override fun deleteDb(context: Context) { - instance?.close() - if (dbName.isEmpty()) return - - val databases = File(context.applicationInfo.dataDir + "/databases") - val db = File(databases, dbName) - db.delete() - - val journal = File(databases, "$dbName-journal") - val shm = File(databases, "$dbName-shm") - val wal = File(databases, "$dbName-wal") - - if (journal.exists()) journal.delete() - if (shm.exists()) shm.delete() - if (wal.exists()) shm.delete() - isInitSubject.onNext(false) - } - - companion object { - private var instance: CodeAppDatabase? = null - private var isInitSubject: BehaviorSubject = BehaviorSubject.create() - var isInit = isInitSubject.toFlowable(BackpressureStrategy.DROP).filter { true } - fun isOpen() = instance?.isOpen == true - fun getInstance() = instance - fun requireInstance() = instance!! - private var dbName: String = "" - - private const val dbNamePrefix = "AppDatabase" - private const val dbNameSuffix = ".db" - - fun init(context: Context, entropyB64: String) { - trace("database init start", type = TraceType.Process) - instance?.close() - val dbUniqueName = - com.getcode.vendor.Base58.encode(entropyB64.toByteArray().subByteArray(0, 3)) - dbName = "$dbNamePrefix-$dbUniqueName$dbNameSuffix" - - instance = - Room.databaseBuilder(context, CodeAppDatabase::class.java, dbName) -// .openHelperFactory(SupportFactory(entropyB64.decodeBase64(), null, false)) - .fallbackToDestructiveMigration() - .build() - - isInitSubject.onNext(true) - trace("database init end", type = TraceType.Process) - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/db/ExchangeDao.kt b/services/code/src/main/java/com/getcode/db/ExchangeDao.kt deleted file mode 100644 index 407b2700c..000000000 --- a/services/code/src/main/java/com/getcode/db/ExchangeDao.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.getcode.db - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.getcode.model.Rate -import com.getcode.model.ExchangeRate -import kotlinx.coroutines.flow.Flow - -@Dao -interface ExchangeDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(rates: List, syncedAt: Long) { - insert(*rates.map { ExchangeRate(it.fx, it.currency, syncedAt) }.toTypedArray()) - } - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(vararg rate: ExchangeRate) - - @Query("SELECT * FROM exchangeData") - fun observeRates(): Flow> - - @Query("SELECT * FROM exchangeData") - suspend fun query(): List - - @Query("DELETE FROM exchangeData") - fun clear() -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/db/GiftCardDao.kt b/services/code/src/main/java/com/getcode/db/GiftCardDao.kt deleted file mode 100644 index 373d55d61..000000000 --- a/services/code/src/main/java/com/getcode/db/GiftCardDao.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.getcode.db - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.getcode.model.GiftCard -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Maybe - - -@Dao -interface GiftCardDao { - @Query("SELECT * FROM GiftCard WHERE `key` = :key") - fun get(key: String): Flowable - - @Query("SELECT * FROM GiftCard WHERE `key` = :key") - fun getMaybe(key: String): Maybe - - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(item: GiftCard) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/db/InMemoryDao.kt b/services/code/src/main/java/com/getcode/db/InMemoryDao.kt deleted file mode 100644 index 0e670f0f6..000000000 --- a/services/code/src/main/java/com/getcode/db/InMemoryDao.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.getcode.db - -import com.getcode.network.repository.BalanceRepository -import com.getcode.network.repository.TransactionRepository -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class InMemoryDao @Inject constructor( - private val balanceRepository: BalanceRepository, - private val transactionRepository: TransactionRepository -) { - fun clear() { - balanceRepository.clearBalance() - transactionRepository.clear() - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/db/RxPrefBoolDao.kt b/services/code/src/main/java/com/getcode/db/RxPrefBoolDao.kt deleted file mode 100644 index 807f26fca..000000000 --- a/services/code/src/main/java/com/getcode/db/RxPrefBoolDao.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.getcode.db - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.getcode.services.model.PrefBool -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Maybe -import kotlinx.coroutines.flow.Flow - -@Dao -interface RxPrefBoolDao { - @Query("SELECT * FROM PrefBool WHERE key = :key") - fun get(key: String): Flowable - @Query("SELECT * FROM PrefBool WHERE key = :key") - fun observe(key: String): Flow - - @Query("SELECT * FROM PrefBool WHERE key = :key") - fun getMaybe(key: String): Maybe - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(item: PrefBool) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/db/RxPrefDoubleDao.kt b/services/code/src/main/java/com/getcode/db/RxPrefDoubleDao.kt deleted file mode 100644 index afccac2eb..000000000 --- a/services/code/src/main/java/com/getcode/db/RxPrefDoubleDao.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.getcode.db - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.getcode.services.model.PrefDouble -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Maybe -import kotlinx.coroutines.flow.Flow - -@Dao -interface RxPrefDoubleDao { - @Query("SELECT * FROM PrefDouble WHERE `key` = :key") - fun get(key: String): Flowable - - @Query("SELECT * FROM PrefDouble WHERE key = :key") - fun observe(key: String): Flow - - @Query("SELECT * FROM PrefDouble WHERE `key` = :key") - fun getMaybe(key: String): Maybe - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(item: PrefDouble) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/db/RxPrefIntDao.kt b/services/code/src/main/java/com/getcode/db/RxPrefIntDao.kt deleted file mode 100644 index 4ca5263bf..000000000 --- a/services/code/src/main/java/com/getcode/db/RxPrefIntDao.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.getcode.db - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.getcode.services.model.PrefInt -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Maybe -import kotlinx.coroutines.flow.Flow - -@Dao -interface RxPrefIntDao { - @Query("SELECT * FROM PrefInt WHERE key = :key") - fun get(key: String): Flowable - - @Query("SELECT * FROM PrefInt WHERE key = :key") - fun observe(key: String): Flow - - @Query("SELECT * FROM PrefInt WHERE key = :key") - fun getMaybe(key: String): Maybe - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(item: PrefInt) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/db/RxPrefStringDao.kt b/services/code/src/main/java/com/getcode/db/RxPrefStringDao.kt deleted file mode 100644 index abad6f804..000000000 --- a/services/code/src/main/java/com/getcode/db/RxPrefStringDao.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.getcode.db - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.getcode.services.model.PrefString -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Maybe -import kotlinx.coroutines.flow.Flow - -@Dao -interface RxPrefStringDao { - @Query("SELECT * FROM PrefString WHERE key = :key") - fun get(key: String): Flowable - - @Query("SELECT * FROM PrefString WHERE key = :key") - fun observe(key: String): Flow - - @Query("SELECT * FROM PrefString WHERE key = :key") - fun getMaybe(key: String): Maybe - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(item: PrefString) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/generator/Generator.kt b/services/code/src/main/java/com/getcode/generator/Generator.kt deleted file mode 100644 index e21ce8993..000000000 --- a/services/code/src/main/java/com/getcode/generator/Generator.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.getcode.generator - -interface Generator { - fun generate(predicate: D): R -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/generator/GiftCardGenerator.kt b/services/code/src/main/java/com/getcode/generator/GiftCardGenerator.kt deleted file mode 100644 index 45ce169ee..000000000 --- a/services/code/src/main/java/com/getcode/generator/GiftCardGenerator.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.getcode.generator - -import com.getcode.crypt.MnemonicPhrase -import com.getcode.solana.organizer.GiftCardAccount -import javax.inject.Inject - -class GiftCardGenerator @Inject constructor( -): Generator { - override fun generate(predicate: MnemonicPhrase?): GiftCardAccount { - return GiftCardAccount.newInstance(predicate) - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/generator/MnemonicGenerator.kt b/services/code/src/main/java/com/getcode/generator/MnemonicGenerator.kt deleted file mode 100644 index f2ca12d11..000000000 --- a/services/code/src/main/java/com/getcode/generator/MnemonicGenerator.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.getcode.generator - -import com.getcode.crypt.MnemonicPhrase -import com.getcode.services.utils.Base58String -import com.getcode.services.utils.Base64String -import javax.inject.Inject - -class MnemonicGenerator @Inject constructor( -): Generator { - - override fun generate(predicate: Base64String): MnemonicPhrase { - return MnemonicPhrase.fromEntropyB64(predicate) - } - - fun generateFromBase58(predicate: Base58String): MnemonicPhrase { - return MnemonicPhrase.fromEntropyB58(predicate) - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/generator/OrganizerGenerator.kt b/services/code/src/main/java/com/getcode/generator/OrganizerGenerator.kt deleted file mode 100644 index f2333ef54..000000000 --- a/services/code/src/main/java/com/getcode/generator/OrganizerGenerator.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.getcode.generator - -import com.getcode.crypt.MnemonicPhrase -import com.getcode.solana.organizer.Organizer -import javax.inject.Inject - -class OrganizerGenerator @Inject constructor(): Generator { - - override fun generate(predicate: MnemonicPhrase): Organizer { - return Organizer.newInstance(predicate) - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/inject/CodeApiModule.kt b/services/code/src/main/java/com/getcode/inject/CodeApiModule.kt deleted file mode 100644 index 18856d2eb..000000000 --- a/services/code/src/main/java/com/getcode/inject/CodeApiModule.kt +++ /dev/null @@ -1,190 +0,0 @@ -package com.getcode.inject - -import android.content.Context -import com.getcode.CodeServicesConfig -import com.getcode.analytics.CodeAnalyticsManager -import com.getcode.analytics.CodeAnalyticsService -import com.getcode.annotations.CodeManagedChannel -import com.getcode.libs.logging.BuildConfig -import com.getcode.network.BalanceController -import com.getcode.network.PrivacyMigration -import com.getcode.network.api.TransactionApiV2 -import com.getcode.network.client.Client -import com.getcode.network.client.TransactionReceiver -import com.getcode.network.exchange.CodeExchange -import com.getcode.network.exchange.Exchange -import com.getcode.network.repository.AccountRepository -import com.getcode.network.repository.BalanceRepository -import com.getcode.network.repository.IdentityRepository -import com.getcode.network.repository.MessagingRepository -import com.getcode.network.repository.PrefRepository -import com.getcode.network.repository.TransactionRepository -import com.getcode.network.service.AccountService -import com.getcode.network.service.ChatService -import com.getcode.network.service.CurrencyService -import com.getcode.network.service.DeviceService -import com.getcode.services.db.CurrencyProvider -import com.getcode.services.manager.MnemonicManager -import com.getcode.services.network.core.NetworkOracle -import com.getcode.services.network.core.NetworkOracleImpl -import com.getcode.services.utils.logging.LoggingClientInterceptor -import com.getcode.utils.CurrencyUtils -import com.getcode.utils.network.NetworkConnectivityListener -import com.mixpanel.android.mpmetrics.MixpanelAPI -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import io.grpc.ManagedChannel -import io.grpc.android.AndroidChannelBuilder -import org.kin.sdk.base.network.api.agora.OkHttpChannelBuilderForcedTls12 -import java.util.concurrent.TimeUnit -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object CodeApiModule { - - @Provides - fun provideNetworkOracle(): NetworkOracle { - return NetworkOracleImpl() - } - - @Singleton - @Provides - fun providesServicesConfig(): CodeServicesConfig { - return CodeServicesConfig() - } - - @Singleton - @Provides - @CodeManagedChannel - fun provideManagedChannel( - @ApplicationContext context: Context, - config: CodeServicesConfig, - ): ManagedChannel { - return AndroidChannelBuilder - .usingBuilder(OkHttpChannelBuilderForcedTls12.forAddress(config.baseUrl, config.port)) - .context(context) - .userAgent(config.userAgent) - .keepAliveTime(config.keepAlive.inWholeMilliseconds, TimeUnit.MILLISECONDS) - .apply { - if (BuildConfig.DEBUG) { - this.intercept(LoggingClientInterceptor()) - } - } - .build() - } - - @Singleton - @Provides - fun provideClient( - identityRepository: IdentityRepository, - transactionRepository: TransactionRepository, - messagingRepository: MessagingRepository, - accountRepository: AccountRepository, - accountService: AccountService, - balanceController: BalanceController, - analytics: CodeAnalyticsService, - prefRepository: PrefRepository, - transactionReceiver: TransactionReceiver, - exchange: Exchange, - networkObserver: NetworkConnectivityListener, - chatService: ChatService, - deviceService: DeviceService, - mnemonicManager: MnemonicManager, - ): Client { - return Client( - identityRepository, - transactionRepository, - messagingRepository, - balanceController, - accountRepository, - accountService, - analytics, - prefRepository, - exchange, - transactionReceiver, - networkObserver, - chatService, - deviceService, - mnemonicManager - ) - } - - @Singleton - @Provides - fun provideBalanceRepository( - ): BalanceRepository { - return BalanceRepository() - } - - @Singleton - @Provides - fun provideBalanceController( - exchange: Exchange, - balanceRepository: BalanceRepository, - transactionRepository: TransactionRepository, - accountRepository: AccountRepository, - privacyMigration: PrivacyMigration, - transactionReceiver: TransactionReceiver, - networkObserver: NetworkConnectivityListener, - currencyUtils: CurrencyUtils, - currencyProvider: CurrencyProvider, - ): BalanceController { - return BalanceController( - exchange = exchange, - balanceRepository = balanceRepository, - transactionRepository = transactionRepository, - accountRepository = accountRepository, - privacyMigration = privacyMigration, - transactionReceiver = transactionReceiver, - networkObserver = networkObserver, - getCurrencyFromCode = { - it?.name?.let(currencyUtils::getCurrency) - }, - suffix = { currency -> currencyProvider.suffix(currency) } - ) - } - - @Singleton - @Provides - fun providesExchange( - currencyService: CurrencyService, - prefRepository: PrefRepository, - currencyProvider: CurrencyProvider, - ): Exchange = CodeExchange( - currencyService = currencyService, - prefs = prefRepository, - preferredCurrency = { currencyProvider.preferredCurrency() }, - defaultCurrency = { currencyProvider.defaultCurrency() } - ) - - @Singleton - @Provides - fun providePrivacyMigration( - transactionRepository: TransactionRepository, - analytics: CodeAnalyticsService, - ): PrivacyMigration { - return PrivacyMigration( - transactionRepository, - analytics - ) - } - - @Singleton - @Provides - fun provideTransactionRepository( - @ApplicationContext context: Context, - transactionApi: TransactionApiV2, - ): TransactionRepository { - return TransactionRepository(transactionApi = transactionApi, context = context) - } - - // TODO: - @Provides - fun providesAnalyticsService( - mixpanelAPI: MixpanelAPI - ): CodeAnalyticsService = CodeAnalyticsManager(mixpanelAPI) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/manager/CashLinkManager.kt b/services/code/src/main/java/com/getcode/manager/CashLinkManager.kt deleted file mode 100644 index 88caab841..000000000 --- a/services/code/src/main/java/com/getcode/manager/CashLinkManager.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.getcode.manager - -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.KinAmount -import com.getcode.network.repository.SendTransactionRepository -import com.getcode.solana.organizer.Organizer -import com.getcode.utils.ErrorUtils -import io.reactivex.rxjava3.disposables.Disposable -import java.util.Timer -import java.util.TimerTask -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.concurrent.schedule - -@Singleton -class CashLinkManager @Inject constructor( - private val sendTransactionRepository: SendTransactionRepository, -) { - private var billDismissTimer: TimerTask? = null - private var sendTransactionDisposable: Disposable? = null - - val amount: KinAmount - get() = sendTransactionRepository.getAmount() - - val rendezvous: KeyPair - get() = sendTransactionRepository.getRendezvous() - - fun awaitBillGrab( - amount: KinAmount, - organizer: Organizer, - owner: KeyPair, - present: (List) -> Unit, - onGrabbed: () -> Unit, - onTimeout: () -> Unit, - onError: (Throwable) -> Unit - ) { - sendTransactionDisposable?.dispose() - val payload = sendTransactionRepository.init(amount, organizer, owner) - sendTransactionDisposable = - sendTransactionRepository.startTransaction() - .subscribe({ - onGrabbed() - }, { - ErrorUtils.handleError(it) - onError(it) - }) - - presentSend(onTimeout) - - present(payload) - } - - private fun presentSend(onTimeout: () -> Unit) { - cancelBillTimeout() - billDismissTimer = Timer().schedule((1000 * 50).toLong()) { - onTimeout() - } - } - - fun cancelSend() { - cancelBillTimeout() - sendTransactionDisposable?.dispose() - } - - fun cancelBillTimeout() { - billDismissTimer?.cancel() - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/manager/GiftCardManager.kt b/services/code/src/main/java/com/getcode/manager/GiftCardManager.kt deleted file mode 100644 index 2ee1c5e41..000000000 --- a/services/code/src/main/java/com/getcode/manager/GiftCardManager.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.getcode.manager - -import com.getcode.crypt.MnemonicPhrase -import com.getcode.generator.GiftCardGenerator -import com.getcode.solana.organizer.GiftCardAccount -import javax.inject.Inject - -class GiftCardManager @Inject constructor( - private val generator: GiftCardGenerator -) { - fun createGiftCard(mnemonic: MnemonicPhrase? = null): GiftCardAccount { - return generator.generate(mnemonic) - } - - fun getEntropy(giftCard: GiftCardAccount): String { - return giftCard.mnemonicPhrase.getBase58EncodedEntropy() - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/manager/ModalManager.kt b/services/code/src/main/java/com/getcode/manager/ModalManager.kt deleted file mode 100644 index b6e1f9889..000000000 --- a/services/code/src/main/java/com/getcode/manager/ModalManager.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.getcode.manager - -import androidx.annotation.DrawableRes -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import java.util.UUID - -object ModalManager { - data class Message( - @DrawableRes - val icon: Int? = null, - val title: String, - val subtitle: String = "", - val positiveText: String, - val negativeText: String? = null, - val tertiaryText: String? = null, - val onPositive: () -> Unit, - val onNegative: () -> Unit = {}, - val onTertiary: () -> Unit = {}, - val onClose: (actionType: ActionType?) -> Unit = {}, - val type: MessageType = MessageType.DEFAULT, -// val isDismissibleByTouchOutside: Boolean = true, - val isDismissibleByBackButton: Boolean = true, - val timeoutSeconds: Int? = null, - val id: Long = UUID.randomUUID().mostSignificantBits, - ) - - private val _messages: MutableStateFlow> = MutableStateFlow( - listOf() - ) - val messages: StateFlow> get() = _messages.asStateFlow() - - fun showMessage(message: Message) { - _messages.update { currentMessages -> - currentMessages + message - } - } - - fun setMessageShown(messageId: Long) { - _messages.update { currentMessages -> - currentMessages.filterNot { it.id == messageId } - } - } - - fun clear() = _messages.update { listOf() } - - fun clearByType(type: MessageType) = _messages.update { it.filterNot { m -> m.type == type } } - - enum class MessageType { DEFAULT } - - enum class ActionType { - Positive, - Negative, - Tertiary - } - -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/manager/SessionManager.kt b/services/code/src/main/java/com/getcode/manager/SessionManager.kt deleted file mode 100644 index da13cfbb6..000000000 --- a/services/code/src/main/java/com/getcode/manager/SessionManager.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.getcode.manager - -import com.getcode.ed25519.Ed25519 -import com.getcode.generator.OrganizerGenerator -import com.getcode.network.client.Client -import com.getcode.network.client.registerInstallation -import com.getcode.network.client.updatePreferences -import com.getcode.services.manager.MnemonicManager -import com.getcode.solana.organizer.Organizer -import com.getcode.services.utils.installationId -import com.getcode.utils.trace -import com.google.firebase.Firebase -import com.google.firebase.installations.installations -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import javax.inject.Inject -import javax.inject.Singleton - - -// TODO: figure out a better naming paradigm b/w this and Session -@Singleton -class SessionManager @Inject constructor( - private val client: Client, - private val mnemonicManager: MnemonicManager, - private val organizerGenerator: OrganizerGenerator, -) { - data class SessionState( - val entropyB64: String? = null, - val keyPair: Ed25519.KeyPair? = null, - val isAuthenticated: Boolean? = null, - val isTimelockUnlocked: Boolean = false, - val organizer: Organizer? = null, - val userPrefsUpdated: Boolean = false, - ) - - fun set(entropyB64: String) { - val mnemonic = mnemonicManager.fromEntropyBase64(entropyB64) - if (getOrganizer()?.mnemonic?.words == mnemonic.words - && getOrganizer()?.ownerKeyPair == authState.value.keyPair - ) return - - val organizer = organizerGenerator.generate(mnemonic) - - update { - SessionState( - entropyB64 = entropyB64, - keyPair = organizer.ownerKeyPair, - isAuthenticated = true, - organizer = organizer, - ) - } - } - - fun clear() { - trace("Clearing session state") - update { - SessionState(entropyB64 = null, keyPair = null, isAuthenticated = false) - } - } - - suspend fun comeAlive(): Result { - val organizer = authState.value.organizer ?: return Result.success(false) - val installationId = Firebase.installations.installationId() - if (installationId != null) { - client.registerInstallation(organizer.ownerKeyPair, installationId) - } - - return client.updatePreferences(organizer) - .onSuccess { - update { it.copy(userPrefsUpdated = true) } - } - .onFailure { - update { it.copy(userPrefsUpdated = true) } - } - } - - companion object { - private val authStateMutable: MutableStateFlow = MutableStateFlow(SessionState()) - val authState: StateFlow - get() = authStateMutable.asStateFlow() - - fun update(function: (SessionState) -> SessionState) { - authStateMutable.update(function) - } - - fun getKeyPair(): Ed25519.KeyPair? = authState.value.keyPair - fun getOrganizer(): Organizer? = authState.value.organizer - fun getCurrentBalance() = authState.value.organizer?.availableBalance - fun isAuthenticated() = authState.value.isAuthenticated - - val entropyB64: String? - get() = authState.value.entropyB64 - } -} diff --git a/services/code/src/main/java/com/getcode/mapper/ChatMessageMapper.kt b/services/code/src/main/java/com/getcode/mapper/ChatMessageMapper.kt deleted file mode 100644 index 2c498ae76..000000000 --- a/services/code/src/main/java/com/getcode/mapper/ChatMessageMapper.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.getcode.mapper - - -import com.getcode.model.chat.ChatMessage -import com.getcode.model.chat.MessageContent -import com.getcode.model.protomapping.invoke -import com.getcode.services.mapper.Mapper -import javax.inject.Inject -import com.codeinc.gen.chat.v1.ChatService.ChatMessage as ApiChatMessage -import com.getcode.model.chat.ChatMessage as DomainChatMessage - -class ChatMessageMapper @Inject constructor( -): Mapper { - override fun map(from: ApiChatMessage): ChatMessage { - - val messageId = from.messageId.value.toList() - val contents = from.contentList.mapNotNull { MessageContent.invoke(it, messageId) } - val isFromSelf = contents.any { it.isFromSelf } - - return ChatMessage( - id = messageId, - senderId = emptyList(), - isFromSelf = isFromSelf, - cursor = from.cursor.value.toList(), - dateMillis = from.ts.seconds * 1_000L, - contents = contents, -// status = if (isFromSelf) MessageStatus.Sent else MessageStatus.Incoming - ) - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/mapper/ChatMetadataMapper.kt b/services/code/src/main/java/com/getcode/mapper/ChatMetadataMapper.kt deleted file mode 100644 index 275c6fca2..000000000 --- a/services/code/src/main/java/com/getcode/mapper/ChatMetadataMapper.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.getcode.mapper - -import com.codeinc.gen.chat.v1.ChatService -import com.getcode.model.chat.Chat -import com.getcode.model.chat.Title -import com.getcode.model.invoke -import com.getcode.services.mapper.Mapper -import javax.inject.Inject - -class ChatMetadataMapper @Inject constructor( -) : Mapper { - override fun map(from: ChatService.ChatMetadata): Chat { - return Chat( - id = from.chatId.value.toByteArray().toList(), - title = Title.invoke(from), - cursor = from.cursor.value.toByteArray().toList(), - canMute = from.canMute, - canUnsubscribe = from.canUnsubscribe, - messages = emptyList(), - // backwards compatibility fields - these are derived from [Chat#members] in V2 - _unreadCount = from.numUnread, - _isMuted = from.isMuted, - _isSubscribed = from.isSubscribed, - ) - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/AccountInfo.kt b/services/code/src/main/java/com/getcode/model/AccountInfo.kt deleted file mode 100644 index d440b9829..000000000 --- a/services/code/src/main/java/com/getcode/model/AccountInfo.kt +++ /dev/null @@ -1,268 +0,0 @@ -package com.getcode.model - -import com.codeinc.gen.account.v1.AccountService -import com.getcode.solana.organizer.AccountType - -data class AccountInfo ( - /// The account's derivation index for applicable account types. When this field - /// doesn't apply, a zero value is provided. - var index: Int, - - /// The type of token account, which infers its intended use. - var accountType: AccountType, - - /// The token account's address - var address: com.getcode.solana.keys.PublicKey, - - /// The owner of the token account, which can also be thought of as a parent - /// account that links to one or more token accounts. This is provided when - /// available. - var owner: com.getcode.solana.keys.PublicKey?, - - /// The token account's authority, which has access to moving funds for the - /// account. This can be the owner account under certain circumstances (eg. - /// ATA, primary account). This is provided when available. - var authority: com.getcode.solana.keys.PublicKey?, - - /// The source of truth for the balance calculation. - var balanceSource: BalanceSource, - - /// The Kin balance in quarks, as observed by Code. This may not reflect the - /// value on the blockchain and could be non-zero even if the account hasn't - /// been created. Use balance_source to determine how this value was calculated. - var balance: Kin, - - /// The state of the account as it pertains to Code's ability to manage funds. - var managementState: ManagementState, - - /// The state of the account on the blockchain. - var blockchainState: BlockchainState, - - /// Whether an account is claimed. This only applies to relevant account types - /// (eg. REMOTE_SEND_GIFT_CARD). - var claimState: ClaimState, - - /// For temporary incoming accounts only. Flag indicates whether client must - /// actively try rotating it by issuing a ReceivePayments intent. In general, - /// clients should wait as long as possible until this flag is true or requiring - /// the funds to send their next payment. - var mustRotate: Boolean, - - /// For account types used as an intermediary for sending money between two - /// users (eg. REMOTE_SEND_GIFT_CARD), this represents the original exchange - /// data used to fund the account. Over time, this value will become stale: - /// 1. Exchange rates will fluctuate, so the total fiat amount will differ. - /// 2. External entities can deposit additional funds into the account, so - /// the balance, in quarks, may be greater than the original quark value. - /// 3. The balance could have been received, so the total balance can show - /// as zero. - var originalKinAmount: KinAmount?, - - /// The relationship with a third party that this account has established with. - /// This only applies to relevant account types (eg. RELATIONSHIP). - var relationship: Relationship?, - - // Time the account was created, if available. For Code accounts, this is - // the time of intent submission. Otherwise, for external accounts, it is - // the time created on the blockchain. - var createdAt: Long?, - - ) { - companion object { - fun newInstance(info: AccountService.TokenAccountInfo): AccountInfo? { - val accountType = AccountType.newInstance(info.accountType, info.relationship) ?: return null - val address = - com.getcode.solana.keys.PublicKey(info.address.value.toByteArray().toList()) - val balanceSource = BalanceSource.getInstance(info.balanceSource) ?: return null - - val managementState = ManagementState.getInstance(info.managementState) ?: return null - val blockchainState = BlockchainState.getInstance(info.blockchainState) ?: return null - val claimState = ClaimState.getInstance(info.claimState) ?: return null - - val owner = com.getcode.solana.keys.PublicKey(info.owner.value.toByteArray().toList()) - val authority = - com.getcode.solana.keys.PublicKey(info.authority.value.toByteArray().toList()) - - val originalCurrency = CurrencyCode.tryValueOf(info.originalExchangeData.currency) - - val originalKinAmount = originalCurrency?.let { - KinAmount.newInstance( - kin = Kin(info.originalExchangeData.quarks), - rate = Rate( - fx = info.originalExchangeData.exchangeRate, - currency = originalCurrency - ) - ) - } - - val relationship = Domain.from(info.relationship.domain.value) - ?.let { Relationship(it) } - - return AccountInfo( - index = info.index.toInt(), - accountType = accountType, - address = address, - owner = owner, - authority = authority, - balanceSource = balanceSource, - balance = Kin(info.balance), - managementState = managementState, - blockchainState = blockchainState, - claimState = claimState, - mustRotate = info.mustRotate, - originalKinAmount = originalKinAmount, - relationship = relationship, - createdAt = info.createdAt.seconds * 1000L - ) - } - } - - enum class ManagementState { - /// The state of the account is unknown. This may be returned when the - /// data source is unstable and a reliable state cannot be determined. - Unknown, - - /// Code does not maintain a management state and won't move funds for this - /// account. - None, - - /// The account is in the process of transitioning to the LOCKED state. - Locking, - - /// The account's funds are locked and Code has co-signing authority. - Locked, - - /// The account is in the process of transitioning to the UNLOCKED state. - Unlocking, - - /// The account's funds are unlocked and Code no longer has co-signing - /// authority. The account must transition to the LOCKED state to have - /// management capabilities. - Unlocked, - - /// The account is in the process of transitioning to the CLOSED state. - Closing, - - /// The account has been closed and doesn't exist on the blockchain. - /// Subsequently, it also has a zero balance. - Closed; - - companion object { - fun getInstance(state: AccountService.TokenAccountInfo.ManagementState): ManagementState? { - return when (state) { - AccountService.TokenAccountInfo.ManagementState.MANAGEMENT_STATE_UNKNOWN -> Unknown - AccountService.TokenAccountInfo.ManagementState.MANAGEMENT_STATE_NONE -> None - AccountService.TokenAccountInfo.ManagementState.MANAGEMENT_STATE_LOCKING -> Locking - AccountService.TokenAccountInfo.ManagementState.MANAGEMENT_STATE_LOCKED -> Locked - AccountService.TokenAccountInfo.ManagementState.MANAGEMENT_STATE_UNLOCKING -> Unlocking - AccountService.TokenAccountInfo.ManagementState.MANAGEMENT_STATE_UNLOCKED -> Unlocked - AccountService.TokenAccountInfo.ManagementState.MANAGEMENT_STATE_CLOSING -> Closing - AccountService.TokenAccountInfo.ManagementState.MANAGEMENT_STATE_CLOSED -> Closed - AccountService.TokenAccountInfo.ManagementState.UNRECOGNIZED -> null - } - - } - } - } - - enum class BlockchainState { - /// The state of the account is unknown. This may be returned when the - /// data source is unstable and a reliable state cannot be determined. - Unknown, - - /// The account does not exist on the blockchain. - DoesntExist, - - /// The account is created and exists on the blockchain. - Exists; - - companion object { - fun getInstance(state: AccountService.TokenAccountInfo.BlockchainState): BlockchainState? { - return when (state) { - AccountService.TokenAccountInfo.BlockchainState.BLOCKCHAIN_STATE_UNKNOWN -> Unknown - AccountService.TokenAccountInfo.BlockchainState.BLOCKCHAIN_STATE_DOES_NOT_EXIST -> DoesntExist - AccountService.TokenAccountInfo.BlockchainState.BLOCKCHAIN_STATE_EXISTS -> Exists - AccountService.TokenAccountInfo.BlockchainState.UNRECOGNIZED -> null - } - } - } - } - - enum class ClaimState { - /// could not be fetched by server. - Unknown, - - /// The account has not yet been claimed. - NotClaimed, - - /// The account is claimed. Attempting to claim it will fail. - Claimed, - - /// The account hasn't been claimed, but is expired. Funds will move - /// back to the issuer. Attempting to claim it will fail. - Expired; - - companion object { - fun getInstance(state: AccountService.TokenAccountInfo.ClaimState): ClaimState? { - return when (state) { - AccountService.TokenAccountInfo.ClaimState.CLAIM_STATE_UNKNOWN -> Unknown - AccountService.TokenAccountInfo.ClaimState.CLAIM_STATE_NOT_CLAIMED -> NotClaimed - AccountService.TokenAccountInfo.ClaimState.CLAIM_STATE_CLAIMED -> Claimed - AccountService.TokenAccountInfo.ClaimState.CLAIM_STATE_EXPIRED -> Expired - AccountService.TokenAccountInfo.ClaimState.UNRECOGNIZED -> null - } - } - } - } - - enum class BalanceSource { - /// The account's balance could not be determined. This may be returned when - /// the data source is unstable and a reliable balance cannot be determined. - Unknown, - - /// The account's balance was fetched directly from a finalized state on the - /// blockchain. - Blockchain, - - /// The account's balance was calculated using cached values in Code. Accuracy - /// is only guaranteed when management_state is LOCKED. - Cache; - - companion object { - fun getInstance(source: AccountService.TokenAccountInfo.BalanceSource): BalanceSource? { - return when (source) { - AccountService.TokenAccountInfo.BalanceSource.BALANCE_SOURCE_UNKNOWN -> Unknown - AccountService.TokenAccountInfo.BalanceSource.BALANCE_SOURCE_BLOCKCHAIN -> Blockchain - AccountService.TokenAccountInfo.BalanceSource.BALANCE_SOURCE_CACHE -> Cache - AccountService.TokenAccountInfo.BalanceSource.UNRECOGNIZED -> null - } - } - - } - } - - data class Relationship(val domain: Domain) -} - -val AccountInfo.displayName: String - get() = when (val type = accountType) { - is AccountType.Bucket -> type.type.name.replace("Bucket", "") - AccountType.Incoming -> "Incoming $index" - AccountType.Outgoing -> "Outgoing $index" - AccountType.Primary -> "Primary" - AccountType.RemoteSend -> "Remote Send" - is AccountType.Relationship -> type.domain.relationshipHost - AccountType.Swap -> "Swap (USDC)" -} - -// An account is deemed unuseable in Code if the management -// state for said account is no longer `locked`. Some accounts may -// be allowed to operated in an 'unlocked' or another state -val AccountInfo.unusable: Boolean - get() = if (managementState == AccountInfo.ManagementState.None) { - // If the account is not managed - // by Code, it is always useable - false - } else { - managementState != AccountInfo.ManagementState.Locked - } diff --git a/services/code/src/main/java/com/getcode/model/AirdropType.kt b/services/code/src/main/java/com/getcode/model/AirdropType.kt deleted file mode 100644 index 952fc950e..000000000 --- a/services/code/src/main/java/com/getcode/model/AirdropType.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.getcode.model - - -import com.codeinc.gen.transaction.v2.TransactionService - -enum class AirdropType { - Unknown, - GiveFirstKin, - GetFirstKin; - - companion object { - fun getInstance(airdropType: TransactionService.AirdropType): AirdropType? { - return when (airdropType) { - TransactionService.AirdropType.UNKNOWN -> Unknown - TransactionService.AirdropType.GIVE_FIRST_KIN -> GiveFirstKin - TransactionService.AirdropType.GET_FIRST_KIN -> GetFirstKin - TransactionService.AirdropType.UNRECOGNIZED -> null - } - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/ClientSignature.kt b/services/code/src/main/java/com/getcode/model/ClientSignature.kt deleted file mode 100644 index bcb751ce8..000000000 --- a/services/code/src/main/java/com/getcode/model/ClientSignature.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.getcode.model - -import com.getcode.solana.keys.Signature - -data class ClientSignature( - val transaction: Signature, - val signature: Signature -) diff --git a/services/code/src/main/java/com/getcode/model/CurrencyRate.kt b/services/code/src/main/java/com/getcode/model/CurrencyRate.kt deleted file mode 100644 index a31b89562..000000000 --- a/services/code/src/main/java/com/getcode/model/CurrencyRate.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.getcode.model - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity -data class CurrencyRate( - @PrimaryKey val id: String, - val rate: Double -) \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/ExchangeRate.kt b/services/code/src/main/java/com/getcode/model/ExchangeRate.kt deleted file mode 100644 index 8c0ef5b6b..000000000 --- a/services/code/src/main/java/com/getcode/model/ExchangeRate.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.getcode.model - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import com.codeinc.gen.transaction.v2.TransactionService -import kotlinx.serialization.Serializable - - -@Serializable -@Entity(tableName = "exchangeData") -data class ExchangeRate( - @ColumnInfo(name = "fiat") - val fx: Double, - @PrimaryKey - val currency: CurrencyCode, - @ColumnInfo(name = "synced_at") - val synced: Long, -) - -fun KinAmount.Companion.fromProtoExchangeData(exchangeData: TransactionService.ExchangeData): KinAmount { - return fromFiatAmount( - kin = Kin(exchangeData.quarks), - fiat = exchangeData.nativeAmount, - fx = exchangeData.exchangeRate, - currencyCode = CurrencyCode.tryValueOf(exchangeData.currency)!! - ) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/FaqItem.kt b/services/code/src/main/java/com/getcode/model/FaqItem.kt deleted file mode 100644 index 1ed2ea70b..000000000 --- a/services/code/src/main/java/com/getcode/model/FaqItem.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.getcode.model - -import androidx.room.Entity -import androidx.room.PrimaryKey - - -@Entity -data class FaqItem( - @PrimaryKey(autoGenerate = true) val uid: Int? = null, - val question: String, - val answer: String - ) \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/Feature.kt b/services/code/src/main/java/com/getcode/model/Feature.kt deleted file mode 100644 index 875521a28..000000000 --- a/services/code/src/main/java/com/getcode/model/Feature.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.getcode.model - -import com.getcode.network.repository.BetaOptions - -sealed interface Feature { - val enabled: Boolean - val available: Boolean -} - -data class BuyModuleFeature( - override val enabled: Boolean = BetaOptions.Defaults.buyModuleEnabled, - override val available: Boolean = false, // server driven availability -): Feature - -data class TipCardFeature( - override val enabled: Boolean = BetaOptions.Defaults.tipsEnabled, - override val available: Boolean = true, // always available -): Feature - -data class TipCardOnHomeScreenFeature( - override val enabled: Boolean = BetaOptions.Defaults.tipCardOnHomeScreen, - override val available: Boolean = true, // always available -): Feature - -data class ConversationCashFeature( - override val enabled: Boolean = BetaOptions.Defaults.conversationCashEnabled, - override val available: Boolean = true, // always available -): Feature - - -data class RequestKinFeature( - override val enabled: Boolean = BetaOptions.Defaults.giveRequestsEnabled, - override val available: Boolean = true, // always available -): Feature - -data class BalanceCurrencyFeature( - override val enabled: Boolean = BetaOptions.Defaults.balanceCurrencySelectionEnabled, - override val available: Boolean = true, // always available -): Feature - -data class CameraGesturesFeature( - override val enabled: Boolean = BetaOptions.Defaults.cameraGesturesEnabled, - override val available: Boolean = true, // always available -): Feature - -data class InvertedDragZoomFeature( - override val enabled: Boolean = BetaOptions.Defaults.invertedDragZoom, - override val available: Boolean = true, // always available -): Feature - -data class FlippableTipCardFeature( - override val enabled: Boolean = BetaOptions.Defaults.canFlipTipCard, - override val available: Boolean = true, // always available -): Feature - -data class GalleryFeature( - override val enabled: Boolean = BetaOptions.Defaults.galleryEnabled, - override val available: Boolean = true, // always available -): Feature \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/GiftCard.kt b/services/code/src/main/java/com/getcode/model/GiftCard.kt deleted file mode 100644 index 54f9bb1e8..000000000 --- a/services/code/src/main/java/com/getcode/model/GiftCard.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.getcode.model - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity -data class GiftCard( - @PrimaryKey val key: String, //base58 - val entropy: String, //base58 - val amount: Long, - val date: Long -) \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/Intent.kt b/services/code/src/main/java/com/getcode/model/Intent.kt deleted file mode 100644 index cd453ceab..000000000 --- a/services/code/src/main/java/com/getcode/model/Intent.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.getcode.model - -sealed class Intent { - data class Transfer( - val id: List, - val amount: KinAmount, - val source: List, - val destination: SendDestination - ) : Intent() - - data class CreateTokenAccount( - val id: List, - val owner: List - ) : Intent() -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/IntentMetadata.kt b/services/code/src/main/java/com/getcode/model/IntentMetadata.kt deleted file mode 100644 index 46f824b90..000000000 --- a/services/code/src/main/java/com/getcode/model/IntentMetadata.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.getcode.model - -import com.codeinc.gen.transaction.v2.TransactionService - -sealed class IntentMetadata { - data object OpenAccounts : IntentMetadata() - data class SendPrivatePayment(val metadata: PaymentMetadata) : IntentMetadata() - data class SendPublicPayment(val metadata: PaymentMetadata) : IntentMetadata() - data object ReceivePaymentsPrivately : IntentMetadata() - data class ReceivePaymentsPublicly(val metadata: PaymentMetadata) : IntentMetadata() - data object UpgradePrivacy : IntentMetadata() - data object MigrateToPrivacy2022 : IntentMetadata() - - companion object { - fun newInstance(metadata: TransactionService.Metadata): IntentMetadata? { - return when (metadata.typeCase) { - TransactionService.Metadata.TypeCase.OPEN_ACCOUNTS -> OpenAccounts - TransactionService.Metadata.TypeCase.RECEIVE_PAYMENTS_PRIVATELY -> ReceivePaymentsPrivately - TransactionService.Metadata.TypeCase.RECEIVE_PAYMENTS_PUBLICLY -> { - getPaymentMetadata( - metadata.receivePaymentsPublicly.exchangeData.currency, - metadata.receivePaymentsPublicly.exchangeData.quarks, - metadata.receivePaymentsPublicly.exchangeData.exchangeRate, - metadata.sendPrivatePayment.isChat, - )?.let { ReceivePaymentsPublicly(it) } - } - TransactionService.Metadata.TypeCase.UPGRADE_PRIVACY -> UpgradePrivacy - TransactionService.Metadata.TypeCase.MIGRATE_TO_PRIVACY_2022 -> MigrateToPrivacy2022 - TransactionService.Metadata.TypeCase.SEND_PRIVATE_PAYMENT -> { - getPaymentMetadata( - metadata.sendPrivatePayment.exchangeData.currency, - metadata.sendPrivatePayment.exchangeData.quarks, - metadata.sendPrivatePayment.exchangeData.exchangeRate, - metadata.sendPrivatePayment.isChat, - )?.let { SendPrivatePayment(it) } - } - TransactionService.Metadata.TypeCase.SEND_PUBLIC_PAYMENT -> { - getPaymentMetadata( - metadata.sendPublicPayment.exchangeData.currency, - metadata.sendPrivatePayment.exchangeData.quarks, - metadata.sendPublicPayment.exchangeData.exchangeRate, - metadata.sendPrivatePayment.isChat, - )?.let { SendPublicPayment(it) } - } - else -> null - } - } - - private fun getPaymentMetadata( - currencyString: String, - quarks: Long, - exchangeRate: Double, - isChat: Boolean, - ): PaymentMetadata? { - val currency = CurrencyCode.tryValueOf(currencyString.uppercase()) - ?: return null - - return PaymentMetadata( - amount = KinAmount.newInstance( - kin = Kin(quarks), - rate = Rate( - fx = exchangeRate, - currency = currency - ) - ), - isChat = isChat, - ) - } - } -} - -data class PaymentMetadata( - val amount: KinAmount, - val isChat: Boolean, -) \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/KinCode.kt b/services/code/src/main/java/com/getcode/model/KinCode.kt deleted file mode 100644 index 7d6e329b4..000000000 --- a/services/code/src/main/java/com/getcode/model/KinCode.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.getcode.model - -import org.kin.sdk.base.models.QuarkAmount -import org.kin.sdk.base.models.solana.read -import org.kin.sdk.base.tools.byteArrayToLong -import org.kin.sdk.base.tools.longToByteArray -import org.kin.sdk.base.tools.toByteArray -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.util.UUID - - -data class KinCode(val type: Type = Type.None, val amount: QuarkAmount, val nonce: Nonce) { - - sealed class Type(val value: Int) { - object Unknown : Type(-1) - object None : Type(0) - - companion object { - @JvmStatic - fun from(value: Int): Type { - return when (value) { - 0 -> None - else -> Unknown - } - } - - } - } - - /** - * Only 11 Bytes LSB observed - */ - data class Nonce(val value: ByteArray) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Nonce) return false - - if (!value.contentEquals(other.value)) return false - - return true - } - - override fun hashCode(): Int { - return value.contentHashCode() - } - - companion object { - fun random(): Nonce { - return Nonce(UUID.randomUUID().toByteArray()) - } - } - } - - /** - * 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ - * |T | Amount | Nonce | - * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ - */ - fun encode(): ByteArray { - return with(ByteArrayOutputStream()) { - write(byteArrayOf(type.value.toByte())) - write(amount.value.longToByteArray()) - write(nonce.value, 0, 11) - toByteArray() - } - } - - companion object { - @JvmStatic - fun decode(bytes: ByteArray): KinCode { - return with(ByteArrayInputStream(bytes)) { - val type = Type.from(read()) - val amount = QuarkAmount(read(8).byteArrayToLong()) - val nonce = Nonce(read(11)) - KinCode(type, amount, nonce) - } - } - } -} diff --git a/services/code/src/main/java/com/getcode/model/Limits.kt b/services/code/src/main/java/com/getcode/model/Limits.kt deleted file mode 100644 index 6a3384f38..000000000 --- a/services/code/src/main/java/com/getcode/model/Limits.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.getcode.model - -import com.codeinc.gen.transaction.v2.TransactionService -import com.codeinc.gen.transaction.v2.TransactionService.BuyModuleLimit -import com.codeinc.gen.transaction.v2.TransactionService.DepositLimit -import kotlin.time.Duration.Companion.hours - -data class Limits( - // Date from which the limits are computed - val sinceDate : Long, - - // Date at which the limits were fetched - val fetchDate: Long, - - // Maximum quarks that may be deposited at any time. Server will guarantee - // this threshold will be below enforced dollar value limits, while also - // ensuring sufficient funds are available for a full organizer that supports - // max payment sends. Total dollar value limits may be spread across many deposits. - val maxDeposit: Kin, - - // Remaining send limits keyed by currency - private val sendLimits: Map, - // Buy limits keyed by currency - private val buyLimits: Map, - - ) { - val isStale: Boolean - get() { - val now = System.currentTimeMillis() - return now - fetchDate > 1.hours.inWholeMilliseconds - } - - fun sendLimitFor(currencyCode: CurrencyCode) : SendLimit? { - return sendLimits[currencyCode] - } - - fun buyLimitFor(currencyCode: CurrencyCode): BuyLimit? { - return buyLimits[currencyCode] - } - - companion object { - fun newInstance( - sinceDate: Long, - fetchDate: Long, - sendLimits: Map, - buyLimits: Map, - deposits: DepositLimit, - ): Limits { - val sends = sendLimits - .mapNotNull { (k, v) -> - val code = CurrencyCode.tryValueOf(k) ?: return@mapNotNull null - val limit = SendLimit( - nextTransaction = v.nextTransaction.toDouble(), - maxPerDay = v.maxPerDay.toDouble(), - maxPerTransaction = v.maxPerTransaction.toDouble(), - ) - - code to limit - }.toMap() - - val buys = buyLimits - .mapValues { (_, v) -> - BuyLimit( - min = v.minPerTransaction.toDouble(), - max = v.maxPerTransaction.toDouble() - ) - } - .mapNotNull { (k, limit) -> - val code = CurrencyCode.tryValueOf(k) ?: return@mapNotNull null - code to limit - }.toMap() - - return Limits( - sinceDate = sinceDate, - fetchDate = fetchDate, - sendLimits = sends, - buyLimits = buys, - maxDeposit = Kin.fromQuarks(deposits.maxQuarks) - ) - } - } -} - -data class SendLimit( - val nextTransaction: Double, - val maxPerTransaction: Double, - val maxPerDay: Double -) { - companion object { - val Zero = SendLimit(0.0, 0.0, 0.0) - } -} - -data class BuyLimit(val min: Double, val max: Double) { - companion object { - val Zero = BuyLimit(0.0, 0.0) - } -} diff --git a/services/code/src/main/java/com/getcode/model/PaymentRequest.kt b/services/code/src/main/java/com/getcode/model/PaymentRequest.kt deleted file mode 100644 index 584cfa7c6..000000000 --- a/services/code/src/main/java/com/getcode/model/PaymentRequest.kt +++ /dev/null @@ -1,199 +0,0 @@ -package com.getcode.model - -import com.codeinc.gen.messaging.v1.MessagingService -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.Signature - -data class StreamMessage(val id: List, val kind: Kind) { - sealed interface Kind { - class ReceiveRequestKind(val receiveRequest: ReceiveRequest): Kind - class PaymentRequestKind(val paymentRequest: PaymentRequest) : Kind - class AirdropKind(val airdrop: Airdrop) : Kind - data class LoginRequestKind(val loginRequest: LoginRequest): Kind - } - - val receiveRequest: ReceiveRequest? = (kind as? Kind.ReceiveRequestKind)?.receiveRequest - val paymentRequest: PaymentRequest? = (kind as? Kind.PaymentRequestKind)?.paymentRequest - val loginRequest: LoginRequest? = (kind as? Kind.LoginRequestKind)?.loginRequest - val airdrop: Airdrop? = (kind as? Kind.AirdropKind)?.airdrop - - companion object { - fun getInstance(message: MessagingService.Message): StreamMessage? { - val kind: Kind = when (message.kindCase) { - MessagingService.Message.KindCase.REQUEST_TO_GRAB_BILL -> { - val account = - PublicKey( - message.requestToGrabBill.requestorAccount.value.toByteArray().toList() - ) - val signature = - Signature( - message.sendMessageRequestSignature.value.toByteArray().toList() - ) - - Kind.PaymentRequestKind( - PaymentRequest( - account = account, - signature = signature - ) - ) - } - MessagingService.Message.KindCase.REQUEST_TO_RECEIVE_BILL -> { - val request = message.requestToReceiveBill - val exchangeData = request.exchangeDataCase - val account = PublicKey( - request.requestorAccount.value.toByteArray().toList() - ) - val signature = Signature( - message.sendMessageRequestSignature.value.toByteArray().toList() - ) - - val domain: Domain? - val verifier: PublicKey? - if (request.hasDomain()) { - val validDomain = Domain.from(request.domain.value) ?: return null - val validVerifier = PublicKey( - request.verifier.value.toByteArray().toList() - ) - - domain = validDomain - verifier = validVerifier - } else { - domain = null - verifier = null - } - - val requestData = when (exchangeData) { - MessagingService.RequestToReceiveBill.ExchangeDataCase.EXACT -> { - val data = request.exact - val currency = CurrencyCode.tryValueOf(data.currency) ?: return null - - val additionalFees = request.additionalFeesList.mapNotNull { - val destination = PublicKey( - it.destination.value.toByteArray().toList() - ) - Fee(destination = destination, it.feeBps) - } - - ReceiveRequest( - account = account, - signature = signature, - amount = ReceiveRequest.Amount.Exact( - value = KinAmount.newInstance( - kin = Kin(data.quarks), - rate = Rate( - fx = data.exchangeRate, - currency = currency - ) - ) - ), - domain = domain, - verifier = verifier, - additionalFees = additionalFees, - ) - } - MessagingService.RequestToReceiveBill.ExchangeDataCase.PARTIAL -> { - val data = request.partial - val currency = CurrencyCode.tryValueOf(data.currency) ?: return null - - - val additionalFees = request.additionalFeesList.mapNotNull { - val destination = PublicKey( - it.destination.value.toByteArray().toList() - ) - Fee(destination = destination, it.feeBps) - } - - ReceiveRequest( - account = account, - signature = signature, - amount = ReceiveRequest.Amount.Partial( - value = Fiat(currency, data.nativeAmount) - ), - domain = domain, - verifier = verifier, - additionalFees - ) - } - else -> return null - } - - Kind.ReceiveRequestKind(requestData) - } - MessagingService.Message.KindCase.AIRDROP_RECEIVED -> { - val type = AirdropType.getInstance(message.airdropReceived.airdropType) - ?: return null - val currency = CurrencyCode.tryValueOf(message.airdropReceived.exchangeData.currency) - ?: return null - - Kind.AirdropKind( - Airdrop( - type = type, - date = message.airdropReceived.timestamp.seconds, - kinAmount = KinAmount.newInstance( - kin = Kin(message.airdropReceived.exchangeData.quarks), - rate = Rate( - fx = message.airdropReceived.exchangeData.exchangeRate, - currency = currency - ) - ) - ) - ) - return null - } - - MessagingService.Message.KindCase.REQUEST_TO_LOGIN -> { - val request = message.requestToLogin - val domain = request.domain?.let { Domain.from(it.value) } ?: return null - val verifier = PublicKey( - request.verifier.value.toByteArray().toList() - ) - val rendezvous = PublicKey( - request.rendezvousKey.toByteArray().toList() - ) - val signature = Signature( - request.signature.value.toByteArray().toList() - ) - - Kind.LoginRequestKind( - LoginRequest(domain, verifier, rendezvous, signature) - ) - } - else -> return null - } - return StreamMessage( - id = message.id.value.toByteArray().toList(), - kind = kind - ) - } - } -} - -data class ReceiveRequest( - val account: PublicKey, - val signature: Signature, - val amount: Amount, - val domain: Domain?, - val verifier: PublicKey?, - val additionalFees: List -) { - sealed interface Amount { - data class Exact(val value: KinAmount): Amount - data class Partial(val value: Fiat): Amount - } -} - -data class PaymentRequest(val account: PublicKey, val signature: Signature) - -data class LoginRequest( - val domain: Domain, - val verifier: PublicKey, - val rendezous: PublicKey, - val signature: Signature, -) - -data class Airdrop(val type: AirdropType, val date: Long, val kinAmount: KinAmount) - -data class Fee( - val destination: PublicKey, - val bps: Int, -) \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/RelationshipBox.kt b/services/code/src/main/java/com/getcode/model/RelationshipBox.kt deleted file mode 100644 index 19c77304b..000000000 --- a/services/code/src/main/java/com/getcode/model/RelationshipBox.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.getcode.model - -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.Relationship -import okhttp3.internal.toImmutableMap -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class RelationshipBox @Inject constructor() { - private val _publicKeys = mutableMapOf() - val publicKeys - get() = _publicKeys.toImmutableMap() - - private val _domains = mutableMapOf() - val domains - get() = _domains.toImmutableMap() - - - fun relationships(largestFirst: Boolean = false): List { - return _domains.values.sortedWith { a, b -> - val comparisonResult = a.partialBalance.compareTo(b.partialBalance) - if (largestFirst) -comparisonResult else comparisonResult - } - } - fun relationshipWith(publicKey: PublicKey) = _publicKeys[publicKey] - fun relationshipWith(domain: Domain) = _domains[domain.relationshipHost] - - fun insert(relationship: Relationship) { - _publicKeys[relationship.getCluster().vaultPublicKey] = relationship - _domains[relationship.domain.relationshipHost] = relationship - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/SendDestination.kt b/services/code/src/main/java/com/getcode/model/SendDestination.kt deleted file mode 100644 index ac195e34c..000000000 --- a/services/code/src/main/java/com/getcode/model/SendDestination.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.getcode.model - -sealed class SendDestination { - data class SendDestinationPublicKey( - val publicKey: List, - ) : SendDestination() - - data class SendDestinationPhone( - val phoneNumber: String - ) : SendDestination() -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/StreamEvent.kt b/services/code/src/main/java/com/getcode/model/StreamEvent.kt deleted file mode 100644 index cdecbb7b8..000000000 --- a/services/code/src/main/java/com/getcode/model/StreamEvent.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.getcode.model - -sealed class StreamEvent(val id: String) { - class SimulationEvent( - id: String, - val isFailed: Boolean, - val rendezvousKey: ByteArray, - val exchangeCurrency: String?, - val exchangeRate: Double?, - val amountNative: Double?, - val kin: Kin?, - val region: String? - ) : StreamEvent(id) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/UpgradeableIntent.kt b/services/code/src/main/java/com/getcode/model/UpgradeableIntent.kt deleted file mode 100644 index 70f5b80a4..000000000 --- a/services/code/src/main/java/com/getcode/model/UpgradeableIntent.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.getcode.model - -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.solana.keys.PublicKey - -class UpgradeableIntent( - val id: PublicKey, - val actions: List, -) { - companion object { - fun newInstance(proto: TransactionService.UpgradeableIntent): UpgradeableIntent { - val intentId = PublicKey(proto.id.value.toByteArray().toList()) - - val actions = proto.actionsList.map { - UpgradeablePrivateAction.newInstance(it) - } - - return UpgradeableIntent(intentId, actions) - } - - } - -} diff --git a/services/code/src/main/java/com/getcode/model/UpgradeablePrivateAction.kt b/services/code/src/main/java/com/getcode/model/UpgradeablePrivateAction.kt deleted file mode 100644 index 402d49e1c..000000000 --- a/services/code/src/main/java/com/getcode/model/UpgradeablePrivateAction.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.getcode.model - -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.instructions.programs.SystemProgram_AdvanceNonce -import com.getcode.solana.instructions.programs.TimelockProgram_TransferWithAuthority -import com.getcode.solana.keys.Hash -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.Signature -import com.getcode.solana.organizer.AccountType - -class UpgradeablePrivateAction( - val id: Int, - val transactionBlob: List, - val clientSignature: Signature, - val sourceAccountType: AccountType, - val sourceDerivationIndex: Long, - val originalDestination: PublicKey, - val originalAmount: Kin, - val treasury: PublicKey, - val recentRoot: Hash, - - val transaction: SolanaTransaction, - val originalNonce: PublicKey, - val originalCommitment: PublicKey, - val originalRecentBlockhash: Hash -) { - companion object { - fun newInstance( - id: Int, - transactionBlob: List, - clientSignature: Signature, - sourceAccountType: AccountType, - sourceDerivationIndex: Long, - originalDestination: PublicKey, - originalAmount: Kin, - treasury: PublicKey, - recentRoot: Hash - ): UpgradeablePrivateAction { - val transaction: SolanaTransaction = SolanaTransaction.fromList(transactionBlob) - ?: throw UpgradeablePrivateActionException.FailedToParseTransactionException() - - val nonceInstruction = - transaction.findInstruction(SystemProgram_AdvanceNonce::newInstance) - ?: throw UpgradeablePrivateActionException.MissingOriginalNonceException() - - val transferInstruction = - transaction.findInstruction( - TimelockProgram_TransferWithAuthority::newInstance - ) - ?: throw UpgradeablePrivateActionException.MissingOriginalCommitmentException() - - - return UpgradeablePrivateAction( - id = id, - transactionBlob = transactionBlob, - clientSignature = clientSignature, - sourceAccountType = sourceAccountType, - sourceDerivationIndex = sourceDerivationIndex, - originalDestination = originalDestination, - originalAmount = originalAmount, - treasury = treasury, - recentRoot = recentRoot, - transaction = transaction, - originalNonce = nonceInstruction.nonce, - originalCommitment = transferInstruction.destination, - originalRecentBlockhash = transaction.recentBlockhash - ) - } - - fun newInstance(proto: TransactionService.UpgradeableIntent.UpgradeablePrivateAction): UpgradeablePrivateAction { - val signature = Signature( - proto.clientSignature.value.toByteArray().toList() - ) - val accountType = - AccountType.newInstance(proto.sourceAccountType) - ?: throw UpgradeablePrivateActionException.DeserializationFailedException() - val originalDestination = - PublicKey( - proto.originalDestination.value.toByteArray().toList() - ) - val treasury = - PublicKey(proto.treasury.value.toByteArray().toList()) - val recentRoot = - Hash(proto.recentRoot.value.toByteArray().toList()) - - return newInstance( - id = proto.actionId, - transactionBlob = proto.transactionBlob.value.toByteArray().toList(), - clientSignature = signature, - sourceAccountType = accountType, - sourceDerivationIndex = proto.sourceDerivationIndex, - originalDestination = originalDestination, - originalAmount = Kin.fromQuarks(proto.originalAmount), - treasury = treasury, - recentRoot = recentRoot - ) - } - } - - sealed class UpgradeablePrivateActionException : Exception() { - class MissingOriginalNonceException : UpgradeablePrivateActionException() - class MissingOriginalCommitmentException : UpgradeablePrivateActionException() - class FailedToParseTransactionException : UpgradeablePrivateActionException() - class DeserializationFailedException : UpgradeablePrivateActionException() - } -} diff --git a/services/code/src/main/java/com/getcode/model/chat/Chat.kt b/services/code/src/main/java/com/getcode/model/chat/Chat.kt deleted file mode 100644 index 0212e4fb5..000000000 --- a/services/code/src/main/java/com/getcode/model/chat/Chat.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.getcode.model.chat - -import com.getcode.model.Cursor -import com.getcode.model.ID -import kotlinx.serialization.Serializable - -/** - * Chat domain model for On-Chain messaging. This serves as a reference to a collection of messages. - * - * @param id Unique chat identifier ([ID]) - * @param title The chat title, which will be localized by server when applicable - * @param canMute Can the user mute this chat? - * @param canUnsubscribe Can the user unsubscribe from this chat? - * @param cursor [Cursor] value for this chat for reference in subsequent GetChatsRequest - * @param messages List of messages within this chat - */ -@Serializable -data class Chat( - val id: ID, - val title: Title?, - private val _unreadCount: Int = 0, - private val _isMuted: Boolean = false, - val canMute: Boolean, - private val _isSubscribed: Boolean = false, - val canUnsubscribe: Boolean, - val cursor: Cursor, - val messages: List -) { - val imageData: Any - get() { - return id - } - - val unreadCount: Int - get() { - return _unreadCount - } - - fun resetUnreadCount(): Chat { - return copy(_unreadCount = 0) - } - - val isMuted: Boolean - get() { - return _isMuted - } - - fun setMuteState(muted: Boolean): Chat { - return copy(_isMuted = muted) - } - - val isSubscribed: Boolean - get() { - return _isSubscribed - } - - fun setSubscriptionState(subscribed: Boolean): Chat { - return copy(_isSubscribed = subscribed) - } - - val newestMessage: ChatMessage? - get() = messages.maxByOrNull { it.dateMillis } - - val lastMessageMillis: Long? - get() = newestMessage?.dateMillis -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/chat/Chats.kt b/services/code/src/main/java/com/getcode/model/chat/Chats.kt deleted file mode 100644 index 983a4b3eb..000000000 --- a/services/code/src/main/java/com/getcode/model/chat/Chats.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.getcode.model.chat - -/** - * Code reference to a V1 [Chat] that serves as a collection of messages associated - * with a notification type (Tips, Cash Payments, Web Payments, etc.) - */ -typealias NotificationCollectionEntity = Chat diff --git a/services/code/src/main/java/com/getcode/model/extensions/AssociatedTokenAccount.kt b/services/code/src/main/java/com/getcode/model/extensions/AssociatedTokenAccount.kt deleted file mode 100644 index 25207b8c1..000000000 --- a/services/code/src/main/java/com/getcode/model/extensions/AssociatedTokenAccount.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.getcode.model.extensions - -import com.getcode.solana.keys.AssociatedTokenAccount -import com.getcode.solana.keys.Mint -import com.getcode.solana.keys.PublicKey - -fun AssociatedTokenAccount.Companion.newInstance( - owner: PublicKey, - mint: Mint -): AssociatedTokenAccount { - return AssociatedTokenAccount( - owner = owner, - ata = PublicKey.deriveAssociatedAccount(owner = owner, mint = mint) - ) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/extensions/ChatMessage.kt b/services/code/src/main/java/com/getcode/model/extensions/ChatMessage.kt deleted file mode 100644 index 79b222326..000000000 --- a/services/code/src/main/java/com/getcode/model/extensions/ChatMessage.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.getcode.model.extensions - -import com.getcode.ed25519.Ed25519 -import com.getcode.model.chat.ChatMessage -import com.getcode.model.chat.MessageContent -import com.getcode.model.decryptMessageUsingNaClBox - -fun ChatMessage.decryptingUsing(keyPair: Ed25519.KeyPair): ChatMessage { - return ChatMessage( - id = id, - senderId = senderId, - isFromSelf = isFromSelf, - dateMillis = dateMillis, - contents = contents.map { - when (it) { - is MessageContent.SodiumBox -> { - val decrypted = it.data.decryptMessageUsingNaClBox(keyPair = keyPair) - if (decrypted != null) { - MessageContent.Decrypted(data = decrypted, isFromSelf = isFromSelf) - } else { - it - } - } - - is MessageContent.Exchange, - is MessageContent.Localized, - is MessageContent.Decrypted, - is MessageContent.RawText, - is MessageContent.Announcement, - is MessageContent.Reaction, - is MessageContent.Reply, - is MessageContent.DeletedMessage, - is MessageContent.Unknown, - is MessageContent.ActionableAnnouncement, - is MessageContent.MessageInReview, - is MessageContent.MessageTip -> it // passthrough - } - } - ) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/extensions/MemoProgram_Memo.kt b/services/code/src/main/java/com/getcode/model/extensions/MemoProgram_Memo.kt deleted file mode 100644 index a05af2ca2..000000000 --- a/services/code/src/main/java/com/getcode/model/extensions/MemoProgram_Memo.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.getcode.model.extensions - -import com.getcode.model.SocialUser -import com.getcode.solana.instructions.programs.MemoProgram_Memo - -fun MemoProgram_Memo.Companion.newInstance(tipMetadata: SocialUser): MemoProgram_Memo { - val memo = "tip:${tipMetadata.platform}:${tipMetadata.username}" - - return MemoProgram_Memo( - memo.toByteArray().toList() - ) -} diff --git a/services/code/src/main/java/com/getcode/model/extensions/PreSwapStateAccount.kt b/services/code/src/main/java/com/getcode/model/extensions/PreSwapStateAccount.kt deleted file mode 100644 index 58768c104..000000000 --- a/services/code/src/main/java/com/getcode/model/extensions/PreSwapStateAccount.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.getcode.model.extensions - -import com.getcode.solana.keys.PreSwapStateAccount -import com.getcode.solana.keys.PublicKey - -fun PreSwapStateAccount.Companion.newInstance( - owner: PublicKey, - source: PublicKey, - destination: PublicKey, - nonce: PublicKey -): PreSwapStateAccount { - return PreSwapStateAccount( - owner = owner, - state = PublicKey.derivePreSwapState(source, destination, nonce) - ) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/extensions/PublicKey.kt b/services/code/src/main/java/com/getcode/model/extensions/PublicKey.kt deleted file mode 100644 index 874f27dd2..000000000 --- a/services/code/src/main/java/com/getcode/model/extensions/PublicKey.kt +++ /dev/null @@ -1,198 +0,0 @@ -package com.getcode.model.extensions - -import com.getcode.crypt.Sha256Hash -import com.getcode.ed25519.Ed25519 -import com.getcode.model.Kin -import com.getcode.solana.instructions.programs.* -import com.getcode.solana.keys.Hash -import com.getcode.solana.keys.Key32.Companion.splitter -import com.getcode.solana.keys.Key32.Companion.subsidizer -import com.getcode.solana.keys.Key32.Companion.timeAuthority -import com.getcode.solana.keys.ProgramDerivedAccount -import com.getcode.solana.keys.PublicKey -import org.kin.sdk.base.tools.longToByteArray -import java.io.ByteArrayOutputStream -import java.io.IOException - -fun PublicKey.Companion.deriveAssociatedAccount(owner: PublicKey, mint: PublicKey): ProgramDerivedAccount { - return findProgramAddress( - seeds = listOf(owner.bytes.toByteArray(), TokenProgram.address.bytes.toByteArray(), mint.bytes.toByteArray()), - programId = AssociatedTokenProgram.address, - ) -} - -fun PublicKey.Companion.deriveTimelockStateAccount( - owner: PublicKey, - lockout: Long -): ProgramDerivedAccount { - val seeds: List = listOf( - "timelock_state".toByteArray(Charsets.UTF_8), - kin.bytes.toByteArray(), - timeAuthority.bytes.toByteArray(), - owner.bytes.toByteArray(), - byteArrayOf(lockout.toByte()) - ) - - return findProgramAddress( - seeds = seeds, - programId = TimelockProgram.address, - ) -} - -fun PublicKey.Companion.deriveTimelockVaultAccount( - stateAccount: PublicKey, - version: Long -): ProgramDerivedAccount { - val seeds: List = listOf( - "timelock_vault".toByteArray(Charsets.UTF_8), - stateAccount.bytes.toByteArray(), - byteArrayOf(version.toByte()) - ) - - return findProgramAddress( - seeds = seeds, - programId = TimelockProgram.address, - ) -} - -fun PublicKey.Companion.deriveLegacyTimelockStateAccount( - owner: PublicKey, - lockout: Long -): ProgramDerivedAccount { - val nonce = SystemProgram.address - val version = byteArrayOf(1) - val pdaPadding = SystemProgram.address - - val seeds: List = listOf( - "timelock_state".toByteArray(Charsets.UTF_8), - version, - kin.bytes.toByteArray(), - subsidizer.bytes.toByteArray(), - nonce.bytes.toByteArray(), - owner.bytes.toByteArray(), - lockout.longToByteArray(), - pdaPadding.bytes.toByteArray() - ) - - return findProgramAddress( - seeds = seeds, - programId = TimelockProgram.legacyAddress, - ) -} - -fun PublicKey.Companion.deriveLegacyTimelockVaultAccount( - stateAccount: PublicKey -): ProgramDerivedAccount { - val seeds: List = listOf( - "timelock_vault".toByteArray(Charsets.UTF_8), - stateAccount.bytes.toByteArray(), - byteArrayOf(0) - ) - - return findProgramAddress( - seeds = seeds, - programId = TimelockProgram.legacyAddress, - ) -} - -/// FindProgramAddress mirrors the implementation of the Solana SDK's FindProgramAddress. Its primary -/// use case (for Kin and Agora) is for deriving associated accounts. -/// -/// Reference: https://github.com/solana-labs/solana/blob/5548e599fe4920b71766e0ad1d121755ce9c63d5/sdk/program/src/pubkey.rs#L234 -/// -fun PublicKey.Companion.findProgramAddress( - seeds: List, - programId: PublicKey -): ProgramDerivedAccount { - for (i in 0..255) { - val bumpValue = 255 - i - try { - val publicKey = deriveProgramAddress(programId, listOf(*seeds.toTypedArray(), byteArrayOf(bumpValue.toByte()))) - return ProgramDerivedAccount(publicKey, bumpValue) - } catch (e: RuntimeException) { - //no-op - } - } - - throw Exception("Unable to find a viable program address nonce") -} - -/// CreateProgramAddress mirrors the implementation of the Solana SDK's CreateProgramAddress. -/// -/// ProgramAddresses are public keys that _do not_ lie on the ed25519 curve to ensure that -/// there is no associated private key. In the event that the program and seed parameters -/// result in a valid public key, ErrInvalidPublicKey is returned. -/// -/// Reference: https://github.com/solana-labs/solana/blob/5548e599fe4920b71766e0ad1d121755ce9c63d5/sdk/program/src/pubkey.rs#L158 -/// -fun PublicKey.Companion.deriveProgramAddress(programId: PublicKey, seeds: List): PublicKey { - fun PublicKey.Companion.getMaxSeeds() = 16 - - val buffer = ByteArrayOutputStream() - require(seeds.size < getMaxSeeds()) { "Max seed size exceeded" } - - for (seed in seeds) { - try { - buffer.write(seed) - } catch (e: IOException) { - throw RuntimeException(e) - } - } - try { - buffer.write(programId.bytes.toByteArray()) - buffer.write("ProgramDerivedAddress".toByteArray()) - } catch (e: IOException) { - throw RuntimeException(e) - } - val hash = Sha256Hash.hash(buffer.toByteArray()) - - val publicKey = PublicKey(hash.toList()) - - // Following the Solana SDK, we want to _reject_ the generated public key - // if it's a valid compressed EdwardsPoint (on the curve). - // - if (Ed25519.onCurve(publicKey.bytes.toByteArray())) { - throw RuntimeException("Invalid seeds, address must fall off the curve") - } - - return PublicKey(hash.toList()) -} - -fun PublicKey.Companion.deriveCommitmentStateAccount(treasury: PublicKey, recentRoot: Hash, transcript: Hash, destination: PublicKey, amount: Kin): ProgramDerivedAccount { - return findProgramAddress( - programId = splitter, - seeds = listOf( - "commitment_state".toByteArray(Charsets.UTF_8), - treasury.bytes.toByteArray(), - recentRoot.bytes.toByteArray(), - transcript.bytes.toByteArray(), - destination.bytes.toByteArray(), - amount.quarks.longToByteArray() - ) - ) -} - -fun PublicKey.Companion.deriveCommitmentVaultAccount(treasury: PublicKey, commitmentState: PublicKey): ProgramDerivedAccount { - return findProgramAddress( - programId = splitter, - seeds = listOf( - "commitment_vault".toByteArray(Charsets.UTF_8), - treasury.bytes.toByteArray(), - commitmentState.bytes.toByteArray() - ) - ) -} - -fun PublicKey.Companion.derivePreSwapState( - source: PublicKey, destination: PublicKey, nonce: PublicKey -): ProgramDerivedAccount { - return findProgramAddress( - programId = SwapValidatorProgram.address, - seeds = listOf( - "pre_swap_state".toByteArray(Charsets.UTF_8), - source.bytes.toByteArray(), - destination.bytes.toByteArray(), - nonce.bytes.toByteArray(), - ) - ) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/extensions/SplitterCommitmentAccounts.kt b/services/code/src/main/java/com/getcode/model/extensions/SplitterCommitmentAccounts.kt deleted file mode 100644 index 3ce695768..000000000 --- a/services/code/src/main/java/com/getcode/model/extensions/SplitterCommitmentAccounts.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.getcode.model.extensions - -import com.getcode.model.Kin -import com.getcode.solana.keys.Hash -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.SplitterCommitmentAccounts -import com.getcode.solana.keys.SplitterTranscript -import com.getcode.solana.organizer.AccountCluster - -fun SplitterCommitmentAccounts.Companion.newInstance( - source: AccountCluster, - destination: PublicKey, - amount: Kin, - treasury: PublicKey, - recentRoot: Hash, - intentId: PublicKey, - actionId: Int -): SplitterCommitmentAccounts { - val transcript = SplitterTranscript( - intentId = intentId, - actionId = actionId, - amount = amount, - source = source.vaultPublicKey, - destination = destination - ) - - return newInstance( - treasury = treasury, - destination = destination, - recentRoot = recentRoot, - transcript = transcript.transcriptHash, - amount = amount - ) -} - -fun SplitterCommitmentAccounts.Companion.newInstance( - treasury: PublicKey, - destination: PublicKey, - recentRoot: Hash, - transcript: Hash, - amount: Kin -): SplitterCommitmentAccounts { - val state = PublicKey.deriveCommitmentStateAccount( - treasury = treasury, - recentRoot = recentRoot, - transcript = transcript, - destination = destination, - amount = amount - ) - - val vault = PublicKey.deriveCommitmentVaultAccount( - treasury = treasury, - commitmentState = state.publicKey - ) - - return SplitterCommitmentAccounts( - treasury = treasury, - destination = destination, - recentRoot = recentRoot, - transcript = transcript, - state = state, - vault = vault, - ) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/extensions/TimelockDerivedAccounts.kt b/services/code/src/main/java/com/getcode/model/extensions/TimelockDerivedAccounts.kt deleted file mode 100644 index 08e09159f..000000000 --- a/services/code/src/main/java/com/getcode/model/extensions/TimelockDerivedAccounts.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.getcode.model.extensions - -import com.getcode.solana.keys.ProgramDerivedAccount -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.TimelockDerivedAccounts - -fun TimelockDerivedAccounts.Companion.newInstance(owner: PublicKey, legacy: Boolean = false): TimelockDerivedAccounts { - val state: ProgramDerivedAccount - val vault: ProgramDerivedAccount - - if (legacy) { - state = - PublicKey.deriveLegacyTimelockStateAccount(owner = owner, lockout = 1_814_400) - vault = PublicKey.deriveLegacyTimelockVaultAccount(stateAccount = state.publicKey) - } else { - state = PublicKey.deriveTimelockStateAccount(owner = owner, lockout = lockoutInDays) - vault = PublicKey.deriveTimelockVaultAccount( - stateAccount = state.publicKey, - version = dataVersion - ) - } - - return TimelockDerivedAccounts( - owner = owner, - state = state, - vault = vault - ) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/intents/IntentCreateAccounts.kt b/services/code/src/main/java/com/getcode/model/intents/IntentCreateAccounts.kt deleted file mode 100644 index 72664d9a7..000000000 --- a/services/code/src/main/java/com/getcode/model/intents/IntentCreateAccounts.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.ed25519.Ed25519 -import com.getcode.model.intents.actions.ActionOpenAccount -import com.getcode.model.intents.actions.ActionType -import com.getcode.model.intents.actions.ActionWithdraw -import com.getcode.model.toPublicKey -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.keys.PublicKey - -class IntentCreateAccounts( - override val id: PublicKey, - override val actionGroup: ActionGroup, - private val organizer: Organizer, -) : IntentType() { - - override fun metadata(): TransactionService.Metadata { - return TransactionService.Metadata.newBuilder() - v - .build() - } - - - companion object { - fun newInstance(organizer: Organizer): IntentCreateAccounts { - val actionsList = mutableListOf().apply { - organizer.allAccounts().map { pair -> - val (type, cluster) = pair - ActionOpenAccount.newInstance( - owner = organizer.tray.owner.getCluster().authority.keyPair.publicKeyBytes.toPublicKey(), - type = type, - accountCluster = cluster - ) - .let { this.add(it) } - - if (type != AccountType.Primary) { - ActionWithdraw.newInstance( - kind = ActionWithdraw.Kind.CloseDormantAccount(type), - cluster = cluster, - destination = organizer.tray.owner.getCluster().vaultPublicKey - ) - .let { this.add(it) } - } - } - } - - return IntentCreateAccounts( - id = Ed25519.createKeyPair().publicKeyBytes.toPublicKey(), - organizer = organizer, - actionGroup = ActionGroup().apply { - actions = actionsList - } - ) - - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/intents/IntentDeposit.kt b/services/code/src/main/java/com/getcode/model/intents/IntentDeposit.kt deleted file mode 100644 index 1786f9ac3..000000000 --- a/services/code/src/main/java/com/getcode/model/intents/IntentDeposit.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.model.Kin -import com.getcode.model.generate -import com.getcode.model.intents.actions.ActionTransfer -import com.getcode.network.repository.toSolanaAccount -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Tray - -class IntentDeposit( - override val id: PublicKey, - private val organizer: Organizer, - private val amount: Kin, - private val source: AccountType, - val resultTray: Tray, - - override val actionGroup: ActionGroup, -) : IntentType() { - override fun metadata(): TransactionService.Metadata { - return TransactionService.Metadata.newBuilder() - .setReceivePaymentsPrivately( - TransactionService.ReceivePaymentsPrivatelyMetadata.newBuilder() - .setSource(organizer.tray.cluster(source).vaultPublicKey.bytes.toSolanaAccount()) - .setQuarks(amount.quarks) - .setIsDeposit(true) - ) - .build() - } - - companion object { - fun newInstance( - source: AccountType, - organizer: Organizer, - amount: Kin - ): IntentDeposit { - val intentId = PublicKey.generate() - val currentTray = organizer.tray.copy() - val startSlotBalance = currentTray.slotsBalance - - // 1. Move all funds from the primary - // account to appropriate slots - - val transfers = currentTray.receive(receivingAccount = source, amount = amount).map { transfer -> - ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyTransfer, - intentId = intentId, - amount = transfer.kin, - source = currentTray.cluster(transfer.from), - destination = currentTray.cluster(transfer.to!!).vaultPublicKey - ) - } - - // 2. Redistribute the funds to prepare for - // future transfers - - val redistributes = currentTray.redistribute().map { exchange -> - ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyExchange, - intentId = intentId, - amount = exchange.kin, - source = currentTray.cluster(exchange.from), - destination = currentTray.cluster(exchange.to!!).vaultPublicKey // Exchanges always provide destination accounts - ) - } - - val endSlotBalance = currentTray.slotsBalance - - // Ensure that balances are consistent - // with what we expect these action to do - if (endSlotBalance - startSlotBalance != amount) { - throw IntentReceive.Companion.IntentReceiveException.BalanceMismatchException() - } - - val group = ActionGroup().apply { - actions = listOf( - *transfers.toTypedArray(), - *redistributes.toTypedArray() - ) - } - - return IntentDeposit( - id = intentId, - source = source, - organizer = organizer, - amount = amount, - actionGroup = group, - resultTray = currentTray - ) - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/intents/IntentEstablishRelationship.kt b/services/code/src/main/java/com/getcode/model/intents/IntentEstablishRelationship.kt deleted file mode 100644 index 5f6f4af5b..000000000 --- a/services/code/src/main/java/com/getcode/model/intents/IntentEstablishRelationship.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.common.v1.Model -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.model.Domain -import com.getcode.model.generate -import com.getcode.model.intents.actions.ActionOpenAccount -import com.getcode.model.toPublicKey -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Relationship -import com.getcode.solana.organizer.Tray - -class IntentEstablishRelationship( - override val id: PublicKey, - override val actionGroup: ActionGroup, - val organizer: Organizer, - val domain: Domain, - val resultTray: Tray, - val relationship: Relationship, -): IntentType() { - override fun metadata(): TransactionService.Metadata { - return TransactionService.Metadata.newBuilder() - .setEstablishRelationship( - TransactionService.EstablishRelationshipMetadata.newBuilder() - .setRelationship( - Model.Relationship.newBuilder() - .setDomain(Model.Domain.newBuilder().setValue(domain.relationshipHost)) - ) - ) - .build() - } - - companion object { - fun newInstance(organizer: Organizer, domain: Domain): IntentEstablishRelationship { - val id = PublicKey.generate() - val currentTray = organizer.tray.copy() - - val relationship = currentTray.createRelationship(domain) - - val ownerKey = currentTray.owner.getCluster().authority.keyPair.publicKeyBytes.toPublicKey() - val actionOpenAccount = ActionOpenAccount.newInstance( - owner = ownerKey, - type = AccountType.Relationship(domain), - accountCluster = relationship.getCluster() - ) - - return IntentEstablishRelationship( - id = id, - organizer = organizer, - domain = domain, - actionGroup = ActionGroup().apply { - actions = listOf(actionOpenAccount) - }, - resultTray = currentTray, - relationship = relationship, - ) - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/intents/IntentMigratePrivacy.kt b/services/code/src/main/java/com/getcode/model/intents/IntentMigratePrivacy.kt deleted file mode 100644 index 8b2107f9c..000000000 --- a/services/code/src/main/java/com/getcode/model/intents/IntentMigratePrivacy.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.crypt.DerivePath -import com.getcode.crypt.DerivedKey -import com.getcode.model.Kin -import com.getcode.model.generate -import com.getcode.model.intents.actions.ActionCloseEmptyAccount -import com.getcode.model.intents.actions.ActionWithdraw -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.AccountCluster -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Tray - -class IntentMigratePrivacy( - override val id: PublicKey, - private val organizer: Organizer, - private val amount: Kin, - - val resultTray: Tray, - - override val actionGroup: ActionGroup, -) : IntentType() { - override fun metadata(): TransactionService.Metadata { - return TransactionService.Metadata.newBuilder() - .setMigrateToPrivacy2022( - TransactionService.MigrateToPrivacy2022Metadata.newBuilder() - .setQuarks(amount.quarks) - ) - .build() - } - - companion object { - fun newInstance(organizer: Organizer, amount: Kin): IntentMigratePrivacy { - val intentId = PublicKey.generate() - - val legacyCluster = AccountCluster.newInstance( - authority = DerivedKey.derive( - path = DerivePath.primary, - mnemonic = organizer.mnemonic - ), - kind = AccountCluster.Kind.Timelock, - legacy = true - ) - - val tray = organizer.tray - val group = ActionGroup() - - if (amount.quarks > 0) { - // If there's a balance in the legacy account - // we'll move the funds over to a new private - // primary account - group.actions = listOf( - ActionWithdraw.newInstance( - kind = ActionWithdraw.Kind.NoPrivacyWithdraw(amount), - cluster = legacyCluster, - destination = organizer.primaryVault, - legacy = true - ) - ) - tray.increment(type = AccountType.Primary, kin = amount) - } else { - // If there's no balance, we can - // simply close the account - group.actions = listOf( - ActionCloseEmptyAccount.newInstance( - type = AccountType.Primary, - cluster = legacyCluster, - legacy = true - ) - ) - } - - return IntentMigratePrivacy( - id = intentId, - organizer = organizer, - amount = amount, - actionGroup = group, - resultTray = tray - ) - } - } - -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/intents/IntentPrivateTransfer.kt b/services/code/src/main/java/com/getcode/model/intents/IntentPrivateTransfer.kt deleted file mode 100644 index 04876c5f3..000000000 --- a/services/code/src/main/java/com/getcode/model/intents/IntentPrivateTransfer.kt +++ /dev/null @@ -1,251 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.chat.v2.ChatService -import com.codeinc.gen.common.v1.Model -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.model.Fee -import com.getcode.model.Kin -import com.getcode.model.KinAmount -import com.getcode.model.SocialUser -import com.getcode.model.chat.Platform -import com.getcode.model.intents.actions.ActionFeePayment -import com.getcode.model.intents.actions.ActionOpenAccount -import com.getcode.model.intents.actions.ActionTransfer -import com.getcode.model.intents.actions.ActionWithdraw -import com.getcode.model.toPublicKey -import com.getcode.network.repository.toSolanaAccount -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Tray -import com.getcode.utils.toByteString -import timber.log.Timber - -sealed interface PrivateTransferMetadata { - data class Tip(val socialUser: SocialUser): PrivateTransferMetadata - data class Chat(val socialUser: SocialUser): PrivateTransferMetadata -} - -class IntentPrivateTransfer( - override val id: PublicKey, - private val organizer: Organizer, - private val destination: PublicKey, - // Amount requested to transfer - private val grossAmount: KinAmount, - // Amount after fees are paid - private val netAmount: KinAmount, - private val fee: Kin, - private val additionalFees: List, - private val isWithdrawal: Boolean, - private val metadata: PrivateTransferMetadata?, - val resultTray: Tray, - - override val actionGroup: ActionGroup, -) : IntentType() { - override fun metadata(): TransactionService.Metadata { - return TransactionService.Metadata.newBuilder() - .setSendPrivatePayment( - TransactionService.SendPrivatePaymentMetadata.newBuilder().apply { - setDestination(this@IntentPrivateTransfer.destination.bytes.toSolanaAccount()) - setIsWithdrawal(this@IntentPrivateTransfer.isWithdrawal) - setExchangeData( - TransactionService.ExchangeData.newBuilder() - .setQuarks(grossAmount.kin.quarks) - .setCurrency(grossAmount.rate.currency.name.lowercase()) - .setExchangeRate(grossAmount.rate.fx) - .setNativeAmount(grossAmount.fiat) - ) - - when (metadata) { - is PrivateTransferMetadata.Chat -> { - setIsChat(true) - setChatId( - Model.ChatId.newBuilder() - .setValue(metadata.socialUser.chatId.toByteString()) - ) - } - is PrivateTransferMetadata.Tip -> { - setIsTip(true) - setTippedUser(TransactionService.TippedUser.newBuilder() - .setPlatformValue(when (Platform.named(metadata.socialUser.platform)) { - Platform.Unknown -> ChatService.Platform.UNKNOWN_PLATFORM_VALUE - Platform.Twitter -> ChatService.Platform.TWITTER_VALUE - }) - .setUsername(metadata.socialUser.username) - ) - } - null -> Unit - } - } - ) - .build() - } - - companion object { - fun newInstance( - rendezvousKey: PublicKey, - organizer: Organizer, - destination: PublicKey, - amount: KinAmount, - fee: Kin, - additionalFees: List, - isWithdrawal: Boolean, - metadata: PrivateTransferMetadata?, - ): IntentPrivateTransfer { - if (fee > amount.kin) { - throw IntentPrivateTransferException.InvalidFeeException() - } - - // Compute all the fees that will be - // paid out of this transaction - val concreteFees = additionalFees.map { - val _fee = amount.kin.calculateFee(it.bps) - _fee to it.destination - } - - var netKin = amount.kin - fee - - // Apply the fee to the gross amount - concreteFees.onEach { (fee, destination) -> - netKin -= fee - } - - val netAmount = KinAmount.newInstance(kin = netKin, rate = amount.rate) - - val currentTray = organizer.tray.copy() - val startBalance = currentTray.availableBalance - - // 1. Move all funds from bucket accounts into the - // outgoing account and prepare to transfer - - val transfers = currentTray.transfer(amount = amount.kin).map { transfer -> - val sourceCluster = currentTray.cluster(transfer.from) - - // If the transfer is to another bucket, it's an internal - // exchange. Otherwise, it is considered a transfer. - if (transfer.to is AccountType.Bucket) { - ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyExchange, - intentId = rendezvousKey, - amount = transfer.kin, - source = sourceCluster, - destination = currentTray.slot((transfer.to as AccountType.Bucket).type).getCluster().vaultPublicKey - ) - } else { - ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyTransfer, - intentId = rendezvousKey, - amount = transfer.kin, - source = sourceCluster, - destination = currentTray.outgoing.getCluster().vaultPublicKey - ) - } - } - - val feePayments = mutableListOf() - - // Code Fee - if (fee > 0) { - feePayments.add( - ActionFeePayment.newInstance( - kind = ActionFeePayment.Kind.Code, - cluster = currentTray.outgoing.getCluster(), - amount = fee - ) - ) - } - - concreteFees.onEach { (feeAmount, destination) -> - feePayments.add( - ActionFeePayment.newInstance( - kind = ActionFeePayment.Kind.ThirdParty(destination), - cluster = currentTray.outgoing.getCluster(), - amount = feeAmount, - ) - ) - } - - // 2. Transfer all collected funds from the temp - // outgoing account to the destination account - - val outgoing = ActionWithdraw.newInstance( - kind = ActionWithdraw.Kind.NoPrivacyWithdraw(netAmount.kin), - cluster = currentTray.outgoing.getCluster(), - destination = destination, - metadata = metadata - ) - - // 3. Redistribute the funds to optimize for a - // subsequent payment out of the buckets - - val redistributes = currentTray.redistribute().map { exchange -> - ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyExchange, - intentId = rendezvousKey, - amount = exchange.kin, - source = currentTray.cluster(exchange.from), - destination = currentTray.cluster(exchange.to!!).vaultPublicKey - // Exchanges always provide destination accounts - ) - } - - // 4. Rotate the outgoing account - - currentTray.incrementOutgoing() - val newOutgoing = currentTray.outgoing - - val rotation = listOf( - ActionOpenAccount.newInstance( - owner = organizer.tray.owner.getCluster().authority.keyPair.publicKeyBytes.toPublicKey(), - type = AccountType.Outgoing, - accountCluster = newOutgoing.getCluster() - ), - ActionWithdraw.newInstance( - kind = ActionWithdraw.Kind.CloseDormantAccount(AccountType.Outgoing), - cluster = newOutgoing.getCluster(), - destination = currentTray.owner.getCluster().vaultPublicKey - ) - ) - - val endBalance = currentTray.availableBalance - - if (startBalance - endBalance != amount.kin) { - Timber.e( - "Expected: ${amount.kin}; actual = ${startBalance - endBalance}; " + - "difference: ${startBalance.quarks - currentTray.availableBalance.quarks - amount.kin.quarks}" - ) - throw IntentPrivateTransferException.BalanceMismatchException() - } - - val group = ActionGroup() - - group.actions += transfers - group.actions += listOf( - *feePayments.toTypedArray(), - outgoing, - *redistributes.toTypedArray(), - *rotation.toTypedArray() - ) - - return IntentPrivateTransfer( - id = rendezvousKey, - organizer = organizer, - destination = destination, - grossAmount = amount, - netAmount = netAmount, - fee = fee, - additionalFees = additionalFees, - isWithdrawal = isWithdrawal, - metadata = metadata, - actionGroup = group, - resultTray = currentTray, - ) - - } - } -} - -sealed class IntentPrivateTransferException: Exception() { - class BalanceMismatchException: IntentPrivateTransferException() - class InvalidFeeException: IntentPrivateTransferException() -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/intents/IntentPublicTransfer.kt b/services/code/src/main/java/com/getcode/model/intents/IntentPublicTransfer.kt deleted file mode 100644 index c54f1b3e9..000000000 --- a/services/code/src/main/java/com/getcode/model/intents/IntentPublicTransfer.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.model.KinAmount -import com.getcode.model.generate -import com.getcode.model.intents.actions.ActionTransfer -import com.getcode.network.repository.toSolanaAccount -import com.getcode.solana.keys.* -import com.getcode.solana.organizer.AccountCluster -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Tray - -class IntentPublicTransfer( - override val id: PublicKey, - private val organizer: Organizer, - private val sourceCluster: AccountCluster, - private val destination: PublicKey, - private val amount: KinAmount, - - val resultTray: Tray, - - override val actionGroup: ActionGroup, -) : IntentType() { - override fun metadata(): TransactionService.Metadata { - return TransactionService.Metadata.newBuilder() - .setSendPublicPayment( - TransactionService.SendPublicPaymentMetadata.newBuilder() - .setSource(sourceCluster.vaultPublicKey.bytes.toSolanaAccount()) - .setDestination(destination.bytes.toSolanaAccount()) - .setIsWithdrawal(true) - .setExchangeData( - TransactionService.ExchangeData.newBuilder() - .setQuarks(amount.kin.quarks) - .setCurrency(amount.rate.currency.name.lowercase()) - .setExchangeRate(amount.rate.fx) - .setNativeAmount(amount.fiat) - ) - ) - .build() - } - - sealed interface Destination { - data class Local(val accountType: AccountType): Destination - data class External(val publicKey: PublicKey): Destination - } - - companion object { - fun newInstance( - organizer: Organizer, - source: AccountType, - destination: Destination, - amount: KinAmount, - ): IntentPublicTransfer { - val id = PublicKey.generate() - val currentTray = organizer.tray.copy() - val sourceCluster = organizer.tray.cluster(source) - - - val target = when (destination) { - is Destination.External -> destination.publicKey - is Destination.Local -> organizer.tray.cluster(destination.accountType).vaultPublicKey - } - - // 1. Transfer all funds in the primary account - // directly to the destination. This is a public - // transfer so no buckets involved and no rotation - // required. - - val transfer = ActionTransfer.newInstance( - kind = ActionTransfer.Kind.NoPrivacyTransfer, - intentId = id, - amount = amount.kin, - source = sourceCluster, - destination = target - ) - - currentTray.decrement(source, kin = amount.kin) - - // If moving funds to an already known account - // we should update the balance accordingly - if (destination is Destination.Local) { - currentTray.increment(destination.accountType, amount.kin) - } - - return IntentPublicTransfer( - id = id, - organizer = organizer, - sourceCluster = sourceCluster, - destination = target, - amount = amount, - actionGroup = ActionGroup().apply { - actions = listOf(transfer) - }, - resultTray = currentTray, - ) - - } - } -} - -sealed class IntentPublicTransferException: Exception() { - class BalanceMismatchException: IntentPublicTransferException() -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/intents/IntentReceive.kt b/services/code/src/main/java/com/getcode/model/intents/IntentReceive.kt deleted file mode 100644 index acb3cea1a..000000000 --- a/services/code/src/main/java/com/getcode/model/intents/IntentReceive.kt +++ /dev/null @@ -1,127 +0,0 @@ -package com.getcode.model.intents - -import android.content.Context -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.model.Kin -import com.getcode.model.generate -import com.getcode.model.intents.actions.* -import com.getcode.model.toPublicKey -import com.getcode.network.repository.toSolanaAccount -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.Tray - -class IntentReceive( - override val id: PublicKey, - private val organizer: Organizer, - private val amount: Kin, - - val resultTray: Tray, - - override val actionGroup: ActionGroup, -) : IntentType() { - override fun metadata(): TransactionService.Metadata { - return TransactionService.Metadata.newBuilder() - .setReceivePaymentsPrivately( - TransactionService.ReceivePaymentsPrivatelyMetadata.newBuilder() - .setSource(organizer.tray.incoming.getCluster().vaultPublicKey.bytes.toSolanaAccount()) - .setQuarks(amount.quarks) - .setIsDeposit(false) - ) - .build() - } - - companion object { - fun newInstance( - context: Context, - organizer: Organizer, - amount: Kin - ): IntentReceive { - val intentId = PublicKey.generate() - val currentTray = organizer.tray.copy() - val startBalance = currentTray.availableBalance - - // 1. Move all funds from the incoming - // account to appropriate slots - - val transfers = currentTray.receive(AccountType.Incoming, amount = amount).map { transfer -> - ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyTransfer, - intentId = intentId, - amount = transfer.kin, - source = currentTray.cluster(transfer.from), - destination = - currentTray.cluster(transfer.to!!).vaultPublicKey - ) - } - - // 2. Redistribute the funds to prepare for - // future transfers - - val redistributes = currentTray.redistribute().map { exchange -> - ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyExchange, - intentId = intentId, - amount = exchange.kin, - source = currentTray.cluster(exchange.from), - destination = - currentTray.cluster(exchange.to!!).vaultPublicKey - // Exchanges always provide destination accounts - ) - } - - // 3. Rotate incoming account - - val oldIncoming = currentTray.incoming - currentTray.incrementIncoming() - val newIncoming = currentTray.incoming - - val rotation = mutableListOf( - ActionCloseEmptyAccount.newInstance( - type = AccountType.Incoming, - cluster = oldIncoming.getCluster() - ), - ActionOpenAccount.newInstance( - owner = organizer.tray.owner.getCluster().authority.keyPair.publicKeyBytes.toPublicKey(), - type = AccountType.Incoming, - accountCluster = newIncoming.getCluster() - ), - ActionWithdraw.newInstance( - kind = ActionWithdraw.Kind.CloseDormantAccount(AccountType.Incoming), - cluster = newIncoming.getCluster(), - destination = organizer.tray.owner.getCluster().vaultPublicKey - ) - ) - - val endBalance = currentTray.availableBalance - - // We're just moving funds from incoming - // account to buckets, the balance - // shouldn't change - if (endBalance != startBalance) { - throw IntentReceiveException.BalanceMismatchException() - } - - val group = ActionGroup().apply { - actions = listOf( - *transfers.toTypedArray(), - *redistributes.toTypedArray(), - *rotation.toTypedArray() - ) - } - - return IntentReceive( - id = intentId, - organizer = organizer, - amount = amount, - actionGroup = group, - resultTray = currentTray, - ) - } - - sealed class IntentReceiveException : Exception() { - class BalanceMismatchException : IntentReceiveException() - } - } -} diff --git a/services/code/src/main/java/com/getcode/model/intents/IntentRemoteReceive.kt b/services/code/src/main/java/com/getcode/model/intents/IntentRemoteReceive.kt deleted file mode 100644 index 7829751a1..000000000 --- a/services/code/src/main/java/com/getcode/model/intents/IntentRemoteReceive.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.getcode.model.intents - -import android.content.Context -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.model.Kin -import com.getcode.model.generate -import com.getcode.model.intents.actions.ActionWithdraw -import com.getcode.network.repository.toSolanaAccount -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.GiftCardAccount -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Tray - -class IntentRemoteReceive( - override val id: PublicKey, - private val organizer: Organizer, - private val giftCard: GiftCardAccount, - private val amount: Kin, - private val isVoidingGiftCard: Boolean, - - val resultTray: Tray, - - override val actionGroup: ActionGroup, -) : IntentType() { - override fun metadata(): TransactionService.Metadata { - return TransactionService.Metadata.newBuilder() - .setReceivePaymentsPublicly( - TransactionService.ReceivePaymentsPubliclyMetadata.newBuilder() - .setSource(giftCard.cluster.vaultPublicKey.bytes.toSolanaAccount()) - .setQuarks(amount.quarks) - .setIsRemoteSend(true) - .setIsIssuerVoidingGiftCard(isVoidingGiftCard) - ) - .build() - } - - companion object { - fun newInstance( - context: Context, - organizer: Organizer, - giftCard: GiftCardAccount, - amount: Kin, - isVoidingGiftCard: Boolean - ): IntentRemoteReceive { - val intentId = PublicKey.generate() - val currentTray = organizer.tray.copy() - val startBalance = currentTray.availableBalance - - val giftCardWithdraw = ActionWithdraw.newInstance( - kind = ActionWithdraw.Kind.NoPrivacyWithdraw(amount), - cluster = giftCard.cluster, - destination = organizer.incomingVault - ) - - currentTray.increment(AccountType.Incoming, amount) - - val endBalance = currentTray.availableBalance - if (endBalance - startBalance != amount) { - throw IntentRemoteReceiveException.BalanceMismatchException() - } - - return IntentRemoteReceive( - id = intentId, - organizer = organizer, - giftCard = giftCard, - amount = amount, - actionGroup = ActionGroup().apply { - actions = listOf(giftCardWithdraw) - }, - resultTray = currentTray, - isVoidingGiftCard = isVoidingGiftCard - ) - } - } - - sealed class IntentRemoteReceiveException : Exception() { - class BalanceMismatchException : IntentRemoteReceiveException() - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/intents/IntentRemoteSend.kt b/services/code/src/main/java/com/getcode/model/intents/IntentRemoteSend.kt deleted file mode 100644 index e542a9c4f..000000000 --- a/services/code/src/main/java/com/getcode/model/intents/IntentRemoteSend.kt +++ /dev/null @@ -1,177 +0,0 @@ -package com.getcode.model.intents - -import android.content.Context -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.model.KinAmount -import com.getcode.model.intents.actions.ActionOpenAccount -import com.getcode.model.intents.actions.ActionTransfer -import com.getcode.model.intents.actions.ActionWithdraw -import com.getcode.model.toPublicKey -import com.getcode.network.repository.toSolanaAccount -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.GiftCardAccount -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Tray -import timber.log.Timber - -class IntentRemoteSend( - override val id: PublicKey, - private val organizer: Organizer, - private val giftCard: GiftCardAccount, - private val amount: KinAmount, - - val resultTray: Tray, - - override val actionGroup: ActionGroup, -) : IntentType() { - override fun metadata(): TransactionService.Metadata { - return TransactionService.Metadata.newBuilder() - .setSendPrivatePayment( - TransactionService.SendPrivatePaymentMetadata.newBuilder() - .setDestination(giftCard.cluster.vaultPublicKey.bytes.toSolanaAccount()) - .setIsWithdrawal(false) - .setIsRemoteSend(true) - .setExchangeData( - TransactionService.ExchangeData.newBuilder() - .setQuarks(amount.kin.quarks) - .setCurrency(amount.rate.currency.name.lowercase()) - .setExchangeRate(amount.rate.fx) - .setNativeAmount(amount.fiat) - ) - ) - .build() - } - - companion object { - fun newInstance( - context: Context, - rendezvousKey: PublicKey, - organizer: Organizer, - giftCard: GiftCardAccount, - amount: KinAmount - ): IntentRemoteSend { - val currentTray = organizer.tray.copy() - val startBalance = currentTray.availableBalance - - // 1. Open gift card account - - val openGiftCard = ActionOpenAccount.newInstance( - owner = giftCard.cluster.authority.keyPair.publicKeyBytes.toPublicKey(), - type = AccountType.RemoteSend, - accountCluster = giftCard.cluster - ) - - // 2. Move all funds from bucket accounts into the - // outgoing account and prepare to transfer - - val transfers = currentTray.transfer(amount = amount.kin).map { transfer -> - val sourceCluster = currentTray.cluster(transfer.from) - - // If the transfer is to another bucket, it's an internal - // exchange. Otherwise, it is considered a transfer. - if (transfer.to is AccountType.Bucket) { - ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyExchange, - intentId = rendezvousKey, - amount = transfer.kin, - source = sourceCluster, - destination = currentTray.slot((transfer.to as AccountType.Bucket).type).getCluster().vaultPublicKey - ) - } else { - ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyTransfer, - intentId = rendezvousKey, - amount = transfer.kin, - source = sourceCluster, - destination = currentTray.outgoing.getCluster().vaultPublicKey - ) - } - } - - // 3. Transfer all collected funds from the temp - // outgoing account to the destination account - - val outgoing = ActionWithdraw.newInstance( - kind = ActionWithdraw.Kind.NoPrivacyWithdraw(amount.kin), - cluster = currentTray.outgoing.getCluster(), - destination = giftCard.cluster.vaultPublicKey - ) - - // 4. Redistribute the funds to optimize for a - // subsequent payment out of the buckets - - val redistributes = currentTray.redistribute().map { exchange -> - ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyExchange, - intentId = rendezvousKey, - amount = exchange.kin, - source = currentTray.cluster(exchange.from), - destination = currentTray.cluster(exchange.to!!).vaultPublicKey - // Exchanges always provide destination accounts - ) - } - - // 5. Rotate the outgoing account - - currentTray.incrementOutgoing() - val newOutgoing = currentTray.outgoing - - val rotation = listOf( - ActionOpenAccount.newInstance( - owner = currentTray.owner.getCluster().authority.keyPair.publicKeyBytes.toPublicKey(), - type = AccountType.Outgoing, - accountCluster = newOutgoing.getCluster() - ), - ActionWithdraw.newInstance( - kind = ActionWithdraw.Kind.CloseDormantAccount(AccountType.Outgoing), - cluster = newOutgoing.getCluster(), - destination = currentTray.owner.getCluster().vaultPublicKey - ) - ) - - // 6. Close gift card account - - val closeGiftCard = ActionWithdraw.newInstance( - kind = ActionWithdraw.Kind.CloseDormantAccount(AccountType.RemoteSend), - cluster = giftCard.cluster, - destination = currentTray.owner.getCluster().vaultPublicKey - ) - - val endBalance = currentTray.availableBalance - - if (startBalance - endBalance != amount.kin) { - Timber.e( - "Expected: ${amount.kin}; actual = ${startBalance - endBalance}; " + - "difference: ${startBalance.quarks - currentTray.availableBalance.quarks - amount.kin.quarks}" - ) - throw IntentRemoteSendException.BalanceMismatchException() - } - - val group = ActionGroup().apply { - actions = listOf( - openGiftCard, - *transfers.toTypedArray(), - outgoing, - *redistributes.toTypedArray(), - *rotation.toTypedArray(), - closeGiftCard - ) - } - - return IntentRemoteSend( - id = rendezvousKey, - organizer = organizer, - giftCard = giftCard, - amount = amount, - actionGroup = group, - resultTray = currentTray, - ) - - } - } - - sealed class IntentRemoteSendException : Exception() { - class BalanceMismatchException : IntentRemoteSendException() - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/intents/IntentType.kt b/services/code/src/main/java/com/getcode/model/intents/IntentType.kt deleted file mode 100644 index 586cf70bd..000000000 --- a/services/code/src/main/java/com/getcode/model/intents/IntentType.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.ed25519.Ed25519 -import com.getcode.model.intents.actions.ActionType -import com.getcode.model.intents.actions.numberActions -import com.getcode.network.integrity.toDeviceToken -import com.getcode.network.repository.* -import com.getcode.solana.Message -import com.getcode.solana.SolanaTransaction -import com.getcode.utils.sign - -abstract class IntentType { - abstract val id: com.getcode.solana.keys.PublicKey - abstract val actionGroup: ActionGroup - - fun getActions() = actionGroup.actions - fun getAction(index: Int) = getActions()[index] - - fun apply(parameters: List) { - if (parameters.size != actionGroup.actions.size) { - throw Exception(Error.InvalidParameterCount.name) - } - - parameters.forEachIndexed { index, parameter -> - if (actionGroup.actions[index].id != parameter.actionId) { - throw Exception(Error.ActionParameterMismatch.name) - } - actionGroup.actions[index].serverParameter = parameter - } - } - - fun transaction(): SolanaTransaction { - val message = actionGroup.actions.flatMap { it.transactions() }.map { it.message } - .let { Message.newInstance(it.map { it.encode().toList() }.flatten()) }!! - val sigs = actionGroup.actions.flatMap { it.signatures() } - - return SolanaTransaction(message, sigs) - } - - fun signatures(): List = - actionGroup.actions.map { it.signatures().firstOrNull() }.mapNotNull { it } - - abstract fun metadata(): TransactionService.Metadata - - fun requestToSubmitSignatures(): TransactionService.SubmitIntentRequest { - return TransactionService.SubmitIntentRequest.newBuilder() - .setSubmitSignatures( - TransactionService.SubmitIntentRequest.SubmitSignatures.newBuilder() - .addAllSignatures(signatures().map { it.bytes.toByteArray().toSignature() }) - ) - .build() - } - - fun requestToSubmitActions(owner: Ed25519.KeyPair, deviceToken: String? = null): TransactionService.SubmitIntentRequest { - val submitActionsBuilder = TransactionService.SubmitIntentRequest.SubmitActions.newBuilder() - submitActionsBuilder.owner = owner.publicKeyBytes.toSolanaAccount() - submitActionsBuilder.id = id.toIntentId() - submitActionsBuilder.metadata = metadata() - submitActionsBuilder.addAllActions(actionGroup.actions.map { it.action() }) - - if (deviceToken != null) { - submitActionsBuilder.setDeviceToken(deviceToken.toDeviceToken()) - } - - submitActionsBuilder.signature = submitActionsBuilder.sign(owner) - - return TransactionService.SubmitIntentRequest.newBuilder() - .setSubmitActions(submitActionsBuilder) - .build() - } - - enum class Error { - InvalidParameterCount, - ActionParameterMismatch - } -} - -class ActionGroup { - var actions: List = listOf() - set(value) { - field = value.numberActions() - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/intents/IntentUpgradePrivacy.kt b/services/code/src/main/java/com/getcode/model/intents/IntentUpgradePrivacy.kt deleted file mode 100644 index 9f7a37258..000000000 --- a/services/code/src/main/java/com/getcode/model/intents/IntentUpgradePrivacy.kt +++ /dev/null @@ -1,149 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.crypt.MnemonicPhrase -import com.getcode.model.Kin -import com.getcode.model.UpgradeableIntent -import com.getcode.model.extensions.newInstance -import com.getcode.model.intents.actions.ActionPrivacyUpgrade -import com.getcode.model.intents.actions.ActionTransfer -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.Signature -import com.getcode.solana.keys.SplitterCommitmentAccounts -import com.getcode.solana.organizer.AccountCluster - -class IntentUpgradePrivacy( - override val id: PublicKey, - override val actionGroup: ActionGroup, -) : IntentType() { - override fun metadata(): TransactionService.Metadata { - return TransactionService.Metadata.newBuilder() - .setUpgradePrivacy(TransactionService.UpgradePrivacyMetadata.getDefaultInstance()) - .build() - } - - companion object { - fun newInstance( - mnemonic: MnemonicPhrase, - upgradeableIntent: UpgradeableIntent - ): IntentUpgradePrivacy { - val actionsMapped = upgradeableIntent.actions.map { upgradeableAction -> - val actionAmount = upgradeableAction.originalAmount - val originalDestination = upgradeableAction.originalDestination - val treasury = upgradeableAction.treasury - val recentRoot = upgradeableAction.recentRoot - val originalNonce = upgradeableAction.originalNonce - val originalRecentBlockhash = upgradeableAction.originalRecentBlockhash - - val sourceCluster = AccountCluster.using( - type = upgradeableAction.sourceAccountType, - index = upgradeableAction.sourceDerivationIndex.toInt(), - mnemonic = mnemonic - ) - - // Validate the server isn't malicious and is providing - // the original details of the transaction - validate( - transactionData = upgradeableAction.transactionBlob, - clientSignature = upgradeableAction.clientSignature, - intentId = upgradeableIntent.id, - actionId = upgradeableAction.id, - amount = actionAmount, - source = sourceCluster, - destination = originalDestination, - originalNonce = originalNonce, - treasury = treasury, - recentRoot = recentRoot - ) - - // We have to derive the original commitment accounts because - // we'll need to verify whether the commitment state account - // is part of the merkle tree provided by server paramaeters - val originalSplitterAccounts = SplitterCommitmentAccounts.newInstance( - source = sourceCluster, - destination = originalDestination, - amount = actionAmount, - treasury = treasury, - recentRoot = recentRoot, - intentId = upgradeableIntent.id, - actionId = upgradeableAction.id - ) - - ActionPrivacyUpgrade.newInstance( - source = sourceCluster, - originalActionID = upgradeableAction.id, - originalCommitmentStateAccount = originalSplitterAccounts.state.publicKey, - originalAmount = actionAmount, - originalNonce = originalNonce, - originalRecentBlockhash = originalRecentBlockhash, - treasury = treasury - ) - } - - return IntentUpgradePrivacy( - id = upgradeableIntent.id, - actionGroup = ActionGroup().apply { this.actions = actionsMapped } - ) - } - - - fun validate( - transactionData: List, - clientSignature: Signature, - intentId: PublicKey, - actionId: Int, - amount: Kin, - source: AccountCluster, - destination: PublicKey, - originalNonce: PublicKey, - treasury: PublicKey, - recentRoot: com.getcode.solana.keys.Hash - ) { - val transaction = SolanaTransaction.fromList(transactionData) - ?: throw IntentUpgradePrivacyException.FailedToParseTransactionException() - - val originalTransfer = ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyTransfer, - intentId = intentId, - amount = amount, - source = source, - destination = destination - ) - - originalTransfer.id = actionId - originalTransfer.serverParameter = ServerParameter( - actionId = actionId, - parameter = ServerParameter.Parameter.TempPrivacy( - treasury = treasury, - recentRoot = recentRoot - ), - configs = listOf( - ServerParameter.Config( - nonce = originalNonce, - blockhash = transaction.recentBlockhash - ) - ), - ) - - val originalTransaction = originalTransfer.transactions()[0] - - if (originalTransaction.encode() != transactionData) { - throw IntentUpgradePrivacyException.TransactionMismatchException() - } - - // (Optional) Reach into transaction and make sure the source is the same - val signature = originalTransaction.sign(source.authority.keyPair).firstOrNull() - - if (signature != clientSignature) { - throw IntentUpgradePrivacyException.SignatureMismatchException() - } - } - } -} - -sealed class IntentUpgradePrivacyException : Exception() { - class FailedToParseTransactionException : IntentUpgradePrivacyException() - class TransactionMismatchException : IntentUpgradePrivacyException() - class SignatureMismatchException : IntentUpgradePrivacyException() -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/intents/ServerParameter.kt b/services/code/src/main/java/com/getcode/model/intents/ServerParameter.kt deleted file mode 100644 index ef1ba45d5..000000000 --- a/services/code/src/main/java/com/getcode/model/intents/ServerParameter.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.model.Kin -import com.getcode.model.toHash -import com.getcode.model.toPublicKey -import com.getcode.solana.keys.Hash -import com.getcode.solana.keys.PublicKey - -class ServerParameter( - val actionId: Int, - val parameter: Parameter?, - val configs: List -) { - data class Config(val nonce: PublicKey, val blockhash: Hash) - - sealed class Parameter { - data class TempPrivacy(val treasury: PublicKey, val recentRoot: Hash): Parameter() - data class PermanentPrivacyUpgrade( - val newCommitment: PublicKey, - val newCommitmentTranscript: Hash, - val newCommitmentDestination: PublicKey, - val newCommitmentAmount: Kin, - val merkleRoot: Hash, - val merkleProof: List, - ): Parameter() - - data class FeePayment(val publicKey: PublicKey): Parameter() - - companion object { - fun newInstance(proto: TransactionService.ServerParameter): Parameter? { - return when (proto.typeCase) { - TransactionService.ServerParameter.TypeCase.TEMPORARY_PRIVACY_TRANSFER -> { - val param = proto.temporaryPrivacyTransfer - val treasury = PublicKey( - param.treasury.value.toByteArray().toList() - ) - val recentRoot = Hash( - param.recentRoot.value.toByteArray().toList() - ) - return TempPrivacy(treasury, recentRoot) - } - TransactionService.ServerParameter.TypeCase.TEMPORARY_PRIVACY_EXCHANGE -> { - val param = proto.temporaryPrivacyExchange - val treasury = PublicKey( - param.treasury.value.toByteArray().toList() - ) - val recentRoot = Hash( - param.recentRoot.value.toByteArray().toList() - ) - return TempPrivacy(treasury, recentRoot) - } - TransactionService.ServerParameter.TypeCase.PERMANENT_PRIVACY_UPGRADE -> { - val param = proto.permanentPrivacyUpgrade - val newCommitment = PublicKey( - param.newCommitment.value.toByteArray().toList() - ) - val newCommitmentTranscript = Hash( - param.newCommitmentTranscript.value.toByteArray().toList() - ) - val newCommitmentDestination = PublicKey( - param.newCommitmentDestination.value.toByteArray().toList() - ) - val merkleRoot = Hash( - param.merkleRoot.value.toByteArray().toList() - ) - - val merkleProof = param.merkleProofList.map { - Hash(it.value.toByteArray().toList()) - } - - PermanentPrivacyUpgrade( - newCommitment = newCommitment, - newCommitmentTranscript = newCommitmentTranscript, - newCommitmentDestination = newCommitmentDestination, - newCommitmentAmount = Kin.fromQuarks(quarks = param.newCommitmentAmount), - merkleRoot = merkleRoot, - merkleProof = merkleProof - ) - } - TransactionService.ServerParameter.TypeCase.FEE_PAYMENT -> { - val param = proto.feePayment - - // PublicKey will be `nil` for .thirdParty fee payments - val optionalDestination = PublicKey( - param.codeDestination.value.toByteArray().toList() - ) - FeePayment(optionalDestination) - } - TransactionService.ServerParameter.TypeCase.OPEN_ACCOUNT, - TransactionService.ServerParameter.TypeCase.CLOSE_EMPTY_ACCOUNT, - TransactionService.ServerParameter.TypeCase.CLOSE_DORMANT_ACCOUNT, - TransactionService.ServerParameter.TypeCase.NO_PRIVACY_WITHDRAW, - TransactionService.ServerParameter.TypeCase.TYPE_NOT_SET, - TransactionService.ServerParameter.TypeCase.NO_PRIVACY_TRANSFER -> null - } - } - - } - } - - companion object { - fun newInstance(proto: TransactionService.ServerParameter): ServerParameter { - return ServerParameter( - actionId = proto.actionId, - parameter = Parameter.newInstance(proto), - configs = proto.noncesList.map { - Config( - nonce = it.nonce.value.toByteArray().toPublicKey(), - blockhash = it.blockhash.value.toByteArray().toHash() - ) - } - ) - } - } -} - - diff --git a/services/code/src/main/java/com/getcode/model/intents/SwapIntent.kt b/services/code/src/main/java/com/getcode/model/intents/SwapIntent.kt deleted file mode 100644 index f0501eee3..000000000 --- a/services/code/src/main/java/com/getcode/model/intents/SwapIntent.kt +++ /dev/null @@ -1,114 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.common.v1.Model.InstructionAccount -import com.codeinc.gen.transaction.v2.TransactionService.SwapRequest -import com.codeinc.gen.transaction.v2.TransactionService.SwapResponse -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.generate -import com.getcode.model.toHash -import com.getcode.model.toPublicKey -import com.getcode.network.repository.toSignature -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.builder.TransactionBuilder -import com.getcode.solana.keys.AccountMeta -import com.getcode.solana.keys.Hash -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.Signature -import com.getcode.solana.organizer.AccountCluster -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.Organizer -import com.google.protobuf.ByteString -import java.lang.IllegalStateException - -class SwapIntent( - val id: PublicKey, - val organizer: Organizer, - val owner: KeyPair, - val swapCluster: AccountCluster, -) { - - var parameters: SwapConfigParameters? = null - - fun sign(parameters: SwapConfigParameters): List { - val transaction = transaction(parameters) - return transaction.sign(organizer.swapKeyPair) - } - - fun transaction(parameters: SwapConfigParameters): SolanaTransaction { - return TransactionBuilder.swap( - fromUsdc = swapCluster, - toPrimary = organizer.primaryVault, - parameters = parameters - ) - } - - companion object { - fun newInstance(organizer: Organizer): SwapIntent { - return SwapIntent( - id = PublicKey.generate(), - organizer = organizer, - owner = organizer.ownerKeyPair, - swapCluster = organizer.tray.cluster(AccountType.Swap), - ) - } - } -} - -fun SwapIntent.requestToSubmitSignatures(): SwapRequest? = runCatching { - parameters ?: throw IllegalStateException("Missing swap parameters") - - return@runCatching SwapRequest.newBuilder() - .setSubmitSignature( - SwapRequest.SubmitSignature.newBuilder() - .setSignature(sign(parameters!!).first().bytes.toByteArray().toSignature()) - .build() - ).build() -}.getOrNull() - -data class SwapConfigParameters( - val payer: PublicKey, - val swapProgram: PublicKey, - val nonce: PublicKey, - val blockHash: Hash, - val maxToSend: Long, - val minToReceive: Long, - val computeUnitLimit: Int, - val computeUnitPrice: Long, - val swapAccounts: List, - val swapData: ByteString, -) { - companion object { - operator fun invoke(proto: SwapResponse.ServerParameters): SwapConfigParameters? { - return runCatching { - val payer = proto.payer.value.toByteArray().toPublicKey() - val swapProgram = proto.swapProgram.value.toByteArray().toPublicKey() - val nonce = proto.nonce.value.toByteArray().toPublicKey() - val blockHash = proto.recentBlockhash.value.toByteArray().toHash() - - SwapConfigParameters( - payer = payer, - swapProgram = swapProgram, - nonce = nonce, - blockHash = blockHash, - maxToSend = proto.maxToSend, - minToReceive = proto.minToReceive, - computeUnitLimit = proto.computeUnitLimit, - computeUnitPrice = proto.computeUnitPrice, - swapAccounts = proto.swapIxnAccountsList.mapNotNull { it.meta() }, - swapData = proto.swapIxnData - ) - }.getOrNull() - } - } -} - -private fun InstructionAccount.meta(): AccountMeta? = runCatching { - val publicKey = PublicKey(account.value.toList()) - AccountMeta( - publicKey = publicKey, - isSigner = isSigner, - isWritable = isWritable, - isPayer = false, - isProgram = false - ) -}.getOrNull() \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/intents/actions/ActionCloseEmptyAccount.kt b/services/code/src/main/java/com/getcode/model/intents/actions/ActionCloseEmptyAccount.kt deleted file mode 100644 index f626ef3cc..000000000 --- a/services/code/src/main/java/com/getcode/model/intents/actions/ActionCloseEmptyAccount.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.getcode.model.intents.actions - -import com.codeinc.gen.common.v1.Model -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.ed25519.Ed25519 -import com.getcode.model.Kin -import com.getcode.model.intents.ServerParameter -import com.getcode.network.repository.toSolanaAccount -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.builder.TransactionBuilder -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.AccountCluster - -class ActionCloseEmptyAccount( - override var id: Int, - override var serverParameter: ServerParameter? = null, - override val signer: Ed25519.KeyPair? = null, - - val type: AccountType, - val cluster: AccountCluster, - val legacy: Boolean - - -) : ActionType() { - override fun transactions(): List { - val timelock = cluster.timelock ?: return emptyList() - return serverParameter?.configs?.map { config -> - TransactionBuilder.closeEmptyAccount( - timelockDerivedAccounts = timelock, - maxDustAmount = Kin.fromKin(1), - nonce = config.nonce, - recentBlockhash = config.blockhash, - legacy = legacy - ) - } ?: listOf() - } - - override fun action(): TransactionService.Action { - return TransactionService.Action.newBuilder() - .apply { - this.id = - this@ActionCloseEmptyAccount.id - this.closeEmptyAccount = TransactionService.CloseEmptyAccountAction.newBuilder().apply { - this.accountType = - if (legacy) Model.AccountType.LEGACY_PRIMARY_2022 - else this@ActionCloseEmptyAccount.type.getAccountType() - this.authority = - this@ActionCloseEmptyAccount.cluster.authority.keyPair.publicKeyBytes.toSolanaAccount() - this.token = - this@ActionCloseEmptyAccount.cluster.vaultPublicKey.bytes.toSolanaAccount() - }.build() - } - .build() - } - - companion object { - fun newInstance( - type: AccountType, - cluster: AccountCluster, - legacy: Boolean = false - ): ActionCloseEmptyAccount { - return ActionCloseEmptyAccount( - id = 0, - type = type, - cluster = cluster, - signer = cluster.authority.keyPair, - legacy = legacy - ) - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/intents/actions/ActionFeePayment.kt b/services/code/src/main/java/com/getcode/model/intents/actions/ActionFeePayment.kt deleted file mode 100644 index 6d56fe75f..000000000 --- a/services/code/src/main/java/com/getcode/model/intents/actions/ActionFeePayment.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.getcode.model.intents.actions - -import com.codeinc.gen.transaction.v2.TransactionService -import com.codeinc.gen.transaction.v2.TransactionService.FeePaymentAction -import com.getcode.ed25519.Ed25519 -import com.getcode.model.Kin -import com.getcode.model.intents.ServerParameter -import com.getcode.network.repository.toSolanaAccount -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.builder.TransactionBuilder -import com.getcode.solana.organizer.AccountCluster - -class ActionFeePayment( - override var id: Int, - override var serverParameter: ServerParameter? = null, - override val signer: Ed25519.KeyPair? = null, - val kind: Kind, - val cluster: AccountCluster, - val amount: Kin, - val configCountRequirement: Int = 1, -): ActionType() { - - sealed interface Kind { - val codeType: Int - data object Code: Kind { - override val codeType: Int = 0 - } - data class ThirdParty(val destination: com.getcode.solana.keys.PublicKey): Kind { - override val codeType: Int = 1 - } - } - - override fun transactions(): List { - val configs = serverParameter?.configs ?: return emptyList() - - val timelock = cluster.timelock ?: return emptyList() - - val destination: com.getcode.solana.keys.PublicKey = when (kind) { - Kind.Code -> { - (serverParameter?.parameter as? ServerParameter.Parameter.FeePayment)?.publicKey ?: return emptyList() - } - is Kind.ThirdParty -> kind.destination - } - - return configs.map { config -> - TransactionBuilder.transfer( - timelockDerivedAccounts = timelock, - destination = destination, - amount = amount, - nonce = config.nonce, - recentBlockhash = config.blockhash, - kreIndex = kreIndex - ) - } - } - - override fun action(): TransactionService.Action { - return TransactionService.Action.newBuilder() - .setId(id) - .setFeePayment( - FeePaymentAction.newBuilder() - .setTypeValue(kind.codeType) - .setAuthority(cluster.authority.keyPair.publicKeyBytes.toSolanaAccount()) - .setSource(cluster.vaultPublicKey.bytes.toSolanaAccount()) - .setAmount(amount.quarks) - .apply { - if (kind is Kind.ThirdParty) { - setDestination(kind.destination.bytes.toSolanaAccount()) - } - } - .build() - ).build() - } - - companion object { - fun newInstance(kind: Kind, cluster: AccountCluster, amount: Kin): ActionFeePayment { - return ActionFeePayment( - id = 0, - signer = cluster.authority.keyPair, - cluster = cluster, - kind = kind, - amount = amount - ) - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/intents/actions/ActionOpenAccount.kt b/services/code/src/main/java/com/getcode/model/intents/actions/ActionOpenAccount.kt deleted file mode 100644 index ce547935b..000000000 --- a/services/code/src/main/java/com/getcode/model/intents/actions/ActionOpenAccount.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.getcode.model.intents.actions - -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.ed25519.Ed25519 -import com.getcode.model.intents.ServerParameter -import com.getcode.network.repository.toSolanaAccount -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.organizer.AccountCluster -import com.getcode.solana.organizer.AccountType -import com.getcode.utils.sign - -class ActionOpenAccount( - override var id: Int, - override var serverParameter: ServerParameter? = null, - override val signer: Ed25519.KeyPair?, - val owner: com.getcode.solana.keys.PublicKey, - val type: AccountType, - val accountCluster: AccountCluster -) : ActionType() { - //static let configCountRequirement: Int = 0 - - override fun transactions(): List = listOf() - - override fun action(): TransactionService.Action { - return TransactionService.Action.newBuilder() - .apply { - this.id = - this@ActionOpenAccount.id - this.openAccount = TransactionService.OpenAccountAction.newBuilder().apply { - this.index = - this@ActionOpenAccount.accountCluster.index.toLong() - this.owner = - this@ActionOpenAccount.owner.bytes.toSolanaAccount() - this.accountType = - this@ActionOpenAccount.type.getAccountType() - this.authority = - this@ActionOpenAccount.accountCluster.authority.keyPair.publicKeyBytes.toSolanaAccount() - this.token = - this@ActionOpenAccount.accountCluster.vaultPublicKey - .bytes.toSolanaAccount() - this.authoritySignature = - this.sign(accountCluster.authority.keyPair) - }.build() - } - .build() - } - - companion object { - fun newInstance( - owner: com.getcode.solana.keys.PublicKey, - type: AccountType, - accountCluster: AccountCluster - ): ActionOpenAccount { - return ActionOpenAccount( - id = 0, - owner = owner, - type = type, - accountCluster = accountCluster, - signer = null - ) - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/intents/actions/ActionPrivacyUpgrade.kt b/services/code/src/main/java/com/getcode/model/intents/actions/ActionPrivacyUpgrade.kt deleted file mode 100644 index 8c972ecad..000000000 --- a/services/code/src/main/java/com/getcode/model/intents/actions/ActionPrivacyUpgrade.kt +++ /dev/null @@ -1,127 +0,0 @@ -package com.getcode.model.intents.actions - -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.ed25519.Ed25519 -import com.getcode.model.Kin -import com.getcode.model.extensions.newInstance -import com.getcode.model.intents.ServerParameter -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.builder.TransactionBuilder -import com.getcode.solana.keys.verifyContained -import com.getcode.solana.organizer.AccountCluster -import timber.log.Timber - -class ActionPrivacyUpgrade( - override var id: Int, - override var serverParameter: ServerParameter? = null, - override val signer: Ed25519.KeyPair?, - - var source: AccountCluster, - var originalActionID: Int, - var originalCommitmentStateAccount: com.getcode.solana.keys.PublicKey, - var originalAmount: Kin, - var originalNonce: com.getcode.solana.keys.PublicKey, - var originalRecentBlockhash: com.getcode.solana.keys.Hash, - var treasury: com.getcode.solana.keys.PublicKey -) : ActionType() { - val configCountRequirement: Int = 1 - - override fun transactions(): List { - serverParameter ?: throw ActionPrivacyUpgradeException.MissingServerParameterException() - - val privacyUpgrade = serverParameter?.parameter - if (privacyUpgrade !is ServerParameter.Parameter.PermanentPrivacyUpgrade) { - throw ActionPrivacyUpgradeException.MissingPrivacyUpgradeParameterException() - } - - /// Validate the merkle proof and ensure that the original commitment - /// accounts exist in the merkle tree provided by the server via the - /// `merkleRoot` and `merkleProof` params - - val leaf = originalCommitmentStateAccount - - val isProofValid = leaf.verifyContained( - privacyUpgrade.merkleRoot, - privacyUpgrade.merkleProof - ) - - Timber.i("isProofValid: $isProofValid") - - if (!isProofValid) { - throw ActionPrivacyUpgradeException.InvalidMerkleProofException() - } - - val timelock = source.timelock ?: throw ActionPrivacyUpgradeException.InvalidSourceException() - - // Server may provide the nonce and recentBlockhash and - // it may match the original but we shouldn't trust it. - // We'll user the original nonce and recentBlockhash that - // the original transaction used. - - val splitterAccounts = com.getcode.solana.keys.SplitterCommitmentAccounts.newInstance( - treasury = treasury, - destination = privacyUpgrade.newCommitmentDestination, - recentRoot = privacyUpgrade.merkleRoot, - transcript = privacyUpgrade.newCommitmentTranscript, - amount = privacyUpgrade.newCommitmentAmount - ) - - val transaction = TransactionBuilder.transfer( - timelockDerivedAccounts = timelock, - destination = splitterAccounts.vault.publicKey, - amount = originalAmount, - nonce = originalNonce, - recentBlockhash = originalRecentBlockhash, - kreIndex = kreIndex - ) - - return listOf(transaction) - - } - - override fun action(): TransactionService.Action { - return TransactionService.Action.newBuilder() - .apply { - this.id = - this@ActionPrivacyUpgrade.id - this.permanentPrivacyUpgrade = - TransactionService.PermanentPrivacyUpgradeAction.newBuilder().apply { - this.actionId = - this@ActionPrivacyUpgrade.originalActionID - }.build() - } - .build() - } - - companion object { - fun newInstance( - source: AccountCluster, - originalActionID: Int, - originalCommitmentStateAccount: com.getcode.solana.keys.PublicKey, - originalAmount: Kin, - originalNonce: com.getcode.solana.keys.PublicKey, - originalRecentBlockhash: com.getcode.solana.keys.Hash, - treasury: com.getcode.solana.keys.PublicKey - ): ActionPrivacyUpgrade { - return ActionPrivacyUpgrade( - id = 0, - signer = source.authority.keyPair, - source = source, - - originalActionID = originalActionID, - originalCommitmentStateAccount = originalCommitmentStateAccount, - originalAmount = originalAmount, - originalNonce = originalNonce, - originalRecentBlockhash = originalRecentBlockhash, - treasury = treasury - ) - } - } -} - -sealed class ActionPrivacyUpgradeException : Exception() { - class MissingServerParameterException : ActionPrivacyUpgradeException() - class MissingPrivacyUpgradeParameterException : ActionPrivacyUpgradeException() - class InvalidMerkleProofException : ActionPrivacyUpgradeException() - class InvalidSourceException: ActionPrivacyUpgradeException() -} diff --git a/services/code/src/main/java/com/getcode/model/intents/actions/ActionTransfer.kt b/services/code/src/main/java/com/getcode/model/intents/actions/ActionTransfer.kt deleted file mode 100644 index d0a575ecb..000000000 --- a/services/code/src/main/java/com/getcode/model/intents/actions/ActionTransfer.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.getcode.model.intents.actions - -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.ed25519.Ed25519 -import com.getcode.model.Kin -import com.getcode.model.extensions.newInstance -import com.getcode.model.intents.ServerParameter -import com.getcode.model.intents.actions.ActionTransfer.Kind.* -import com.getcode.network.repository.toSolanaAccount -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.builder.TransactionBuilder -import com.getcode.solana.organizer.AccountCluster - -class ActionTransfer( - override var id: Int, - override var serverParameter: ServerParameter? = null, - override val signer: Ed25519.KeyPair? = null, - - val kind: Kind, - val intentId: com.getcode.solana.keys.PublicKey, - val amount: Kin, - val source: AccountCluster, - val destination: com.getcode.solana.keys.PublicKey, -) : ActionType() { - - override fun transactions(): List { - val serverParameter = serverParameter ?: return emptyList() - val timelock = source.timelock ?: return emptyList() - - val tempPrivacyParameter = serverParameter.parameter - - val resolvedDestination: com.getcode.solana.keys.PublicKey = if (tempPrivacyParameter is ServerParameter.Parameter.TempPrivacy) { - val splitterAccounts = com.getcode.solana.keys.SplitterCommitmentAccounts.newInstance( - source = source, - destination = destination, - amount = amount, - treasury = tempPrivacyParameter.treasury, - recentRoot = tempPrivacyParameter.recentRoot, - intentId = intentId, - actionId = id - ) - - splitterAccounts.vault.publicKey - } else { - destination - } - - return serverParameter.configs.map { config -> - TransactionBuilder.transfer( - timelockDerivedAccounts = timelock, - destination = resolvedDestination, - amount = amount, - nonce = config.nonce, - recentBlockhash = config.blockhash, - kreIndex = kreIndex - ) - } - } - - override fun action(): TransactionService.Action { - return TransactionService.Action.newBuilder() - .apply { - this.id = - this@ActionTransfer.id - - when (kind) { - TempPrivacyTransfer -> { - this.temporaryPrivacyTransfer = - TransactionService.TemporaryPrivacyTransferAction.newBuilder().apply { - this.source = - this@ActionTransfer.source.vaultPublicKey.bytes.toSolanaAccount() - this.destination = - this@ActionTransfer.destination.bytes.toSolanaAccount() - this.authority = - this@ActionTransfer.source.authority.keyPair.publicKeyBytes.toSolanaAccount() - this.amount = - this@ActionTransfer.amount.quarks - }.build() - } - TempPrivacyExchange -> { - this.temporaryPrivacyExchange = - TransactionService.TemporaryPrivacyExchangeAction.newBuilder().apply { - this.source = - this@ActionTransfer.source.vaultPublicKey.bytes.toSolanaAccount() - this.destination = - this@ActionTransfer.destination.bytes.toSolanaAccount() - this.authority = - this@ActionTransfer.source.authority.keyPair.publicKeyBytes.toSolanaAccount() - this.amount = - this@ActionTransfer.amount.quarks - }.build() - } - NoPrivacyTransfer -> { - this.noPrivacyTransfer = - TransactionService.NoPrivacyTransferAction.newBuilder().apply { - this.source = - this@ActionTransfer.source.vaultPublicKey.bytes.toSolanaAccount() - this.destination = - this@ActionTransfer.destination.bytes.toSolanaAccount() - this.authority = - this@ActionTransfer.source.authority.keyPair.publicKeyBytes.toSolanaAccount() - this.amount = - this@ActionTransfer.amount.quarks - }.build() - } - } - }.build() - - - } - - companion object { - fun newInstance( - kind: Kind, - intentId: com.getcode.solana.keys.PublicKey, - amount: Kin, - source: AccountCluster, - destination: com.getcode.solana.keys.PublicKey - ): ActionTransfer { - return ActionTransfer( - id = 0, - signer = source.authority.keyPair, - kind = kind, - intentId = intentId, - amount = amount, - source = source, - destination = destination - ) - } - - const val configCountRequirement: Int = 1 - } - - enum class Kind { - TempPrivacyTransfer, - TempPrivacyExchange, - NoPrivacyTransfer, - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/intents/actions/ActionType.kt b/services/code/src/main/java/com/getcode/model/intents/actions/ActionType.kt deleted file mode 100644 index d6f233926..000000000 --- a/services/code/src/main/java/com/getcode/model/intents/actions/ActionType.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.getcode.model.intents.actions - -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.ed25519.Ed25519 -import com.getcode.model.intents.ServerParameter -import com.getcode.solana.SolanaTransaction - -abstract class ActionType { - abstract var id: Int - abstract var serverParameter: ServerParameter? - abstract val signer: Ed25519.KeyPair? - - //abstract var configCountRequirement: Int - - abstract fun transactions(): List - - fun signatures(): List { - return signer?.let { s -> - transactions().map { transaction -> transaction.sign(s).first() } - }.orEmpty() - } - - abstract fun action(): TransactionService.Action - - companion object { - const val kreIndex: Int = 268 - } -} - -fun List.numberActions(): List { - return this.mapIndexed { index, _ -> - this[index].apply { this.id = index } - } -} diff --git a/services/code/src/main/java/com/getcode/model/intents/actions/ActionWithdraw.kt b/services/code/src/main/java/com/getcode/model/intents/actions/ActionWithdraw.kt deleted file mode 100644 index 6aedc5f89..000000000 --- a/services/code/src/main/java/com/getcode/model/intents/actions/ActionWithdraw.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.getcode.model.intents.actions - -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.ed25519.Ed25519 -import com.getcode.model.Kin -import com.getcode.model.intents.PrivateTransferMetadata -import com.getcode.model.intents.ServerParameter -import com.getcode.model.toPublicKey -import com.getcode.network.repository.toSolanaAccount -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.builder.TransactionBuilder -import com.getcode.solana.organizer.AccountCluster -import com.getcode.solana.organizer.AccountType - -class ActionWithdraw( - override var id: Int, - override var serverParameter: ServerParameter? = null, - override val signer: Ed25519.KeyPair?, - - val kind: Kind, - - val cluster: AccountCluster, - val destination: com.getcode.solana.keys.PublicKey, - val legacy: Boolean, - val metadata: PrivateTransferMetadata? = null, -) : ActionType() { - - override fun transactions(): List { - val timelock = cluster.timelock ?: return emptyList() - return serverParameter?.configs?.map { config -> - TransactionBuilder.closeDormantAccount( - authority = cluster.authority.keyPair.publicKeyBytes.toPublicKey(), - timelockDerivedAccounts = timelock, - destination = destination, - nonce = config.nonce, - recentBlockhash = config.blockhash, - kreIndex = kreIndex, - legacy = legacy, - metadata = metadata, - ) - }.orEmpty() - } - - override fun action(): TransactionService.Action { - return TransactionService.Action.newBuilder() - .apply { - this.id = - this@ActionWithdraw.id - - when (kind) { - is Kind.CloseDormantAccount -> { - this.closeDormantAccount = - TransactionService.CloseDormantAccountAction.newBuilder().apply { - this.accountType = - this@ActionWithdraw.kind.accountType.getAccountType() - this.authority = - this@ActionWithdraw.cluster.authority.keyPair.publicKeyBytes.toSolanaAccount() - this.token = - this@ActionWithdraw.cluster.vaultPublicKey.bytes.toSolanaAccount() - this.destination = - this@ActionWithdraw.destination.bytes.toSolanaAccount() - }.build() - } - is Kind.NoPrivacyWithdraw -> { - this.noPrivacyWithdraw = - TransactionService.NoPrivacyWithdrawAction.newBuilder().apply { - this.authority = - this@ActionWithdraw.cluster.authority.keyPair.publicKeyBytes.toSolanaAccount() - this.source = - this@ActionWithdraw.cluster.vaultPublicKey.bytes.toSolanaAccount() - this.destination = - this@ActionWithdraw.destination.bytes.toSolanaAccount() - this.amount = - this@ActionWithdraw.kind.amount.quarks - this.shouldClose = - true - }.build() - } - } - }.build() - } - - companion object { - fun newInstance( - kind: Kind, - cluster: AccountCluster, - destination: com.getcode.solana.keys.PublicKey, - legacy: Boolean = false, - metadata: PrivateTransferMetadata? = null, - ): ActionWithdraw { - return ActionWithdraw( - id = 0, - signer = cluster.authority.keyPair, - - kind = kind, - cluster = cluster, - destination = destination, - legacy = legacy, - metadata = metadata - ) - } - - const val configCountRequirement: Int = 1 - } - - sealed class Kind { - data class CloseDormantAccount(val accountType: AccountType) : Kind() - data class NoPrivacyWithdraw(val amount: Kin): Kind() - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/notifications/CodeNotification.kt b/services/code/src/main/java/com/getcode/model/notifications/CodeNotification.kt deleted file mode 100644 index 0e7e5691b..000000000 --- a/services/code/src/main/java/com/getcode/model/notifications/CodeNotification.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.getcode.model.notifications - -import com.getcode.model.chat.MessageContent - -data class CodeNotification( - val type: NotificationType, - val title: String, - val body: MessageContent, -) - -enum class NotificationType { - ChatMessage, - Twitter, - ExecuteSwap, - Unknown; - - fun isNotifiable() = this != ExecuteSwap - - companion object { - fun tryValueOf(value: String): NotificationType { - return runCatching { valueOf(value) }.getOrNull() ?: Unknown - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/notifications/NotificationParser.kt b/services/code/src/main/java/com/getcode/model/notifications/NotificationParser.kt deleted file mode 100644 index 470bf2fc3..000000000 --- a/services/code/src/main/java/com/getcode/model/notifications/NotificationParser.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.getcode.model.notifications - -import com.codeinc.gen.chat.v1.ChatService -import com.getcode.model.chat.MessageContent -import com.getcode.model.protomapping.invoke -import com.getcode.utils.ErrorUtils -import com.getcode.utils.decodeBase64 -import com.google.firebase.messaging.RemoteMessage -import timber.log.Timber - -private const val NOTIFICATION_TYPE_KEY = "code_notification_type" -private const val NOTIFICATION_TITLE_KEY = "chat_title" -private const val NOTIFICATION_CONTENT_KEY = "message_content" - -fun RemoteMessage.parse(): CodeNotification? { - Timber.d("data=$data") - val typeString = data[NOTIFICATION_TYPE_KEY].let { - if (it == null) { - Timber.e("$NOTIFICATION_TYPE_KEY unspecified") - ErrorUtils.handleError(Throwable("$NOTIFICATION_TYPE_KEY unspecified")) - return null - } - it - } - - val type = NotificationType.tryValueOf(typeString) - if (type == NotificationType.Unknown) { - Timber.e("Unknown notification type: $typeString") - ErrorUtils.handleError(Throwable("Unknown notification type: $typeString")) - return null - } - - if (!type.isNotifiable()) return CodeNotification(type, "", MessageContent.Localized("", false)) - - val chatTitle = data[NOTIFICATION_TITLE_KEY].let { - if (it == null) { - Timber.e("$NOTIFICATION_TITLE_KEY unspecified") - ErrorUtils.handleError(Throwable("$NOTIFICATION_TITLE_KEY unspecified")) - return null - } - it - } - val messageContent = data[NOTIFICATION_CONTENT_KEY].let { - if (it == null) { - Timber.e("$NOTIFICATION_CONTENT_KEY unspecified") - ErrorUtils.handleError(Throwable("$NOTIFICATION_CONTENT_KEY unspecified")) - return null - } - it - } - - val messageData = messageContent.decodeBase64() - val rawContent = runCatching { - ChatService.Content.parseFrom(messageData) }.getOrNull().let { - if (it == null) { - Timber.e("unable to parse message content") - ErrorUtils.handleError(Throwable("unable to parse message content")) - return null - } - it - } - val content = MessageContent.invoke(rawContent).let { - if (it == null) { - Timber.e("failed to convert MessageContent") - ErrorUtils.handleError(Throwable("failed to convert MessageContent")) - return null - } - it - } - return CodeNotification(type, chatTitle, content) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/protomapping/ChatType.kt b/services/code/src/main/java/com/getcode/model/protomapping/ChatType.kt deleted file mode 100644 index 64938fd28..000000000 --- a/services/code/src/main/java/com/getcode/model/protomapping/ChatType.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.getcode.model.protomapping - -import com.codeinc.gen.chat.v2.ChatService -import com.getcode.model.chat.ChatType - -operator fun ChatType.Companion.invoke(proto: ChatService.ChatType): ChatType { - return runCatching { types[proto.ordinal] }.getOrNull() ?: ChatType.Unknown -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/protomapping/MessageContent.kt b/services/code/src/main/java/com/getcode/model/protomapping/MessageContent.kt deleted file mode 100644 index e88168d69..000000000 --- a/services/code/src/main/java/com/getcode/model/protomapping/MessageContent.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.getcode.model.protomapping - -import com.codeinc.gen.chat.v1.ChatService -import com.getcode.model.EncryptedData -import com.getcode.model.Fiat -import com.getcode.model.GenericAmount -import com.getcode.model.ID -import com.getcode.model.Kin -import com.getcode.model.KinAmount -import com.getcode.model.Rate -import com.getcode.model.chat.MessageContent -import com.getcode.model.chat.Reference -import com.getcode.model.chat.Verb -import com.getcode.model.toPublicKey - -operator fun MessageContent.Companion.invoke( - proto: ChatService.Content, - messageId: ID? = null, -): MessageContent? { - return when (proto.typeCase) { - ChatService.Content.TypeCase.SERVER_LOCALIZED -> MessageContent.Localized( - isFromSelf = false, - value = proto.serverLocalized.keyOrText - ) - - ChatService.Content.TypeCase.EXCHANGE_DATA -> { - val verb = Verb.invoke(proto.exchangeData.verb) - val isFromSelf = !verb.increasesBalance - when (proto.exchangeData.exchangeDataCase) { - ChatService.ExchangeDataContent.ExchangeDataCase.EXACT -> { - val exact = proto.exchangeData.exact - val currency = - com.getcode.model.CurrencyCode.tryValueOf(exact.currency) ?: return null - val kinAmount = KinAmount.newInstance( - kin = Kin.fromQuarks(exact.quarks), - rate = Rate( - fx = exact.exchangeRate, - currency = currency - ) - ) - - MessageContent.Exchange( - isFromSelf = isFromSelf, - amount = GenericAmount.Exact(kinAmount), - verb = verb, - reference = messageId?.let { Reference.IntentId(it) }, - ) - } - - ChatService.ExchangeDataContent.ExchangeDataCase.PARTIAL -> { - val partial = proto.exchangeData.partial - val currency = - com.getcode.model.CurrencyCode.tryValueOf(partial.currency) ?: return null - - val fiat = Fiat( - currency = currency, - amount = partial.nativeAmount - ) - - MessageContent.Exchange( - isFromSelf = isFromSelf, - amount = GenericAmount.Partial(fiat), - verb = verb, - reference = messageId?.let { Reference.IntentId(it) }, - ) - } - - ChatService.ExchangeDataContent.ExchangeDataCase.EXCHANGEDATA_NOT_SET -> return null - else -> return null - } - } - - ChatService.Content.TypeCase.NACL_BOX -> { - val encryptedContent = proto.naclBox - val peerPublicKey = - encryptedContent.peerPublicKey.value.toByteArray().toPublicKey() - - val data = com.getcode.model.EncryptedData( - peerPublicKey = peerPublicKey, - nonce = encryptedContent.nonce.toByteArray().toList(), - encryptedData = encryptedContent.encryptedPayload.toByteArray().toList(), - ) - MessageContent.SodiumBox(isFromSelf = false, data = data) - } - - ChatService.Content.TypeCase.TYPE_NOT_SET -> return null - else -> return null - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/protomapping/Platform.kt b/services/code/src/main/java/com/getcode/model/protomapping/Platform.kt deleted file mode 100644 index 5fa7b3c80..000000000 --- a/services/code/src/main/java/com/getcode/model/protomapping/Platform.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.getcode.model.protomapping - -import com.codeinc.gen.chat.v2.ChatService -import com.getcode.model.chat.Platform -import com.getcode.model.chat.Platform.Unknown -import com.getcode.model.chat.Platform.entries - -operator fun Platform.Companion.invoke(proto: ChatService.Platform): Platform { - return runCatching { entries[proto.ordinal] }.getOrNull() ?: Unknown -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/protomapping/Pointer.kt b/services/code/src/main/java/com/getcode/model/protomapping/Pointer.kt deleted file mode 100644 index d78cd17eb..000000000 --- a/services/code/src/main/java/com/getcode/model/protomapping/Pointer.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.getcode.model.protomapping - -import com.codeinc.gen.chat.v2.ChatService -import com.getcode.model.chat.Pointer -import com.getcode.model.uuid - -operator fun Pointer.Companion.invoke(proto: ChatService.Pointer): Pointer { - val memberId = proto.memberId.value.toList() - val messageId = proto.value.value.toList().uuid ?: return Pointer.Unknown(memberId) - - return when (proto.type) { - ChatService.PointerType.UNKNOWN_POINTER_TYPE -> Pointer.Unknown(proto.memberId.value.toList()) - ChatService.PointerType.READ -> Pointer.Read(memberId, messageId) - ChatService.PointerType.DELIVERED -> Pointer.Delivered(memberId, messageId) - ChatService.PointerType.SENT -> Pointer.Sent(memberId, messageId) - ChatService.PointerType.UNRECOGNIZED -> Pointer.Unknown(memberId) - else -> Pointer.Unknown(memberId) - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/protomapping/Reference.kt b/services/code/src/main/java/com/getcode/model/protomapping/Reference.kt deleted file mode 100644 index ccb8faefc..000000000 --- a/services/code/src/main/java/com/getcode/model/protomapping/Reference.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.getcode.model.protomapping - -import com.codeinc.gen.chat.v2.ChatService.ExchangeDataContent -import com.codeinc.gen.chat.v2.ChatService.ExchangeDataContent.ReferenceCase -import com.getcode.model.chat.Reference -import com.getcode.model.chat.Reference.IntentId -import com.getcode.model.chat.Reference.NoneSet -import com.getcode.model.chat.Reference.Signature - -operator fun Reference.Companion.invoke(proto: ExchangeDataContent): Reference { - return when (proto.referenceCase) { - ReferenceCase.INTENT -> IntentId(proto.intent.value.toByteArray().toList()) - ReferenceCase.SIGNATURE -> Signature(proto.signature.value.toByteArray().toList()) - ReferenceCase.REFERENCE_NOT_SET -> NoneSet - null -> NoneSet - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/protomapping/TwitterUser.kt b/services/code/src/main/java/com/getcode/model/protomapping/TwitterUser.kt deleted file mode 100644 index 1f7d698c2..000000000 --- a/services/code/src/main/java/com/getcode/model/protomapping/TwitterUser.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.getcode.model.protomapping - -import com.codeinc.gen.user.v1.IdentityService -import com.codeinc.gen.user.v1.friendshipCostOrNull -import com.getcode.model.CurrencyCode -import com.getcode.model.Fiat -import com.getcode.model.TwitterUser -import com.getcode.model.TwitterUser.VerificationStatus -import com.getcode.solana.keys.PublicKey - -operator fun TwitterUser.Companion.invoke(proto: IdentityService.TwitterUser): TwitterUser? { - val avatarUrl = proto.profilePicUrl - - val tipAddress = runCatching { PublicKey.fromByteString(proto.tipAddress.value) }.getOrNull() ?: return null - - return TwitterUser( - username = proto.username, - displayName = proto.name, - imageUrl = avatarUrl, - followerCount = proto.followerCount, - tipAddress = tipAddress, - verificationStatus = VerificationStatus.entries.getOrNull(proto.verifiedTypeValue) ?: VerificationStatus.unknown, - costOfFriendship = proto.friendshipCostOrNull?.let { - val currency = CurrencyCode.tryValueOf(it.currency) ?: return@let null - Fiat(currency, it.nativeAmount) - } ?: Fiat(currency = CurrencyCode.USD, amount = 1.00), - isFriend = runCatching { proto.isFriend }.getOrNull() ?: false, - chatId = proto.friendChatId.value.toList() - ) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/model/protomapping/Verb.kt b/services/code/src/main/java/com/getcode/model/protomapping/Verb.kt deleted file mode 100644 index 9200b2dd0..000000000 --- a/services/code/src/main/java/com/getcode/model/protomapping/Verb.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.getcode.model.protomapping - -import com.codeinc.gen.chat.v1.ChatService -import com.getcode.model.chat.Verb -import com.getcode.model.chat.Verb.Deposited -import com.getcode.model.chat.Verb.Gave -import com.getcode.model.chat.Verb.Paid -import com.getcode.model.chat.Verb.Purchased -import com.getcode.model.chat.Verb.Received -import com.getcode.model.chat.Verb.ReceivedTip -import com.getcode.model.chat.Verb.Returned -import com.getcode.model.chat.Verb.Sent -import com.getcode.model.chat.Verb.SentTip -import com.getcode.model.chat.Verb.Spent -import com.getcode.model.chat.Verb.Unknown -import com.getcode.model.chat.Verb.Withdrew - -fun Verb.Companion.invoke(proto: ChatService.ExchangeDataContent.Verb): Verb { - return when (proto) { - com.codeinc.gen.chat.v1.ChatService.ExchangeDataContent.Verb.UNKNOWN -> Unknown - com.codeinc.gen.chat.v1.ChatService.ExchangeDataContent.Verb.GAVE -> Gave - com.codeinc.gen.chat.v1.ChatService.ExchangeDataContent.Verb.RECEIVED -> Received - com.codeinc.gen.chat.v1.ChatService.ExchangeDataContent.Verb.WITHDREW -> Withdrew - com.codeinc.gen.chat.v1.ChatService.ExchangeDataContent.Verb.DEPOSITED -> Deposited - com.codeinc.gen.chat.v1.ChatService.ExchangeDataContent.Verb.SENT -> Sent - com.codeinc.gen.chat.v1.ChatService.ExchangeDataContent.Verb.RETURNED -> Returned - com.codeinc.gen.chat.v1.ChatService.ExchangeDataContent.Verb.SPENT -> Spent - com.codeinc.gen.chat.v1.ChatService.ExchangeDataContent.Verb.PAID -> Paid - com.codeinc.gen.chat.v1.ChatService.ExchangeDataContent.Verb.PURCHASED -> Purchased - com.codeinc.gen.chat.v1.ChatService.ExchangeDataContent.Verb.UNRECOGNIZED -> Unknown - com.codeinc.gen.chat.v1.ChatService.ExchangeDataContent.Verb.RECEIVED_TIP -> ReceivedTip - com.codeinc.gen.chat.v1.ChatService.ExchangeDataContent.Verb.SENT_TIP -> SentTip - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/BalanceController.kt b/services/code/src/main/java/com/getcode/network/BalanceController.kt deleted file mode 100644 index 41df32b1f..000000000 --- a/services/code/src/main/java/com/getcode/network/BalanceController.kt +++ /dev/null @@ -1,243 +0,0 @@ -package com.getcode.network - -import com.getcode.manager.SessionManager -import com.getcode.model.Currency -import com.getcode.model.CurrencyCode -import com.getcode.model.Rate -import com.getcode.network.client.TransactionReceiver -import com.getcode.network.exchange.Exchange -import com.getcode.network.repository.AccountRepository -import com.getcode.network.repository.BalanceRepository -import com.getcode.network.repository.TransactionRepository -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Tray -import com.getcode.utils.FormatUtils -import com.getcode.utils.trace -import io.reactivex.rxjava3.core.Completable -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import timber.log.Timber -import java.util.Locale -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -data class BalanceDisplay( - val marketValue: Double = 0.0, - val formattedValue: String = "", - val currency: Currency? = null, -) - -open class BalanceController @Inject constructor( - exchange: Exchange, - networkObserver: com.getcode.utils.network.NetworkConnectivityListener, - private val balanceRepository: BalanceRepository, - private val transactionRepository: TransactionRepository, - private val accountRepository: AccountRepository, - private val privacyMigration: PrivacyMigration, - private val transactionReceiver: TransactionReceiver, - private val getCurrencyFromCode: (CurrencyCode?) -> Currency?, - val suffix: (Currency?) -> String, -) { - private val scope = CoroutineScope(Dispatchers.IO) - fun observeRawBalance(): Flow = balanceRepository.balanceFlow - - val rawBalance: Double - get() = balanceRepository.balanceFlow.value - - private val _balanceDisplay = MutableStateFlow(null) - - val formattedBalance: StateFlow - get() = _balanceDisplay - .stateIn(scope, SharingStarted.Eagerly, BalanceDisplay()) - - init { - networkObserver.state - .map { it.connected } - .onEach { connected -> - if (connected) { - com.getcode.utils.network.retryable { fetchBalanceSuspend() } - } - } - .flatMapLatest { - combine( - exchange.observeLocalRate() - .flowOn(Dispatchers.IO) - .onEach { - val display = _balanceDisplay.value ?: BalanceDisplay() - _balanceDisplay.value = - display.copy(currency = getCurrencyFromCode(it.currency)) - } - .onEach { exchange.fetchRatesIfNeeded() }, - balanceRepository.balanceFlow, - ) { rate, balance -> - rate to balance.coerceAtLeast(0.0) - }.map { (rate, balance) -> - refreshBalance(balance, rate) - } - }.distinctUntilChanged().onEach { (marketValue, amountText) -> - val display = _balanceDisplay.value ?: BalanceDisplay() - _balanceDisplay.value = - display.copy(marketValue = marketValue, formattedValue = amountText) - }.launchIn(scope) - } - - fun setTray(organizer: Organizer, tray: Tray) { - organizer.set(tray) - balanceRepository.setBalance(organizer.availableBalance.toKinTruncatingLong().toDouble()) - } - - fun fetchBalance(): Completable { - trace("fetchBalance") - if (SessionManager.isAuthenticated() != true) { - Timber.d("FetchBalance - Not authenticated") - return Completable.complete() - } - val owner = SessionManager.getKeyPair() - ?: return Completable.error(IllegalStateException("Missing Owner")) - - fun getTokenAccountInfos(): Completable { - return accountRepository.getTokenAccountInfos(owner) - .flatMapCompletable { infos -> - val organizer = SessionManager.getOrganizer() - ?: return@flatMapCompletable Completable.error(IllegalStateException("Missing Organizer")) - - scope.launch { - organizer.setAccountInfo(infos) - SessionManager.update { it.copy(organizer = organizer) } - } - balanceRepository.setBalance(organizer.availableBalance.toKinValueDouble()) - transactionReceiver.receiveFromIncomingCompletable(organizer) - } - .timeout(15, TimeUnit.SECONDS) - } - - return getTokenAccountInfos() - .doOnSubscribe { - Timber.i("Fetching Balance account info") - } - .onErrorResumeNext { - Timber.i("Error: ${it.javaClass.simpleName} ${it.cause}") - val organizer = - SessionManager.getOrganizer() ?: return@onErrorResumeNext Completable.error( - IllegalStateException("Missing Organizer") - ) - - when (it) { - is AccountRepository.FetchAccountInfosException.MigrationRequiredException -> { - val amountToMigrate = it.accountInfo.balance - privacyMigration.migrateToPrivacy( - amountToMigrate = amountToMigrate, - organizer = organizer - ) - .ignoreElement() - .concatWith(getTokenAccountInfos()) - } - - is AccountRepository.FetchAccountInfosException.NotFoundException -> { - transactionRepository.createAccounts( - organizer = organizer - ) - .ignoreElement() - .concatWith(getTokenAccountInfos()) - } - - else -> { - Completable.error(it) - } - } - } - } - - - suspend fun fetchBalanceSuspend() { - Timber.d("fetching balance") - if (SessionManager.isAuthenticated() != true) { - Timber.d("FetchBalance - Not authenticated") - return - } - val owner = SessionManager.getKeyPair() ?: throw IllegalStateException("Missing Owner") - - try { - val accountInfo = accountRepository.getTokenAccountInfos(owner).blockingGet() - val organizer = - SessionManager.getOrganizer() ?: throw IllegalStateException("Missing Organizer") - - organizer.setAccountInfo(accountInfo) - SessionManager.update { it.copy(organizer = organizer) } - balanceRepository.setBalance(organizer.availableBalance.toKinValueDouble()) - transactionReceiver.receiveFromIncoming(organizer) - transactionRepository.swapIfNeeded(organizer) - } catch (ex: Exception) { - Timber.i("Error: ${ex.javaClass.simpleName} ${ex.message}") - val organizer = - SessionManager.getOrganizer() ?: throw IllegalStateException("Missing Organizer") - - when (ex) { - is AccountRepository.FetchAccountInfosException.MigrationRequiredException -> { - val amountToMigrate = ex.accountInfo.balance - privacyMigration.migrateToPrivacy( - amountToMigrate = amountToMigrate, - organizer = organizer - ) - } - - is AccountRepository.FetchAccountInfosException.NotFoundException -> { - transactionRepository.createAccounts( - organizer = organizer - ) - } - } - } - } - - private fun refreshBalance(balance: Double, rate: Rate): Pair { - val preferredCurrency = getCurrencyFromCode(rate.currency) - val fiatValue = FormatUtils.getFiatValue(balance, rate.fx) - - val prefix = - formatPrefix(preferredCurrency).takeIf { it != preferredCurrency?.code }.orEmpty() - - val amountText = StringBuilder().apply { - append(prefix) - append(formatAmount(fiatValue, preferredCurrency)) - val suffix = suffix(preferredCurrency) - if (suffix.isNotEmpty()) { - append(" ") - append(suffix) - } - }.toString() - - Timber.d("formatted balance is now $prefix $amountText in ${preferredCurrency?.code}") - - return fiatValue to amountText - } - - private fun formatPrefix(selectedCurrency: Currency?): String { - if (selectedCurrency == null) return "" - return if (!isKin(selectedCurrency)) selectedCurrency.symbol else "" - } - - private fun isKin(selectedCurrency: Currency): Boolean = - selectedCurrency.code == com.getcode.model.CurrencyCode.KIN.name - - private fun formatAmount(amount: Double, currency: Currency?): String { - return if (amount % 1 == 0.0 || currency?.code == com.getcode.model.CurrencyCode.KIN.name) { - String.format(Locale.getDefault(), "%,.0f", amount) - } else { - String.format(Locale.getDefault(), "%,.2f", amount) - } - } -} diff --git a/services/code/src/main/java/com/getcode/network/IdentityManager.kt b/services/code/src/main/java/com/getcode/network/IdentityManager.kt deleted file mode 100644 index a1d4db62e..000000000 --- a/services/code/src/main/java/com/getcode/network/IdentityManager.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.getcode.network - -import com.getcode.manager.SessionManager -import com.getcode.utils.base58 -import com.getcode.utils.bytes -import com.getcode.vendor.Base58 -import java.util.UUID -import javax.inject.Inject - -class IdentityManager @Inject constructor() { - fun generateVerificationTweet(accountName: String): String? { - val authority = SessionManager.getOrganizer()?.tray?.owner?.getCluster()?.authority - val tipAddress = SessionManager.getOrganizer()?.primaryVault - ?.let { Base58.encode(it.byteArray) } - - if (tipAddress != null && authority != null) { - val nonce = UUID.randomUUID() - val signature = authority.keyPair.sign(nonce.bytes.toByteArray()) - val verificationMessage = listOf( - accountName, - tipAddress, - Base58.encode(nonce.bytes.toByteArray()), - signature.base58 - ).joinToString(":") - - return verificationMessage - } - - return null - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/NotificationCollectionHistoryController.kt b/services/code/src/main/java/com/getcode/network/NotificationCollectionHistoryController.kt deleted file mode 100644 index c08fca3eb..000000000 --- a/services/code/src/main/java/com/getcode/network/NotificationCollectionHistoryController.kt +++ /dev/null @@ -1,235 +0,0 @@ -package com.getcode.network - -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData -import androidx.paging.PagingSource -import androidx.paging.cachedIn -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.manager.SessionManager -import com.getcode.model.chat.ChatMessage -import com.getcode.model.Cursor -import com.getcode.model.ID -import com.getcode.model.chat.MessageStatus -import com.getcode.model.chat.NotificationCollectionEntity -import com.getcode.model.chat.Title -import com.getcode.network.client.Client -import com.getcode.network.client.advancePointer -import com.getcode.network.client.fetchChats -import com.getcode.network.client.fetchMessagesFor -import com.getcode.network.client.setMuted -import com.getcode.network.client.setSubscriptionState -import com.getcode.network.source.CollectionPagingSource -import com.getcode.util.resources.ResourceHelper -import com.getcode.util.resources.ResourceType -import com.getcode.utils.TraceType -import com.getcode.utils.encodeBase64 -import com.getcode.utils.trace -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import timber.log.Timber -import java.util.Locale -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class NotificationCollectionHistoryController @Inject constructor( - private val client: Client, -) : CoroutineScope by CoroutineScope(Dispatchers.IO) { - - private val collectionEntries = MutableStateFlow?>(null) - - val notifications: StateFlow?> - get() = collectionEntries - .stateIn(this, SharingStarted.Eagerly, emptyList()) - - var loadingCollections: Boolean = false - - private val pagerMap = mutableMapOf>() - private val collectionFlows = mutableMapOf>>() - - private val pagingConfig = PagingConfig(pageSize = 20) - - fun reset() { - pagerMap.clear() - collectionFlows.clear() - } - - private fun collectionPager(collectionId: ID) = Pager(pagingConfig) { - pagerMap[collectionId] ?: CollectionPagingSource( - client = client, - owner = owner()!!, - collection = collectionEntries.value?.find { it.id == collectionId }, - onMessagesFetched = { messages -> - val collection = collectionEntries.value?.find { it.id == collectionId } ?: return@CollectionPagingSource - updateCollectionWithMessages(collection, messages) - } - ).also { - pagerMap[collectionId] = it - } - } - - private fun updateCollectionWithMessages(collection: NotificationCollectionEntity, messages: List) { - val updatedMessages = (collection.messages + messages).distinctBy { it.id } - val updatedChat = collection.copy(messages = updatedMessages) - val collections = collectionEntries.value?.map { - if (it.id == updatedChat.id) { - updatedChat - } else { - it - } - }?.sortedByDescending { it.lastMessageMillis } - collectionEntries.update { collections } - } - - fun collectionFlow(collectionId: ID) = - collectionFlows[collectionId] ?: collectionPager(collectionId).flow.cachedIn(GlobalScope).also { - collectionFlows[collectionId] = it - } - - val unreadCount = notifications - .filterNotNull() - // Ignore muted collections and unsubscribed collections - .map { it.filter { c -> !c.isMuted && c.isSubscribed } } - .map { it.sumOf { c -> c.unreadCount } } - - private fun owner(): KeyPair? = SessionManager.getKeyPair() - - suspend fun fetch(update: Boolean = false) { - if (loadingCollections) return - - val updatedWithMessages = mutableListOf() - val containers = fetchCollectionsWithoutMessages() - trace( - message = "Fetched ${containers.count()} collections", - type = TraceType.Silent - ) - - if (!update) { - pagerMap.clear() - collectionFlows.clear() - collectionEntries.value = containers - - loadingCollections = true - } - - containers.onEach { collection -> - val result = fetchLatestMessageForCollection(collection) - result.onSuccess { message -> - if (message != null) { - updatedWithMessages.add(collection.copy(messages = listOf(message))) - } - }.onFailure { - updatedWithMessages.add(collection) - } - } - - loadingCollections = false - collectionEntries.value = updatedWithMessages.sortedByDescending { it.lastMessageMillis } - } - - suspend fun advanceReadPointer(collectionId: ID) { - val owner = owner() ?: return - - collectionEntries.update { - it?.toMutableList()?.apply collections@{ - indexOfFirst { collection -> collection.id == collectionId } - .takeIf { index -> index >= 0 } - ?.let { index -> - val collection = this[index] - val newestMessage = collection.newestMessage - if (newestMessage != null) { - client.advancePointer( - owner = owner, - chat = collection, - to = newestMessage.id, - status = MessageStatus.Read - ).onSuccess { - this[index] = collection.resetUnreadCount() - } - } - } - }?.toList() - } - } - - suspend fun setMuted(collection: NotificationCollectionEntity, muted: Boolean): Result { - val owner = owner() ?: return Result.failure(Throwable("No owner detected")) - - collectionEntries.update { - it?.toMutableList()?.apply collections@{ - indexOfFirst { item -> item.id == collection.id } - .takeIf { index -> index >= 0 } - ?.let { index -> - val c = this[index] - Timber.d("changing mute state for collection locally") - this[index] = c.setMuteState(muted) - } - }?.toList() - } - - return client.setMuted(owner, collection, muted) - } - - suspend fun setSubscribed(collection: NotificationCollectionEntity, subscribed: Boolean): Result { - val owner = owner() ?: return Result.failure(Throwable("No owner detected")) - - collectionEntries.update { - it?.toMutableList()?.apply collections@{ - indexOfFirst { item -> item.id == collection.id } - .takeIf { index -> index >= 0 } - ?.let { index -> - val c = this[index] - Timber.d("changing subscribed state for collection locally") - this[index] = c.setSubscriptionState(subscribed) - } - }?.toList() - } - - return client.setSubscriptionState(owner, collection, subscribed) - } - - private suspend fun fetchLatestMessageForCollection(collection: NotificationCollectionEntity): Result { - val encodedId = collection.id.toByteArray().encodeBase64() - Timber.d("fetching last message for $encodedId") - val owner = owner() ?: return Result.success(null) - return client.fetchMessagesFor(owner, collection, limit = 1) - .onFailure { - Timber.e(t = it, "Failed to fetch messages for $encodedId.") - }.map { it.getOrNull(0) } - } - - private suspend fun fetchCollectionsWithoutMessages(): List { - val owner = owner() ?: return emptyList() - val result = client.fetchChats(owner) - return result.getOrNull().orEmpty() - } -} - -fun Title?.localized(resources: ResourceHelper): String { - return when (val t = this) { - is Title.Domain -> { - t.value.capitalize(Locale.getDefault()) - } - - is Title.Localized -> { - val resId = resources.getIdentifier( - t.value, - ResourceType.String, - ).let { if (it == 0) null else it } - - resId?.let { resources.getString(it) } ?: t.value - } - - else -> "Anonymous" - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/PrivacyMigration.kt b/services/code/src/main/java/com/getcode/network/PrivacyMigration.kt deleted file mode 100644 index 9a600aa8f..000000000 --- a/services/code/src/main/java/com/getcode/network/PrivacyMigration.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.getcode.network - -import com.getcode.analytics.CodeAnalyticsService -import com.getcode.model.Kin -import com.getcode.model.intents.IntentType -import com.getcode.network.repository.TransactionRepository -import com.getcode.solana.organizer.Organizer -import io.reactivex.rxjava3.core.Single -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class PrivacyMigration @Inject constructor( - internal val transactionRepository: TransactionRepository, - private val analyticsManager: CodeAnalyticsService, -) { - - fun migrateToPrivacy( - amountToMigrate: Kin, - organizer: Organizer - ): Single { - Timber.i("Start MigrateToPrivacy") - return transactionRepository.createAccounts(organizer = organizer) - .doOnSuccess { analyticsManager.migration(amountToMigrate) } - .flatMap { - transactionRepository.migrateToPrivacy( - amount = amountToMigrate, - organizer = organizer, - ) - } - .flatMap { - // There's nothing to receive if we're - // migrating an empty account - if (amountToMigrate > 0) { - transactionRepository.receiveFromPrimary( - amount = amountToMigrate, - organizer = organizer - ) - } else { - Single.just(it) - } - } - } - -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/TipController.kt b/services/code/src/main/java/com/getcode/network/TipController.kt deleted file mode 100644 index 8e0c3c0d6..000000000 --- a/services/code/src/main/java/com/getcode/network/TipController.kt +++ /dev/null @@ -1,195 +0,0 @@ -package com.getcode.network - -import com.getcode.manager.SessionManager -import com.getcode.services.model.CodePayload -import com.getcode.services.model.PrefsBool -import com.getcode.services.model.PrefsString -import com.getcode.model.SocialUser -import com.getcode.model.TwitterUser -import com.getcode.network.client.Client -import com.getcode.network.client.fetchTwitterUser -import com.getcode.network.repository.BetaFlagsRepository -import com.getcode.network.repository.PrefRepository -import com.getcode.network.repository.TwitterUserFetchError -import com.getcode.services.utils.getOrPutIfNonNull -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import timber.log.Timber -import java.util.Timer -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.concurrent.fixedRateTimer - -typealias TipUser = Pair - - -@Singleton -class TipController @Inject constructor( - private val client: Client, - betaFlags: BetaFlagsRepository, - private val prefRepository: PrefRepository, -) { - - companion object { - private const val POLL_FREQUENCY_LOOKING_SECS = 5L - } - - private var pollTimer: Timer? = null - private var lastPoll: Long = 0L - private val scope = CoroutineScope(Dispatchers.IO) - - private var cachedUsers = mutableMapOf() - - var scannedUserData: TipUser? = null - private set - var userMetadata: TwitterUser? = null - private set - - val connectedAccount: StateFlow = prefRepository.observeOrDefault(PrefsString.KEY_TIP_ACCOUNT, "") - .map { runCatching { Json.decodeFromString(it) }.getOrNull() } - .distinctUntilChanged() - .stateIn( - scope = scope, - started = SharingStarted.Eagerly, - initialValue = null - ) - - val verificationInProgress: StateFlow = prefRepository.observeOrDefault(PrefsBool.STARTED_TIP_CONNECT, false) - .distinctUntilChanged() - .stateIn( - scope = scope, - started = SharingStarted.Eagerly, - initialValue = false - ) - - val showTwitterSplat: Flow = - combine( - connectedAccount, - betaFlags.observe().map { it.tipsEnabled }, - prefRepository.observeOrDefault(PrefsBool.SEEN_TIP_CARD, false) - ) { connected, tipsEnabled, seen -> - connected != null && !seen && tipsEnabled - } - - private fun startPollTimer() { - if (connectedAccount.value != null) return - - Timber.d("twitter poll start") - pollTimer?.cancel() - pollTimer = - fixedRateTimer("twitterPollTimer", false, 0, 1000 * POLL_FREQUENCY_LOOKING_SECS) { - scope.launch { - val time = System.currentTimeMillis() - val isPastThrottle = - time - lastPoll > 1000 * (POLL_FREQUENCY_LOOKING_SECS / 2.0) || lastPoll == 0L - - if (isPastThrottle) { - callForConnectedUser() - } - } - } - } - - private suspend fun callForConnectedUser() { - Timber.d("twitter poll call") - val organizer = SessionManager.getOrganizer() ?: return - val tipAddress = organizer.primaryVault - // only set lastPoll if we actively attempt to reach RPC - lastPoll = System.currentTimeMillis() - client.fetchTwitterUser(organizer, tipAddress) - .onSuccess { - Timber.d("current user twitter connected @ ${it.username}") - prefRepository.set(PrefsString.KEY_TIP_ACCOUNT, Json.encodeToString(it)) - stopTimer() - } - .onFailure { - when (it) { - is TwitterUserFetchError -> { - prefRepository.set(PrefsString.KEY_TIP_ACCOUNT, "") - } - } - } - } - - init { - SessionManager.authState - .map { it.organizer } - .filterNotNull() - .map { it.primaryVault } - .onEach { checkForConnection() } - .launchIn(scope) - } - - fun checkForConnection() { - if (!verificationInProgress.value) { - scope.launch { - callForConnectedUser() - } - return - } - startPollTimer() - } - - suspend fun fetch(username: String, payload: CodePayload) { - val metadata = fetch(username) - scannedUserData = username to payload - userMetadata = metadata - } - - suspend fun fetch(username: String): TwitterUser? { - val organizer = SessionManager.getOrganizer() ?: return null - val key = username.lowercase() - return cachedUsers.getOrPutIfNonNull(key) { - Timber.d("fetching user $username") - client.fetchTwitterUser(organizer, username).getOrThrow() - } - } - - fun reset() { - scannedUserData = null - userMetadata = null - } - - fun onSeenTipCardBanner() { - prefRepository.set(PrefsBool.DISMISSED_TIP_CARD_BANNER, true) - endVerification() - } - - suspend fun hasSeenTipCard(): Boolean { - return prefRepository.get(PrefsBool.SEEN_TIP_CARD, false) - } - - fun clearTwitterSplat() { - prefRepository.set(PrefsBool.SEEN_TIP_CARD, true) - } - - fun startVerification() { - prefRepository.set(PrefsBool.STARTED_TIP_CONNECT, true) - } - - private fun endVerification() { - prefRepository.set(PrefsBool.STARTED_TIP_CONNECT, false) - } - - fun stopTimer() { - stopTimerInternal() - } - - private fun stopTimerInternal() { - Timber.d("twitter poll stop") - pollTimer?.cancel() - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/TwitterUserController.kt b/services/code/src/main/java/com/getcode/network/TwitterUserController.kt deleted file mode 100644 index 079c57a2f..000000000 --- a/services/code/src/main/java/com/getcode/network/TwitterUserController.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.getcode.network - -import com.getcode.manager.SessionManager -import com.getcode.model.TwitterUser -import com.getcode.network.client.Client -import com.getcode.network.client.fetchTwitterUser -import com.getcode.network.repository.BetaFlagsRepository -import com.getcode.network.repository.PrefRepository -import com.getcode.services.utils.getOrPutIfNonNull -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class TwitterUserController @Inject constructor( - private val client: Client, - betaFlags: BetaFlagsRepository, - private val prefRepository: PrefRepository, -) { - private var cachedUsers = mutableMapOf() - - - suspend fun fetchUser(username: String, ignoreCache: Boolean = false): TwitterUser? { - val organizer = SessionManager.getOrganizer() ?: return null - val key = username.lowercase() - - if (ignoreCache) { - val user = client.fetchTwitterUser(organizer, username).getOrThrow() - cachedUsers[key] = user - return user - } - - return cachedUsers.getOrPutIfNonNull(key) { - Timber.d("fetching user $username") - client.fetchTwitterUser(organizer, username).getOrThrow() - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/api/AccountApi.kt b/services/code/src/main/java/com/getcode/network/api/AccountApi.kt deleted file mode 100644 index f7dcbdc77..000000000 --- a/services/code/src/main/java/com/getcode/network/api/AccountApi.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.getcode.network.api - -import com.codeinc.gen.account.v1.AccountGrpc -import com.codeinc.gen.account.v1.AccountService -import com.codeinc.gen.account.v1.AccountService.LinkAdditionalAccountsRequest -import com.codeinc.gen.account.v1.AccountService.LinkAdditionalAccountsResponse -import com.getcode.annotations.CodeManagedChannel -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.network.repository.toSolanaAccount -import com.getcode.services.network.core.GrpcApi -import com.getcode.utils.sign -import io.grpc.ManagedChannel -import io.reactivex.rxjava3.core.Scheduler -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import javax.inject.Inject - - -class AccountApi @Inject constructor( - @CodeManagedChannel - managedChannel: ManagedChannel, - private val scheduler: Scheduler = Schedulers.io(), -) : GrpcApi(managedChannel) { - private val api = AccountGrpc.newStub(managedChannel).withWaitForReady() - - fun isCodeAccount(owner: KeyPair): Flow { - val request = AccountService.IsCodeAccountRequest.newBuilder() - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .apply { setSignature(sign(owner)) } - .build() - - return api::isCodeAccount - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - fun getTokenAccountInfos(request: AccountService.GetTokenAccountInfosRequest): Single { - return api::getTokenAccountInfos - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun linkAdditionalAccounts(owner: KeyPair, linkedAccount: KeyPair): Flow { - val request = LinkAdditionalAccountsRequest.newBuilder() - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .setSwapAuthority(linkedAccount.publicKeyBytes.toSolanaAccount()) - .let { it.addAllSignatures(listOf(it.sign(owner), it.sign(linkedAccount))) } - .build() - - return api::linkAdditionalAccounts - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/api/ChatApi.kt b/services/code/src/main/java/com/getcode/network/api/ChatApi.kt deleted file mode 100644 index 74598272e..000000000 --- a/services/code/src/main/java/com/getcode/network/api/ChatApi.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.getcode.network.api - -import com.codeinc.gen.chat.v1.ChatGrpc -import com.codeinc.gen.chat.v1.ChatService -import com.getcode.annotations.CodeManagedChannel -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.Cursor -import com.getcode.model.ID -import com.getcode.network.repository.toSolanaAccount -import com.getcode.services.network.core.GrpcApi -import com.getcode.utils.sign -import com.getcode.utils.toByteString -import io.grpc.ManagedChannel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import javax.inject.Inject - -class ChatApi @Inject constructor( - @CodeManagedChannel - managedChannel: ManagedChannel -) : GrpcApi(managedChannel) { - private val api = ChatGrpc.newStub(managedChannel).withWaitForReady() - - fun fetchChats(owner: KeyPair): Flow { - val request = ChatService.GetChatsRequest.newBuilder() - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .apply { setSignature(sign(owner)) } - .build() - - return api::getChats - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - fun fetchChatMessages( - owner: KeyPair, - chatId: ID, - cursor: Cursor? = null, - limit: Int? = null - ): Flow { - val builder = ChatService.GetMessagesRequest.newBuilder() - .setChatId( - ChatService.ChatId.newBuilder() - .setValue(chatId.toByteArray().toByteString()) - .build() - ) - - if (cursor != null) { - builder.setCursor( - ChatService.Cursor.newBuilder() - .setValue(cursor.toByteString()) - ) - } - - if (limit != null) { - builder.setPageSize(limit) - } - - builder.setDirection(ChatService.GetMessagesRequest.Direction.DESC) - - val request = builder - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .apply { setSignature(sign(owner)) } - .build() - - return api::getMessages - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - fun advancePointer(owner: KeyPair, chatId: ID, to: ID, kind: ChatService.Pointer.Kind): Flow { - val request = ChatService.AdvancePointerRequest.newBuilder() - .setChatId( - ChatService.ChatId.newBuilder() - .setValue(chatId.toByteArray().toByteString()) - .build() - ).setPointer( - ChatService.Pointer.newBuilder() - .setKind(kind) - .setValue( - ChatService.ChatMessageId.newBuilder() - .setValue(to.toByteArray().toByteString()) - ) - ).setOwner(owner.publicKeyBytes.toSolanaAccount()) - .apply { setSignature(sign(owner)) } - .build() - - return api::advancePointer - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - fun setMuteState(owner: KeyPair, chatId: ID, muted: Boolean): Flow { - val request = ChatService.SetMuteStateRequest.newBuilder() - .setChatId( - ChatService.ChatId.newBuilder() - .setValue(chatId.toByteArray().toByteString()) - .build() - ).setIsMuted(muted) - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .apply { setSignature(sign(owner)) } - .build() - - return api::setMuteState - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - fun setSubscriptionState( - owner: KeyPair, - chatId: ID, - subscribed: Boolean - ): Flow { - val request = ChatService.SetSubscriptionStateRequest.newBuilder() - .setChatId( - ChatService.ChatId.newBuilder() - .setValue(chatId.toByteArray().toByteString()) - .build() - ).setIsSubscribed(subscribed) - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .apply { setSignature(sign(owner)) } - .build() - - return api::setSubscriptionState - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/api/CurrencyApi.kt b/services/code/src/main/java/com/getcode/network/api/CurrencyApi.kt deleted file mode 100644 index 3168f7c93..000000000 --- a/services/code/src/main/java/com/getcode/network/api/CurrencyApi.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.getcode.network.api - -import com.codeinc.gen.currency.v1.CurrencyGrpc -import com.codeinc.gen.currency.v1.CurrencyService -import com.getcode.annotations.CodeManagedChannel -import com.getcode.services.network.core.GrpcApi -import io.grpc.ManagedChannel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import javax.inject.Inject - -class CurrencyApi @Inject constructor( - @CodeManagedChannel - managedChannel: ManagedChannel, -) : GrpcApi(managedChannel) { - private val api = CurrencyGrpc.newStub(managedChannel).withWaitForReady() - - fun getRates(request: CurrencyService.GetAllRatesRequest = CurrencyService.GetAllRatesRequest.getDefaultInstance()): Flow = - api::getAllRates - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) -} diff --git a/services/code/src/main/java/com/getcode/network/api/DeviceApi.kt b/services/code/src/main/java/com/getcode/network/api/DeviceApi.kt deleted file mode 100644 index f9ce8f886..000000000 --- a/services/code/src/main/java/com/getcode/network/api/DeviceApi.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.getcode.network.api - -import com.codeinc.gen.common.v1.Model -import com.codeinc.gen.device.v1.DeviceGrpc -import com.codeinc.gen.device.v1.DeviceService -import com.getcode.annotations.CodeManagedChannel -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.network.repository.sign -import com.getcode.network.repository.toSolanaAccount -import com.getcode.services.network.core.GrpcApi -import io.grpc.ManagedChannel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import javax.inject.Inject - -class DeviceApi @Inject constructor( - @CodeManagedChannel - managedChannel: ManagedChannel, -): GrpcApi(managedChannel) { - - private val api = DeviceGrpc.newStub(managedChannel).withWaitForReady() - - fun registerInstallation(owner: KeyPair, installationId: String) : Flow { - val request = DeviceService.RegisterLoggedInAccountsRequest.newBuilder() - .setAppInstall(Model.AppInstallId.newBuilder().setValue(installationId)) - .addOwners(owner.publicKeyBytes.toSolanaAccount()) - .apply { - addAllSignatures(listOf(sign(owner))) - } - .build() - - return api::registerLoggedInAccounts - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - fun fetchInstallationAccounts(installationId: String): Flow { - val request = DeviceService.GetLoggedInAccountsRequest.newBuilder() - .setAppInstall(Model.AppInstallId.newBuilder().setValue(installationId)) - .build() - - return api::getLoggedInAccounts - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/api/IdentityApi.kt b/services/code/src/main/java/com/getcode/network/api/IdentityApi.kt deleted file mode 100644 index daf91bfcc..000000000 --- a/services/code/src/main/java/com/getcode/network/api/IdentityApi.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.getcode.network.api - -import com.codeinc.gen.user.v1.IdentityGrpc -import com.codeinc.gen.user.v1.IdentityService -import com.codeinc.gen.user.v1.IdentityService.GetTwitterUserRequest -import com.codeinc.gen.user.v1.IdentityService.LoginToThirdPartyAppRequest -import com.codeinc.gen.user.v1.IdentityService.UpdatePreferencesRequest -import com.getcode.annotations.CodeManagedChannel -import com.getcode.services.network.core.GrpcApi -import io.grpc.ManagedChannel -import io.reactivex.rxjava3.annotations.NonNull -import io.reactivex.rxjava3.core.Scheduler -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.flowOn -import javax.inject.Inject - -class IdentityApi @Inject constructor( - @CodeManagedChannel - managedChannel: ManagedChannel, - private val scheduler: Scheduler = Schedulers.io(), -) : GrpcApi(managedChannel) { - private val api = IdentityGrpc.newStub(managedChannel).withWaitForReady() - - fun linkAccount(request: IdentityService.LinkAccountRequest): @NonNull Single { - return api::linkAccount - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun unlinkAccount(request: IdentityService.UnlinkAccountRequest): @NonNull Single { - return api::unlinkAccount - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun getUser(request: IdentityService.GetUserRequest): @NonNull Single { - return api::getUser - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun loginToThirdParty(request: LoginToThirdPartyAppRequest) = api::loginToThirdPartyApp - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - - fun updatePreferences(request: UpdatePreferencesRequest) = api::updatePreferences - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - - fun fetchTwitterUser(request: GetTwitterUserRequest) = api::getTwitterUser - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) -} diff --git a/services/code/src/main/java/com/getcode/network/api/MessagingApi.kt b/services/code/src/main/java/com/getcode/network/api/MessagingApi.kt deleted file mode 100644 index 9f979d83a..000000000 --- a/services/code/src/main/java/com/getcode/network/api/MessagingApi.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.getcode.network.api - -import com.codeinc.gen.messaging.v1.MessagingGrpc -import com.codeinc.gen.messaging.v1.MessagingService.AckMessagesRequest -import com.codeinc.gen.messaging.v1.MessagingService.AckMesssagesResponse -import com.codeinc.gen.messaging.v1.MessagingService.OpenMessageStreamRequest -import com.codeinc.gen.messaging.v1.MessagingService.OpenMessageStreamResponse -import com.codeinc.gen.messaging.v1.MessagingService.PollMessagesRequest -import com.codeinc.gen.messaging.v1.MessagingService.SendMessageRequest -import com.codeinc.gen.messaging.v1.MessagingService.SendMessageResponse -import com.getcode.annotations.CodeManagedChannel -import com.getcode.services.network.core.GrpcApi -import io.grpc.ManagedChannel -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Scheduler -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import javax.inject.Inject - -class MessagingApi @Inject constructor( - @CodeManagedChannel - managedChannel: ManagedChannel, - private val scheduler: Scheduler = Schedulers.io() -) : GrpcApi(managedChannel) { - private val api = MessagingGrpc.newStub(managedChannel).withWaitForReady() - - fun openMessageStream(request: OpenMessageStreamRequest): Flowable = - api::openMessageStream - .callAsCancellableFlowable(request) - .subscribeOn(scheduler) - - fun ackMessages(request: AckMessagesRequest): Single = - api::ackMessages - .callAsSingle(request) - .subscribeOn(scheduler) - - fun sendMessage(request: SendMessageRequest): Single = - api::sendMessage - .callAsSingle(request) - .subscribeOn(scheduler) - - fun pollMessages(request: PollMessagesRequest) = api::pollMessages - .callAsSingle(request) - .subscribeOn(scheduler) -} diff --git a/services/code/src/main/java/com/getcode/network/api/PhoneApi.kt b/services/code/src/main/java/com/getcode/network/api/PhoneApi.kt deleted file mode 100644 index eeb93dc4b..000000000 --- a/services/code/src/main/java/com/getcode/network/api/PhoneApi.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.getcode.network.api - -import com.codeinc.gen.phone.v1.PhoneVerificationGrpc -import com.codeinc.gen.phone.v1.PhoneVerificationService -import com.getcode.annotations.CodeManagedChannel -import com.getcode.services.network.core.GrpcApi -import io.grpc.ManagedChannel -import io.reactivex.rxjava3.annotations.NonNull -import io.reactivex.rxjava3.core.Scheduler -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import javax.inject.Inject - -class PhoneApi @Inject constructor( - @CodeManagedChannel - managedChannel: ManagedChannel, - private val scheduler: Scheduler = Schedulers.io(), -) : GrpcApi(managedChannel) { - private val api = PhoneVerificationGrpc.newStub(managedChannel).withWaitForReady() - - fun sendVerificationCode(request: PhoneVerificationService.SendVerificationCodeRequest): @NonNull Single { - return api::sendVerificationCode - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun checkVerificationCode(request: PhoneVerificationService.CheckVerificationCodeRequest): @NonNull Single { - return api::checkVerificationCode - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun getAssociatedPhoneNumber(request: PhoneVerificationService.GetAssociatedPhoneNumberRequest): @NonNull Single { - return api::getAssociatedPhoneNumber - .callAsSingle(request) - .subscribeOn(scheduler) - } -} diff --git a/services/code/src/main/java/com/getcode/network/api/PushApi.kt b/services/code/src/main/java/com/getcode/network/api/PushApi.kt deleted file mode 100644 index 4e59faa57..000000000 --- a/services/code/src/main/java/com/getcode/network/api/PushApi.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.getcode.network.api - -import com.codeinc.gen.push.v1.PushGrpc -import com.codeinc.gen.push.v1.PushService -import com.getcode.annotations.CodeManagedChannel -import com.getcode.services.network.core.GrpcApi -import io.grpc.ManagedChannel -import io.reactivex.rxjava3.annotations.NonNull -import io.reactivex.rxjava3.core.Scheduler -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import javax.inject.Inject - -class PushApi @Inject constructor( - @CodeManagedChannel - managedChannel: ManagedChannel, - private val scheduler: Scheduler = Schedulers.io(), -) : GrpcApi(managedChannel) { - private val api = PushGrpc.newStub(managedChannel).withWaitForReady() - - fun addToken(request: PushService.AddTokenRequest): @NonNull Single { - return api::addToken - .callAsSingle(request) - .subscribeOn(scheduler) - } -} diff --git a/services/code/src/main/java/com/getcode/network/api/TransactionApiV2.kt b/services/code/src/main/java/com/getcode/network/api/TransactionApiV2.kt deleted file mode 100644 index ef85cc626..000000000 --- a/services/code/src/main/java/com/getcode/network/api/TransactionApiV2.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.getcode.network.api - -import com.codeinc.gen.transaction.v2.TransactionGrpc -import com.codeinc.gen.transaction.v2.TransactionService -import com.codeinc.gen.transaction.v2.TransactionService.SwapRequest -import com.codeinc.gen.transaction.v2.TransactionService.SwapResponse -import com.getcode.annotations.CodeManagedChannel -import com.getcode.services.network.core.GrpcApi -import io.grpc.ManagedChannel -import io.grpc.stub.StreamObserver -import io.reactivex.rxjava3.core.Scheduler -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import javax.inject.Inject - -class TransactionApiV2 @Inject constructor( - @CodeManagedChannel - managedChannel: ManagedChannel, - private val scheduler: Scheduler = Schedulers.io(), -) : GrpcApi(managedChannel) { - private val api = TransactionGrpc.newStub(managedChannel).withWaitForReady() - - fun submitIntent(request: StreamObserver): StreamObserver { - return api.submitIntent(request) - } - - fun airdrop(request: TransactionService.AirdropRequest): Single { - return api::airdrop - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun getPrivacyUpgradeStatus(request: TransactionService.GetPrivacyUpgradeStatusRequest): Single { - return api::getPrivacyUpgradeStatus - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun getPaymentHistory(request: TransactionService.GetPaymentHistoryRequest): Single { - return api::getPaymentHistory - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun getLimits(request: TransactionService.GetLimitsRequest): Single { - return api::getLimits - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun getIntentMetadata(request: TransactionService.GetIntentMetadataRequest): Single { - return api::getIntentMetadata - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun canWithdrawToAccount(request: TransactionService.CanWithdrawToAccountRequest): Single { - return api::canWithdrawToAccount - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun getPrioritizedIntentsForPrivacyUpgrade(request: TransactionService.GetPrioritizedIntentsForPrivacyUpgradeRequest): Single { - return api::getPrioritizedIntentsForPrivacyUpgrade - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun swap(observer: StreamObserver): StreamObserver { - return api.swap(observer) - } - - fun declareFiatPurchase(request: TransactionService.DeclareFiatOnrampPurchaseAttemptRequest): Flow { - return api::declareFiatOnrampPurchaseAttempt - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - -} diff --git a/services/code/src/main/java/com/getcode/network/client/Client.kt b/services/code/src/main/java/com/getcode/network/client/Client.kt deleted file mode 100644 index f7fc5c18e..000000000 --- a/services/code/src/main/java/com/getcode/network/client/Client.kt +++ /dev/null @@ -1,115 +0,0 @@ -package com.getcode.network.client - -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.OnLifecycleEvent -import com.getcode.analytics.CodeAnalyticsService -import com.getcode.manager.SessionManager -import com.getcode.network.BalanceController -import com.getcode.network.exchange.Exchange -import com.getcode.network.repository.AccountRepository -import com.getcode.network.repository.IdentityRepository -import com.getcode.network.repository.MessagingRepository -import com.getcode.network.repository.PrefRepository -import com.getcode.network.repository.TransactionRepository -import com.getcode.network.service.AccountService -import com.getcode.network.service.ChatService -import com.getcode.network.service.DeviceService -import com.getcode.services.manager.MnemonicManager -import com.getcode.utils.ErrorUtils -import com.getcode.utils.network.NetworkConnectivityListener -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import timber.log.Timber -import java.util.Timer -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.concurrent.fixedRateTimer - -internal const val TAG = "Client" - -@Singleton -class Client @Inject constructor( - internal val identityRepository: IdentityRepository, - internal val transactionRepository: TransactionRepository, - internal val messagingRepository: MessagingRepository, - internal val balanceController: BalanceController, - internal val accountRepository: AccountRepository, - internal val accountService: AccountService, - internal val analytics: CodeAnalyticsService, - internal val prefRepository: PrefRepository, - internal val exchange: Exchange, - internal val transactionReceiver: TransactionReceiver, - internal val networkObserver: NetworkConnectivityListener, - internal val chatService: ChatService, - internal val deviceService: DeviceService, - internal val mnemonicManager: MnemonicManager, -) : LifecycleObserver { - - private val scope = CoroutineScope(Dispatchers.IO) - - private var pollTimer: Timer? = null - private var lastPoll: Long = 0L - - private fun startPollTimerWhenAuthenticated() { - Timber.tag(TAG).i("Creating poll timer") - scope.launch { - SessionManager.authState - .map { it.isAuthenticated } - .filterNotNull() - .filter { it } - .onEach { - Timber.tag(TAG).i("User Authenticated - starting timer") - startPollTimer() - this.cancel() - }.launchIn(this) - } - } - - private fun startPollTimer() { - pollTimer?.cancel() - pollTimer = fixedRateTimer("pollTimer", false, 0, 1000 * 60) { - scope.launch { - Timber.tag(TAG).i("Timer Polling") - - val time = System.currentTimeMillis() - val isPastThrottle = time - lastPoll > 1000 * 30 || lastPoll == 0L - - if (SessionManager.isAuthenticated() == true && isPastThrottle) { - poll() - lastPoll = time - } - } - } - } - - private suspend fun poll() { - if (networkObserver.isConnected) { - try { - balanceController.fetchBalanceSuspend() - exchange.fetchRatesIfNeeded() - } catch (e: Exception) { - ErrorUtils.handleError(e) - } - fetchLimits().andThen(fetchPrivacyUpgrades()).blockingSubscribe() - } - } - - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) - fun startTimer() { - startPollTimerWhenAuthenticated() - } - - @OnLifecycleEvent(Lifecycle.Event.ON_STOP) - fun stopTimer() { - Timber.tag(TAG).i("Cancelling Poller") - pollTimer?.cancel() - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/client/Client_Account.kt b/services/code/src/main/java/com/getcode/network/client/Client_Account.kt deleted file mode 100644 index abe48e824..000000000 --- a/services/code/src/main/java/com/getcode/network/client/Client_Account.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.getcode.network.client - -import com.getcode.ed25519.Ed25519.KeyPair - -suspend fun Client.isCodeAccount(owner: KeyPair): Result { - return accountService.isCodeAccount(owner) -} - -suspend fun Client.linkAdditionalAccount(owner: KeyPair, linkedAccount: KeyPair): Result { - return accountService.linkAdditionalAccounts(owner, linkedAccount) -} diff --git a/services/code/src/main/java/com/getcode/network/client/Client_Chat.kt b/services/code/src/main/java/com/getcode/network/client/Client_Chat.kt deleted file mode 100644 index 68fbc8a40..000000000 --- a/services/code/src/main/java/com/getcode/network/client/Client_Chat.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.getcode.network.client - -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.manager.SessionManager -import com.getcode.model.Cursor -import com.getcode.model.Domain -import com.getcode.model.ID -import com.getcode.model.chat.Chat -import com.getcode.model.chat.ChatMessage -import com.getcode.model.chat.MessageStatus -import com.getcode.model.chat.NotificationCollectionEntity -import com.getcode.model.extensions.decryptingUsing -import com.getcode.utils.TraceType -import com.getcode.utils.base58 -import com.getcode.utils.trace -import timber.log.Timber - -suspend fun Client.fetchChats(owner: KeyPair): Result> { - val chats = chatService.fetchChats(owner) - .onSuccess { - Timber.d("v1 chats fetched=${it.count()}") - }.onFailure { - trace( - "Failed fetching chats from V1", - type = TraceType.Error - ) - } - - return chats - .map { list -> - list.sortedByDescending { it.lastMessageMillis } - .distinctBy { it.id } - } -} - -suspend fun Client.setMuted(owner: KeyPair, chat: Chat, muted: Boolean): Result { - return chatService.setMuteState(owner, chat.id, muted) -} - -suspend fun Client.setSubscriptionState( - owner: KeyPair, - chat: Chat, - subscribed: Boolean -): Result { - return chatService.setSubscriptionState(owner, chat.id, subscribed) -} - -suspend fun Client.fetchMessagesFor( - owner: KeyPair, - chat: Chat, - cursor: Cursor? = null, - limit: Int? = null -): Result> { - return chatService.fetchMessagesFor(owner, chat, cursor, limit) - .mapCatching { messages -> - val organizer = SessionManager.getOrganizer() ?: return@mapCatching messages - val domain = Domain.from(chat.title?.value) ?: return@mapCatching messages - - val relationship = organizer.relationshipFor(domain) ?: return@mapCatching messages - - val hasEncryptedContent = messages.firstOrNull { it.hasEncryptedContent } != null - if (hasEncryptedContent) { - messages.map { message -> - message.decryptingUsing(relationship.getCluster().authority.keyPair) - } - } else { - messages - } - } - .onSuccess { - Timber.d("messages fetched=${it.count()} for ${chat.id.base58}") - if (it.isNotEmpty()) { - Timber.d("start=${it.minOf { it.dateMillis }}, end=${it.maxOf { it.dateMillis }}") - } - }.onFailure { - Timber.e(t = it, "Failed fetching messages.") - } -} - -suspend fun Client.advancePointer( - owner: KeyPair, - chat: Chat, - to: ID, - status: MessageStatus = MessageStatus.Read, -): Result { - return chatService.advancePointer(owner, chat.id, to, status) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/client/Client_Device.kt b/services/code/src/main/java/com/getcode/network/client/Client_Device.kt deleted file mode 100644 index ce9d74a3b..000000000 --- a/services/code/src/main/java/com/getcode/network/client/Client_Device.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.getcode.network.client - -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.solana.keys.PublicKey -import timber.log.Timber - -suspend fun Client.registerInstallation( - owner: KeyPair, - installationId: String, -): Result { - return deviceService.registerInstallation(owner, installationId) - .onSuccess { - Timber.d("Registered installation: $installationId") - } -} - -suspend fun Client.fetchInstallationAccounts( - installationId: String -): Result> { - return deviceService.fetchInstallations(installationId) - .onSuccess { - Timber.d("fetched ${it.count()} accounts") - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/client/Client_Identity.kt b/services/code/src/main/java/com/getcode/network/client/Client_Identity.kt deleted file mode 100644 index 6c0de5895..000000000 --- a/services/code/src/main/java/com/getcode/network/client/Client_Identity.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.getcode.network.client - -import com.getcode.ed25519.Ed25519 -import com.getcode.model.TwitterUser -import com.getcode.solana.organizer.Organizer -import java.util.Locale - -suspend fun Client.loginToThirdParty(rendezvous: com.getcode.solana.keys.PublicKey, relationship: Ed25519.KeyPair): Result { - return identityRepository.loginToThirdParty(rendezvous, relationship) -} - -suspend fun Client.updatePreferences(organizer: Organizer): Result { - return identityRepository.updatePreferences( - locale = Locale.getDefault(), - owner = organizer.ownerKeyPair - ) -} - -suspend fun Client.fetchTwitterUser( - organizer: Organizer, - username: String -): Result { - return identityRepository.fetchTwitterUserByUsername(organizer.ownerKeyPair, username) -} - -suspend fun Client.fetchTwitterUser( - organizer: Organizer, - address: com.getcode.solana.keys.PublicKey -): Result { - return identityRepository.fetchTwitterUserByAddress(organizer.ownerKeyPair, address) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/client/Client_Messaging.kt b/services/code/src/main/java/com/getcode/network/client/Client_Messaging.kt deleted file mode 100644 index 357f3cf65..000000000 --- a/services/code/src/main/java/com/getcode/network/client/Client_Messaging.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.getcode.network.client - -import com.codeinc.gen.messaging.v1.MessagingService -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.Domain -import com.getcode.model.Fiat - -suspend fun Client.sendRequestToLogin( - domain: Domain, - verifier: KeyPair, - rendezvous: KeyPair -): Result { - return messagingRepository.sendRequestToLogin(domain, verifier, rendezvous) -} - -suspend fun Client.sendRequestToReceiveBill( - destination: com.getcode.solana.keys.PublicKey, - fiat: Fiat, - rendezvous: KeyPair -): Result { - return messagingRepository.sendRequestToReceiveBill(destination, fiat, rendezvous) -} - -suspend fun Client.rejectLogin(rendezvous: KeyPair): Result { - return messagingRepository.rejectLogin(rendezvous) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/client/Client_Transaction.kt b/services/code/src/main/java/com/getcode/network/client/Client_Transaction.kt deleted file mode 100644 index 38620adec..000000000 --- a/services/code/src/main/java/com/getcode/network/client/Client_Transaction.kt +++ /dev/null @@ -1,639 +0,0 @@ -package com.getcode.network.client - -import android.annotation.SuppressLint -import com.getcode.db.CodeAppDatabase -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.manager.SessionManager -import com.getcode.model.AccountInfo -import com.getcode.model.Domain -import com.getcode.model.Fee -import com.getcode.model.GiftCard -import com.getcode.model.ID -import com.getcode.model.IntentMetadata -import com.getcode.model.Kin -import com.getcode.model.KinAmount -import com.getcode.model.Limits -import com.getcode.model.Rate -import com.getcode.model.generate -import com.getcode.model.intents.IntentDeposit -import com.getcode.model.intents.IntentEstablishRelationship -import com.getcode.model.intents.IntentPrivateTransfer -import com.getcode.model.intents.IntentPublicTransfer -import com.getcode.model.intents.IntentRemoteSend -import com.getcode.model.intents.PrivateTransferMetadata -import com.getcode.model.intents.SwapIntent -import com.getcode.network.repository.TransactionRepository -import com.getcode.network.repository.WithdrawException -import com.getcode.network.repository.initiateSwap -import com.getcode.services.utils.flowInterval -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.base58 -import com.getcode.solana.organizer.GiftCardAccount -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Relationship -import com.getcode.utils.TraceType -import com.getcode.utils.trace -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.takeWhile -import kotlinx.coroutines.withContext -import timber.log.Timber -import java.util.Calendar -import java.util.GregorianCalendar -import java.util.UUID -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger -import kotlin.math.min - -fun Client.createAccounts(organizer: Organizer): Completable { - return transactionRepository.createAccounts(organizer) - .ignoreElement() -} - -fun Client.transfer( - amount: KinAmount, - fee: Kin, - additionalFees: List, - organizer: Organizer, - rendezvousKey: PublicKey, - destination: PublicKey, - isWithdrawal: Boolean, - metadata: PrivateTransferMetadata? = null, -): Completable { - return transferWithResultSingle( - amount, - fee, - additionalFees, - organizer, - rendezvousKey, - destination, - isWithdrawal - ).flatMapCompletable { - if (it.isSuccess) { - Timber.d("transfer successful") - Completable.complete() - } else { - Completable.error(it.exceptionOrNull() ?: Throwable("Failed to complete transfer")) - } - } -} - -fun Client.transferWithResultSingle( - amount: KinAmount, - fee: Kin, - additionalFees: List, - organizer: Organizer, - rendezvousKey: PublicKey, - destination: PublicKey, - isWithdrawal: Boolean, - metadata: PrivateTransferMetadata? = null, -): Single> { - return getTransferPreflightAction(amount.kin) - .andThen(Single.defer { - transactionRepository.transfer( - amount, fee, additionalFees, organizer, rendezvousKey, destination, isWithdrawal, metadata - ) - }) - .map { - if (it is IntentPrivateTransfer) { - balanceController.setTray(organizer, it.resultTray) - } - it - }.map { Result.success(it.id.bytes) } - .onErrorReturn { Result.failure(it) } -} - -fun Client.transferWithResult( - amount: KinAmount, - fee: Kin, - additionalFees: List, - organizer: Organizer, - rendezvousKey: PublicKey, - destination: PublicKey, - isWithdrawal: Boolean, - metadata: PrivateTransferMetadata? = null, -): Result { - return transferWithResultSingle( - amount = amount, - fee = fee, - additionalFees = additionalFees, - organizer = organizer, - rendezvousKey = rendezvousKey, - destination = destination, - isWithdrawal = isWithdrawal, - metadata = metadata, - ).blockingGet() -} - - -fun Client.sendRemotely( - amount: KinAmount, - rendezvousKey: PublicKey, - giftCard: GiftCardAccount -): Completable { - return Completable.defer { - val organizer = SessionManager.getOrganizer()!! - val truncatedAmount = amount.truncating() - getTransferPreflightAction(truncatedAmount.kin) - .andThen( - sendRemotely( - amount = truncatedAmount, - organizer = organizer, - rendezvousKey = rendezvousKey, - giftCard = giftCard - ) - .doOnComplete { - val giftCardItem = GiftCard( - key = giftCard.cluster.vaultPublicKey.base58(), - entropy = mnemonicManager.getEncodedBase58(giftCard.mnemonicPhrase), - amount = truncatedAmount.kin.quarks, - date = System.currentTimeMillis() - ) - CodeAppDatabase.requireInstance().giftCardDao().insert(giftCardItem) - } - ) - } -} - -fun Client.receiveRemote(giftCard: GiftCardAccount): Single { - // Before we can receive from the gift card account - // we have to determine the balance of the account - return accountRepository.getTokenAccountInfos(giftCard.cluster.authority.keyPair) - .flatMap { infos -> - val info: AccountInfo = infos.values.firstOrNull() - ?: return@flatMap Single.error(RemoteSendException.FailedToFetchGiftCardInfoException()) - val kinAmount = info.originalKinAmount - ?: return@flatMap Single.error(RemoteSendException.GiftCardBalanceNotFoundException()) - - if (info.claimState == AccountInfo.ClaimState.Claimed) { - return@flatMap Single.error(RemoteSendException.GiftCardClaimedException()) - } - - if (info.claimState == AccountInfo.ClaimState.Claimed || info.claimState == AccountInfo.ClaimState.Unknown) { - return@flatMap Single.error(RemoteSendException.GiftCardExpiredException()) - } - - val organizer = SessionManager.getOrganizer()!! - - transactionReceiver.receiveRemotely( - giftCard = giftCard, - amount = info.balance, - organizer = organizer, - isVoiding = false - ) - .toSingleDefault(kinAmount) - } -} - -suspend fun Client.receiveRemoteSuspend(giftCard: GiftCardAccount): KinAmount = - withContext(Dispatchers.IO) { - // Before we can receive from the gift card account - // we have to determine the balance of the account - - val information = - accountRepository.getTokenAccountInfosSuspend(giftCard.cluster.authority.keyPair) - - val info: AccountInfo = information.values.firstOrNull() - ?: throw RemoteSendException.FailedToFetchGiftCardInfoException() - - val kinAmount = info.originalKinAmount - ?: throw RemoteSendException.GiftCardBalanceNotFoundException() - - if (info.claimState == AccountInfo.ClaimState.Claimed) { - throw RemoteSendException.GiftCardClaimedException() - } - - if (info.claimState == AccountInfo.ClaimState.Claimed || info.claimState == AccountInfo.ClaimState.Unknown) { - throw RemoteSendException.GiftCardExpiredException() - } - - val organizer = SessionManager.getOrganizer()!! - - transactionReceiver.receiveRemotelySuspend( - giftCard = giftCard, - amount = info.balance, - organizer = organizer, - isVoiding = false - ) - - balanceController.fetchBalanceSuspend() - - return@withContext kinAmount - } - -@SuppressLint("CheckResult") -suspend fun Client.cancelRemoteSend( - giftCard: GiftCardAccount, - amount: Kin, - organizer: Organizer -): Result = runCatching { - transactionReceiver.receiveRemotely( - amount = amount, - organizer = organizer, - giftCard = giftCard, - isVoiding = true - ).blockingAwait() - - balanceController.fetchBalanceSuspend() - - balanceController.rawBalance -} - - -sealed class RemoteSendException : Exception() { - class FailedToFetchGiftCardInfoException : RemoteSendException() - class GiftCardBalanceNotFoundException : RemoteSendException() - class GiftCardClaimedException : RemoteSendException() - class GiftCardExpiredException : RemoteSendException() -} - -fun Client.withdrawExternally( - amount: KinAmount, - organizer: Organizer, - destination: PublicKey -): Completable { - if (amount.kin.fractionalQuarks().quarks != 0L) { - throw WithdrawException.InvalidFractionalKinAmountException() - } - - if (amount.kin > organizer.availableBalance) { - throw WithdrawException.InsufficientFundsException() - } - - val intent = PublicKey.generate() - - val steps = mutableListOf() - steps.add("Attempting withdrawal...") - val primaryBalance = organizer.availableDepositBalance.toKinTruncating() - - // If the primary account has less Kin than the amount - // requested for withdrawal, we'll need to execute a - // private transfer to the primary account before we - // can make a public transfer to destination - return if (primaryBalance < amount.kin) { - var missingBalance = amount.kin - primaryBalance - steps.add("Amount exceeds primary balance.") - steps.add("Missing balance: $missingBalance") - - // 1. If we're missing funds, we'll pull funds - // from relationship accounts first. - if (missingBalance > 0) { - val receivedFromRelationships = - transactionReceiver.receiveFromRelationship(organizer, limit = missingBalance) - missingBalance -= receivedFromRelationships - - steps.add("Pulled from relationships: $receivedFromRelationships") - steps.add("Missing balance: $missingBalance") - } - - // 2. If we still need funds to fulfill the withdrawal - // it's likely that they are stuck in incoming and bucket - // accounts. We'll need to pull those out into primary. - if (missingBalance > 0) { - - // 3. It's possible that there's funds still left in - // an incoming account. If we're still missing funds - // for withdrawal, we'll pull from incoming. - if (transactionReceiver.availableIncomingAmount(organizer) > 0) { - val receivedFromIncoming = transactionReceiver.receiveFromIncoming( - organizer = organizer - ) - missingBalance -= receivedFromIncoming - - steps.add("Pulled from incoming: $receivedFromIncoming") - steps.add("Missing balance: $missingBalance") - } - } - - - // 4. In the event that it's a full withdrawal or if - // more funds are required, we'll need to do a private - // transfer from bucket accounts. - if (missingBalance > 0) { - // Move funds into primary from buckets - transfer( - amount = KinAmount.newInstance(kin = missingBalance, rate = Rate.oneToOne), - fee = Kin.fromKin(0), - additionalFees = emptyList(), - organizer = organizer, - rendezvousKey = intent, - destination = organizer.primaryVault, - isWithdrawal = true - ).doOnComplete { - steps.add("Pulled from buckets: $missingBalance") - }.concatWith(fetchLimits()).concatWith(balanceController.fetchBalance()) - } else { - // 5. Update balances and limits after the withdrawal since - // it's likely that this withdrawal affected both but at the - // very least, we need updated balances for all accounts. - balanceController.fetchBalance() - } - } else { - Completable.complete() - }.doOnComplete { - Timber.d(steps.joinToString("\n")) - }.concatWith( - // 6. Execute withdrawal - withdraw( - amount = amount, - organizer = organizer, - destination = destination - ) - ).doOnComplete { - trace( - tag = "Trx", - message = "Withdraw completed", - type = TraceType.Process - ) - } -} - -private fun Client.withdraw( - amount: KinAmount, - organizer: Organizer, - destination: PublicKey -): Completable { - return Completable.defer { - transactionRepository.withdraw( - amount, organizer, destination - ) - .map { - if (it is IntentPublicTransfer) { - balanceController.setTray(organizer, it.resultTray) - } - } - .ignoreElement() - } -} - -fun Client.sendRemotely( - amount: KinAmount, - organizer: Organizer, - rendezvousKey: PublicKey, - giftCard: GiftCardAccount -): Completable { - return Completable.defer { - transactionRepository.sendRemotely( - amount, organizer, rendezvousKey, giftCard - ) - .map { - if (it is IntentRemoteSend) { - balanceController.setTray(organizer, it.resultTray) - } - } - .ignoreElement() - } -} - - -suspend fun Client.requestFirstKinAirdrop( - owner: KeyPair, -): Result { - Timber.d("requesting airdrop") - return transactionRepository.requestFirstKinAirdrop(owner) -} - -fun Client.pollIntentMetadata( - owner: KeyPair, - intentId: PublicKey, - maxAttempts: Int = 50, - debugLogs: Boolean = false, -): Flow { - val stopped = AtomicBoolean() - val attemptCount = AtomicInteger() - - if (debugLogs) { - Timber.tag("codescan").i("pollIntentMetadata: start polling") - } - - return flowInterval({ 50L * (attemptCount.get() / 10) }) - .takeWhile { !stopped.get() && attemptCount.get() < maxAttempts } - .map { attemptCount.incrementAndGet() } - .onEach { - if (debugLogs) { - Timber.tag("codescan").i("pollIntentMetadata: [${it}] fetch data") - } - } - .map { transactionRepository.fetchIntentMetadata(owner, intentId) } - .filter { !stopped.get() } - .mapNotNull { it.getOrNull() } - .map { - if (debugLogs) { - Timber.tag("codescan") - .i("pollMatchingRendezvous: stop polling :: took ${attemptCount.get()} attempts") - } - stopped.set(true) - it - } -} - -fun Client.fetchTransactionLimits( - owner: KeyPair, - isForce: Boolean = false -): Limits? { - val time = System.currentTimeMillis() - - val isStale = transactionRepository.areLimitsState - - if (!isStale && !isForce) { - return transactionRepository.limits - } - - Timber.i("fetchTransactionLimits") - lastLimitsFetch = time - - val date: Calendar = GregorianCalendar() - date.set(Calendar.HOUR_OF_DAY, 0) - date.set(Calendar.MINUTE, 0) - date.set(Calendar.SECOND, 0) - date.set(Calendar.MILLISECOND, 0) - - val seconds = date.timeInMillis / 1000 - return transactionRepository.fetchLimits(owner, seconds) - .subscribeOn(Schedulers.io()) - .blockingFirst() -} - -fun Client.fetchDestinationMetadata(destination: PublicKey): Single { - return transactionRepository.fetchDestinationMetadata(destination) -} - -// ----- -private var lastLimitsFetch: Long = 0L - -fun Client.fetchLimits(isForce: Boolean = false): Completable { - val owner = SessionManager.getKeyPair() ?: return Completable.complete() - if (!CodeAppDatabase.isOpen()) return Completable.complete() - fetchTransactionLimits(owner, isForce) - return Completable.complete() -} - -fun Client.receiveIfNeeded(): Completable { - val organizer = SessionManager.getOrganizer() ?: return Completable.complete() - - if (organizer.slotsBalance < transactionRepository.maxDeposit) { - receiveFromRelationships(organizer, upTo = transactionRepository.maxDeposit) - } - - return Completable.concatArray( - receiveFromPrimaryIfWithinLimits(organizer), - transactionReceiver.receiveFromIncomingCompletable(organizer) - ) -} - -fun Client.receiveFromPrimaryIfWithinLimits(organizer: Organizer): Completable { - Timber.d("receive within limits") - val depositBalance = organizer.availableDepositBalance.toKinTruncating() - - // Nothing to deposit - if (!depositBalance.hasWholeKin()) { - Timber.d("nothing to deposit ($depositBalance)") - return Completable.complete() - } - - // We want to deposit the smaller of the two: balance in the - // primary account or the max allowed amount provided by server - return Single.just(transactionRepository.maxDeposit.toKinTruncatingLong()) - .map { maxDeposit -> - Pair( - Kin.fromKin(min(depositBalance.toKinValueDouble(), maxDeposit.toDouble())), - Kin.fromKin(maxDeposit) - ) - } - .filter { pair -> - val (depositAmount, _) = pair - depositAmount.hasWholeKin().also { Timber.d("hasWholeKin=$it") } - } - .flatMapSingle { pair -> - val (depositAmount, maxDeposit) = pair - Timber.i( - "Receiving from primary: ${depositAmount.toKin()}, Max allowed deposit: ${maxDeposit.toKin()}" - ) - transactionRepository.receiveFromPrimary(depositAmount, organizer) - } - .map { intent -> - if (intent is IntentDeposit) { - balanceController.setTray(organizer, intent.resultTray) - } - } - .ignoreElement() - .andThen { - trace( - tag = "Trx", - message = "Received from primary", - type = TraceType.Process - ) - } - .andThen { fetchLimits(true) } -} - -fun Client.fetchPrivacyUpgrades(): Completable { - val owner = SessionManager.getKeyPair() ?: return Completable.complete() - val organizer = SessionManager.getOrganizer() ?: return Completable.complete() - - return transactionRepository.fetchUpgradeableIntents(owner) - .flatMapCompletable { intents -> - Timber.w("Fetch Privacy size: ${intents.size}") - val completableList = mutableListOf() - - intents.forEachIndexed { index, intent -> - val completable = - transactionRepository.upgradePrivacy( - organizer.mnemonic, - intent - ) - .doOnSuccess { - analytics.upgradePrivacy( - successful = true, - intentId = intent.id, - actionCount = intent.actions.size - ) - Timber.i("Privacy Upgrade - success") - } - .doOnError { - analytics.upgradePrivacy( - successful = false, - intentId = intent.id, - actionCount = intent.actions.size - ) - Timber.i("Privacy Upgrade - failure") - } - .ignoreElement() - - completableList.add(completable) - } - - Completable.mergeArray(*completableList.toTypedArray()) - } -} - -fun Client.getTransferPreflightAction(amount: Kin): Completable { - val organizer = SessionManager.getOrganizer() ?: return Completable.complete() - val neededKin = - if (amount > organizer.slotsBalance) amount - organizer.slotsBalance else Kin.fromKin(0) - - // If the there's insufficient funds in the slots - // we'll need to top them up from incoming, relationship - // and primary accounts, in that order. - return if (neededKin > 0) { - // 1. Receive funds from incoming accounts as those - // will rotate more frequently than other types of accounts - val receivedKin = transactionReceiver.receiveFromIncoming(organizer) - Timber.d("received ${receivedKin.quarks} from incoming") - // 2. Pull funds from relationships if there's still funds - // missing in buckets after the receiving from primary - if (receivedKin < neededKin) { - Timber.d("attempt to pull funds from relationship to get to ${neededKin.quarks}") - val result = transactionReceiver.receiveFromRelationship(organizer, limit = neededKin - receivedKin) - Timber.d("received ${result.quarks} from relationships") - } - - // 3. If the amount is still larger than what's available - // in the slots, we'll need to move funds from primary - // deposits into slots after receiving - if (amount > organizer.slotsBalance) { - Timber.d("receive from primary") - receiveFromPrimaryIfWithinLimits(organizer) - } else { - Completable.complete() - } - } else { - Completable.complete() - } -} - -fun Client.receiveFromRelationships(organizer: Organizer, upTo: Kin? = null): Kin { - return transactionReceiver.receiveFromRelationship(organizer, upTo) -} - -@SuppressLint("CheckResult") -@Throws -fun Client.establishRelationshipSingle(organizer: Organizer, domain: Domain): Single { - return transactionRepository.establishRelationshipSingle(organizer, domain) -} - -@Suppress("RedundantSuspendModifier") -@SuppressLint("CheckResult") -@Throws -suspend fun Client.awaitEstablishRelationship( - organizer: Organizer, - domain: Domain -): Result { - return transactionRepository.establishRelationship(organizer, domain) - .map { it.relationship } -} - -suspend fun Client.initiateSwap(organizer: Organizer): Result { - return transactionRepository.initiateSwap(organizer) -} - -suspend fun Client.declareFiatPurchase(owner: KeyPair, amount: KinAmount, nonce: UUID) : Result { - return transactionRepository.declareFiatPurchase(owner, amount, nonce) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/client/TransactionReceiver.kt b/services/code/src/main/java/com/getcode/network/client/TransactionReceiver.kt deleted file mode 100644 index d73b72ad0..000000000 --- a/services/code/src/main/java/com/getcode/network/client/TransactionReceiver.kt +++ /dev/null @@ -1,166 +0,0 @@ -package com.getcode.network.client - -import android.content.Context -import com.getcode.model.Kin -import com.getcode.model.intents.IntentPublicTransfer -import com.getcode.model.intents.IntentReceive -import com.getcode.model.intents.IntentRemoteReceive -import com.getcode.network.repository.BalanceRepository -import com.getcode.network.repository.TransactionRepository -import com.getcode.solana.organizer.GiftCardAccount -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Tray -import com.getcode.utils.ErrorUtils -import com.getcode.utils.TraceType -import com.getcode.utils.trace -import dagger.hilt.android.qualifiers.ApplicationContext -import io.reactivex.rxjava3.core.Completable -import timber.log.Timber -import javax.inject.Inject - -class TransactionReceiver @Inject constructor( - @ApplicationContext - private val context: Context, - private val balanceRepository: BalanceRepository, - private val transactionRepository: TransactionRepository -) { - fun receiveRemotely( - amount: Kin, - organizer: Organizer, - giftCard: GiftCardAccount, - isVoiding: Boolean - ): Completable { - return Completable.defer { - transactionRepository.receiveRemotely( - context, amount, organizer, giftCard, isVoiding - ) - .map { - if (it is IntentRemoteReceive) { - setTray(organizer, it.resultTray) - } - } - .ignoreElement() - } - } - - suspend fun receiveRemotelySuspend( - giftCard: GiftCardAccount, - amount: Kin, - organizer: Organizer, - isVoiding: Boolean - ) { - val intent = transactionRepository.receiveRemotely( - context = context, - amount = amount, - organizer = organizer, - giftCard = giftCard, - isVoiding = isVoiding - ).blockingGet() - - if (intent is IntentRemoteReceive) { - setTray(organizer, intent.resultTray) - } - } - - fun receiveFromRelationship(organizer: Organizer, limit: Kin? = null): Kin { - var receivedTotal = Kin.fromKin(0) - - runCatching loop@{ - organizer.relationshipsLargestFirst().onEach { relationship -> - Timber.d("Receiving from relationships: domain ${relationship.domain.urlString} balance ${relationship.partialBalance}") - - // Ignore empty relationship accounts - if (relationship.partialBalance > 0) { - val intent = transactionRepository.receiveFromRelationship( - relationship = relationship, - organizer = organizer - ).blockingGet() - - trace( - tag = "Trx", - message = "Received from relationship", - type = TraceType.Process, - metadata = { - "domain" to relationship.domain.relationshipHost - "kin" to relationship.partialBalance - } - ) - - receivedTotal += relationship.partialBalance - - trace( - tag = "Trx", - message = "Received from incoming", - type = TraceType.Process - ) - - if (intent is IntentPublicTransfer) { - setTray(organizer, intent.resultTray) - } - - // Bail early if a limit is set - if (limit != null && receivedTotal >= limit) { - return@loop // break loop - } - } - } - }.onFailure { - ErrorUtils.handleError(it) - it.printStackTrace() - } - - return receivedTotal - } - - fun receiveFromIncoming(organizer: Organizer): Kin { - val incomingBalance = availableIncomingAmount(organizer) - return if (incomingBalance <= 0) { - Kin.fromKin(0) - } else { - receiveFromIncoming( - amount = incomingBalance, - organizer = organizer - ).blockingAwait() - incomingBalance - } - } - - fun receiveFromIncomingCompletable(organizer: Organizer): Completable { - val incomingBalance = availableIncomingAmount(organizer) - return if (incomingBalance <= 0) { - Completable.complete() - } else { - receiveFromIncoming( - amount = incomingBalance, - organizer = organizer - ) - } - } - - fun receiveFromIncoming(amount: Kin, organizer: Organizer): Completable { - trace( - "receiveFromIncoming $amount", - type = TraceType.Silent - ) - return transactionRepository.receiveFromIncoming( - context, amount, organizer - ).map { - if (it is IntentReceive) { - setTray(organizer, it.resultTray) - } - }.ignoreElement() - } - - suspend fun swapIfNeeded(organizer: Organizer) { - transactionRepository.swapIfNeeded(organizer) - } - - private fun setTray(organizer: Organizer, tray: Tray) { - organizer.set(tray) - balanceRepository.setBalance(organizer.availableBalance.toKinTruncatingLong().toDouble()) - } - - fun availableIncomingAmount(organizer: Organizer): Kin { - return organizer.availableIncomingBalance.toKinTruncating() - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/exchange/Exchange.kt b/services/code/src/main/java/com/getcode/network/exchange/Exchange.kt deleted file mode 100644 index c7216c9c8..000000000 --- a/services/code/src/main/java/com/getcode/network/exchange/Exchange.kt +++ /dev/null @@ -1,236 +0,0 @@ -package com.getcode.network.exchange - -import com.getcode.db.CodeAppDatabase -import com.getcode.services.model.PrefsString -import com.getcode.model.Rate -import com.getcode.network.repository.PrefRepository -import com.getcode.network.service.CurrencyService -import com.getcode.utils.TraceType -import com.getcode.util.format -import com.getcode.utils.network.retryable -import com.getcode.utils.trace -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import kotlinx.datetime.Instant -import java.util.Date -import javax.inject.Inject -import kotlin.time.Duration.Companion.minutes - - -class CodeExchange @Inject constructor( - private val currencyService: CurrencyService, - prefs: PrefRepository, - private val preferredCurrency: suspend () -> com.getcode.model.Currency?, - private val defaultCurrency: suspend () -> com.getcode.model.Currency?, -) : Exchange, CoroutineScope by CoroutineScope(Dispatchers.IO) { - - private val db = CodeAppDatabase.getInstance() - - private var _entryRate = MutableStateFlow(Rate.oneToOne) - override val entryRate: Rate - get() = _entryRate.value - - override fun observeEntryRate(): Flow = _entryRate - - private val _localRate = MutableStateFlow(Rate.oneToOne) - override val localRate - get() = _localRate.value - - override fun observeLocalRate(): Flow = _localRate - - private var rateDate: Long = System.currentTimeMillis() - - private var localCurrency: com.getcode.model.CurrencyCode? = null - private var entryCurrency: com.getcode.model.CurrencyCode? = null - - private val _rates = MutableStateFlow(emptyMap()) - private var rates = RatesBox(0, emptyMap()) - set(value) { - field = value - _rates.value = value.rates - } - - override fun rates() = rates.rates - override fun observeRates(): Flow> = _rates - - private val isStale: Boolean - get() { - if (rates.rates.isEmpty()) return true - // Remember, the exchange rates date is the server-provided - // date-of-rate and not the time the rate was fetched. It - // might be reasonable for the server to return a date that - // is dated 11 minutes or older. - val threshold = 20.minutes.inWholeMilliseconds - return System.currentTimeMillis() - rates.dateMillis > threshold - } - - init { - launch { - localCurrency = com.getcode.model.CurrencyCode.tryValueOf(preferredCurrency()?.code.orEmpty()) - entryCurrency = com.getcode.model.CurrencyCode.tryValueOf(defaultCurrency()?.code.orEmpty()) - - prefs.observeOrDefault(PrefsString.KEY_ENTRY_CURRENCY, "") - .map { it.takeIf { it.isNotEmpty() } } - .map { com.getcode.model.CurrencyCode.tryValueOf(it.orEmpty()) } - .mapNotNull { preferred -> - preferred ?: com.getcode.model.CurrencyCode.tryValueOf(defaultCurrency()?.code.orEmpty()) - }.onEach { setEntryCurrency(it) } - .launchIn(this@CodeExchange) - } - - launch { - db?.exchangeDao()?.query()?.let { exchangeData -> - val rates = exchangeData.map { Rate(it.fx, it.currency) } - val dateMillis = exchangeData.minOf { it.synced } - set(RatesBox(dateMillis = dateMillis, rates = rates)) - } - - fetchRatesIfNeeded() - } - - prefs.observeOrDefault(PrefsString.KEY_LOCAL_CURRENCY, "") - .map { it.takeIf { it.isNotEmpty() } } - .map { com.getcode.model.CurrencyCode.tryValueOf(it.orEmpty()) } - .mapNotNull { preferred -> - preferred ?: com.getcode.model.CurrencyCode.tryValueOf(defaultCurrency()?.code.orEmpty()) - }.onEach { setLocalCurrency(it) } - .launchIn(this) - } - - override suspend fun fetchRatesIfNeeded() { - if (isStale) { - retryable( - call = { - currencyService.getRates() - .onSuccess { (updatedRates, date) -> - db?.exchangeDao()?.insert(rates = updatedRates, syncedAt = date) - set(RatesBox(date, updatedRates)) - } - } - ) - } - - updateRates() - } - - private fun setEntryCurrency(currency: com.getcode.model.CurrencyCode) { - entryCurrency = currency - updateRates() - } - - private fun setLocalCurrency(currency: com.getcode.model.CurrencyCode) { - localCurrency = currency - updateRates() - } - - private suspend fun set(ratesBox: RatesBox) { - rates = ratesBox - rateDate = ratesBox.dateMillis - - setLocalEntryCurrencyIfNeeded() - updateRates() - } - - private suspend fun setLocalEntryCurrencyIfNeeded() { - if (entryCurrency != null) { - return - } - - val localRegionCurrency = defaultCurrency() ?: return - val currency = com.getcode.model.CurrencyCode.tryValueOf(localRegionCurrency.code) - entryCurrency = currency - } - - override fun rateFor(currencyCode: com.getcode.model.CurrencyCode): Rate? = rates.rateFor(currencyCode) - - override fun rateForUsd(): Rate? = rates.rateForUsd() - - private fun updateRates() { - if (rates.isEmpty) { - return - } - - val localRate = localCurrency?.let { rates.rateFor(it) } - val localChanged = _localRate.value != localRate - if (localChanged) { - _localRate.value = if (localRate != null) { - trace( - tag = "Background", - message = "Updated the local currency: $localCurrency, " + - "Staleness ${System.currentTimeMillis() - rates.dateMillis} ms, " + - "Date: ${Date(rates.dateMillis)}", - type = TraceType.Process - ) - localRate - } else { - trace( - tag = "Background", - message = "local:: Rate for $localCurrency not found. Defaulting to USD.", - type = TraceType.Process - ) - rates.rateForUsd()!! - } - } - - - val entryRate = entryCurrency?.let { rates.rateFor(it) } - val entryChanged = _entryRate.value != entryRate - if (entryChanged) { - _entryRate.value = if (entryRate != null) { - trace( - tag = "Background", - message = "Updated the entry currency: $entryCurrency, " + - "Staleness ${System.currentTimeMillis() - rates.dateMillis} ms, " + - "Date: ${Date(rates.dateMillis)}", - type = TraceType.Process - ) - entryRate - } else { - trace( - tag = "Background", - message = "entry:: Rate for $entryCurrency not found. Defaulting to USD.", - type = TraceType.Process - ) - rates.rateForUsd()!! - } - } - - if (localChanged || entryChanged) { - trace(tag = "Background", - message = "Updated rates", - type = TraceType.Process, - metadata = { - "date" to Instant.fromEpochMilliseconds(rates.dateMillis) - .format("yyyy-MM-dd HH:mm:ss") - } - ) - } - } -} - -private data class RatesBox(val dateMillis: Long, val rates: Map) { - constructor(dateMillis: Long, rates: List) : this( - dateMillis, - rates.associateBy { it.currency }) - - val isEmpty: Boolean - get() = rates.isEmpty() - - fun rateFor(currencyCode: com.getcode.model.CurrencyCode): Rate? = rates[currencyCode] - - fun rateFor(currency: com.getcode.model.Currency): Rate? { - val currencyCode = com.getcode.model.CurrencyCode.tryValueOf(currency.code) - return currencyCode?.let { rates[it] } - } - - fun rateForUsd(): Rate? { - return rates[com.getcode.model.CurrencyCode.USD] - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/integrity/DeviceCheck.kt b/services/code/src/main/java/com/getcode/network/integrity/DeviceCheck.kt deleted file mode 100644 index 7c917187f..000000000 --- a/services/code/src/main/java/com/getcode/network/integrity/DeviceCheck.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.getcode.network.integrity - -import android.annotation.SuppressLint -import android.content.Context -import android.provider.Settings -import android.util.Base64 -import com.codeinc.gen.common.v1.Model -import com.fingerprintjs.android.fpjs_pro.Configuration -import com.fingerprintjs.android.fpjs_pro.FingerprintJS -import com.fingerprintjs.android.fpjs_pro.FingerprintJSFactory -import com.getcode.services.BuildConfig -import com.getcode.services.BuildConfig.GOOGLE_CLOUD_PROJECT_NUMBER -import com.google.android.gms.tasks.Task -import com.google.android.play.core.integrity.IntegrityManager -import com.google.android.play.core.integrity.IntegrityManagerFactory -import com.google.android.play.core.integrity.IntegrityTokenRequest -import com.google.android.play.core.integrity.IntegrityTokenResponse -import io.reactivex.rxjava3.core.BackpressureStrategy -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Single -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException - -data class DeviceTokenResult( - val token: String? -) - -object DeviceCheck: CoroutineScope by CoroutineScope(Dispatchers.IO) { - - private lateinit var integrityManager: IntegrityManager - - private var fingerprint: FingerprintJS? = null - private lateinit var deviceId: String - - @SuppressLint("HardwareIds") - fun register(context: Context) { - integrityManager = IntegrityManagerFactory.create(context) - deviceId = Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) - - fingerprint = runCatching { - val factory = FingerprintJSFactory(context) - val configuration = Configuration( - apiKey = BuildConfig.FINGERPRINT_API_KEY, - region = Configuration.Region.US, - ) - - factory.createInstance(configuration) - }.getOrNull() - } - - private fun handleAppCheckError(error: Throwable): Boolean { - error.printStackTrace() - return true - } - - @Deprecated("Replace with Result variant") - fun integrityResponseSingle(): Single { - return Single.create { emitter -> - tokenResponse( - onToken = { emitter.onSuccess(DeviceTokenResult(it)) }, - onError = { emitter.onError(it) } - ) - } - } - - @Deprecated("Replace with Result variant") - fun integrityResponseFlowable( - backpressureStrategy: BackpressureStrategy = BackpressureStrategy.BUFFER - ): Flowable { - return Flowable.create({ emitter -> - tokenResponse( - onToken = { emitter.onNext(DeviceTokenResult(it)) }, - onError = { emitter.onError(it) } - ) - }, backpressureStrategy) - } - - suspend fun integrityResponse(): DeviceTokenResult = suspendCancellableCoroutine { cont -> - tokenResponse( - onToken = { cont.resume(DeviceTokenResult(it)) }, - onError = { cont.resumeWithException(it) } - ) - } - - private fun visitorIdentifier(onIdentifier: (Result) -> Unit) { - fingerprint?.getVisitorId( - listener = { - onIdentifier(Result.success(it.visitorId)) }, - errorListener = { - val error = Throwable("Device Check failed:: ${it.description}") -// ErrorUtils.handleError(error) -// onIdentifier(Result.failure(error)) - onIdentifier(Result.success(null)) - } - ) ?: onIdentifier(Result.success(null)) - } - private fun tokenResponse(onToken: (String?) -> Unit, onError: (Throwable) -> Unit) { - visitorIdentifier { result -> - if (result.isFailure) { - onError(result.exceptionOrNull()!!) - return@visitorIdentifier - } - - val tag = result.getOrNull() - val rawNonce = "$tag $deviceId" - - val nonce = Base64.encodeToString(rawNonce.toByteArray(), Base64.NO_WRAP) - // Request the integrity token by providing a nonce. - val integrityTokenResponse: Task = - integrityManager.requestIntegrityToken( - IntegrityTokenRequest.builder() - .setCloudProjectNumber(GOOGLE_CLOUD_PROJECT_NUMBER.toLong()) - .setNonce(nonce) - .build() - ) - - integrityTokenResponse.addOnSuccessListener { - onToken(it.token()) - }.addOnFailureListener { error -> - if (!handleAppCheckError(error)) { - onError(error) - return@addOnFailureListener - } - - onToken(null) - } - } - } -} - -fun String.toDeviceToken() = Model.DeviceToken.newBuilder().setValue(this).build() \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/repository/AccountRepository.kt b/services/code/src/main/java/com/getcode/network/repository/AccountRepository.kt deleted file mode 100644 index 629a2e19b..000000000 --- a/services/code/src/main/java/com/getcode/network/repository/AccountRepository.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.getcode.network.repository - -import com.codeinc.gen.account.v1.AccountService -import com.codeinc.gen.common.v1.Model -import com.getcode.ed25519.Ed25519 -import com.getcode.model.* -import com.getcode.network.api.AccountApi -import com.getcode.solana.keys.PublicKey -import com.getcode.utils.getPublicKeyBase58 -import io.reactivex.rxjava3.core.Single -import timber.log.Timber -import javax.inject.Inject - -private const val TAG = "AccountRepository" - -@Deprecated("Replaced with Account Service") -class AccountRepository @Inject constructor( - private val accountApi: AccountApi -) { - fun getTokenAccountInfosSuspend( - owner: Ed25519.KeyPair, - ): Map { - val request = AccountService.GetTokenAccountInfosRequest.newBuilder() - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .apply { setSignature(sign(owner)) } - .build() - - val tokenAccount = accountApi.getTokenAccountInfos(request) - .flatMap { response -> - when (response.result) { - AccountService.GetTokenAccountInfosResponse.Result.OK -> { - val container = mutableMapOf() - - for ((base58, info) in response.tokenAccountInfosMap) { - val account = PublicKey.fromBase58(base58) - val accountInfo = AccountInfo.newInstance(info) - if (accountInfo == null) { - Timber.i("Failed to parse account info: $info") - continue - } - - if (info.accountType == Model.AccountType.LEGACY_PRIMARY_2022) { - Timber.i("Owner requires migration: ${owner.getPublicKeyBase58()}") - throw FetchAccountInfosException.MigrationRequiredException(accountInfo) - } - - container[account] = accountInfo - } - Single.just(container) - } - AccountService.GetTokenAccountInfosResponse.Result.NOT_FOUND -> { - Timber.i("Account not found for owner: ${owner.getPublicKeyBase58()}") - throw FetchAccountInfosException.NotFoundException() - } - else -> { - Timber.i("Unknown exception") - throw FetchAccountInfosException.UnknownException() - } - } - } - - return tokenAccount.blockingGet() - } - - fun getTokenAccountInfos( - owner: Ed25519.KeyPair, - ): Single> { - val request = AccountService.GetTokenAccountInfosRequest.newBuilder() - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .apply { setSignature(sign(owner)) } - .build() - - Timber.d("token info fetch") - return accountApi.getTokenAccountInfos(request) - .flatMap { response -> - when (response.result) { - AccountService.GetTokenAccountInfosResponse.Result.OK -> { - Timber.d("token account infos fetched") - val container = mutableMapOf() - - for ((base58, info) in response.tokenAccountInfosMap) { - val account = PublicKey.fromBase58(base58) - val accountInfo = AccountInfo.newInstance(info) - if (accountInfo == null) { - Timber.i("Failed to parse account info: $info") - continue - } - - if (info.accountType == Model.AccountType.LEGACY_PRIMARY_2022) { - Timber.i("Owner requires migration: ${owner.getPublicKeyBase58()}") - return@flatMap Single.error(FetchAccountInfosException.MigrationRequiredException(accountInfo)) - } - - container[account] = accountInfo - } - Timber.d("token account infos handled") - Single.just(container) - } - AccountService.GetTokenAccountInfosResponse.Result.NOT_FOUND -> { - Timber.i("Account not found for owner: ${owner.getPublicKeyBase58()}") - Single.error(FetchAccountInfosException.NotFoundException()) - } - else -> { - Timber.i("Unknown exception") - Single.error(FetchAccountInfosException.UnknownException()) - } - } - } - } - - sealed class FetchAccountInfosException : Exception() { - class MigrationRequiredException(val accountInfo: AccountInfo) : FetchAccountInfosException() - class NotFoundException : FetchAccountInfosException() - class UnknownException : FetchAccountInfosException() - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/repository/AppSettingsRepository.kt b/services/code/src/main/java/com/getcode/network/repository/AppSettingsRepository.kt deleted file mode 100644 index 128de602d..000000000 --- a/services/code/src/main/java/com/getcode/network/repository/AppSettingsRepository.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.getcode.network.repository - -import com.getcode.analytics.CodeAnalyticsService -import com.getcode.services.model.AppSetting -import com.getcode.services.model.PrefsBool -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import javax.inject.Inject - -data class AppSettings( - val cameraStartByDefault: Boolean, - val requireBiometrics: Boolean, -) { - companion object { - val Defaults = AppSettings( - cameraStartByDefault = true, - requireBiometrics = false, - ) - } -} -class AppSettingsRepository @Inject constructor( - private val prefRepository: PrefRepository, - private val analytics: CodeAnalyticsService, -) { - - fun observe(): Flow = AppSettings.Defaults.let { defaults -> - combine( - prefRepository.observeOrDefault(PrefsBool.CAMERA_START_BY_DEFAULT, defaults.cameraStartByDefault), - prefRepository.observeOrDefault(PrefsBool.REQUIRE_BIOMETRICS, defaults.requireBiometrics) - ) { camera, biometrics -> - AppSettings( - cameraStartByDefault = camera, - requireBiometrics = biometrics - ) - } - } - - suspend fun get(setting: AppSetting): Boolean { - return when (setting) { - PrefsBool.CAMERA_START_BY_DEFAULT -> prefRepository.get( - PrefsBool.CAMERA_START_BY_DEFAULT, - AppSettings.Defaults.cameraStartByDefault - ) - - PrefsBool.REQUIRE_BIOMETRICS -> prefRepository.get( - PrefsBool.REQUIRE_BIOMETRICS, - AppSettings.Defaults.requireBiometrics - ) - } - } - - fun update(setting: AppSetting, value: Boolean, fromUser: Boolean = true) { - if (fromUser) { - analytics.appSettingToggled(setting, value) - } - when (setting) { - PrefsBool.CAMERA_START_BY_DEFAULT -> { - prefRepository.set(PrefsBool.CAMERA_START_BY_DEFAULT, value) - } - - PrefsBool.REQUIRE_BIOMETRICS -> { - prefRepository.set(PrefsBool.REQUIRE_BIOMETRICS, value) - } - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/repository/BalanceRepository.kt b/services/code/src/main/java/com/getcode/network/repository/BalanceRepository.kt deleted file mode 100644 index 53600bec6..000000000 --- a/services/code/src/main/java/com/getcode/network/repository/BalanceRepository.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.getcode.network.repository - - -import kotlinx.coroutines.flow.MutableStateFlow -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class BalanceRepository @Inject constructor() { - - val balanceFlow = MutableStateFlow(-1.0) - - fun setBalance(balance: Double) { - balanceFlow.value = balance - } - - fun clearBalance() { - balanceFlow.value = 0.0 - } - -} diff --git a/services/code/src/main/java/com/getcode/network/repository/BetaFlagsRepository.kt b/services/code/src/main/java/com/getcode/network/repository/BetaFlagsRepository.kt deleted file mode 100644 index 51b4ec66a..000000000 --- a/services/code/src/main/java/com/getcode/network/repository/BetaFlagsRepository.kt +++ /dev/null @@ -1,162 +0,0 @@ -package com.getcode.network.repository - -import com.getcode.services.model.PrefsBool -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import javax.inject.Inject - -data class BetaOptions( - val showNetworkDropOff: Boolean, - val canViewBuckets: Boolean, - val tickOnScan: Boolean, - val debugScanTimesEnabled: Boolean, - val displayErrors: Boolean, - val giveRequestsEnabled: Boolean, - val buyModuleEnabled: Boolean, - val chatUnsubEnabled: Boolean, - val tipsEnabled: Boolean, - val conversationCashEnabled: Boolean, - val balanceCurrencySelectionEnabled: Boolean, - val kadoWebViewEnabled: Boolean, - val shareTweetToTip: Boolean, - val tipCardOnHomeScreen: Boolean, - val cameraGesturesEnabled: Boolean, - val invertedDragZoom: Boolean, - val canFlipTipCard: Boolean, - val galleryEnabled: Boolean, -) { - companion object { - // Default states for various beta flags in app. - val Defaults = BetaOptions( - showNetworkDropOff = false, - canViewBuckets = false, - tickOnScan = false, - debugScanTimesEnabled = false, - displayErrors = false, - giveRequestsEnabled = false, - buyModuleEnabled = true, - chatUnsubEnabled = false, - tipsEnabled = true, - conversationCashEnabled = false, - balanceCurrencySelectionEnabled = true, - kadoWebViewEnabled = false, - shareTweetToTip = true, - tipCardOnHomeScreen = true, - cameraGesturesEnabled = true, - invertedDragZoom = false, - canFlipTipCard = false, - galleryEnabled = true - ) - } -} - -class BetaFlagsRepository @Inject constructor( - private val prefRepository: PrefRepository, -) { - suspend fun isEnabled() = prefRepository.get(PrefsBool.IS_DEBUG_ALLOWED, false) - - fun enableBeta(allowed: Boolean) { - prefRepository.set( - PrefsBool.IS_DEBUG_ALLOWED, - allowed, - ) - - if (!allowed) { - prefRepository.set(PrefsBool.IS_DEBUG_ACTIVE, false) - } - } - - fun observe(): Flow = BetaOptions.Defaults.let { defaults -> - combine( - observeBetaFlag(PrefsBool.SHOW_CONNECTIVITY_STATUS, default = defaults.showNetworkDropOff), - observeBetaFlag(PrefsBool.BUCKET_DEBUGGER_ENABLED, default = defaults.canViewBuckets), - observeBetaFlag(PrefsBool.VIBRATE_ON_SCAN, default = defaults.tickOnScan), - observeBetaFlag(PrefsBool.LOG_SCAN_TIMES, default = defaults.debugScanTimesEnabled), - observeBetaFlag(PrefsBool.GIVE_REQUESTS_ENABLED, default = defaults.giveRequestsEnabled), - observeBetaFlag(PrefsBool.BUY_MODULE_ENABLED, default = defaults.buyModuleEnabled), - observeBetaFlag(PrefsBool.CHAT_UNSUB_ENABLED, default = defaults.chatUnsubEnabled), - observeBetaFlag(PrefsBool.TIPS_ENABLED, default = defaults.tipsEnabled), - observeBetaFlag(PrefsBool.CONVERSATION_CASH_ENABLED, default = defaults.conversationCashEnabled), - observeBetaFlag(PrefsBool.BALANCE_CURRENCY_SELECTION_ENABLED, defaults.balanceCurrencySelectionEnabled), - observeBetaFlag(PrefsBool.DISPLAY_ERRORS, default = defaults.displayErrors), - observeBetaFlag(PrefsBool.KADO_WEBVIEW_ENABLED, default = defaults.kadoWebViewEnabled), - observeBetaFlag(PrefsBool.SHARE_TWEET_TO_TIP, default = defaults.shareTweetToTip), - observeBetaFlag(PrefsBool.TIP_CARD_ON_HOMESCREEN, defaults.tipCardOnHomeScreen), - observeBetaFlag(PrefsBool.CAMERA_GESTURES_ENABLED, defaults.cameraGesturesEnabled), - observeBetaFlag(PrefsBool.CAMERA_DRAG_INVERTED, defaults.invertedDragZoom), - observeBetaFlag(PrefsBool.TIP_CARD_FLIPPABLE, defaults.canFlipTipCard), - observeBetaFlag(PrefsBool.GALLERY_ENABLED, defaults.galleryEnabled), - ) { - BetaOptions( - showNetworkDropOff = it[0], - canViewBuckets = it[1], - tickOnScan = it[2], - debugScanTimesEnabled = it[3], - giveRequestsEnabled = it[4], - buyModuleEnabled = it[5], - chatUnsubEnabled = it[6], - tipsEnabled = it[7], - conversationCashEnabled = it[8], - balanceCurrencySelectionEnabled = it[9], - displayErrors = it[10], - kadoWebViewEnabled = it[11], - shareTweetToTip = it[12], - tipCardOnHomeScreen = it[13], - cameraGesturesEnabled = it[14], - invertedDragZoom = it[15], - canFlipTipCard = it[16], - galleryEnabled = it[17], - ) - } - } - - private fun observeBetaFlag(flag: PrefsBool, default: Boolean = false): Flow { - return combine( - prefRepository.observeOrDefault(PrefsBool.IS_DEBUG_ALLOWED, false), - prefRepository.observeOrDefault(flag, default) - ) { a, b -> - b.takeIf { a } ?: default - } - } - - suspend fun isEnabled(flag: PrefsBool): Boolean { - return prefRepository.get(flag, default(flag)) - } - - private fun default(flag: PrefsBool): Boolean { - return with(BetaOptions.Defaults) { - when (flag) { - PrefsBool.BALANCE_CURRENCY_SELECTION_ENABLED -> balanceCurrencySelectionEnabled - PrefsBool.BUCKET_DEBUGGER_ENABLED -> canViewBuckets - PrefsBool.BUY_MODULE_ENABLED -> buyModuleEnabled - PrefsBool.CAMERA_GESTURES_ENABLED -> cameraGesturesEnabled - PrefsBool.CAMERA_DRAG_INVERTED -> invertedDragZoom - PrefsBool.CHAT_UNSUB_ENABLED -> chatUnsubEnabled - PrefsBool.CONVERSATION_CASH_ENABLED -> conversationCashEnabled - PrefsBool.DISPLAY_ERRORS -> displayErrors - PrefsBool.GALLERY_ENABLED -> galleryEnabled - PrefsBool.GIVE_REQUESTS_ENABLED -> giveRequestsEnabled - PrefsBool.KADO_WEBVIEW_ENABLED -> kadoWebViewEnabled - PrefsBool.LOG_SCAN_TIMES -> debugScanTimesEnabled - PrefsBool.SHARE_TWEET_TO_TIP -> shareTweetToTip - PrefsBool.SHOW_CONNECTIVITY_STATUS -> showNetworkDropOff - PrefsBool.TIPS_ENABLED -> tipsEnabled - PrefsBool.TIP_CARD_FLIPPABLE -> canFlipTipCard - PrefsBool.TIP_CARD_ON_HOMESCREEN -> tipCardOnHomeScreen - PrefsBool.VIBRATE_ON_SCAN -> tickOnScan - PrefsBool.BUY_MODULE_AVAILABLE -> false - PrefsBool.CAMERA_START_BY_DEFAULT -> false - PrefsBool.DISMISSED_TIP_CARD_BANNER -> false - PrefsBool.ESTABLISH_CODE_RELATIONSHIP -> false - PrefsBool.HAS_REMOVED_LOCAL_CURRENCY -> false - PrefsBool.IS_DEBUG_ACTIVE -> false - PrefsBool.IS_DEBUG_ALLOWED -> false - PrefsBool.IS_ELIGIBLE_GET_FIRST_KIN_AIRDROP -> false - PrefsBool.IS_ELIGIBLE_GIVE_FIRST_KIN_AIRDROP -> false - PrefsBool.REQUIRE_BIOMETRICS -> false - PrefsBool.SEEN_TIP_CARD -> false - PrefsBool.STARTED_TIP_CONNECT -> false - } - } - } -} diff --git a/services/code/src/main/java/com/getcode/network/repository/Extensions.kt b/services/code/src/main/java/com/getcode/network/repository/Extensions.kt deleted file mode 100644 index 0fc90890d..000000000 --- a/services/code/src/main/java/com/getcode/network/repository/Extensions.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.getcode.network.repository - -import com.codeinc.gen.common.v1.Model -import com.getcode.ed25519.Ed25519 -import com.getcode.utils.toByteString -import com.google.protobuf.MessageLite -import java.io.ByteArrayOutputStream - -fun isMock() = false - -fun ByteArray.toUserId(): Model.UserId { - return Model.UserId.newBuilder().setValue(this.toByteString()).build() -} - -fun String.toPhoneNumber(): Model.PhoneNumber { - return Model.PhoneNumber.newBuilder().setValue(this).build() -} - -fun List.toSolanaAccount(): Model.SolanaAccountId { - return Model.SolanaAccountId.newBuilder().setValue(this.toByteArray().toByteString()) - .build() -} - -fun ByteArray.toSolanaAccount(): Model.SolanaAccountId { - return Model.SolanaAccountId.newBuilder().setValue(this.toByteString()) - .build() -} - -fun ByteArray.toSignature(): Model.Signature { - return Model.Signature.newBuilder().setValue(this.toByteString()) - .build() -} - -fun com.getcode.solana.keys.PublicKey.toIntentId(): Model.IntentId { - return Model.IntentId.newBuilder().setValue(this.byteArray.toByteString()).build() -} - -fun MessageLite.Builder.sign(owner: Ed25519.KeyPair): Model.Signature { - val bos = ByteArrayOutputStream() - this.buildPartial().writeTo(bos) - return Ed25519.sign(bos.toByteArray(), owner).toSignature() -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/repository/FeatureRepository.kt b/services/code/src/main/java/com/getcode/network/repository/FeatureRepository.kt deleted file mode 100644 index c83cbb1b6..000000000 --- a/services/code/src/main/java/com/getcode/network/repository/FeatureRepository.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.getcode.network.repository - -import com.getcode.model.BalanceCurrencyFeature -import com.getcode.model.BuyModuleFeature -import com.getcode.model.CameraGesturesFeature -import com.getcode.services.model.PrefsBool -import com.getcode.model.RequestKinFeature -import com.getcode.model.TipCardFeature -import com.getcode.model.TipCardOnHomeScreenFeature -import com.getcode.model.ConversationCashFeature -import com.getcode.model.FlippableTipCardFeature -import com.getcode.model.GalleryFeature -import com.getcode.model.InvertedDragZoomFeature -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map -import javax.inject.Inject - -/** - * Collates [BetaOptions] with server availability (stored in [PrefRepository]). - */ -class FeatureRepository @Inject constructor( - private val betaFlags: BetaFlagsRepository, - prefRepository: PrefRepository, -) { - val buyModule = combine( - betaFlags.observe().map { it.buyModuleEnabled }, - prefRepository.observeOrDefault(PrefsBool.BUY_MODULE_AVAILABLE, false) - ) { enabled, available -> BuyModuleFeature(enabled, available) } - - val tipCards = betaFlags.observe().map { TipCardFeature(it.tipsEnabled) } - val tipCardOnHomeScreen = - betaFlags.observe().map { TipCardOnHomeScreenFeature(it.tipCardOnHomeScreen) } - val tipCardFlippable = betaFlags.observe().map { FlippableTipCardFeature(it.canFlipTipCard) } - val conversationsCash = - betaFlags.observe().map { ConversationCashFeature(it.conversationCashEnabled) } - - val cameraGestures = betaFlags.observe().map { CameraGesturesFeature(it.cameraGesturesEnabled) } - val invertedDragZoom = betaFlags.observe().map { InvertedDragZoomFeature(it.invertedDragZoom) } - - val galleryEnabled = betaFlags.observe().map { GalleryFeature(it.galleryEnabled) } - - val requestKin = betaFlags.observe().map { RequestKinFeature(it.giveRequestsEnabled) } - - val balanceCurrencySelection = - betaFlags.observe().map { BalanceCurrencyFeature(it.balanceCurrencySelectionEnabled) } - - suspend fun isEnabled(feature: PrefsBool): Boolean = betaFlags.isEnabled(feature) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/repository/IdentityRepository.kt b/services/code/src/main/java/com/getcode/network/repository/IdentityRepository.kt deleted file mode 100644 index 002ba1d6a..000000000 --- a/services/code/src/main/java/com/getcode/network/repository/IdentityRepository.kt +++ /dev/null @@ -1,436 +0,0 @@ -package com.getcode.network.repository - -import com.codeinc.gen.common.v1.Model -import com.codeinc.gen.phone.v1.PhoneVerificationService -import com.codeinc.gen.user.v1.IdentityService -import com.codeinc.gen.user.v1.IdentityService.GetTwitterUserRequest -import com.codeinc.gen.user.v1.IdentityService.LoginToThirdPartyAppRequest -import com.codeinc.gen.user.v1.IdentityService.LoginToThirdPartyAppResponse -import com.codeinc.gen.user.v1.IdentityService.UpdatePreferencesRequest -import com.getcode.db.CodeAppDatabase -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.AirdropType -import com.getcode.services.model.PrefsBool -import com.getcode.services.model.PrefsString -import com.getcode.model.TwitterUser -import com.getcode.model.protomapping.invoke -import com.getcode.network.api.IdentityApi -import com.getcode.services.network.core.NetworkOracle -import com.getcode.utils.ErrorUtils -import com.getcode.utils.decodeBase64 -import com.getcode.utils.encodeBase64 -import com.getcode.utils.toByteString -import com.google.common.collect.Sets -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Single -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import timber.log.Timber -import java.util.Locale -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -class IdentityRepository @Inject constructor( - private val identityApi: IdentityApi, - private val networkOracle: NetworkOracle, - private val prefRepository: PrefRepository, - private val phoneRepository: PhoneRepository -) { - data class GetUserResponse( - val userId: List, - val dataContainerId: List, - val enableDebugOptions: Boolean, - val eligibleAirdrops: Set, - val isPhoneNumberLinked: Boolean, - val buyModuleAvailable: Boolean, - ) - - fun getUser( - keyPair: KeyPair, - phoneValue: String - ): Flowable { - if (isMock()) return Flowable.just( - GetUserResponse( - userId = listOf(), - dataContainerId = listOf(), - enableDebugOptions = false, - eligibleAirdrops = setOf(), - isPhoneNumberLinked = true, - buyModuleAvailable = false, - ) - ) - - val request = - IdentityService.GetUserRequest.newBuilder() - .setPhoneNumber(phoneValue.toPhoneNumber()) - .setOwnerAccountId(keyPair.publicKeyBytes.toSolanaAccount()) - .apply { setSignature(sign(keyPair)) } - .build() - - return identityApi.getUser(request) - .map { - GetUserResponse( - userId = it.user?.id?.value?.toList() ?: throw Exception("Error: Null data"), - dataContainerId = it.dataContainerId?.value?.toList() - ?: throw Exception("Error: Null data"), - enableDebugOptions = it.enableInternalFlags, - eligibleAirdrops = it.eligibleAirdropsList?.mapNotNull { value -> - AirdropType.getInstance( - value - ) - }?.toSet() ?: throw Exception("Error: Null data"), - isPhoneNumberLinked = it.phone?.isLinked ?: throw Exception("Error: Null data"), - buyModuleAvailable = it.enableBuyModule, - ) - } - .let { networkOracle.managedRequest(it) } - .doOnNext { user -> - // TODO: There's some duplicated DB saving code in AuthManager. It's possible for - // view inconsistencies with expected state, since I suspect Database.isOpen() - // is false by the time we execute this piece of code in some flows (eg. in - // particular I've noticed logging in through seed phrase input). - if (CodeAppDatabase.isOpen()) { - prefRepository.set( - PrefsString.KEY_USER_ID, - user.userId.toByteArray().encodeBase64() - ) - prefRepository.set( - PrefsString.KEY_DATA_CONTAINER_ID, - user.dataContainerId.toByteArray().encodeBase64() - ) - phoneRepository.phoneLinked.value = true - prefRepository.set( - PrefsBool.IS_DEBUG_ALLOWED, - user.enableDebugOptions - ) - prefRepository.set( - PrefsBool.BUY_MODULE_AVAILABLE, - user.buyModuleAvailable - ) - prefRepository.set( - PrefsBool.IS_ELIGIBLE_GET_FIRST_KIN_AIRDROP, - user.eligibleAirdrops.contains(AirdropType.GetFirstKin) - ) - prefRepository.set( - PrefsBool.IS_ELIGIBLE_GIVE_FIRST_KIN_AIRDROP, - user.eligibleAirdrops.contains(AirdropType.GiveFirstKin) - ) - } - } - } - - suspend fun getUserContainerId() = prefRepository.get( - PrefsString.KEY_DATA_CONTAINER_ID, - "" - ).decodeBase64().toList() - - fun getUserLocal(): Flowable { - return Flowable.zip( - prefRepository.getFlowable(PrefsString.KEY_USER_ID), - prefRepository.getFlowable(PrefsString.KEY_DATA_CONTAINER_ID), - prefRepository.getFlowable(PrefsBool.IS_DEBUG_ALLOWED), - prefRepository.getFlowable(PrefsBool.IS_ELIGIBLE_GET_FIRST_KIN_AIRDROP), - prefRepository.getFlowable(PrefsBool.IS_ELIGIBLE_GIVE_FIRST_KIN_AIRDROP), - Flowable.just(phoneRepository.phoneLinked), - prefRepository.getFlowable(PrefsBool.BUY_MODULE_AVAILABLE), - ) { userId, dataContainerId, isDebugAllowed, isEligibleGetFirstKinAirdrop, isEligibleGiveFirstKinAirdrop, isPhoneNumberLinked, buyModuleAvailable -> - val eligibleAirdrops = Sets.newHashSet() - if (isEligibleGetFirstKinAirdrop) { - eligibleAirdrops.add(AirdropType.GetFirstKin) - } - if (isEligibleGiveFirstKinAirdrop) { - eligibleAirdrops.add(AirdropType.GiveFirstKin) - } - GetUserResponse( - userId = userId.decodeBase64().toList(), - dataContainerId = dataContainerId.decodeBase64().toList(), - enableDebugOptions = isDebugAllowed, - eligibleAirdrops = eligibleAirdrops, - isPhoneNumberLinked = isPhoneNumberLinked.value, - buyModuleAvailable = buyModuleAvailable, - ) - } - .filter { it.userId.isNotEmpty() && it.dataContainerId.isNotEmpty() } - .distinctUntilChanged() - } - - - fun linkAccount( - keyPair: KeyPair, - phoneValue: String, - code: String - ): Single { - if (isMock()) return Single.just(LinkAccountResult.Success) - .delay(1, TimeUnit.SECONDS) - - val request = - IdentityService.LinkAccountRequest.newBuilder() - .setPhone( - PhoneVerificationService.PhoneLinkingToken.newBuilder() - .setPhoneNumber(phoneValue.toPhoneNumber()) - .setCode( - PhoneVerificationService.VerificationCode.newBuilder() - .setValue(code) - ) - ) - .setOwnerAccountId(keyPair.publicKeyBytes.toSolanaAccount()) - - .apply { setSignature(sign(keyPair)) } - .build() - - return identityApi.linkAccount(request) - .map { it.result } - .let { networkOracle.managedRequest(it) } - .map { - when (it) { - IdentityService.LinkAccountResponse.Result.OK -> LinkAccountResult.Success - IdentityService.LinkAccountResponse.Result.INVALID_TOKEN -> LinkAccountResult.Error.InvalidCode - IdentityService.LinkAccountResponse.Result.RATE_LIMITED -> LinkAccountResult.Error.RateLimit - IdentityService.LinkAccountResponse.Result.UNRECOGNIZED -> LinkAccountResult.Error.Unrecognized - else -> LinkAccountResult.Error.Other - } - } - .firstOrError() - } - - fun unlinkAccount( - keyPair: KeyPair, - phoneValue: String - ): Single { - if (isMock()) return Single.just(UnlinkAccountResult.Success) - .delay(1, TimeUnit.SECONDS) - - val request = - IdentityService.UnlinkAccountRequest.newBuilder() - .setPhoneNumber(phoneValue.toPhoneNumber()) - .setOwnerAccountId(keyPair.publicKeyBytes.toSolanaAccount()) - .apply { setSignature(sign(keyPair)) } - .build() - - return identityApi.unlinkAccount(request) - .map { it.result } - .let { networkOracle.managedRequest(it) } - .map { - when (it) { - IdentityService.UnlinkAccountResponse.Result.OK -> UnlinkAccountResult.Success - else -> UnlinkAccountResult.Error - } - } - .doOnComplete { - phoneRepository.phoneNumber = "" - phoneRepository.phoneLinked.value = false - } - .firstOrError() - } - - suspend fun updatePreferences( - locale: Locale, - owner: KeyPair, - ): Result { - val localeTag = locale.language - Timber.i("Attempting to update locale to $localeTag") - val containerId = getUserContainerId() - val request = UpdatePreferencesRequest.newBuilder() - .setContainerId( - Model.DataContainerId.newBuilder().setValue(containerId.toByteString()).build() - ).setOwnerAccountId(owner.publicKeyBytes.toSolanaAccount()) - .setLocale( - Model.Locale.newBuilder().setValue(localeTag).build() - ).apply { - setSignature(sign(owner)) - }.build() - - - return try { - networkOracle.managedRequest(identityApi.updatePreferences(request)) - .map { response -> - when (val result = response.result) { - IdentityService.UpdatePreferencesResponse.Result.OK -> { - Timber.d("updatePreferences success = locale set: ${localeTag}") - Result.success(true) - } - - IdentityService.UpdatePreferencesResponse.Result.INVALID_LOCALE -> { - val error = Throwable("Error: (${localeTag}) ${result.name}") - ErrorUtils.handleError(error) - Result.failure(error) - } - - IdentityService.UpdatePreferencesResponse.Result.UNRECOGNIZED -> { - val error = Throwable("Error: updatePreferences Unrecognized request.") - ErrorUtils.handleError(error) - Result.failure(error) - } - - else -> { - val error = Throwable("Error: updatePreferences Unknown Error") - ErrorUtils.handleError(error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val msg = Throwable("Locale:: $localeTag " + e.localizedMessage) - ErrorUtils.handleError(msg) - Result.failure(msg) - } - } - - suspend fun loginToThirdParty( - rendezvous: com.getcode.solana.keys.PublicKey, - relationship: KeyPair - ): Result { - val request = LoginToThirdPartyAppRequest.newBuilder() - .setIntentId(rendezvous.toIntentId()) - .setUserId(relationship.publicKeyBytes.toSolanaAccount()) - .apply { setSignature(sign(relationship)) }.build() - - return try { - networkOracle.managedRequest(identityApi.loginToThirdParty(request)) - .map { response -> - when (val result = response.result) { - LoginToThirdPartyAppResponse.Result.OK -> Result.success(Unit) - LoginToThirdPartyAppResponse.Result.REQUEST_NOT_FOUND, - LoginToThirdPartyAppResponse.Result.PAYMENT_REQUIRED, - LoginToThirdPartyAppResponse.Result.LOGIN_NOT_SUPPORTED, - LoginToThirdPartyAppResponse.Result.DIFFERENT_LOGIN_EXISTS, - LoginToThirdPartyAppResponse.Result.INVALID_ACCOUNT -> { - val error = Throwable("Error: ${result.name}") - ErrorUtils.handleError(error) - Result.failure(error) - } - - LoginToThirdPartyAppResponse.Result.UNRECOGNIZED -> { - val error = Throwable("Error: loginToThirdParty Unrecognized request.") - ErrorUtils.handleError(error) - Result.failure(error) - } - - else -> { - val error = Throwable("Error: loginToThirdParty Unknown") - ErrorUtils.handleError(error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - ErrorUtils.handleError(e) - Result.failure(e) - } - } - - suspend fun fetchTwitterUserByUsername(owner: KeyPair, username: String): Result { - val request = GetTwitterUserRequest.newBuilder() - .setRequestor(owner.publicKeyBytes.toSolanaAccount()) - .setUsername(username) - .build() - - return try { - networkOracle.managedRequest(identityApi.fetchTwitterUser(request)) - .map { response -> - when (response.result) { - IdentityService.GetTwitterUserResponse.Result.OK -> { - val user = TwitterUser.invoke(response.twitterUser) - if (user == null) { - val error = - Throwable("Error: failed to parse twitter user.") - ErrorUtils.handleError(error) - Result.failure(error) - } else { - Result.success(user) - } - } - - IdentityService.GetTwitterUserResponse.Result.NOT_FOUND -> { - val error = Throwable("Error: user $username not found.") - Result.failure(error) - } - - IdentityService.GetTwitterUserResponse.Result.UNRECOGNIZED -> { - val error = Throwable("Error: fetchTwitterUser Unrecognized request.") - ErrorUtils.handleError(error) - Result.failure(error) - } - - else -> { - val error = Throwable("Error: fetchTwitterUser Unknown") - ErrorUtils.handleError(error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - e.printStackTrace() - ErrorUtils.handleError(e) - Result.failure(e) - } - } - - suspend fun fetchTwitterUserByAddress(owner: KeyPair, address: com.getcode.solana.keys.PublicKey): Result { - val request = GetTwitterUserRequest.newBuilder() - .setRequestor(owner.publicKeyBytes.toSolanaAccount()) - .setTipAddress(address.byteArray.toSolanaAccount()) - .build() - - return try { - Timber.d("fetchTwitterUserByAddress") - networkOracle.managedRequest(identityApi.fetchTwitterUser(request)) - .map { response -> - when (response.result) { - IdentityService.GetTwitterUserResponse.Result.OK -> { - val user = TwitterUser.invoke(response.twitterUser) - if (user == null) { - val error = TwitterUserFetchError.FailedToParse() - ErrorUtils.handleError(error) - Result.failure(error) - } else { - Result.success(user) - } - } - - IdentityService.GetTwitterUserResponse.Result.NOT_FOUND -> { - val error = TwitterUserFetchError.NotFound() - Result.failure(error) - } - - IdentityService.GetTwitterUserResponse.Result.UNRECOGNIZED -> { - val error = TwitterUserFetchError.UnrecognizedRequest() - ErrorUtils.handleError(error) - Result.failure(error) - } - - else -> { - val error = TwitterUserFetchError.Unknown() - ErrorUtils.handleError(error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - ErrorUtils.handleError(e) - Result.failure(e) - } - } -} - -sealed class TwitterUserFetchError : Exception() { - class Unknown: TwitterUserFetchError() - class UnrecognizedRequest: TwitterUserFetchError() - class NotFound: TwitterUserFetchError() - class FailedToParse: TwitterUserFetchError() -} - -sealed interface LinkAccountResult { - data object Success: LinkAccountResult - sealed interface Error: LinkAccountResult { - data object InvalidCode : Error - data object RateLimit: Error - data object Unrecognized : Error - data object Other: Error - } -} - -sealed interface UnlinkAccountResult { - data object Success: UnlinkAccountResult - data object Error: UnlinkAccountResult -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/repository/MessagingRepository.kt b/services/code/src/main/java/com/getcode/network/repository/MessagingRepository.kt deleted file mode 100644 index 04ff5c561..000000000 --- a/services/code/src/main/java/com/getcode/network/repository/MessagingRepository.kt +++ /dev/null @@ -1,315 +0,0 @@ -package com.getcode.network.repository - -import com.codeinc.gen.common.v1.Model -import com.codeinc.gen.messaging.v1.MessagingService -import com.codeinc.gen.messaging.v1.MessagingService.ClientRejectedLogin -import com.codeinc.gen.messaging.v1.MessagingService.CodeScanned -import com.codeinc.gen.messaging.v1.MessagingService.PollMessagesRequest -import com.codeinc.gen.messaging.v1.MessagingService.RendezvousKey -import com.codeinc.gen.transaction.v2.TransactionService -import com.getcode.ed25519.Ed25519 -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.Domain -import com.getcode.model.Fiat -import com.getcode.model.PaymentRequest -import com.getcode.model.StreamMessage -import com.getcode.model.toPublicKey -import com.getcode.network.api.MessagingApi -import com.getcode.services.network.core.INFINITE_STREAM_TIMEOUT -import com.getcode.services.network.core.NetworkOracle -import com.getcode.utils.ErrorUtils -import com.getcode.utils.getPublicKeyBase58 -import com.getcode.utils.hexEncodedString -import com.google.protobuf.ByteString -import com.google.protobuf.Timestamp -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.reactive.asFlow -import timber.log.Timber -import java.io.ByteArrayOutputStream -import javax.inject.Inject - -private const val TAG = "MessagingRepository" - -class MessagingRepository @Inject constructor( - private val messagingApi: MessagingApi, - private val networkOracle: NetworkOracle, -) { - - fun openMessageStream( - rendezvousKeyPair: KeyPair, - ): Flowable { - Timber.i("openMessageStream") - - val request = MessagingService.OpenMessageStreamRequest.newBuilder() - .setRendezvousKey( - RendezvousKey.newBuilder().setValue( - ByteString.copyFrom(rendezvousKeyPair.publicKeyBytes) - ) - ) - .build() - - return messagingApi.openMessageStream(request) - .let { networkOracle.managedRequest(it, INFINITE_STREAM_TIMEOUT) } - .map { - Timber.d("message stream response received") - it.messagesList - .filter { message -> - message.kindCase == MessagingService.Message.KindCase.REQUEST_TO_GRAB_BILL - } - } - .doOnNext { messagesList -> - if (messagesList.isEmpty()) { - return@doOnNext - } - ackMessages(rendezvousKeyPair, messagesList.map { it.id }) - .subscribe({ Timber.d("acked") }, ErrorUtils::handleError) - } - .filter { it.isNotEmpty() } - .map { messagesList -> - messagesList.map { message -> - val account = - message.requestToGrabBill.requestorAccount.value.toByteArray().toPublicKey() - val signature = - com.getcode.solana.keys.Signature( - message.sendMessageRequestSignature.value.toByteArray().toList() - ) - PaymentRequest(account, signature) - }.first() - } - .retry(10L) { - it.printStackTrace() - true - } - .subscribeOn(Schedulers.computation()) - } - - private fun ackMessages( - rendezvousKeyPair: KeyPair, - messageIds: List - ): Completable { - val request = MessagingService.AckMessagesRequest.newBuilder() - .addAllMessageIds(messageIds) - .setRendezvousKey( - RendezvousKey.newBuilder().setValue( - ByteString.copyFrom(rendezvousKeyPair.publicKeyBytes) - ) - ) - .build() - - return networkOracle.managedRequest(messagingApi.ackMessages(request)) - .flatMapCompletable { - if (it.result == MessagingService.AckMesssagesResponse.Result.OK) { - Timber.i("ackMessages: Result.OK: $messageIds") - Completable.complete() - } else { - Completable.error(RuntimeException("Failed to ack message with ids: $messageIds")) - } - } - } - - fun verifyRequestToGrabBill( - destination: com.getcode.solana.keys.PublicKey, - rendezvousKey: KeyPair, - signature: com.getcode.solana.keys.Signature - ): Boolean { - val messageData = sendRequestToGrabBill(destination = destination).build().toByteArray() - return rendezvousKey.verify(signature.byteArray, messageData) - } - - suspend fun sendRequestToLogin( - domain: Domain, - verifier: KeyPair, - rendezvous: KeyPair, - ): Result { - val message = requestToLogin(domain, verifier, rendezvous) - return sendRendezvousMessage(message, rendezvous) - } - - fun sendRequestToGrabBill( - destination: ByteArray, - rendezvousKeyPair: KeyPair, - ): Flowable { - val requestor = destination.toSolanaAccount() - val paymentRequest = MessagingService.RequestToGrabBill.newBuilder() - .setRequestorAccount(requestor) - val message = MessagingService.Message.newBuilder() - .setRequestToGrabBill(paymentRequest) - - return sendRendezvousMessageFlowable(message, rendezvousKeyPair) - .doOnEach { - Timber.i("sendRequestForPayment: result: ${it.value?.result}") - } - } - - suspend fun sendRequestToReceiveBill( - destination: com.getcode.solana.keys.PublicKey, - fiat: Fiat, - rendezvous: KeyPair - ): Result { - val message = MessagingService.Message.newBuilder() - .setRequestToReceiveBill( - MessagingService.RequestToReceiveBill.newBuilder() - .setRequestorAccount(destination.byteArray.toSolanaAccount()) - .setPartial( - TransactionService.ExchangeDataWithoutRate.newBuilder() - .setCurrency(fiat.currency.name) - .setNativeAmount(fiat.amount) - ) - .build() - ) - - return sendRendezvousMessage(message, rendezvous) - } - - fun fetchMessages(rendezvous: KeyPair): Result> { - val request = PollMessagesRequest.newBuilder() - .setRendezvousKey( - RendezvousKey.newBuilder() - .setValue(ByteString.copyFrom(rendezvous.publicKeyBytes)) - ).apply { setSignature(sign(rendezvous)) } - .build() - - return networkOracle.managedRequest(messagingApi.pollMessages(request)) - .observeOn(Schedulers.io()) - .map { response -> - Timber.d("response=${response.messagesList}") - response.messagesList.mapNotNull { m -> StreamMessage.getInstance(m) } - }.firstOrError().blockingGet().runCatching { this } - } - - suspend fun codeScanned(rendezvous: KeyPair): Result { - val message = MessagingService.Message.newBuilder() - .setCodeScanned( - CodeScanned.newBuilder() - .setTimestamp( - Timestamp.newBuilder().setSeconds(System.currentTimeMillis() / 1_000) - ) - ) - - return sendRendezvousMessage(message, rendezvous) - } - - suspend fun rejectPayment(rendezvous: KeyPair): Result { - val rejection = MessagingService.ClientRejectedPayment.newBuilder() - .setIntentId(com.getcode.solana.keys.PublicKey.fromBase58(rendezvous.getPublicKeyBase58()).toIntentId()) - .build() - - val message = MessagingService.Message.newBuilder() - .setClientRejectedPayment(rejection) - - return sendRendezvousMessage(message, rendezvous) - } - - suspend fun rejectLogin(rendezvous: KeyPair): Result { - val message = MessagingService.Message - .newBuilder() - .setClientRejectedLogin( - ClientRejectedLogin.newBuilder() - .setTimestamp( - Timestamp.newBuilder().setSeconds(System.currentTimeMillis() / 1_000) - ) - ) - - return sendRendezvousMessage(message, rendezvous) - } - - private fun sendRequestToGrabBill(destination: com.getcode.solana.keys.PublicKey): MessagingService.Message.Builder { - return MessagingService.Message - .newBuilder() - .setRequestToGrabBill( - MessagingService.RequestToGrabBill - .newBuilder() - .setRequestorAccount(destination.bytes.toSolanaAccount()) - ) - } - - private fun requestToLogin( - domain: Domain, - verifier: KeyPair, - rendezvous: KeyPair - ): MessagingService.Message.Builder { - return MessagingService.Message - .newBuilder() - .setRequestToLogin( - MessagingService.RequestToLogin - .newBuilder() - .setDomain( - Model.Domain.newBuilder() - .setValue(domain.relationshipHost) - ) - .setRendezvousKey( - RendezvousKey.newBuilder() - .setValue(ByteString.copyFrom(rendezvous.publicKeyBytes)) - ).setVerifier(verifier.publicKeyBytes.toSolanaAccount()) - .let { - val bos = ByteArrayOutputStream() - it.buildPartial().writeTo(bos) - it.setSignature(Ed25519.sign(bos.toByteArray(), rendezvous).toSignature()) - } - ) - } - - private suspend fun sendRendezvousMessage( - message: MessagingService.Message.Builder, - rendezvous: KeyPair - ): Result { - val signature = ByteArrayOutputStream().let { - message.buildPartial().writeTo(it) - val signed = Ed25519.sign(it.toByteArray(), rendezvous) - Model.Signature.newBuilder().setValue(ByteString.copyFrom(signed)) - } - - val request = MessagingService.SendMessageRequest.newBuilder() - .setMessage(message) - .setRendezvousKey( - RendezvousKey.newBuilder().setValue( - ByteString.copyFrom(rendezvous.publicKeyBytes) - ) - ) - .setSignature(signature) - .build() - - return runCatching { - messagingApi.sendMessage(request) - .let { networkOracle.managedRequest(it) } - .asFlow() - .firstOrNull() ?: throw IllegalArgumentException() - }.onSuccess { - Timber.i( - "message sent: ${ - it.messageId.value.toList().hexEncodedString() - }: result: ${it.result}" - ) - }.onFailure { - ErrorUtils.handleError(it) - Timber.e(t = it, message = "Failed to send rendezvous message.") - } - } - - private fun sendRendezvousMessageFlowable( - message: MessagingService.Message.Builder, - rendezvous: KeyPair - ): Flowable { - val signature = ByteArrayOutputStream().let { - message.buildPartial().writeTo(it) - val signed = Ed25519.sign(it.toByteArray(), rendezvous) - Model.Signature.newBuilder().setValue(ByteString.copyFrom(signed)) - } - - val request = MessagingService.SendMessageRequest.newBuilder() - .setMessage(message) - .setRendezvousKey( - RendezvousKey.newBuilder().setValue( - ByteString.copyFrom(rendezvous.publicKeyBytes) - ) - ) - .setSignature(signature) - .build() - - return messagingApi.sendMessage(request) - .let { networkOracle.managedRequest(it) } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/repository/ObservableVariable.kt b/services/code/src/main/java/com/getcode/network/repository/ObservableVariable.kt deleted file mode 100644 index 42714577f..000000000 --- a/services/code/src/main/java/com/getcode/network/repository/ObservableVariable.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.getcode.network.repository - -import io.reactivex.rxjava3.subjects.BehaviorSubject - -class ObservableVariable(private val defaultValue: T) { - var value: T = defaultValue - set(value) { - field = value - observable.onNext(value) - } - val observable = BehaviorSubject.createDefault(value) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/repository/PaymentRepository.kt b/services/code/src/main/java/com/getcode/network/repository/PaymentRepository.kt deleted file mode 100644 index 8568b4a9b..000000000 --- a/services/code/src/main/java/com/getcode/network/repository/PaymentRepository.kt +++ /dev/null @@ -1,297 +0,0 @@ -package com.getcode.network.repository - -import android.annotation.SuppressLint -import com.getcode.analytics.CodeAnalyticsService -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.manager.SessionManager -import com.getcode.services.model.CodePayload -import com.getcode.model.ID -import com.getcode.model.Kin -import com.getcode.model.KinAmount -import com.getcode.model.LoginRequest -import com.getcode.model.SocialUser -import com.getcode.model.fromFiatAmount -import com.getcode.model.generate -import com.getcode.model.intents.PrivateTransferMetadata -import com.getcode.model.toPublicKey -import com.getcode.network.BalanceController -import com.getcode.network.client.Client -import com.getcode.network.client.establishRelationshipSingle -import com.getcode.network.client.fetchLimits -import com.getcode.network.client.transferWithResult -import com.getcode.network.exchange.Exchange -import com.getcode.solana.keys.PublicKey -import com.getcode.utils.ErrorUtils -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withContext -import timber.log.Timber -import javax.inject.Inject -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException - - -class PaymentRepository @Inject constructor( - private val exchange: Exchange, - private val messagingRepository: MessagingRepository, - private val client: Client, - private val analytics: CodeAnalyticsService, - private val balanceController: BalanceController, -) : CoroutineScope by CoroutineScope(Dispatchers.IO) { - - fun attemptLogin(payload: CodePayload): Pair? { - return runCatching { - // 1. Fetch message metadata for this payload to get the - // domain for which we'll need to establish a relationship - val loginAttempt = messagingRepository - .fetchMessages(payload.rendezvous) - .getOrNull() - ?.takeIf { it.isNotEmpty() && it.first().loginRequest != null } - ?.firstOrNull()?.loginRequest - ?: throw PaymentError.MessageForRendezvousNotFound() - - codeScanned(payload.rendezvous) - return payload to loginAttempt - }.onFailure { ErrorUtils.handleError(it) }.getOrNull() - } - - suspend fun attemptRequest(payload: CodePayload): Pair? { - val fiat = payload.fiat - if (fiat == null) { - Timber.d("payload does not contain Fiat value") - return null - } - - exchange.fetchRatesIfNeeded() - val rate = exchange.rateFor(fiat.currency) - if (rate == null) { - Timber.d("Unable to determine rate") - return null - } - - Timber.d("Rate for ${rate.currency.name}: ${rate.fx}") - Timber.d("fiat value = ${fiat.amount}") - - val amount = KinAmount.fromFiatAmount(fiat.amount, rate.fx, fiat.currency) - - Timber.d("amount=${amount.fiat}, ${amount.kin}, ${amount.rate}") - - codeScanned(payload.rendezvous) - - return amount to payload - } - - private fun codeScanned(rendezvousKey: KeyPair) = launch { - messagingRepository.codeScanned(rendezvousKey) - .onSuccess { - Timber.d("code scanned message sent successfully") - }.onFailure { - Timber.e(t = it, message = "code scanned message sent unsuccessfully") - } - } - - @SuppressLint("CheckResult") - suspend fun completePayment(amount: KinAmount, rendezvousKey: KeyPair) { - // 1. ensure we have exchange rates and compute the fees for this transaction - withContext(Dispatchers.IO) { - exchange.fetchRatesIfNeeded() - } - - var paymentAmount = amount - return suspendCancellableCoroutine { cont -> - runCatching { - val rateUsd = exchange.rateForUsd() ?: throw PaymentError.NoExchangeData() - - val fee = KinAmount.fromFiatAmount(fiat = 0.01, rate = rateUsd) - Timber.d("Computed fee for transaction=${fee.kin}") - - - // 2. Between the time the kin value was computed previously and - // now, the exchange rates might have changed. Let's recompute the - // Kin value from the fiat value we had before but using a more - // current exchange rate - val newRate = exchange.rateFor(amount.rate.currency) - ?: throw PaymentError.ExchangeForCurrencyNotFound() - paymentAmount = KinAmount.fromFiatAmount( - fiat = amount.fiat, - rate = newRate - ) - - analytics.recomputed(amount.rate.fx, newRate.fx) - Timber.d("In: ${amount.rate.fx}, Out:${newRate.fx}") - - // 3. Fetch message metadata for this payload that - // will tell us where to send the funds. - val messages = messagingRepository - .fetchMessages(rendezvousKey) - .getOrNull() - ?.takeIf { it.isNotEmpty() && it.first().receiveRequest != null } - ?: throw PaymentError.MessageForRendezvousNotFound() - - val message = messages.first() - val receiveRequest = message.receiveRequest!! - - val organizer = - SessionManager.getOrganizer() ?: throw PaymentError.OrganizerNotFound() - - // 4. Establish a relationship if a domain is provided. If a verifier - // is present that means the domain has been verified by the server. - val domain = receiveRequest.domain - if (domain != null) { - if ( - receiveRequest.verifier != null && - organizer.relationshipFor(domain) == null - ) { - client.establishRelationshipSingle(organizer, domain).blockingGet() - } - } - - // 5. Complete the transfer. - val transferResult = client.transferWithResult( - amount = paymentAmount.copy(kin = paymentAmount.kin.toKinTruncating()), - fee = fee.kin, - additionalFees = receiveRequest.additionalFees, - organizer = organizer, - rendezvousKey = rendezvousKey.publicKeyBytes.toPublicKey(), - destination = receiveRequest.account, - isWithdrawal = true - ) - - if (transferResult.isSuccess) { - Completable.concatArray( - balanceController.fetchBalance(), - client.fetchLimits(isForce = true) - ).observeOn(Schedulers.io()).doOnComplete { - analytics.transfer( - amount = paymentAmount, - successful = true, - ) - cont.resume(Unit) - }.subscribe() - } else { - // pass exception down to onFailure for isolated handling - throw transferResult.exceptionOrNull() - ?: Throwable("Unable to complete payment") - } - }.onFailure { error -> - analytics.transfer( - amount = paymentAmount, - successful = false - ) - ErrorUtils.handleError(error) - cont.resumeWithException(error) - } - } - } - - suspend fun rejectPayment(payload: CodePayload) { - messagingRepository.rejectPayment(payload.rendezvous) - } - - suspend fun completeTipPayment(socialUser: SocialUser, amount: KinAmount) { - return suspendCancellableCoroutine { cont -> - val organizer = SessionManager.getOrganizer() ?: throw PaymentError.OrganizerNotFound() - - // Generally, we would use the rendezvous key that - // was generated from the scan code payload, however, - // tip codes are inherently deterministic and won't - // change so we need a unique rendezvous for every tx. - val rendezvous = PublicKey.generate() - - runCatching { - val transferResult = client.transferWithResult( - amount = amount, - organizer = organizer, - fee = Kin.fromKin(0), - additionalFees = emptyList(), - rendezvousKey = rendezvous, - destination = socialUser.tipAddress, - isWithdrawal = true, - metadata = PrivateTransferMetadata.Tip(socialUser), - ) - - if (transferResult.isSuccess) { - Completable.concatArray( - balanceController.fetchBalance(), - client.fetchLimits(isForce = true) - ).observeOn(Schedulers.io()).doOnComplete { - analytics.transferForTip(amount = amount, successful = true) - cont.resume(Unit) - }.subscribe() - } else { - // pass exception down to onFailure for isolated handling - throw transferResult.exceptionOrNull() - ?: Throwable("Unable to complete payment") - } - }.onFailure { error -> - ErrorUtils.handleError(error) - cont.resumeWithException(error) - } - } - } - - suspend fun payForFriendship(user: SocialUser, amount: KinAmount): ID { - return suspendCancellableCoroutine { cont -> - val organizer = SessionManager.getOrganizer() ?: throw PaymentError.OrganizerNotFound() - - // Generally, we would use the rendezvous key that - // was generated from the scan code payload, however, - // tip codes are inherently deterministic and won't - // change so we need a unique rendezvous for every tx. - val rendezvous = PublicKey.generate() - - runCatching { - val transferResult = client.transferWithResult( - amount = amount, - organizer = organizer, - fee = Kin.fromKin(0), - additionalFees = emptyList(), - rendezvousKey = rendezvous, - destination = user.tipAddress, - isWithdrawal = true, - metadata = PrivateTransferMetadata.Chat(user), - ) - - if (transferResult.isSuccess) { - Completable.concatArray( - balanceController.fetchBalance(), - client.fetchLimits(isForce = true) - ).observeOn(Schedulers.io()).doOnComplete { -// analytics.transferForTip(amount = amount, successful = true) - cont.resume(transferResult.getOrNull().orEmpty()) - }.subscribe() - } else { - // pass exception down to onFailure for isolated handling - throw transferResult.exceptionOrNull() - ?: Throwable("Unable to complete payment") - } - }.onFailure { error -> - ErrorUtils.handleError(error) - cont.resumeWithException(error) - } - } - } -} - -sealed interface PaymentError { - val message: String? - - data class NoExchangeData(override val message: String? = "No exchange data") : PaymentError, - Throwable(message) - - data class InvalidPayload(override val message: String? = "invalid payload") : PaymentError, - Throwable(message) - - data class OrganizerNotFound(override val message: String? = "Organizer not found") : - PaymentError, Throwable(message) - - data class ExchangeForCurrencyNotFound(override val message: String? = "exchange for currency not found") : - PaymentError, Throwable(message) - - data class MessageForRendezvousNotFound(override val message: String? = "message for rendezvous not found") : - PaymentError, Throwable(message) -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/repository/PhoneRepository.kt b/services/code/src/main/java/com/getcode/network/repository/PhoneRepository.kt deleted file mode 100644 index 3fca0be72..000000000 --- a/services/code/src/main/java/com/getcode/network/repository/PhoneRepository.kt +++ /dev/null @@ -1,163 +0,0 @@ -package com.getcode.network.repository - -import com.codeinc.gen.phone.v1.PhoneVerificationService -import com.getcode.db.CodeAppDatabase -import com.getcode.ed25519.Ed25519 -import com.getcode.network.api.PhoneApi -import com.getcode.network.integrity.DeviceCheck -import com.getcode.network.integrity.toDeviceToken -import com.getcode.services.network.core.NetworkOracle -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Single -import kotlinx.coroutines.flow.MutableStateFlow -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class PhoneRepository @Inject constructor( - private val phoneApi: PhoneApi, - private val networkOracle: NetworkOracle -) { - - var phoneNumber: String = "" - var phoneLinked: MutableStateFlow = MutableStateFlow(false) - - data class GetAssociatedPhoneNumberResponse( - val isSuccess: Boolean, - val isLinked: Boolean, - val isUnlocked: Boolean, - val phoneNumber: String - ) - - fun sendVerificationCode( - phoneValue: String - ): Flowable { - if (isMock()) return Single.just(OtpVerificationResult.Success).toFlowable() - - return DeviceCheck.integrityResponseFlowable() - .flatMap { tokenResult -> - val request = - PhoneVerificationService.SendVerificationCodeRequest.newBuilder() - .setPhoneNumber(phoneValue.toPhoneNumber()) - .apply { - if (tokenResult.token != null) { - setDeviceToken(tokenResult.token.toDeviceToken()) - } - }.build() - - - phoneApi.sendVerificationCode(request) - .map { it.result } - .let { networkOracle.managedRequest(it) } - .map { ret -> - when (ret) { - PhoneVerificationService.SendVerificationCodeResponse.Result.OK -> OtpVerificationResult.Success - PhoneVerificationService.SendVerificationCodeResponse.Result.NOT_INVITED -> OtpVerificationResult.Error.NotInvited - PhoneVerificationService.SendVerificationCodeResponse.Result.RATE_LIMITED -> OtpVerificationResult.Error.RateLimited - PhoneVerificationService.SendVerificationCodeResponse.Result.INVALID_PHONE_NUMBER -> OtpVerificationResult.Error.InvalidPhoneNumber - PhoneVerificationService.SendVerificationCodeResponse.Result.UNSUPPORTED_PHONE_TYPE -> OtpVerificationResult.Error.UnsupportedPhoneType - PhoneVerificationService.SendVerificationCodeResponse.Result.UNSUPPORTED_COUNTRY -> OtpVerificationResult.Error.UnsupportedCountry - PhoneVerificationService.SendVerificationCodeResponse.Result.UNSUPPORTED_DEVICE -> OtpVerificationResult.Error.UnsupportedDevice - PhoneVerificationService.SendVerificationCodeResponse.Result.UNRECOGNIZED -> OtpVerificationResult.Error.Unrecognized - else -> OtpVerificationResult.Error.Other - } - } - } - } - - fun checkVerificationCode( - phoneValue: String, - otpInput: String - ): Flowable { - if (isMock()) return Flowable.just(CheckVerificationResult.Success) - - val request = PhoneVerificationService.CheckVerificationCodeRequest.newBuilder() - .setPhoneNumber(phoneValue.toPhoneNumber()) - .setCode(PhoneVerificationService.VerificationCode.newBuilder().setValue(otpInput)) - .build() - - return phoneApi.checkVerificationCode(request) - .map { it.result } - .let { networkOracle.managedRequest(it) } - .map { result -> - when (result) { - PhoneVerificationService.CheckVerificationCodeResponse.Result.OK -> CheckVerificationResult.Success - PhoneVerificationService.CheckVerificationCodeResponse.Result.INVALID_CODE -> CheckVerificationResult.Error.InvalidCode - PhoneVerificationService.CheckVerificationCodeResponse.Result.NO_VERIFICATION -> CheckVerificationResult.Error.NoVerification - PhoneVerificationService.CheckVerificationCodeResponse.Result.UNRECOGNIZED -> CheckVerificationResult.Error.Unrecognized - else -> CheckVerificationResult.Error.Other - } - } - } - - fun fetchAssociatedPhoneNumber( - keyPair: Ed25519.KeyPair - ): Flowable { - if (isMock()) { - return Flowable.just( - GetAssociatedPhoneNumberResponse(true, true, false, "+12223334455") - ) - } - - val request = PhoneVerificationService.GetAssociatedPhoneNumberRequest.newBuilder() - .setOwnerAccountId( - keyPair.publicKeyBytes.toSolanaAccount() - ).apply { setSignature(sign(keyPair)) } - .build() - - return phoneApi.getAssociatedPhoneNumber(request) - .let { networkOracle.managedRequest(it) } - .map { phone -> - val isSuccess = - phone.result == PhoneVerificationService.GetAssociatedPhoneNumberResponse.Result.OK - val isUnlocked = - phone.result == PhoneVerificationService.GetAssociatedPhoneNumberResponse.Result.UNLOCKED_TIMELOCK_ACCOUNT - - GetAssociatedPhoneNumberResponse( - isSuccess, - phone.isLinked, - isUnlocked, - phone.phoneNumber.value - ) - } - .flatMap { response -> CodeAppDatabase.isInit.map { response } } - .doOnNext { phone -> - phoneNumber = phone.phoneNumber - phoneLinked.value = phone.isLinked - } - //.onErrorResumeNext { getAssociatedPhoneNumberLocal().map { Pair(true, it) } } - } - - fun getAssociatedPhoneNumberLocal(): Flowable { - return Flowable.zip( - Flowable.just(phoneLinked), - Flowable.just(phoneNumber) - ) { v1, v2 -> - GetAssociatedPhoneNumberResponse(true, v1.value, false, v2) - } - } -} - -sealed interface OtpVerificationResult { - data object Success: OtpVerificationResult - sealed interface Error: OtpVerificationResult { - data object InvalidPhoneNumber : Error - data object NotInvited: Error - data object RateLimited: Error - data object UnsupportedPhoneType : Error - data object UnsupportedCountry : Error - data object UnsupportedDevice : Error - data object Unrecognized : Error - data object Other: Error - } -} - -sealed interface CheckVerificationResult { - data object Success: CheckVerificationResult - sealed interface Error: CheckVerificationResult { - data object InvalidCode : Error - data object NoVerification : Error - data object Unrecognized : Error - data object Other: Error - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/repository/PrefRepository.kt b/services/code/src/main/java/com/getcode/network/repository/PrefRepository.kt deleted file mode 100644 index 39addf2ac..000000000 --- a/services/code/src/main/java/com/getcode/network/repository/PrefRepository.kt +++ /dev/null @@ -1,153 +0,0 @@ -package com.getcode.network.repository - -import com.getcode.db.CodeAppDatabase -import com.getcode.services.model.PrefBool -import com.getcode.services.model.PrefInt -import com.getcode.services.model.PrefString -import com.getcode.services.model.PrefsBool -import com.getcode.services.model.PrefsInt -import com.getcode.services.model.PrefsString -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import kotlinx.coroutines.reactive.asFlow -import timber.log.Timber -import javax.inject.Inject - - -class PrefRepository @Inject constructor(): CoroutineScope by CoroutineScope(Dispatchers.IO) { - - suspend fun get(key: PrefsString, default: String): String { - return observeOrDefault(key, default).firstOrNull() ?: default - } - - suspend fun get(key: PrefsBool, default: Boolean): Boolean { - return observeOrDefault(key, default).firstOrNull() ?: default - } - - suspend fun get(key: PrefsInt, default: Long): Long { - return observeOrDefault(key, default).firstOrNull() ?: default - } - - - fun getFlowable(key: PrefsString): Flowable { - val db = CodeAppDatabase.getInstance() ?: return Flowable.empty() - return db.prefStringDao().get(key.value) - .subscribeOn(Schedulers.computation()) - .map { it.value } - .distinctUntilChanged() - } - - fun getFlowable(key: PrefsBool): Flowable { - val db = CodeAppDatabase.getInstance() ?: return Flowable.empty() - return db.prefBoolDao().get(key.value) - .subscribeOn(Schedulers.computation()) - .map { it.value } - .distinctUntilChanged() - } - - fun observeOrDefault(key: PrefsBool, default: Boolean): Flow { - return CodeAppDatabase.isInit - .asFlow() - .map { CodeAppDatabase.getInstance() } - .flatMapLatest { - it ?: return@flatMapLatest flowOf(default) - it.prefBoolDao().observe(key.value).map { it?.value ?: default } - } - .flowOn(Dispatchers.IO) - - } - - fun observeOrDefault(key: PrefsString, default: String): Flow { - return CodeAppDatabase.isInit - .asFlow() - .map { CodeAppDatabase.getInstance() } - .flatMapLatest { - it ?: return@flatMapLatest flowOf(default).also { Timber.e("observe string ; DB not available") } - it.prefStringDao().observe(key.value) - .map { it?.value ?: default } - } - .flowOn(Dispatchers.IO) - } - - fun observeOrDefault(key: PrefsInt, default: Long): Flow { - return CodeAppDatabase.isInit - .asFlow() - .map { CodeAppDatabase.getInstance() } - .flatMapLatest { - it ?: return@flatMapLatest flowOf(default).also { Timber.e("observe long ; DB not available") } - it.prefIntDao().observe(key.value) - .map { it?.value ?: default } - } - .flowOn(Dispatchers.IO) - } - - fun getFlowable(key: String): Flowable { - val db = CodeAppDatabase.getInstance() ?: return Flowable.empty() - return db.prefIntDao().get(key) - .subscribeOn(Schedulers.computation()) - .map { it.value } - .distinctUntilChanged() - } - - fun getFirstOrDefault(key: PrefsString, default: String): Single { - val db = CodeAppDatabase.getInstance() ?: return Single.just(default) - return db.prefStringDao().getMaybe(key.value) - .subscribeOn(Schedulers.computation()) - .map { it.value } - .defaultIfEmpty(default) - } - - fun getFirstOrDefault(key: PrefsBool, default: Boolean): Single { - val db = CodeAppDatabase.getInstance() ?: return Single.just(default) - return db.prefBoolDao().getMaybe(key.value) - .subscribeOn(Schedulers.computation()) - .map { it.value } - .defaultIfEmpty(default) - } - - fun getFirstOrDefault(key: String, default: Int): Single { - val db = CodeAppDatabase.getInstance() ?: return Single.just(default.toLong()) - return db.prefIntDao().getMaybe(key) - .subscribeOn(Schedulers.computation()) - .map { it.value } - .defaultIfEmpty(default.toLong()) - } - - suspend fun set(vararg list: Pair) { - list.forEach { pair -> - CodeAppDatabase.getInstance()?.prefStringDao()?.insert(PrefString(pair.first.value, pair.second)) - } - } - - fun set(key: PrefsString, value: String) = launch { - set(key to value) - } - - fun set(key: String, value: Int) = set(key, value.toLong()) - - fun set(key: String, value: Long) { - launch { - CodeAppDatabase.getInstance()?.prefIntDao()?.insert(PrefInt(key, value)) - } - } - - fun set(key: PrefsBool, value: Boolean) { - launch { - runCatching { - val db = CodeAppDatabase.getInstance() ?: throw IllegalStateException("No DB") - db.prefBoolDao().insert(PrefBool(key.value, value)) - }.onFailure { Timber.d(it.message) }.onSuccess { Timber.d("saved ${key.value} => $value") } - } - } - -} diff --git a/services/code/src/main/java/com/getcode/network/repository/PushRepository.kt b/services/code/src/main/java/com/getcode/network/repository/PushRepository.kt deleted file mode 100644 index 60ca74346..000000000 --- a/services/code/src/main/java/com/getcode/network/repository/PushRepository.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.getcode.network.repository - -import com.codeinc.gen.common.v1.Model -import com.codeinc.gen.push.v1.PushService -import com.getcode.manager.SessionManager -import com.getcode.services.model.PrefsString -import com.getcode.network.api.PushApi -import com.getcode.services.network.core.NetworkOracle -import com.getcode.utils.ErrorUtils -import com.getcode.utils.decodeBase64 -import com.getcode.utils.toByteString -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.reactive.asFlow -import timber.log.Timber -import javax.inject.Inject - -class PushRepository @Inject constructor( - private val pushApi: PushApi, - private val networkOracle: NetworkOracle, - private val prefs: PrefRepository, -) { - - suspend fun updateToken(token: String, installationId: String?): Result { - Timber.i("google token $token") - val owner = SessionManager.getKeyPair() ?: return Result.failure(Throwable("No owner available")) - val containerId = prefs.get(PrefsString.KEY_DATA_CONTAINER_ID, "").takeIf { it.isNotEmpty() } - ?.decodeBase64()?.toList() ?: return Result.failure(Throwable("No container id available")) - - val request = - PushService.AddTokenRequest.newBuilder() - .setPushToken(token) - .setContainerId(Model.DataContainerId.newBuilder().setValue(containerId.toByteString())) - .setAppInstall(Model.AppInstallId.newBuilder().setValue(installationId)) - .setOwnerAccountId(owner.publicKeyBytes.toSolanaAccount()) - .setTokenType(PushService.TokenType.FCM_ANDROID) - .apply { setSignature(sign(owner)) } - .build() - - return try { - networkOracle.managedRequest(pushApi.addToken(request)) - .asFlow() - .map { response -> - when (response.result) { - PushService.AddTokenResponse.Result.OK -> Result.success(true) - PushService.AddTokenResponse.Result.INVALID_PUSH_TOKEN -> { - val error = Throwable("Error: INVALID_PUSH_TOKEN") - Result.failure(error) - } - PushService.AddTokenResponse.Result.UNRECOGNIZED -> { - val error = Throwable("Error: UNRECOGNIZED") - Result.failure(error) - } - else -> { - val error = Throwable("Error: Unknown") - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - ErrorUtils.handleError(e) - Result.failure(e) - } - } -} diff --git a/services/code/src/main/java/com/getcode/network/repository/ReceiveTransactionRepository.kt b/services/code/src/main/java/com/getcode/network/repository/ReceiveTransactionRepository.kt deleted file mode 100644 index 62532bb88..000000000 --- a/services/code/src/main/java/com/getcode/network/repository/ReceiveTransactionRepository.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.getcode.network.repository - -import com.codeinc.gen.messaging.v1.MessagingService -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.IntentMetadata -import com.getcode.model.toPublicKey -import com.getcode.network.client.Client -import com.getcode.network.client.pollIntentMetadata -import com.getcode.solana.organizer.Organizer -import com.getcode.utils.ErrorUtils -import io.reactivex.rxjava3.core.Flowable -import kotlinx.coroutines.rx3.asFlowable -import javax.inject.Inject - -class ReceiveTransactionRepository @Inject constructor( - private val messagingRepository: MessagingRepository, - private val client: Client -) { - fun start(organizer: Organizer, rendezvous: KeyPair, debug: Boolean = false): Flowable { - return messagingRepository.sendRequestToGrabBill( - destination = organizer.incomingVault.byteArray, - rendezvousKeyPair = rendezvous - ) - .flatMap { paymentRequestResponse -> - if (paymentRequestResponse.result != MessagingService.SendMessageResponse.Result.OK) { - Flowable.error(Exception("Error: ${paymentRequestResponse.result.name}")) - } else { - client.pollIntentMetadata( - owner = organizer.ownerKeyPair, - intentId = rendezvous.publicKeyBytes.toPublicKey(), - debugLogs = debug, - ).asFlowable() - } - } - .doOnError { - ErrorUtils.handleError(it) - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/repository/SendTransactionRepository.kt b/services/code/src/main/java/com/getcode/network/repository/SendTransactionRepository.kt deleted file mode 100644 index f3a31c1d1..000000000 --- a/services/code/src/main/java/com/getcode/network/repository/SendTransactionRepository.kt +++ /dev/null @@ -1,135 +0,0 @@ -package com.getcode.network.repository - -import com.getcode.analytics.CodeAnalyticsService -import com.getcode.ed25519.Ed25519 -import com.getcode.services.model.CodePayload -import com.getcode.model.IntentMetadata -import com.getcode.model.Kin -import com.getcode.model.KinAmount -import com.getcode.services.model.Kind -import com.getcode.model.toPublicKey -import com.getcode.network.client.Client -import com.getcode.network.client.pollIntentMetadata -import com.getcode.network.client.transfer -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.Organizer -import com.getcode.services.utils.nonce -import io.reactivex.rxjava3.core.Flowable -import kotlinx.coroutines.rx3.asFlowable -import javax.inject.Inject - - -class SendTransactionRepository @Inject constructor( - private val messagingRepository: MessagingRepository, - private val analytics: CodeAnalyticsService, - private val client: Client, -) { - private lateinit var amount: KinAmount - private lateinit var organizer: Organizer - private lateinit var owner: Ed25519.KeyPair - private lateinit var payload: CodePayload - private lateinit var payloadData: List - - private lateinit var rendezvousKey: Ed25519.KeyPair - private var receivingAccount: PublicKey? = null - - fun init(amount: KinAmount, organizer: Organizer, owner: Ed25519.KeyPair): List { - this.amount = amount - this.organizer = organizer - this.owner = owner - - this.payload = CodePayload( - kind = Kind.Cash, - value = amount.kin, - nonce = nonce, - ) - - this.payloadData = payload.codeData.toList() - this.rendezvousKey = payload.rendezvous - this.receivingAccount = null - - return payloadData - } - - fun startTransaction(): Flowable { - return messagingRepository.openMessageStream(rendezvousKey) - .firstOrError() - .flatMapPublisher { paymentRequest -> - // 1. Validate that destination hasn't been tampered with by - // verifying the signature matches one that has been signed - // with the rendezvous key. - - val isValid = messagingRepository.verifyRequestToGrabBill( - destination = paymentRequest.account, - rendezvousKey = rendezvousKey, - signature = paymentRequest.signature - ) - - if (!isValid) { - analytics.transfer( - amount = amount, - successful = false - ) - - Flowable.error(SendTransactionException.DestinationSignatureInvalidException()) - } else { - analytics.transfer( - amount = amount, - successful = true - ) - - // 2. Send the funds to destination - sendFundsAndPoll(organizer, paymentRequest.account) - } - } - .doOnError { - analytics.transfer( - amount = amount, - successful = false - ) - } - } - - private fun sendFundsAndPoll( - organizer: Organizer, - destination: PublicKey - ): Flowable { - if (receivingAccount == destination) { - // Ensure that we're processing one, and only one - // transaction for each instance of SendTransaction. - // Completion will be called by the first invocation - // of this function. - return Flowable.error(SendTransactionException.DuplicateTransferException()) - } - - receivingAccount = destination - - // It's possible that we have funds sitting in the incoming - // account counting towards the active balance. If we don't - // deposit the funds and the transaction size exceeds what's - // in the buckets, the send will fail. - return client.transfer( - amount = amount.copy(kin = amount.kin.toKinTruncating()), - fee = Kin.fromKin(0), - additionalFees = emptyList(), - organizer = organizer, - rendezvousKey = rendezvousKey.publicKeyBytes.toPublicKey(), - destination = destination, - isWithdrawal = false - ) - .andThen( - client.pollIntentMetadata( - owner = organizer.ownerKeyPair, - intentId = rendezvousKey.publicKeyBytes.toPublicKey() - ).asFlowable() - ) - } - - fun getAmount() = amount - fun getRendezvous() = rendezvousKey - - sealed class SendTransactionException : Exception() { - class DuplicateTransferException : SendTransactionException() - class DestinationSignatureInvalidException : SendTransactionException() - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/repository/StatusRepository.kt b/services/code/src/main/java/com/getcode/network/repository/StatusRepository.kt deleted file mode 100644 index 0fbfc40e3..000000000 --- a/services/code/src/main/java/com/getcode/network/repository/StatusRepository.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.getcode.network.repository - -import io.reactivex.rxjava3.core.Single -import okhttp3.Call -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import org.json.JSONObject - -class StatusRepository { - fun getIsUpgradeRequired(currentVersionCode: Int): Single { - val request: Request = Request.Builder() - .url("https://app.getcode.com/status") - .build() - val call: Call = OkHttpClient().newCall(request) - - return Single.create { - val response: Response = call.execute() - if (!response.isSuccessful) { - it.onError(Exception()) - return@create - } - val json = JSONObject(response.body?.string().orEmpty()) - val minimumVersion = json.getInt("minimumClientVersion") - val isUpgradeRequired = currentVersionCode < minimumVersion - it.onSuccess(isUpgradeRequired) - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/repository/TransactionRepository.kt b/services/code/src/main/java/com/getcode/network/repository/TransactionRepository.kt deleted file mode 100644 index f05733482..000000000 --- a/services/code/src/main/java/com/getcode/network/repository/TransactionRepository.kt +++ /dev/null @@ -1,801 +0,0 @@ -package com.getcode.network.repository - -import android.annotation.SuppressLint -import android.content.Context -import com.codeinc.gen.common.v1.Model -import com.codeinc.gen.transaction.v2.TransactionService -import com.codeinc.gen.transaction.v2.TransactionService.DeclareFiatOnrampPurchaseAttemptResponse -import com.codeinc.gen.transaction.v2.TransactionService.ExchangeDataWithoutRate -import com.codeinc.gen.transaction.v2.TransactionService.SubmitIntentResponse -import com.codeinc.gen.transaction.v2.TransactionService.SubmitIntentResponse.ResponseCase.ERROR -import com.codeinc.gen.transaction.v2.TransactionService.SubmitIntentResponse.ResponseCase.SERVER_PARAMETERS -import com.codeinc.gen.transaction.v2.TransactionService.SubmitIntentResponse.ResponseCase.SUCCESS -import com.getcode.services.BuildConfig -import com.getcode.crypt.MnemonicPhrase -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.* -import com.getcode.model.extensions.newInstance -import com.getcode.model.intents.ActionGroup -import com.getcode.model.intents.IntentCreateAccounts -import com.getcode.model.intents.IntentDeposit -import com.getcode.model.intents.IntentEstablishRelationship -import com.getcode.model.intents.IntentMigratePrivacy -import com.getcode.model.intents.IntentPrivateTransfer -import com.getcode.model.intents.IntentPublicTransfer -import com.getcode.model.intents.IntentReceive -import com.getcode.model.intents.IntentRemoteReceive -import com.getcode.model.intents.IntentRemoteSend -import com.getcode.model.intents.IntentType -import com.getcode.model.intents.IntentUpgradePrivacy -import com.getcode.model.intents.PrivateTransferMetadata -import com.getcode.model.intents.ServerParameter -import com.getcode.network.api.TransactionApiV2 -import com.getcode.network.integrity.DeviceCheck -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.diff -import com.getcode.solana.keys.AssociatedTokenAccount -import com.getcode.solana.keys.Mint -import com.getcode.solana.keys.base58 -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.GiftCardAccount -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Relationship -import com.getcode.utils.ErrorUtils -import com.getcode.utils.TraceType -import com.getcode.utils.bytes -import com.getcode.utils.toByteString -import com.getcode.utils.trace -import com.google.protobuf.Timestamp -import dagger.hilt.android.qualifiers.ApplicationContext -import io.grpc.stub.StreamObserver -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.subjects.SingleSubject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.reactive.asFlow -import timber.log.Timber -import java.util.UUID -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.time.Duration.Companion.seconds - -private const val TAG = "TransactionRepositoryV2" - -@Singleton -class TransactionRepository @Inject constructor( - @ApplicationContext val context: Context, - val transactionApi: TransactionApiV2, -) : CoroutineScope by CoroutineScope(Dispatchers.IO) { - - var limits: Limits? = null - private set - - val areLimitsState: Boolean - get() = limits == null || limits?.isStale == true - - private var lastSwap: Long = 0L - - var maxDeposit: Kin = Kin.fromKin(0) - - fun setMaximumDeposit(deposit: Kin) { - maxDeposit = deposit - } - - fun buyLimitFor(currencyCode: CurrencyCode): BuyLimit? { - return limits?.buyLimitFor(currencyCode) - } - - fun sendLimitFor(currencyCode: CurrencyCode): SendLimit? { - return limits?.sendLimitFor(currencyCode) - } - - fun hasAvailableTransactionLimit(amount: KinAmount): Boolean { - return (sendLimitFor(amount.rate.currency)?.nextTransaction ?: 0.0) >= amount.fiat - } - - fun hasAvailableDailyLimit(): Boolean { - return (sendLimitFor(currencyCode = CurrencyCode.USD)?.nextTransaction ?: 0.0) > 0 - } - - private fun setLimits(limits: Limits) { - trace("updating limits") - this.limits = limits - } - - fun clear() { - Timber.d("clearing transactions") - maxDeposit = Kin.fromKin(0) - } - - fun createAccounts(organizer: Organizer): Single { - if (isMock()) return Single.just( - IntentCreateAccounts( - id = com.getcode.solana.keys.PublicKey(bytes = listOf()), - actionGroup = ActionGroup(), - organizer = organizer - ) as IntentType - ) - .delay(1, TimeUnit.SECONDS) - - val createAccounts = IntentCreateAccounts.newInstance(organizer) - - return DeviceCheck.integrityResponseSingle() - .flatMap { tokenResult -> - submit(createAccounts, organizer.tray.owner.getCluster().authority.keyPair, tokenResult.token) - } - } - - fun transfer( - amount: KinAmount, - fee: Kin, - additionalFees: List, - organizer: Organizer, - rendezvousKey: com.getcode.solana.keys.PublicKey, - destination: com.getcode.solana.keys.PublicKey, - isWithdrawal: Boolean, - metadata: PrivateTransferMetadata? = null, - ): Single { - if (isMock()) return Single.just( - IntentPrivateTransfer( - id = com.getcode.solana.keys.PublicKey(bytes = listOf()), - actionGroup = ActionGroup(), - organizer = organizer, - destination = destination, - grossAmount = amount, - netAmount = amount, - fee = fee, - additionalFees = emptyList(), - resultTray = organizer.tray, - isWithdrawal = isWithdrawal, - metadata = metadata - ) as IntentType - ) - .delay(1, TimeUnit.SECONDS) - - val intent = IntentPrivateTransfer.newInstance( - rendezvousKey = rendezvousKey, - organizer = organizer, - destination = destination, - amount = amount.copy(kin = amount.kin.toKinTruncating()), - fee = fee, - additionalFees = additionalFees, - isWithdrawal = isWithdrawal, - metadata = metadata - ) - - return submit(intent = intent, owner = organizer.tray.owner.getCluster().authority.keyPair) - } - - fun receiveFromIncoming( - context: Context, - amount: Kin, - organizer: Organizer - ): Single { - val intent = IntentReceive.newInstance( - context = context, - organizer = organizer, - amount = amount.toKinTruncating() - ) - return submit(intent = intent, owner = organizer.tray.owner.getCluster().authority.keyPair) - } - - suspend fun swapIfNeeded(organizer: Organizer) { - // We need to check and see if the USDC account has a balance, - // if so, we'll initiate a swap to Kin. The nuance here is that - // the balance of the USDC account is reported as `Kin`, where the - // quarks represent the lamport balance of the account. - val info = organizer.info(AccountType.Swap) ?: return - if (info.balance.quarks <= 0) return - - val timeout = 45.seconds - - // Ensure that it's been at least `timeout` seconds since we try - // another swap if one is already in-flight. - if (System.currentTimeMillis() - lastSwap < timeout.inWholeMilliseconds) return - - lastSwap = System.currentTimeMillis() - - initiateSwap(organizer) - .onFailure { ErrorUtils.handleError(it) } - } - - fun receiveFromPrimary(amount: Kin, organizer: Organizer): Single { - val intent = IntentDeposit.newInstance( - source = AccountType.Primary, - organizer = organizer, - amount = amount.toKinTruncating() - ) - return submit(intent = intent, owner = organizer.tray.owner.getCluster().authority.keyPair) - } - - fun receiveFromRelationship( - relationship: Relationship, - organizer: Organizer - ): Single { - val intent = IntentPublicTransfer.newInstance( - source = AccountType.Relationship(relationship.domain), - organizer = organizer, - amount = KinAmount.newInstance(relationship.partialBalance, Rate.oneToOne), - destination = IntentPublicTransfer.Destination.Local(AccountType.Primary) - ) - return submit(intent, owner = organizer.tray.owner.getCluster().authority.keyPair) - } - - fun withdraw( - amount: KinAmount, - organizer: Organizer, - destination: com.getcode.solana.keys.PublicKey - ): Single { - val intent = IntentPublicTransfer.newInstance( - organizer = organizer, - amount = amount, - destination = IntentPublicTransfer.Destination.External(destination), - source = AccountType.Primary, - ) - - return submit(intent = intent, owner = organizer.tray.owner.getCluster().authority.keyPair) - } - - fun upgradePrivacy( - mnemonic: MnemonicPhrase, - upgradeableIntent: UpgradeableIntent - ): Single { - val intent = IntentUpgradePrivacy.newInstance( - mnemonic = mnemonic, - upgradeableIntent = upgradeableIntent - ) - return submit(intent, owner = mnemonic.getSolanaKeyPair()) - } - - fun sendRemotely( - amount: KinAmount, - organizer: Organizer, - rendezvousKey: com.getcode.solana.keys.PublicKey, - giftCard: GiftCardAccount - ): Single { - val intent = IntentRemoteSend.newInstance( - context = context, - rendezvousKey = rendezvousKey, - organizer = organizer, - giftCard = giftCard, - amount = amount, - ) - return submit(intent, owner = organizer.tray.owner.getCluster().authority.keyPair) - } - - fun receiveRemotely( - context: Context, - amount: Kin, - organizer: Organizer, - giftCard: GiftCardAccount, - isVoiding: Boolean - ): Single { - val intent = IntentRemoteReceive.newInstance( - context = context, - organizer = organizer, - giftCard = giftCard, - amount = amount, - isVoidingGiftCard = isVoiding - ) - return submit(intent, owner = organizer.tray.owner.getCluster().authority.keyPair) - } - - fun migrateToPrivacy( - amount: Kin, - organizer: Organizer - ): Single { - val intent = IntentMigratePrivacy.newInstance( - organizer = organizer, - amount = amount, - ) - - return submit(intent, owner = organizer.tray.owner.getCluster().authority.keyPair) - } - - private fun submit(intent: IntentType, owner: KeyPair, deviceToken: String? = null): Single { - Timber.i("Submit ${intent.javaClass.simpleName}") - val subject = SingleSubject.create() - - var serverMessageStream: StreamObserver? = null - val serverResponse = object : StreamObserver { - override fun onNext(value: SubmitIntentResponse?) { - Timber.i("Received: " + value?.responseCase?.name.orEmpty()) - - when (value?.responseCase) { - // 2. Upon successful submission of intent action the server will - // respond with parameters that we'll need to apply to the intent - // before crafting and signing the transactions. - SERVER_PARAMETERS -> { - try { - intent.apply( - value.serverParameters.serverParametersList - .map { p -> ServerParameter.newInstance(p) } - ) - - val submitSignatures = intent.requestToSubmitSignatures() - serverMessageStream?.onNext(submitSignatures) - - Timber.i( - "Received ${value.serverParameters.serverParametersList.size} parameters. Submitting signatures..." - ) - } catch (e: Exception) { - if (BuildConfig.DEBUG) { - e.printStackTrace() - } - Timber.i( - "Received ${value.serverParameters.serverParametersList.size} parameters but failed to apply them: ${e.javaClass.simpleName} ${e.message})" - ) - subject.onError(ErrorSubmitIntentException(ErrorSubmitIntent.Unknown)) - } - } - // 3. If submitted transaction signatures are valid and match - // the server, we'll receive a success for the submitted intent. - SUCCESS -> { - Timber.i("Success.") - serverMessageStream?.onCompleted() - subject.onSuccess(intent) - } - // 3. If the submitted transaction signatures don't match, the - // intent is considered failed. Something must have gone wrong - // on the transaction creation or signing on our side. - ERROR -> { - val errors = mutableListOf() - - value.error.errorDetailsList.forEach { error -> - when (error.typeCase) { - TransactionService.ErrorDetails.TypeCase.REASON_STRING -> { - errors.add("Reason: ${error.reasonString}") - } - TransactionService.ErrorDetails.TypeCase.INVALID_SIGNATURE -> { - val expected = SolanaTransaction.fromList(error.invalidSignature.expectedTransaction.value.toByteArray().toList()) - val produced = intent.transaction() - errors.addAll( - listOf( - "Action index: ${error.invalidSignature.actionId}", - "Invalid signature: ${ - com.getcode.solana.keys.Signature( - error.invalidSignature.providedSignature.value.toByteArray() - .toList() - ).base58()}", - "Transaction bytes: ${error.invalidSignature.expectedTransaction.value}", - "Transaction expected: $expected", - "Android produced: $produced" - ) - ) - - expected?.diff(produced) - } - TransactionService.ErrorDetails.TypeCase.DENIED -> { - errors.add("Denied: ${error.denied.reason}") - } - else -> Unit - } - - trace( - "Error: ${errors.joinToString("\n")}", - type = TraceType.Error - ) - } - - serverMessageStream?.onCompleted() - subject.onError( - ErrorSubmitIntentException( - ErrorSubmitIntent.invoke(value.error), - ) - ) - } - - else -> { - Timber.i("Else case. ${value?.responseCase}") - serverMessageStream?.onCompleted() - subject.onError(ErrorSubmitIntentException(ErrorSubmitIntent.Unknown)) - } - } - } - - override fun onError(t: Throwable?) { - Timber.i("onError: " + t?.message.orEmpty()) - subject.onError(ErrorSubmitIntentException(ErrorSubmitIntent.Unknown, t)) - } - - override fun onCompleted() { - Timber.i("onCompleted") - } - } - serverMessageStream = transactionApi.submitIntent(serverResponse) - - // 1. Send `submitActions` request with - // actions generated by the intent - serverMessageStream.onNext(intent.requestToSubmitActions(owner, deviceToken)) - - return subject - } - - fun establishRelationshipSingle( - organizer: Organizer, - domain: Domain, - ) : Single { - val intent = IntentEstablishRelationship.newInstance(organizer, domain) - - return submit(intent = intent, organizer.tray.owner.getCluster().authority.keyPair) - .map { intent } - .doOnSuccess { Timber.d("established relationship") } - .doOnError { Timber.e(t = it, message = "failed to establish relationship") } - } - - @SuppressLint("CheckResult") - suspend fun establishRelationship( - organizer: Organizer, - domain: Domain - ): Result { - val intent = IntentEstablishRelationship.newInstance(organizer, domain) - - return runCatching { - submit(intent = intent, organizer.tray.owner.getCluster().authority.keyPair) - .map { intent } - .toFlowable() - .asFlow() - .firstOrNull() ?: throw IllegalArgumentException() - }.onSuccess { - Timber.d("established relationship") - }.onFailure { - ErrorUtils.handleError(it) - Timber.e(t = it, message = "failed to establish relationship") - } - } - - suspend fun declareFiatPurchase(owner: KeyPair, amount: KinAmount, nonce: UUID): Result { - val request = TransactionService.DeclareFiatOnrampPurchaseAttemptRequest.newBuilder() - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .setPurchaseAmount(ExchangeDataWithoutRate.newBuilder() - .setCurrency(amount.rate.currency.name.lowercase()) - .setNativeAmount(amount.fiat) - ).setNonce( - Model.UUID.newBuilder().setValue(nonce.bytes.toByteString()) - ).apply { setSignature(sign(owner)) }.build() - - return runCatching { - transactionApi.declareFiatPurchase(request) - .firstOrNull() ?: throw IllegalArgumentException() - }.map { response -> - when (response.result) { - DeclareFiatOnrampPurchaseAttemptResponse.Result.OK -> Result.success(Unit) - DeclareFiatOnrampPurchaseAttemptResponse.Result.INVALID_OWNER -> { - val error = Throwable("Error: INVALID_OWNER") - ErrorUtils.handleError(error) - Result.failure(error) - } - DeclareFiatOnrampPurchaseAttemptResponse.Result.UNSUPPORTED_CURRENCY -> { - val error = Throwable("Error: UNSUPPORTED_CURRENCY") - ErrorUtils.handleError(error) - Result.failure(error) - } - DeclareFiatOnrampPurchaseAttemptResponse.Result.AMOUNT_EXCEEDS_MAXIMUM -> { - val error = Throwable("Error: AMOUNT_EXCEEDS_MAXIMUM") - ErrorUtils.handleError(error) - Result.failure(error) - } - DeclareFiatOnrampPurchaseAttemptResponse.Result.UNRECOGNIZED -> { - val error = Throwable("Error: UNRECOGNIZED") - ErrorUtils.handleError(error) - Result.failure(error) - } - else -> { - val error = Throwable("Error: Unknown Error") - ErrorUtils.handleError(error) - Result.failure(error) - } - } - } - } - - // TODO: potentially make this more generic in the event we introduce more airdrop types - // that can be requested for - suspend fun requestFirstKinAirdrop(owner: KeyPair): Result { - val request = TransactionService.AirdropRequest.newBuilder() - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .setAirdropType(TransactionService.AirdropType.GET_FIRST_KIN) - .apply { setSignature(sign(owner)) } - .build() - - return runCatching { - transactionApi.airdrop(request) - .toFlowable() - .asFlow() - .flowOn(Dispatchers.IO) - .map { - when (it.result) { - TransactionService.AirdropResponse.Result.OK -> { - KinAmount.fromProtoExchangeData(it.exchangeData) - } - - TransactionService.AirdropResponse.Result.ALREADY_CLAIMED -> { - throw AirdropException.AlreadyClaimedException() - } - - TransactionService.AirdropResponse.Result.UNAVAILABLE -> { - throw AirdropException.UnavailableException() - } - - else -> { - throw AirdropException.UnknownException() - } - } - }.first() - } - } - - suspend fun fetchIntentMetadata( - owner: KeyPair, - intentId: com.getcode.solana.keys.PublicKey - ): Result { - val request = TransactionService.GetIntentMetadataRequest.newBuilder() - .setIntentId(intentId.toIntentId()) - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .apply { setSignature(sign(owner)) } - .build() - - return runCatching { - transactionApi.getIntentMetadata(request) - .toFlowable() - .asFlow() - .flowOn(Dispatchers.IO) - .map { - if (it.result != TransactionService.GetIntentMetadataResponse.Result.OK) { - throw IllegalStateException() - } else { - IntentMetadata.newInstance(it.metadata) ?: throw IllegalStateException() - } - } - .firstOrNull() ?: throw IllegalStateException() - } - } - - @SuppressLint("CheckResult") - fun fetchLimits( - owner: KeyPair, - timestamp: Long - ): Flowable { - val request = TransactionService.GetLimitsRequest.newBuilder() - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .setConsumedSince(Timestamp.newBuilder().setSeconds(timestamp)) - .apply { setSignature(sign(owner)) } - .build() - - return transactionApi.getLimits(request) - .flatMap { - if (it.result != TransactionService.GetLimitsResponse.Result.OK) { - Single.error(IllegalStateException()) - } else { - Limits.newInstance( - sinceDate = timestamp, - fetchDate = System.currentTimeMillis(), - sendLimits = it.sendLimitsByCurrencyMap, - buyLimits = it.buyModuleLimitsByCurrencyMap, - deposits = it.depositLimit - ).let { limits -> - Single.just(limits) - } - } - } - .doOnSuccess { - setLimits(it) - setMaximumDeposit(it.maxDeposit) - trace( - tag = "Trx", - message = "Fetched limits", - type = TraceType.Process, - metadata = { - val sendLimit = it.sendLimitFor(CurrencyCode.USD) - if (sendLimit != null) { - "limitNextTx" to sendLimit - } - } - ) - } - .doOnError(ErrorUtils::handleError) - .toFlowable() - } - - fun fetchDestinationMetadata(destination: com.getcode.solana.keys.PublicKey): Single { - val request = TransactionService.CanWithdrawToAccountRequest.newBuilder() - .setAccount(destination.bytes.toSolanaAccount()) - .build() - - return transactionApi.canWithdrawToAccount(request).map { - DestinationMetadata.newInstance( - destination = destination, - isValid = it.isValidPaymentDestination, - kind = DestinationMetadata.Kind.tryValueOf(it.accountType.name) - ?: DestinationMetadata.Kind.Unknown - ) - } - .onErrorReturn { - DestinationMetadata.newInstance( - destination = destination, - isValid = false, - kind = DestinationMetadata.Kind.Unknown - ) - } - } - - fun fetchUpgradeableIntents(owner: KeyPair): Single> { - val request = TransactionService.GetPrioritizedIntentsForPrivacyUpgradeRequest.newBuilder() - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .setLimit(100) //TODO: implement paging - .apply { setSignature(sign(owner)) } - .build() - - return transactionApi.getPrioritizedIntentsForPrivacyUpgrade(request).flatMap { - when (it.result) { - TransactionService.GetPrioritizedIntentsForPrivacyUpgradeResponse.Result.OK -> { - try { - val items = it.itemsList.map { item -> UpgradeableIntent.newInstance(item) } - Single.just(items) - } catch (e: UpgradeablePrivateAction.UpgradeablePrivateActionException.DeserializationFailedException) { - Single.error(FetchUpgradeableIntentsException.DeserializationException()) - } catch (e: Exception) { - Single.error(e) - } - } - - TransactionService.GetPrioritizedIntentsForPrivacyUpgradeResponse.Result.NOT_FOUND -> { - Single.just(listOf()) - } - - else -> { - Single.error(FetchUpgradeableIntentsException.UnknownException()) - } - } - } - } - - data class DestinationMetadata( - val destination: com.getcode.solana.keys.PublicKey, - val isValid: Boolean, - val kind: Kind, - - val hasResolvedDestination: Boolean, - val resolvedDestination: com.getcode.solana.keys.PublicKey - ) { - enum class Kind { - Unknown, - TokenAccount, - OwnerAccount; - - companion object { - fun tryValueOf(value: String): Kind? { - return try { - valueOf(value) - } catch (e: Exception) { - null - } - } - } - } - - companion object { - fun newInstance( - destination: com.getcode.solana.keys.PublicKey, - isValid: Boolean, - kind: Kind - ): DestinationMetadata { - val hasResolvedDestination: Boolean - val resolvedDestination: com.getcode.solana.keys.PublicKey - - when (kind) { - Kind.Unknown, Kind.TokenAccount -> { - hasResolvedDestination = false - resolvedDestination = destination - } - - Kind.OwnerAccount -> { - hasResolvedDestination = true - resolvedDestination = - AssociatedTokenAccount.newInstance(owner = destination, mint = Mint.kin).ata.publicKey - } - } - - return DestinationMetadata( - destination = destination, - isValid = isValid, - kind = kind, - hasResolvedDestination = hasResolvedDestination, - resolvedDestination = resolvedDestination - ) - } - } - } -} - -class ErrorSubmitIntentException( - val errorSubmitIntent: ErrorSubmitIntent, - cause: Throwable? = null, - val messageString: String = "" -) : Exception(cause) { - override val message: String - get() = "${errorSubmitIntent.javaClass.simpleName} $messageString" -} - -enum class DeniedReason { - Unspecified, - TooManyFreeAccountsForPhoneNumber, - TooManyFreeAccountsForDevice, - UnsupportedCountry, - UnsupportedDevice; - - companion object { - fun fromValue(value: Int): DeniedReason { - return entries.firstOrNull { it.ordinal == value } ?: Unspecified - } - } -} - -sealed class ErrorSubmitIntent(val value: Int) { - data class Denied(val reasons: List = emptyList()): ErrorSubmitIntent(0) - data class InvalidIntent(val reasons: List): ErrorSubmitIntent(1) - data object SignatureError: ErrorSubmitIntent(2) - data class StaleState(val reasons: List): ErrorSubmitIntent(3) - data object Unknown: ErrorSubmitIntent(-1) - data object DeviceTokenUnavailable: ErrorSubmitIntent(-2) - - override fun toString(): String { - return when (this) { - is Denied -> "denied(${reasons.joinToString()})" - DeviceTokenUnavailable -> "deviceTokenUnavailable" - is InvalidIntent -> "invalidIntent(${reasons.joinToString()})" - SignatureError -> "signatureError" - is StaleState -> "staleState(${reasons.joinToString()})" - Unknown -> "unknown" - } - } - - companion object { - operator fun invoke(proto: SubmitIntentResponse.Error): ErrorSubmitIntent { - val reasonStrings = proto.errorDetailsList.mapNotNull { - when (it.typeCase) { - TransactionService.ErrorDetails.TypeCase.REASON_STRING -> - it.reasonString.reason.takeIf { reason -> reason.isNotEmpty() } - else -> null - } - } - return when (proto.code) { - SubmitIntentResponse.Error.Code.DENIED -> { - val reasons = proto.errorDetailsList.mapNotNull { - if (!it.hasDenied()) return@mapNotNull null - DeniedReason.fromValue(it.denied.codeValue) - } - - Denied(reasons) - } - SubmitIntentResponse.Error.Code.INVALID_INTENT -> InvalidIntent(reasonStrings) - SubmitIntentResponse.Error.Code.SIGNATURE_ERROR -> SignatureError - SubmitIntentResponse.Error.Code.STALE_STATE -> StaleState(reasonStrings) - SubmitIntentResponse.Error.Code.UNRECOGNIZED -> Unknown - else -> return Unknown - } - } - } -} - -sealed class WithdrawException : Exception() { - class InvalidFractionalKinAmountException : WithdrawException() - class InsufficientFundsException : WithdrawException() -} - -sealed class FetchUpgradeableIntentsException : Exception() { - class DeserializationException : FetchUpgradeableIntentsException() - class UnknownException : FetchUpgradeableIntentsException() -} - -sealed class AirdropException : Exception() { - class AlreadyClaimedException : AirdropException() - class UnavailableException : AirdropException() - class UnknownException : AirdropException() -} diff --git a/services/code/src/main/java/com/getcode/network/repository/TransactionRepository_Swap.kt b/services/code/src/main/java/com/getcode/network/repository/TransactionRepository_Swap.kt deleted file mode 100644 index f2f0d7a82..000000000 --- a/services/code/src/main/java/com/getcode/network/repository/TransactionRepository_Swap.kt +++ /dev/null @@ -1,173 +0,0 @@ -package com.getcode.network.repository - -import com.codeinc.gen.transaction.v2.TransactionService -import com.codeinc.gen.transaction.v2.TransactionService.SwapRequest -import com.codeinc.gen.transaction.v2.TransactionService.SwapRequest.Initiate -import com.codeinc.gen.transaction.v2.TransactionService.SwapResponse -import com.getcode.model.intents.SwapConfigParameters -import com.getcode.model.intents.SwapIntent -import com.getcode.model.intents.requestToSubmitSignatures -import com.getcode.services.observers.BidirectionalStreamReference -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.diff -import com.getcode.solana.keys.base58 -import com.getcode.solana.organizer.Organizer -import com.getcode.utils.ErrorUtils -import io.grpc.stub.StreamObserver -import kotlinx.coroutines.suspendCancellableCoroutine -import timber.log.Timber -import kotlin.coroutines.resume - -typealias BidirectionalSwapStream = BidirectionalStreamReference - -suspend fun TransactionRepository.initiateSwap(organizer: Organizer): Result { - val intent = SwapIntent.newInstance(organizer) - - Timber.d("Swap ID: ${intent.id.base58()}") - - return submit(intent) -} - -private suspend fun TransactionRepository.submit(intent: SwapIntent): Result = suspendCancellableCoroutine { cont -> - val reference = BidirectionalSwapStream(this) - - // Intentionally creates a retain-cycle using closures to ensure that we have - // a strong reference to the stream at all times. Doing so ensures that the - // callers don't have to manage the pointer to this stream and keep it alive - reference.retain() - - reference.stream = transactionApi.swap(object : StreamObserver { - override fun onNext(value: SwapResponse?) { - when (val response = value?.responseCase) { - // 2. Upon successful submission of intent action the server will - // respond with parameters that we'll need to apply to the intent - // before crafting and signing the transactions. - SwapResponse.ResponseCase.SERVER_PARAMETERS -> { - try { - val configParameters = SwapConfigParameters.invoke(value.serverParameters) ?: throw IllegalArgumentException() - intent.parameters = configParameters - - val submitSignatures = intent.requestToSubmitSignatures() - reference.stream?.onNext(submitSignatures) - Timber.d("Sent swap request, Intent: ${intent.id.base58()}") - } catch (e: Exception) { - Timber.e(t = e, message = "Received parameters but failed to apply them. Intent: ${intent.id.base58()}") - cont.resume(Result.failure(e)) - } - } - - // 3. If submitted transaction signatures are valid and match - // the server, we'll receive a success for the submitted intent. - SwapResponse.ResponseCase.SUCCESS -> { - Timber.d("Success: ${value.success.codeValue}, Intent: ${intent.id.base58()}") - reference.stream?.onCompleted() - } - - // 3. If the submitted transaction signatures don't match, the - // intent is considered failed. Something must have gone wrong - // on the transaction creation or signing on our side. - SwapResponse.ResponseCase.ERROR -> { - - val errors = mutableListOf() - - value.error.errorDetailsList.forEach { error -> - when (error.typeCase) { - TransactionService.ErrorDetails.TypeCase.REASON_STRING -> { - errors.add("Reason: ${error.reasonString}") - } - TransactionService.ErrorDetails.TypeCase.INVALID_SIGNATURE -> { - val expected = SolanaTransaction.fromList(error.invalidSignature.expectedTransaction.value.toByteArray().toList()) - val produced = intent.transaction(intent.parameters!!) - errors.addAll( - listOf( - "Action index: ${error.invalidSignature.actionId}", - "Invalid signature: ${ - com.getcode.solana.keys.Signature( - error.invalidSignature.providedSignature.value.toByteArray() - .toList() - ).base58()}", - "Transaction bytes: ${error.invalidSignature.expectedTransaction.value}", - "Transaction expected: $expected", - "Android produced: $produced" - ) - ) - - expected?.diff(produced) - } - TransactionService.ErrorDetails.TypeCase.DENIED -> { - errors.add("Denied: ${error.denied.reason}") - } - else -> Unit - } - - Timber.e( - "Error: ${errors.joinToString("\n")}" - ) - } - - reference.stream?.onCompleted() - cont.resume( - Result.failure( - ErrorSubmitSwapIntentException( - ErrorSubmitSwapIntent.valueOf(value.error.codeValue), - ) - ) - ) - } - else -> { - reference.stream?.onCompleted() - } - } - } - - override fun onError(t: Throwable?) { - Timber.i("onError: " + t?.message.orEmpty()) - t?.let { - ErrorUtils.handleError(it) - } - cont.resume( - Result.failure( - ErrorSubmitSwapIntentException(ErrorSubmitSwapIntent.Unknown, t) - ) - ) - } - - override fun onCompleted() { - Timber.i("onCompleted") - } - }) - - val initiateSwap = SwapRequest.newBuilder() - .setInitiate(Initiate.newBuilder() - .setOwner(intent.owner.publicKeyBytes.toSolanaAccount()) - .setSwapAuthority(intent.swapCluster.authorityPublicKey.byteArray.toSolanaAccount()) - .apply { setSignature(sign(intent.owner)) } - .build() - ).build() - - reference.stream?.onNext(initiateSwap) -} - -class ErrorSubmitSwapIntentException( - val errorSubmitSwapIntent: ErrorSubmitSwapIntent, - cause: Throwable? = null, - val messageString: String = "" -) : Exception(cause) { - override val message: String - get() = "${errorSubmitSwapIntent.name} $messageString" -} - -enum class ErrorSubmitSwapIntent(val value: Int) { - Denied(0), - InvalidIntent(1), - SignatureError(2), - StaleState(3), - Unknown(-1), - DeviceTokenUnavailable(-2); - - companion object { - fun valueOf(value: Int): ErrorSubmitSwapIntent { - return entries.firstOrNull { it.value == value } ?: Unknown - } - } -} diff --git a/services/code/src/main/java/com/getcode/network/service/AccountService.kt b/services/code/src/main/java/com/getcode/network/service/AccountService.kt deleted file mode 100644 index 546f0e425..000000000 --- a/services/code/src/main/java/com/getcode/network/service/AccountService.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.getcode.network.service - -import com.codeinc.gen.account.v1.AccountService -import com.getcode.ed25519.Ed25519 -import com.getcode.network.api.AccountApi -import com.getcode.services.network.core.NetworkOracle -import com.getcode.utils.ErrorUtils -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import timber.log.Timber -import javax.inject.Inject - -class AccountService @Inject constructor( - private val api: AccountApi, - private val networkOracle: NetworkOracle, -) { - - suspend fun isCodeAccount(owner: Ed25519.KeyPair): Result { - return try { - networkOracle.managedRequest(api.isCodeAccount(owner)) - .map { response -> - when (response.result) { - AccountService.IsCodeAccountResponse.Result.OK -> { - Result.success(true) - } - - AccountService.IsCodeAccountResponse.Result.NOT_FOUND -> { - val error = Throwable("Error: account not found") - Timber.e(t = error) - Result.failure(error) - } - AccountService.IsCodeAccountResponse.Result.UNLOCKED_TIMELOCK_ACCOUNT -> { - val error = Throwable("Error: unlocked timelock account") - Timber.e(t = error) - Result.failure(error) - } - AccountService.IsCodeAccountResponse.Result.UNRECOGNIZED -> { - val error = Throwable("Error: Unrecognized request.") - Timber.e(t = error) - Result.failure(error) - } - else -> { - val error = Throwable("Error: Unknown") - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - ErrorUtils.handleError(e) - Result.failure(e) - } - } - - suspend fun linkAdditionalAccounts(owner: Ed25519.KeyPair, linkedAccount: Ed25519.KeyPair): Result { - return try { - networkOracle.managedRequest(api.linkAdditionalAccounts(owner, linkedAccount)) - .map { response -> - when (response.result) { - AccountService.LinkAdditionalAccountsResponse.Result.OK -> { - Result.success(Unit) - } - AccountService.LinkAdditionalAccountsResponse.Result.DENIED -> { - val error = Throwable("Error: Denied.") - Timber.e(t = error) - Result.failure(error) - } - AccountService.LinkAdditionalAccountsResponse.Result.INVALID_ACCOUNT -> { - val error = Throwable("Error: Invalid account.") - Timber.e(t = error) - Result.failure(error) - } - AccountService.LinkAdditionalAccountsResponse.Result.UNRECOGNIZED -> { - val error = Throwable("Error: Unrecognized request.") - Timber.e(t = error) - Result.failure(error) - } - else -> { - val error = Throwable("Error: Unknown") - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - ErrorUtils.handleError(e) - Result.failure(e) - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/service/ChatService.kt b/services/code/src/main/java/com/getcode/network/service/ChatService.kt deleted file mode 100644 index e97fee0a0..000000000 --- a/services/code/src/main/java/com/getcode/network/service/ChatService.kt +++ /dev/null @@ -1,237 +0,0 @@ -package com.getcode.network.service - -import com.codeinc.gen.chat.v1.ChatService -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.mapper.ChatMessageMapper -import com.getcode.mapper.ChatMetadataMapper -import com.getcode.model.Cursor -import com.getcode.model.ID -import com.getcode.model.chat.Chat -import com.getcode.model.chat.ChatMessage -import com.getcode.model.chat.MessageStatus -import com.getcode.model.description -import com.getcode.network.api.ChatApi -import com.getcode.services.network.core.NetworkOracle -import com.getcode.utils.ErrorUtils -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import timber.log.Timber -import javax.inject.Inject - -/** - * Abstraction layer to handle [ChatApi] request results and map to domain models - */ -class ChatService @Inject constructor( - private val api: ChatApi, - private val chatMapper: ChatMetadataMapper, - private val messageMapper: ChatMessageMapper, - private val networkOracle: NetworkOracle, -) { - private fun observeChats(owner: KeyPair): Flow>> { - return networkOracle.managedRequest(api.fetchChats(owner)) - .map { response -> - when (response.result) { - ChatService.GetChatsResponse.Result.OK -> { - Result.success(response.chatsList.map(chatMapper::map)) - } - - ChatService.GetChatsResponse.Result.NOT_FOUND -> { - val error = Throwable("Error: chats not found for owner") - Timber.e(t = error) - Result.failure(error) - } - - ChatService.GetChatsResponse.Result.UNRECOGNIZED -> { - val error = Throwable("Error: Unrecognized request.") - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = Throwable("Error: Unknown") - Timber.e(t = error) - Result.failure(error) - } - } - } - } - - @Throws(NoSuchElementException::class) - suspend fun fetchChats(owner: KeyPair): Result> { - return runCatching { observeChats(owner).first() }.getOrDefault(Result.success(emptyList())) - } - - suspend fun setMuteState(owner: KeyPair, chatId: ID, muted: Boolean): Result { - return try { - networkOracle.managedRequest(api.setMuteState(owner, chatId, muted)) - .map { response -> - when (response.result) { - ChatService.SetMuteStateResponse.Result.OK -> { - Result.success(muted) - } - - ChatService.SetMuteStateResponse.Result.CHAT_NOT_FOUND -> { - val error = Throwable("Error: chat not found for $chatId") - Timber.e(t = error) - Result.failure(error) - } - - ChatService.SetMuteStateResponse.Result.CANT_MUTE -> { - val error = Throwable("Error: Unable to change mute state for $chatId.") - Timber.e(t = error) - Result.failure(error) - } - - ChatService.SetMuteStateResponse.Result.UNRECOGNIZED -> { - val error = Throwable("Error: Unrecognized request.") - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = Throwable("Error: Unknown") - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - ErrorUtils.handleError(e) - Result.failure(e) - } - } - - suspend fun setSubscriptionState( - owner: KeyPair, - chatId: ID, - subscribed: Boolean, - ): Result { - return try { - networkOracle.managedRequest(api.setSubscriptionState(owner, chatId, subscribed)) - .map { response -> - when (response.result) { - ChatService.SetSubscriptionStateResponse.Result.OK -> { - Result.success(subscribed) - } - - ChatService.SetSubscriptionStateResponse.Result.CHAT_NOT_FOUND -> { - val error = Throwable("Error: chat not found for $chatId") - Timber.e(t = error) - Result.failure(error) - } - - ChatService.SetSubscriptionStateResponse.Result.CANT_UNSUBSCRIBE -> { - val error = Throwable("Error: Unable to change mute state for $chatId.") - Timber.e(t = error) - Result.failure(error) - } - - ChatService.SetSubscriptionStateResponse.Result.UNRECOGNIZED -> { - val error = Throwable("Error: Unrecognized request.") - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = Throwable("Error: Unknown") - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - ErrorUtils.handleError(e) - Result.failure(e) - } - } - - suspend fun fetchMessagesFor( - owner: KeyPair, - chat: Chat, - cursor: Cursor? = null, - limit: Int? = null - ): Result> { - return try { - networkOracle.managedRequest(api.fetchChatMessages(owner, chat.id, cursor, limit)) - .map { response -> - when (response.result) { - ChatService.GetMessagesResponse.Result.OK -> { - Result.success(response.messagesList.map { - messageMapper.map(it) - }) - } - - ChatService.GetMessagesResponse.Result.NOT_FOUND -> { - val error = - Throwable("Error: messages not found for chat ${chat.id.description}") - Result.failure(error) - } - - ChatService.GetMessagesResponse.Result.UNRECOGNIZED -> { - val error = Throwable("Error: Unrecognized request.") - Result.failure(error) - } - - else -> { - val error = Throwable("Error: Unknown") - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - ErrorUtils.handleError(e) - Result.failure(e) - } - } - - suspend fun advancePointer( - owner: KeyPair, - chatId: ID, - to: ID, - status: MessageStatus, - ): Result { - - val kind = when (status) { - MessageStatus.Read -> ChatService.Pointer.Kind.READ - else -> return Result.failure(Throwable("V1 Chat Service only supported READ")) - } - return try { - networkOracle.managedRequest(api.advancePointer(owner, chatId, to, kind)) - .map { response -> - when (response.result) { - ChatService.AdvancePointerResponse.Result.OK -> { - Result.success(Unit) - } - - ChatService.AdvancePointerResponse.Result.CHAT_NOT_FOUND -> { - val error = Throwable("Error: chat not found $chatId") - Timber.e(t = error) - Result.failure(error) - } - - ChatService.AdvancePointerResponse.Result.MESSAGE_NOT_FOUND -> { - val error = Throwable("Error: message not found $to") - Timber.e(t = error) - Result.failure(error) - } - - ChatService.AdvancePointerResponse.Result.UNRECOGNIZED -> { - val error = Throwable("Error: Unrecognized request.") - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = Throwable("Error: Unknown") - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - ErrorUtils.handleError(e) - Result.failure(e) - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/service/CurrencyService.kt b/services/code/src/main/java/com/getcode/network/service/CurrencyService.kt deleted file mode 100644 index 513184fd0..000000000 --- a/services/code/src/main/java/com/getcode/network/service/CurrencyService.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.getcode.network.service - -import com.getcode.model.Rate -import com.getcode.network.api.CurrencyApi -import com.getcode.services.network.core.NetworkOracle -import com.getcode.utils.ErrorUtils -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import timber.log.Timber -import javax.inject.Inject -import kotlin.time.Duration.Companion.convert -import kotlin.time.DurationUnit -import kotlin.time.ExperimentalTime - -data class ApiRateResult( - val rates: List, - val dateMillis: Long, -) - -class CurrencyService @Inject constructor( - private val api: CurrencyApi, - private val networkOracle: NetworkOracle, -) { - @OptIn(ExperimentalTime::class) - suspend fun getRates(): Result { - Timber.d("fetching rates") - return try { - networkOracle.managedRequest(api.getRates()) - .map { response -> - val rates = response.ratesMap.mapNotNull { (key, value) -> - val currency = com.getcode.model.CurrencyCode.tryValueOf(key) ?: return@mapNotNull null - Rate(fx = value, currency = currency) - }.toMutableList() - - if (rates.none { it.currency == com.getcode.model.CurrencyCode.KIN }) { - rates.add(Rate(fx = 1.0, currency = com.getcode.model.CurrencyCode.KIN)) - } - - Result.success(ApiRateResult( - rates = rates.toList(), - dateMillis = convert( - value = response.asOf.seconds.toDouble(), - sourceUnit = DurationUnit.SECONDS, - targetUnit = DurationUnit.MILLISECONDS - ).toLong() - )) - }.first() - } catch (e: Exception) { - ErrorUtils.handleError(e) - Result.failure(e) - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/service/DeviceService.kt b/services/code/src/main/java/com/getcode/network/service/DeviceService.kt deleted file mode 100644 index 588d9a0e6..000000000 --- a/services/code/src/main/java/com/getcode/network/service/DeviceService.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.getcode.network.service - -import com.codeinc.gen.device.v1.DeviceService -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.network.api.DeviceApi -import com.getcode.services.network.core.NetworkOracle -import com.getcode.solana.keys.PublicKey -import com.getcode.utils.ErrorUtils -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import timber.log.Timber -import javax.inject.Inject - -class DeviceService @Inject constructor( - private val api: DeviceApi, - private val networkOracle: NetworkOracle, -) { - suspend fun registerInstallation(owner: KeyPair, installationId: String): Result { - return try { - networkOracle.managedRequest(api.registerInstallation(owner, installationId)) - .map { response -> - when (response.result) { - DeviceService.RegisterLoggedInAccountsResponse.Result.OK -> Result.success(Unit) - - DeviceService.RegisterLoggedInAccountsResponse.Result.INVALID_OWNER -> { - val error = Throwable("Error: Invalid owner.") - Timber.e(t = error) - Result.failure(error) - } - - DeviceService.RegisterLoggedInAccountsResponse.Result.UNRECOGNIZED -> { - val error = Throwable("Error: Unrecognized request.") - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = Throwable("Error: Unknown") - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - ErrorUtils.handleError(e) - Result.failure(e) - } - } - - suspend fun fetchInstallations(installationId: String): Result> { - return try { - networkOracle.managedRequest(api.fetchInstallationAccounts(installationId)) - .map { response -> - when (response.result) { - DeviceService.GetLoggedInAccountsResponse.Result.OK -> { - Result.success(response.ownersList.map { - PublicKey( - it.value.toByteArray().toList() - ) - }) - } - DeviceService.GetLoggedInAccountsResponse.Result.UNRECOGNIZED -> { - val error = Throwable("Error: Unrecognized request.") - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = Throwable("Error: Unknown") - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - ErrorUtils.handleError(e) - Result.failure(e) - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/network/source/CollectionPagingSource.kt b/services/code/src/main/java/com/getcode/network/source/CollectionPagingSource.kt deleted file mode 100644 index c634ad51a..000000000 --- a/services/code/src/main/java/com/getcode/network/source/CollectionPagingSource.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.getcode.network.source - -import androidx.paging.PagingSource -import androidx.paging.PagingState -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.chat.ChatMessage -import com.getcode.model.Cursor -import com.getcode.model.chat.NotificationCollectionEntity -import com.getcode.network.client.Client -import com.getcode.network.client.fetchMessagesFor - -class CollectionPagingSource( - private val client: Client, - private val owner: KeyPair, - private val collection: NotificationCollectionEntity?, - private val onMessagesFetched: (List) -> Unit, -) : PagingSource() { - override suspend fun load( - params: LoadParams - ): LoadResult { - val nextCursor = params.key - - collection ?: return LoadResult.Error(Throwable("Chat not found")) - val response = client.fetchMessagesFor(owner, collection, cursor = nextCursor, limit = 20) - - response.exceptionOrNull()?.let { - return LoadResult.Error(it) - } - - val messages = response.getOrDefault(emptyList()) - onMessagesFetched(messages) - return LoadResult.Page( - data = messages, - prevKey = null, // Only paging forward. - nextKey = messages.lastOrNull()?.cursor - ) - } - - override fun getRefreshKey(state: PagingState): Cursor? { - // Try to find the page key of the closest page to anchorPosition from - // either the prevKey or the nextKey; you need to handle nullability - // here. - // * prevKey == null -> anchorPage is the first page. - // * nextKey == null -> anchorPage is the last page. - // * both prevKey and nextKey are null -> anchorPage is the - // initial page, so return null. - return state.anchorPosition?.let { anchorPosition -> - val anchorPage = state.closestPageToPosition(anchorPosition) - anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/services/model/PrefsBool.kt b/services/code/src/main/java/com/getcode/services/model/PrefsBool.kt deleted file mode 100644 index 8e932dbf9..000000000 --- a/services/code/src/main/java/com/getcode/services/model/PrefsBool.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.getcode.services.model - -// User setting exposed in Settings -> App Settings -sealed interface AppSetting - -sealed class PrefsBool(val value: String) { - // internal routing - data object IS_DEBUG_ACTIVE: PrefsBool("debug_menu_active"), InternalRouting - data object IS_DEBUG_ALLOWED: PrefsBool("debug_menu_allowed"), InternalRouting - data object IS_ELIGIBLE_GET_FIRST_KIN_AIRDROP: PrefsBool("is_eligible_get_first_kin_airdrop"), - InternalRouting - data object IS_ELIGIBLE_GIVE_FIRST_KIN_AIRDROP: PrefsBool("is_eligible_give_first_kin_airdrop"), - InternalRouting - data object HAS_REMOVED_LOCAL_CURRENCY: PrefsBool("removed_local_currency"), InternalRouting - data object DISMISSED_TIP_CARD_BANNER : PrefsBool("dismissed_tip_card_banner"), InternalRouting - data object SEEN_TIP_CARD : PrefsBool("seen_tip_card"), InternalRouting - data object STARTED_TIP_CONNECT: PrefsBool("started_tip_connect"), InternalRouting - - data object BUY_MODULE_AVAILABLE : PrefsBool("buy_module_available"), InternalRouting - - // app settings - data object CAMERA_START_BY_DEFAULT: PrefsBool("camera_start_default"), AppSetting - data object REQUIRE_BIOMETRICS: PrefsBool("require_biometrics"), AppSetting - - // dev settings - data object ESTABLISH_CODE_RELATIONSHIP : PrefsBool("establish_code_relationship_enabled"), - DevSetting - - // beta flags - data object BUCKET_DEBUGGER_ENABLED: PrefsBool("debug_buckets"), BetaFlag - data object VIBRATE_ON_SCAN: PrefsBool("vibrate_on_scan"), BetaFlag - data object LOG_SCAN_TIMES: PrefsBool("debug_scan_times"), BetaFlag - data object DISPLAY_ERRORS: PrefsBool("debug_display_errors"), BetaFlag - data object SHOW_CONNECTIVITY_STATUS: PrefsBool("debug_no_network"), BetaFlag - data object GIVE_REQUESTS_ENABLED: PrefsBool("give_requests_enabled"), BetaFlag - data object BUY_MODULE_ENABLED : PrefsBool("buy_kin_enabled"), BetaFlag, Launched - data object CHAT_UNSUB_ENABLED: PrefsBool("chat_unsub_enabled"), BetaFlag - data object TIPS_ENABLED : PrefsBool("tips_enabled"), BetaFlag, Launched - data object CONVERSATION_CASH_ENABLED: PrefsBool("convo_cash_enabled"), BetaFlag - data object BALANCE_CURRENCY_SELECTION_ENABLED: PrefsBool("balance_currency_enabled"), BetaFlag, - Launched - data object KADO_WEBVIEW_ENABLED : PrefsBool("kado_inapp_enabled"), BetaFlag - data object SHARE_TWEET_TO_TIP : PrefsBool("share_tweet_to_tip"), BetaFlag, Launched - data object TIP_CARD_ON_HOMESCREEN: PrefsBool("tip_card_on_home_screen"), BetaFlag, Launched - data object TIP_CARD_FLIPPABLE: PrefsBool("tipcard_flippable"), BetaFlag - data object CAMERA_GESTURES_ENABLED: PrefsBool("camera_gestures_enabled"), BetaFlag, Launched - data object CAMERA_DRAG_INVERTED: PrefsBool("camera_drag_inverted"), BetaFlag, Deprecated - data object GALLERY_ENABLED: PrefsBool("gallery_enabled"), BetaFlag, Launched -} - -val APP_SETTINGS: List = listOf( - PrefsBool.CAMERA_START_BY_DEFAULT, - PrefsBool.REQUIRE_BIOMETRICS -) \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/solana/builder/TransactionBuilder.kt b/services/code/src/main/java/com/getcode/solana/builder/TransactionBuilder.kt deleted file mode 100644 index 3c093c3eb..000000000 --- a/services/code/src/main/java/com/getcode/solana/builder/TransactionBuilder.kt +++ /dev/null @@ -1,262 +0,0 @@ -package com.getcode.solana.builder - -import com.getcode.solana.keys.Hash -import com.getcode.model.Kin -import com.getcode.model.extensions.newInstance -import com.getcode.model.intents.PrivateTransferMetadata -import com.getcode.model.intents.SwapConfigParameters -import com.getcode.solana.Instruction -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.TransferType -import com.getcode.solana.keys.description -import com.getcode.solana.instructions.programs.* -import com.getcode.solana.keys.Key32.Companion.mock -import com.getcode.solana.keys.Key32.Companion.subsidizer -import com.getcode.solana.keys.Key32.Companion.timeAuthority -import com.getcode.solana.keys.Mint -import com.getcode.solana.keys.PreSwapStateAccount -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.TimelockDerivedAccounts -import com.getcode.solana.organizer.AccountCluster -import timber.log.Timber - -object TransactionBuilder { - - fun closeEmptyAccount( - timelockDerivedAccounts: TimelockDerivedAccounts, - maxDustAmount: Kin, - nonce: PublicKey, - recentBlockhash: Hash, - legacy: Boolean = false - ): SolanaTransaction { - return SolanaTransaction.newInstance( - payer = subsidizer, - recentBlockhash = recentBlockhash, - instructions = listOf( - SystemProgram_AdvanceNonce( - nonce = nonce, - authority = subsidizer - ).instruction(), - TimelockProgram_BurnDustWithAuthority( - timelock = timelockDerivedAccounts.state.publicKey, - vault = timelockDerivedAccounts.vault.publicKey, - vaultOwner = timelockDerivedAccounts.owner, - timeAuthority = timeAuthority, - mint = Mint.kin, - payer = subsidizer, - bump = timelockDerivedAccounts.state.bump.toByte(), - maxAmount = maxDustAmount, - legacy = legacy - ).instruction(), - - TimelockProgram_CloseAccounts( - timelock = timelockDerivedAccounts.state.publicKey, - vault = timelockDerivedAccounts.vault.publicKey, - closeAuthority = subsidizer, - payer = subsidizer, - bump = timelockDerivedAccounts.state.bump.toByte(), - legacy = legacy - ).instruction(), - ) - ) - } - - - fun transfer( - timelockDerivedAccounts: TimelockDerivedAccounts, - destination: PublicKey, - amount: Kin, - nonce: PublicKey, - recentBlockhash: Hash, - kreIndex: Int - ): SolanaTransaction { - return SolanaTransaction.newInstance( - payer = subsidizer, - recentBlockhash = recentBlockhash, - instructions = listOf( - SystemProgram_AdvanceNonce( - nonce = nonce, - authority = subsidizer - ).instruction(), - - MemoProgram_Memo.newInstance( - transferType = TransferType.p2p, - kreIndex = kreIndex - ).instruction(), - - TimelockProgram_TransferWithAuthority( - timelock = timelockDerivedAccounts.state.publicKey, - vault = timelockDerivedAccounts.vault.publicKey, - vaultOwner = timelockDerivedAccounts.owner, - timeAuthority = timeAuthority, - destination = destination, - payer = subsidizer, - bump = timelockDerivedAccounts.state.bump.toByte(), - quarks = amount.quarks - ).instruction(), - ) - ) - } - - fun closeDormantAccount( - authority: PublicKey, - timelockDerivedAccounts: TimelockDerivedAccounts, - destination: PublicKey, - nonce: PublicKey, - recentBlockhash: Hash, - kreIndex: Int, - legacy: Boolean = false, - metadata: PrivateTransferMetadata? = null, - ): SolanaTransaction { - val instructions = mutableListOf() - - instructions.add(SystemProgram_AdvanceNonce(nonce = nonce, authority = subsidizer).instruction()) - instructions.add( - MemoProgram_Memo.newInstance( - transferType = TransferType.p2p, - kreIndex = kreIndex - ).instruction(), - ) - - when (metadata) { - is PrivateTransferMetadata.Chat -> Unit - is PrivateTransferMetadata.Tip -> { - instructions.add(MemoProgram_Memo.newInstance(metadata.socialUser).instruction()) - } - null -> Unit - } - - instructions.addAll( - listOf( - TimelockProgram_RevokeLockWithAuthority( - timelock = timelockDerivedAccounts.state.publicKey, - vault = timelockDerivedAccounts.vault.publicKey, - closeAuthority = subsidizer, - payer = subsidizer, - bump = timelockDerivedAccounts.state.bump.toByte(), - legacy = legacy - ).instruction(), - - TimelockProgram_DeactivateLock( - timelock = timelockDerivedAccounts.state.publicKey, - vaultOwner = authority, - payer = subsidizer, - bump = timelockDerivedAccounts.state.bump.toByte(), - legacy = legacy - ).instruction(), - - TimelockProgram_Withdraw( - timelock = timelockDerivedAccounts.state.publicKey, - vault = timelockDerivedAccounts.vault.publicKey, - vaultOwner = authority, - destination = destination, - payer = subsidizer, - bump = timelockDerivedAccounts.state.bump.toByte(), - legacy = legacy - ).instruction(), - - TimelockProgram_CloseAccounts( - timelock = timelockDerivedAccounts.state.publicKey, - vault = timelockDerivedAccounts.vault.publicKey, - closeAuthority = subsidizer, - payer = subsidizer, - bump = timelockDerivedAccounts.state.bump.toByte(), - legacy = legacy, - ).instruction() - ), - ) - - return SolanaTransaction.newInstance( - payer = subsidizer, - recentBlockhash = recentBlockhash, - instructions = instructions, - ) - } - - - // Swap performs an on-chain swap. The high-level flow mirrors SubmitIntent - // closely. However, due to the time-sensitive nature and unreliability of - // swaps, they do not fit within the broader intent system. This results in - // a few key differences: - // * Transactions are submitted on a best-effort basis outside of the Code - // Sequencer within the RPC handler - // * Balance changes are applied after the transaction has finalized - // * Transactions use recent blockhashes over a nonce - // - // The transaction will have the following instruction format: - // 1. ComputeBudget::SetComputeUnitLimit - // 2. ComputeBudget::SetComputeUnitPrice - // 3. SwapValidator::PreSwap - // 4. Dynamic swap instruction - // 5. SwapValidator::PostSwap - // - // Note: Currently limited to swapping USDC to Kin. - // Note: Kin is deposited into the primary account. - // - fun swap( - fromUsdc: AccountCluster, - toPrimary: PublicKey, - parameters: SwapConfigParameters - ): SolanaTransaction { - val payer = parameters.payer - val destination = toPrimary - - val stateAccount = PreSwapStateAccount.newInstance( - owner = mock, - source = fromUsdc.vaultPublicKey, - destination = destination, - nonce = parameters.nonce - ) - - Timber.d("swap accounts=${parameters.swapAccounts.map { it.description }}") - val remainingAccounts = parameters.swapAccounts.filter { - (it.isSigner || it.isWritable) && - (it.publicKey != fromUsdc.authorityPublicKey && - it.publicKey != fromUsdc.vaultPublicKey && - it.publicKey != destination) - } - - return SolanaTransaction.newInstance( - payer = payer, - recentBlockhash = parameters.blockHash, - instructions = listOf( - ComputeBudgetProgram_SetComputeUnitLimit( - limit = parameters.computeUnitLimit, - bump = stateAccount.state.bump.toByte(), - ).instruction(), - - ComputeBudgetProgram_SetComputeUnitPrice( - microLamports = parameters.computeUnitPrice, - bump = stateAccount.state.bump.toByte(), - ).instruction(), - - SwapValidatorProgram_PreSwap( - preSwapState = stateAccount.state.publicKey, - user = fromUsdc.authorityPublicKey, - source = fromUsdc.vaultPublicKey, - destination = destination, - nonce = parameters.nonce, - payer = payer, - remainingAccounts = remainingAccounts, - ).instruction(), - - Instruction( - program = parameters.swapProgram, - accounts = parameters.swapAccounts, - data = parameters.swapData.toList(), - ), - - SwapValidatorProgram_PostSwap( - stateBump = stateAccount.state.bump.toByte(), - maxToSend = parameters.maxToSend, - minToReceive = parameters.minToReceive, - preSwapState = stateAccount.state.publicKey, - source = fromUsdc.vaultPublicKey, - destination = destination, - payer = payer, - ).instruction() - ) - ) - } - -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/solana/organizer/AccountCluster.kt b/services/code/src/main/java/com/getcode/solana/organizer/AccountCluster.kt deleted file mode 100644 index 04cffbb2c..000000000 --- a/services/code/src/main/java/com/getcode/solana/organizer/AccountCluster.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.getcode.solana.organizer - -import com.getcode.crypt.DerivedKey -import com.getcode.crypt.MnemonicPhrase -import com.getcode.model.extensions.newInstance -import com.getcode.model.toPublicKey -import com.getcode.solana.keys.AssociatedTokenAccount -import com.getcode.solana.keys.Mint -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.TimelockDerivedAccounts - - -class AccountCluster( - val index: Int, - val authority: DerivedKey, - val derivation: Derivation -) { - - val authorityPublicKey: PublicKey - get() = authority.keyPair.publicKeyBytes.toPublicKey() - - val vaultPublicKey: PublicKey - get() = when (derivation) { - is Derivation.Timelock -> timelock!!.vault.publicKey - is Derivation.Usdc -> ata!!.ata.publicKey - } - sealed interface Kind { - data object Timelock: Kind - data object Usdc: Kind - } - - sealed interface Derivation { - data class Timelock(val accounts: TimelockDerivedAccounts): Derivation - data class Usdc(val account: AssociatedTokenAccount): Derivation - } - - val timelock: TimelockDerivedAccounts? - get() = (derivation as? Derivation.Timelock)?.accounts - - val ata: AssociatedTokenAccount? - get() = (derivation as? Derivation.Usdc)?.account - - companion object { - fun newInstanceLazy(authority: DerivedKey, index: Int = 0, kind: Kind, legacy: Boolean = false): Lazy { - return lazy { newInstance(authority, index, kind, legacy) } - } - - fun newInstance(authority: DerivedKey, index: Int = 0, kind: Kind, legacy: Boolean = false): AccountCluster { - return AccountCluster( - index = index, - authority = authority, - derivation = when (kind) { - Kind.Timelock -> Derivation.Timelock( - TimelockDerivedAccounts.newInstance( - owner = PublicKey(authority.keyPair.publicKeyBytes.toList()), - legacy = legacy - ) - ) - Kind.Usdc -> { - Derivation.Usdc( - AssociatedTokenAccount.newInstance( - owner = authority.keyPair.publicKeyBytes.toPublicKey(), - mint = Mint.usdc - ) - ) - } - }, - ) - } - - fun using(type: AccountType, index: Int, mnemonic: MnemonicPhrase): AccountCluster { - return newInstance( - index = index, - authority = DerivedKey.derive( - path = type.getDerivationPath(index), - mnemonic = mnemonic - ), - kind = Kind.Timelock - ) - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as AccountCluster - - if (authority != other.authority) return false - if (derivation != other.derivation) return false - - return true - } - - override fun hashCode(): Int { - var result = authority.hashCode() - result = 31 * result + derivation.hashCode() - return result - } - -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/solana/organizer/AccountType.kt b/services/code/src/main/java/com/getcode/solana/organizer/AccountType.kt deleted file mode 100644 index 278399d91..000000000 --- a/services/code/src/main/java/com/getcode/solana/organizer/AccountType.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.getcode.solana.organizer - -import com.codeinc.gen.common.v1.Model -import com.getcode.model.Domain - -sealed interface AccountType { - data object Primary : AccountType - data object Incoming : AccountType - data object Outgoing : AccountType - data class Bucket(val type: SlotType) : AccountType - data object RemoteSend: AccountType - - data class Relationship(val domain: Domain): AccountType - - data object Swap: AccountType - - fun sortOrder() = when (this) { - Primary -> 0 - Incoming -> 1 - Outgoing -> 2 - is Bucket -> { - when (type) { - SlotType.Bucket1 -> 3 - SlotType.Bucket10 -> 4 - SlotType.Bucket100 -> 5 - SlotType.Bucket1k -> 6 - SlotType.Bucket10k -> 7 - SlotType.Bucket100k -> 8 - SlotType.Bucket1m -> 9 - } - } - Swap -> 10 - is Relationship -> 11 - RemoteSend -> 12 - } - - fun getDerivationPath(index: Int): com.getcode.crypt.DerivePath { - return when (this) { - Primary -> com.getcode.crypt.DerivePath.primary - Incoming -> com.getcode.crypt.DerivePath.getBucketIncoming(index) - Outgoing -> com.getcode.crypt.DerivePath.getBucketOutgoing(index) - is Bucket -> type.getDerivationPath() - RemoteSend -> { - // Remote send accounts are standard Solana accounts - // and should use a standard derivation path that - // would be compatible with other 3rd party wallets - com.getcode.crypt.DerivePath.primary - } - is Relationship -> com.getcode.crypt.DerivePath.relationship(domain) - Swap -> com.getcode.crypt.DerivePath.swap - } - } - - fun getAccountType(): Model.AccountType { - return when (this) { - Primary -> Model.AccountType.PRIMARY - Incoming -> Model.AccountType.TEMPORARY_INCOMING - Outgoing -> Model.AccountType.TEMPORARY_OUTGOING - is Bucket -> { - when (this.type) { - SlotType.Bucket1 -> Model.AccountType.BUCKET_1_KIN - SlotType.Bucket10 -> Model.AccountType.BUCKET_10_KIN - SlotType.Bucket100 -> Model.AccountType.BUCKET_100_KIN - SlotType.Bucket1k -> Model.AccountType.BUCKET_1_000_KIN - SlotType.Bucket10k -> Model.AccountType.BUCKET_10_000_KIN - SlotType.Bucket100k -> Model.AccountType.BUCKET_100_000_KIN - SlotType.Bucket1m -> Model.AccountType.BUCKET_1_000_000_KIN - } - } - RemoteSend -> Model.AccountType.REMOTE_SEND_GIFT_CARD - is Relationship -> Model.AccountType.RELATIONSHIP - Swap -> Model.AccountType.SWAP - } - } - - companion object { - fun newInstance(accountType: Model.AccountType, relationship: Model.Relationship? = null): AccountType? { - return when (accountType) { - Model.AccountType.PRIMARY -> Primary - Model.AccountType.TEMPORARY_INCOMING -> Incoming - Model.AccountType.TEMPORARY_OUTGOING -> Outgoing - Model.AccountType.BUCKET_1_KIN -> Bucket(SlotType.Bucket1) - Model.AccountType.BUCKET_10_KIN -> Bucket(SlotType.Bucket10) - Model.AccountType.BUCKET_100_KIN -> Bucket(SlotType.Bucket100) - Model.AccountType.BUCKET_1_000_KIN -> Bucket(SlotType.Bucket1k) - Model.AccountType.BUCKET_10_000_KIN -> Bucket(SlotType.Bucket10k) - Model.AccountType.BUCKET_100_000_KIN -> Bucket(SlotType.Bucket100k) - Model.AccountType.BUCKET_1_000_000_KIN -> Bucket(SlotType.Bucket1m) - Model.AccountType.UNKNOWN -> null - Model.AccountType.LEGACY_PRIMARY_2022 -> Primary - Model.AccountType.REMOTE_SEND_GIFT_CARD -> RemoteSend - Model.AccountType.UNRECOGNIZED -> null - Model.AccountType.RELATIONSHIP -> { - val domain = Domain.from(relationship?.domain?.value) ?: return null - Relationship(domain) - } - Model.AccountType.SWAP -> Swap - } - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/solana/organizer/GiftCardAccount.kt b/services/code/src/main/java/com/getcode/solana/organizer/GiftCardAccount.kt deleted file mode 100644 index 77a307e96..000000000 --- a/services/code/src/main/java/com/getcode/solana/organizer/GiftCardAccount.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.getcode.solana.organizer - -import com.getcode.crypt.DerivePath -import com.getcode.crypt.DerivedKey -import com.getcode.crypt.MnemonicPhrase - -class GiftCardAccount( - val mnemonicPhrase: MnemonicPhrase, - val cluster: AccountCluster -) { - companion object { - fun newInstance(mnemonicPhrase: MnemonicPhrase? = null): GiftCardAccount { - val phrase = mnemonicPhrase ?: MnemonicPhrase.generate() - - return GiftCardAccount( - mnemonicPhrase = phrase, - cluster = AccountCluster.newInstance( - authority = DerivedKey.derive( - path = DerivePath.primary, - mnemonic = phrase, - ), - kind = AccountCluster.Kind.Timelock, - ) - ) - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/solana/organizer/Organizer.kt b/services/code/src/main/java/com/getcode/solana/organizer/Organizer.kt deleted file mode 100644 index 4570a61a5..000000000 --- a/services/code/src/main/java/com/getcode/solana/organizer/Organizer.kt +++ /dev/null @@ -1,162 +0,0 @@ -package com.getcode.solana.organizer - -import com.getcode.crypt.MnemonicPhrase -import com.getcode.model.AccountInfo -import com.getcode.model.Domain -import com.getcode.model.Kin -import com.getcode.model.unusable -import com.getcode.solana.keys.* -import com.getcode.utils.TraceType -import com.getcode.utils.getPublicKeyBase58 -import com.getcode.utils.trace -import timber.log.Timber - -class Organizer( - val tray: Tray, - val mnemonic: MnemonicPhrase, - private var accountInfos: Map = mapOf(), -) { - val slotsBalance get() = tray.slotsBalance - val availableBalance get() = tray.availableBalance - val availableDepositBalance get() = tray.owner.partialBalance - val availableIncomingBalance get() = tray.incoming.partialBalance - val availableRelationshipBalance get() = tray.availableRelationshipBalance - val ownerKeyPair get() = tray.owner.getCluster().authority.keyPair - val swapKeyPair get() = tray.swap.getCluster().authority.keyPair - val swapDepositAddress get() = swapKeyPair.getPublicKeyBase58() - val primaryVault get() = tray.owner.getCluster().vaultPublicKey - val incomingVault get() = tray.incoming.getCluster().vaultPublicKey - - val isUnuseable: Boolean get() = accountInfos.any { it.value.unusable } - - val createdAtMillis: Long? get() = accountInfos.mapNotNull { it.value.createdAt }.minOrNull() - - val isUnlocked: Boolean - get() = accountInfos.values.any { info -> - info.managementState != AccountInfo.ManagementState.Locked - } - - fun set(tray: Tray) { - this.tray.slots = tray.slots - this.tray.owner = tray.owner - this.tray.incoming = tray.incoming - this.tray.outgoing = tray.outgoing - this.tray.mnemonic = tray.mnemonic - } - - fun setBalances(balances: Map) { - tray.setBalances(balances) - } - - fun allAccounts() = tray.allAccounts() - - fun info(accountType: AccountType): AccountInfo? { - val account = tray.cluster(accountType).vaultPublicKey - return accountInfos[account] - } - - fun setAccountInfo(infos: Map) { - this.accountInfos = infos - tray.createRelationships(infos) - propagateBalances() - - trace( - tag = "Organizer", - message = "Fetched account infos", - type = TraceType.Process, - metadata = { - "tray" to tray.reportableRepresentation() - } - ) - } - - fun getAccountInfo() = accountInfos - - val buckets: List - get() = accountInfos.values.toList().sortedBy { it.accountType.sortOrder() } - - fun propagateBalances() { - val balances = mutableMapOf() - com.getcode.utils.timedTrace("propagate balances") { - for ((vaultPublicKey, info) in accountInfos) { - if (tray.publicKey(info.accountType) == vaultPublicKey) { - balances[info.accountType] = info.balance - } else { - // The public key above doesn't match any accounts - // that the Tray is aware of. If we're dealing with - // temp I/O accounts then we likely just need to - // update the index and try again - when (info.accountType) { - AccountType.Incoming, AccountType.Outgoing -> { - // Update the index - tray.setIndex(info.index, accountType = info.accountType) - Timber.i("Updating ${info.accountType} index to: ${info.index}") - - // Ensure that the account matches - if (tray.publicKey(info.accountType) != vaultPublicKey) { - Timber.i("Indexed account mismatch. This isn't suppose to happen.") - continue - } - balances[info.accountType] = info.balance - } - - AccountType.Primary, - is AccountType.Bucket, - AccountType.RemoteSend, - is AccountType.Relationship, - AccountType.Swap -> { - Timber.i("Non-indexed account mismatch. Account doesn't match server-provided account. Something is definitely wrong") - } - } - } - } - } - - setBalances(balances) - } - - fun relationshipFor(domain: Domain): Relationship? { - return tray.relationships.relationshipWith(domain) - } - - fun relationshipsLargestFirst(): List { - return tray.relationships.relationships(largestFirst = true).also { - Timber.d("relationships=${it.joinToString { it.domain.urlString }}") - } - } - - companion object { - fun newInstance( - mnemonic: MnemonicPhrase - ): Organizer { - val tray = Tray.newInstance(mnemonic) - return Organizer( - mnemonic = mnemonic, - tray = tray, - ) - } - } -} - -enum class Denomination { - ones, - tens, - hundreds, - thousands, - tenThousands, - hundredThousands, - millions; - - val derivationPath: com.getcode.crypt.DerivePath - get() { - return when (this) { - ones -> com.getcode.crypt.DerivePath.bucket1 - tens -> com.getcode.crypt.DerivePath.bucket10 - hundreds -> com.getcode.crypt.DerivePath.bucket100 - thousands -> com.getcode.crypt.DerivePath.bucket1k - tenThousands -> com.getcode.crypt.DerivePath.bucket10k - hundredThousands -> com.getcode.crypt.DerivePath.bucket100k - millions -> com.getcode.crypt.DerivePath.bucket1m - } - } -} diff --git a/services/code/src/main/java/com/getcode/solana/organizer/PartialAccount.kt b/services/code/src/main/java/com/getcode/solana/organizer/PartialAccount.kt deleted file mode 100644 index 4922ba973..000000000 --- a/services/code/src/main/java/com/getcode/solana/organizer/PartialAccount.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.getcode.solana.organizer - -import com.getcode.model.Kin - -data class PartialAccount(val cluster: Lazy, var partialBalance: Kin = Kin.fromKin(0)) { - fun getCluster() = cluster.value -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/solana/organizer/Relationship.kt b/services/code/src/main/java/com/getcode/solana/organizer/Relationship.kt deleted file mode 100644 index 11ac4cf27..000000000 --- a/services/code/src/main/java/com/getcode/solana/organizer/Relationship.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.getcode.solana.organizer - -import com.getcode.crypt.DerivePath -import com.getcode.crypt.DerivedKey -import com.getcode.crypt.MnemonicPhrase -import com.getcode.model.Domain -import com.getcode.model.Kin - -class Relationship( - val domain: Domain, - val mnemonic: MnemonicPhrase, - var partialBalance: Kin = Kin.fromKin(0), -) { - private lateinit var cluster: Lazy - - fun getCluster() = cluster.value - - companion object { - fun newInstance( - domain: Domain, - mnemonic: MnemonicPhrase, - partialKinBalance: Kin = Kin.fromKin(0), - ): Relationship { - val cluster = AccountCluster.newInstanceLazy( - DerivedKey.derive( - path = DerivePath.relationship(domain), - mnemonic = mnemonic - ), - kind = AccountCluster.Kind.Timelock, - ) - - return Relationship( - domain = domain, - mnemonic = mnemonic, - partialBalance = partialKinBalance - ).apply { - this.cluster = cluster - } - } - } -} \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/solana/organizer/Slot.kt b/services/code/src/main/java/com/getcode/solana/organizer/Slot.kt deleted file mode 100644 index 712c01329..000000000 --- a/services/code/src/main/java/com/getcode/solana/organizer/Slot.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.getcode.solana.organizer - -import com.getcode.crypt.DerivePath -import com.getcode.crypt.DerivedKey -import com.getcode.crypt.MnemonicPhrase -import com.getcode.model.Kin - - -data class Slot( - var partialBalance: Kin, - val type: SlotType, - private val cluster: Lazy -) { - val billValue: Int = type.getBillValue() - - fun billCount(): Long { - return (partialBalance.toKinValueDouble() / type.getBillValue()).toLong() - } - - fun getCluster() = cluster.value - - companion object { - fun newInstance( - partialBalance: Kin = Kin.fromQuarks(0), - type: SlotType, - mnemonic: MnemonicPhrase - ): Slot { - return Slot( - partialBalance = partialBalance, - type = type, - cluster = lazy { - AccountCluster.newInstance( - DerivedKey.derive( - type.getDerivationPath(), - mnemonic - ), - kind = AccountCluster.Kind.Timelock, - ) - } - ) - } - } -} - -enum class SlotType { - Bucket1, - Bucket10, - Bucket100, - Bucket1k, - Bucket10k, - Bucket100k, - Bucket1m; -} - -fun SlotType.getBillValue(): Int = - when (this.ordinal) { - SlotType.Bucket1.ordinal -> 1 - SlotType.Bucket10.ordinal -> 10 - SlotType.Bucket100.ordinal -> 100 - SlotType.Bucket1k.ordinal -> 1_000 - SlotType.Bucket10k.ordinal -> 10_000 - SlotType.Bucket100k.ordinal -> 100_000 - SlotType.Bucket1m.ordinal -> 1_000_000 - else -> throw IllegalStateException() - } - -fun SlotType.getDerivationPath(): DerivePath = - when (this.ordinal) { - SlotType.Bucket1.ordinal -> DerivePath.bucket1 - SlotType.Bucket10.ordinal -> DerivePath.bucket10 - SlotType.Bucket100.ordinal -> DerivePath.bucket100 - SlotType.Bucket1k.ordinal -> DerivePath.bucket1k - SlotType.Bucket10k.ordinal -> DerivePath.bucket10k - SlotType.Bucket100k.ordinal -> DerivePath.bucket100k - SlotType.Bucket1m.ordinal -> DerivePath.bucket1m - else -> null - }.let { it ?: throw IllegalStateException() } diff --git a/services/code/src/main/java/com/getcode/solana/organizer/Tray.kt b/services/code/src/main/java/com/getcode/solana/organizer/Tray.kt deleted file mode 100644 index e3b59e37d..000000000 --- a/services/code/src/main/java/com/getcode/solana/organizer/Tray.kt +++ /dev/null @@ -1,957 +0,0 @@ -package com.getcode.solana.organizer - -import com.getcode.crypt.DerivePath -import com.getcode.crypt.DerivedKey -import com.getcode.crypt.MnemonicPhrase -import com.getcode.model.AccountInfo -import com.getcode.model.Domain -import com.getcode.model.Kin -import com.getcode.model.RelationshipBox -import com.getcode.model.description -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.base58 -import com.getcode.utils.TraceType -import com.getcode.services.utils.padded -import com.getcode.utils.trace -import kotlin.math.min - -class Tray( - var slots: List, - var owner: PartialAccount, - var swap: PartialAccount, - var incoming: PartialAccount, - var outgoing: PartialAccount, - var mnemonic: MnemonicPhrase -) { - var slotsBalance: Kin = Kin.fromKin(0) - get() = slots.map { it.partialBalance }.reduce { acc, slot -> acc + slot } - private set - - var availableBalance: Kin = Kin.fromKin(0) - get() = slotsBalance + availableDepositBalance + availableIncomingBalance + availableRelationshipBalance - private set - - private val availableDepositBalance: Kin - get() = owner.partialBalance - - private val availableIncomingBalance: Kin - get() = incoming.partialBalance - - var relationships = RelationshipBox() - internal set - - var availableRelationshipBalance: Kin = Kin.fromKin(0) - get() = relationships.publicKeys.values.map { it.partialBalance } - .reduceOrNull { acc, slot -> acc + slot } ?: Kin.fromKin(0) - private set - - fun slot(type: SlotType): Slot { - return slots.first { it.type == type } - } - - fun slotDown(type: SlotType): Slot? { - val index = slots.indexOfFirst { it.type == type } - if (index > 0) { - return slots[index - 1] - } - return null - } - - fun slotUp(type: SlotType): Slot? { - val index = slots.indexOfFirst { it.type == type } - if (index < slots.size - 1) { - return slots[index + 1] - } - return null - } - - fun increment(type: AccountType, kin: Kin) { - when (type) { - AccountType.Primary -> owner.partialBalance += kin - AccountType.Incoming -> incoming.partialBalance += kin - AccountType.Outgoing -> outgoing.partialBalance += kin - is AccountType.Bucket -> slots[type.type.ordinal].partialBalance += kin - AccountType.RemoteSend -> throw IllegalStateException("Remote send account unsupported") - is AccountType.Relationship -> { - val relationship = relationships.relationshipWith(type.domain) - ?: throw IllegalStateException("Relationship for ${type.domain.relationshipHost}) not found in ${relationships.domains}") - relationships.insert( - relationship.apply { - partialBalance += kin - } - ) - } - - AccountType.Swap -> swap.partialBalance += kin - } - } - - fun decrement(type: AccountType, kin: Kin) { - when (type) { - AccountType.Primary -> owner.partialBalance -= kin - AccountType.Incoming -> incoming.partialBalance -= kin - AccountType.Outgoing -> outgoing.partialBalance -= kin - is AccountType.Bucket -> slots[type.type.ordinal].partialBalance -= kin - AccountType.RemoteSend -> throw IllegalStateException("Remote send account unsupported") - is AccountType.Relationship -> { - val relationship = relationships.relationshipWith(type.domain) - ?: throw IllegalStateException("Relationship for ${type.domain.relationshipHost}) not found in ${relationships.domains}") - relationships.insert( - relationship.apply { - partialBalance -= kin - } - ) - } - AccountType.Swap -> swap.partialBalance -= kin - } - } - - fun setBalances(balances: Map) { - owner.partialBalance = balances[AccountType.Primary] ?: owner.partialBalance - incoming.partialBalance = balances[AccountType.Incoming] ?: incoming.partialBalance - outgoing.partialBalance = balances[AccountType.Outgoing] ?: outgoing.partialBalance - - slots[0].partialBalance = balances[AccountType.Bucket(SlotType.Bucket1)] ?: slots[0].partialBalance - slots[1].partialBalance = balances[AccountType.Bucket(SlotType.Bucket10)] ?: slots[1].partialBalance - slots[2].partialBalance = balances[AccountType.Bucket(SlotType.Bucket100)] ?: slots[2].partialBalance - slots[3].partialBalance = balances[AccountType.Bucket(SlotType.Bucket1k)] ?: slots[3].partialBalance - slots[4].partialBalance = balances[AccountType.Bucket(SlotType.Bucket10k)] ?: slots[4].partialBalance - slots[5].partialBalance = balances[AccountType.Bucket(SlotType.Bucket100k)] ?: slots[5].partialBalance - slots[6].partialBalance = balances[AccountType.Bucket(SlotType.Bucket1m)] ?: slots[6].partialBalance - - balances.filter { (type, _) -> type is AccountType.Relationship } - .mapNotNull { (type, amount) -> - val relationshipType = type as? AccountType.Relationship ?: return@mapNotNull null - relationshipType to amount - } - .onEach { (relationship, amount) -> - val domain = relationship.domain - setBalance(domain, amount) - } - } - - private fun setBalance(domain: Domain, balance: Kin) { - val relationship = relationships.relationshipWith(domain) ?: return - relationships.insert(relationship.apply { - partialBalance = balance - }) - } - - fun partialBalance(type: AccountType): Kin { - return when (type) { - is AccountType.Primary -> owner.partialBalance - is AccountType.Incoming -> incoming.partialBalance - is AccountType.Outgoing -> outgoing.partialBalance - is AccountType.Bucket -> slot(type.type).partialBalance - AccountType.RemoteSend -> throw IllegalStateException("Remote send account unsupported") - is AccountType.Relationship -> { - val relationship = relationships.relationshipWith(type.domain) - ?: throw IllegalStateException("Relationship for ${type.domain.relationshipHost}) not found in ${relationships.domains}") - - return relationship.partialBalance - } - - AccountType.Swap -> swap.partialBalance - } - } - - fun createRelationships(accountInfos: Map) { - val domains= accountInfos - .mapNotNull { it.value.relationship?.domain } - - domains.onEach { createRelationship(it) } - } - - fun createRelationship(domain: Domain): Relationship { - val relationship = Relationship.newInstance(domain, mnemonic) - relationships.insert(relationship) - return relationship - } - - fun incrementIncoming() { - setIndex(incoming.getCluster().index + 1, AccountType.Incoming) - } - - fun incrementOutgoing() { - setIndex(outgoing.getCluster().index + 1, AccountType.Outgoing) - } - - fun setIndex(index: Int, accountType: AccountType) { - when (accountType) { - AccountType.Incoming -> { - incoming = PartialAccount(cluster = incoming(index, mnemonic)) - } - AccountType.Outgoing -> { - outgoing = PartialAccount(cluster = outgoing(index, mnemonic)) - } - - is AccountType.Bucket, - AccountType.Primary, - is AccountType.Relationship, - AccountType.RemoteSend, - AccountType.Swap -> { - throw IllegalStateException() - } - } - } - - fun allAccounts(): List> { - return listOf( - Pair(AccountType.Primary, owner.getCluster()), - Pair(AccountType.Incoming, incoming.getCluster()), - Pair(AccountType.Outgoing, outgoing.getCluster()), - *slots.map { Pair(AccountType.Bucket(it.type), it.getCluster()) }.toTypedArray(), - ) - } - - fun publicKey(accountType: AccountType): PublicKey { - return cluster(accountType).vaultPublicKey - } - - fun cluster(accountType: AccountType): AccountCluster { - return when (accountType) { - AccountType.Primary -> owner.getCluster() - AccountType.Incoming -> incoming.getCluster() - AccountType.Outgoing -> outgoing.getCluster() - is AccountType.Bucket -> slot(accountType.type).getCluster() - AccountType.RemoteSend -> throw IllegalStateException("Remote send account unsupported") - is AccountType.Relationship -> { - relationships.relationshipWith(domain = accountType.domain)!!.getCluster() - } - - AccountType.Swap -> swap.getCluster() - } - } - - fun copy(): Tray { - return Tray( - slots = slots.map { it.copy() }, - owner = owner.copy(), - swap = swap.copy(), - incoming = incoming.copy(), - outgoing = outgoing.copy(), - mnemonic = mnemonic, - ).apply tray@{ - this@tray.relationships = this@Tray.relationships - } - } - - companion object { - fun newInstance( - mnemonic: MnemonicPhrase - ): Tray { - return Tray( - mnemonic = mnemonic, - slots = listOf( - Slot.newInstance( - type = SlotType.Bucket1, - mnemonic = mnemonic - ), - Slot.newInstance( - type = SlotType.Bucket10, - mnemonic = mnemonic - ), - Slot.newInstance( - type = SlotType.Bucket100, - mnemonic = mnemonic - ), - Slot.newInstance( - type = SlotType.Bucket1k, - mnemonic = mnemonic - ), - Slot.newInstance( - type = SlotType.Bucket10k, - mnemonic = mnemonic - ), - Slot.newInstance( - type = SlotType.Bucket100k, - mnemonic = mnemonic - ), - Slot.newInstance( - type = SlotType.Bucket1m, - mnemonic = mnemonic - ), - ), - incoming = PartialAccount(incoming(0, mnemonic)), - outgoing = PartialAccount(outgoing( 0, mnemonic)), - owner = PartialAccount( - cluster = AccountCluster.newInstanceLazy( - authority = DerivedKey.derive(DerivePath.primary, mnemonic), - kind = AccountCluster.Kind.Timelock, - ) - ), - swap = PartialAccount( - cluster = AccountCluster.newInstanceLazy( - authority = DerivedKey.derive(DerivePath.swap, mnemonic), - kind = AccountCluster.Kind.Usdc, - ) - ) - ) - } - - fun incoming(index: Int, mnemonic: MnemonicPhrase): Lazy { - return lazy { - AccountCluster.newInstance( - authority = DerivedKey.derive( - DerivePath.getBucketIncoming(index), - mnemonic - ), - index = index, - kind = AccountCluster.Kind.Timelock, - ) - } - } - - fun outgoing(index: Int, mnemonic: MnemonicPhrase): Lazy { - return lazy { - AccountCluster.newInstance( - authority = DerivedKey.derive( - DerivePath.getBucketOutgoing(index), - mnemonic - ), - index = index, - kind = AccountCluster.Kind.Timelock, - ) - } - } - } - - - // MARK: - Redistribute - - - /// Redistribute the bills in the organizer to ensure there are no gaps - /// in consecutive slots. - /// - /// For example, avoid this: - /// ---------------------------------------------------------------- - /// | slot 0 | slot 1 | slot 2 | slot 3 | slot 4 | slot 5 | slot 6 | - /// ---------------------------------------------------------------- - /// | 1 | 0 | 10 | 10 | 0 | 0 | 0 | = 1,101 - /// ^---------^--- not optimal - /// - /// Instead, we want this: - /// ---------------------------------------------------------------- - /// | slot 0 | slot 1 | slot 2 | slot 3 | slot 4 | slot 5 | slot 6 | - /// ---------------------------------------------------------------- - /// | 11 | 9 | 9 | 10 | 0 | 0 | 0 | = 1,101 - /// ^---------^--------┘ split the 10 downwards - /// - /// The examples above both have the same total balance, but the second - /// example should allow for more efficient payments later down the line. - /// - /// We also try to limit the number of bills in each slot as a secondary - /// goal. This is done by recursively exchanging large bills for smaller - /// bills and vice versa with rules around how many of each denomination - /// to keep. Typically, you never need more than 9 pennies to make any - /// payment. - /// - /// Algorithm: - /// -------------------------------------------------------------------- - /// 1) First we take large bills and exchange them for smaller bills one - /// at a time. We do this recursively until we can't exchange any more - /// large bills to small ones. This spreads out our total balance over - /// as many slots as possible. - /// - /// 2) Then we take smaller bills and exchange them for larger bills if - /// we have more than needed in any slot. This reduces the number of - /// bills we have in total. - /// - /// This algorithm guarantees that we will never have gaps (zero balance) - /// between consecutive slots (e.g. 1000, 0, 10, 1). - /// --------------------------------------------------------------------- - /// - /// TODO: this algorithm could be optimized to reduce the number of - /// transactions - fun redistribute(): List { - val exchanges = mutableListOf() - - exchanges.addAll( - exchangeLargeToSmall() - ) - - exchanges.addAll( - exchangeSmallToLarge() - ) - - return exchanges - } - - fun receive(receivingAccount: AccountType, amount: Kin): List { - if (partialBalance(receivingAccount) < amount) throw OrganizerException.InvalidSlotBalanceException() - - val container = mutableListOf() - - var remainingAmount = amount - - for (i in (slots.size - 1 downTo 0)) { - val currentSlot = slots[i] - - val howManyFit: Int = (remainingAmount.toKinValueDouble() / currentSlot.billValue).toInt() - if (howManyFit > 0) { - val amountToDeposit = Kin.fromKin(howManyFit * currentSlot.billValue) - - normalize(slotType = currentSlot.type, amount = amountToDeposit) { subAmount -> - container.add( - InternalExchange( - from = receivingAccount, - to = AccountType.Bucket(currentSlot.type), - kin = subAmount - ) - ) - } - - decrement(receivingAccount, amountToDeposit) - increment(AccountType.Bucket(currentSlot.type), amountToDeposit) - - remainingAmount -= amountToDeposit - } - } - - return container - } - - /// Recursive function to exchange large bills to smaller bills (when - /// possible). For example, if we have dimes but no pennies, we should - /// break a dime into pennies. - /// - - fun exchangeLargeToSmall(layer: Int = 0): List { - val padding = "-".repeat(layer + 1) + "|" - - val exchanges = mutableListOf() - - for (i in 1..slots.size) { - val currentSlot = slots[slots.size - i] // Backwards - val smallerSlot = slotDown(currentSlot.type) - - trace( - "$padding o Checking slot: ${currentSlot.type}", - type = TraceType.Silent - ) - - if (smallerSlot == null) { - // We're at the lowest denomination - // so we can't exchange anymore. - trace( - "$padding x Last slot", - type = TraceType.Silent - ) - break - } - - if (currentSlot.billCount() <= 0) { - // Nothing to exchange, the current slot is empty. - trace( - "$padding x Empty", - type = TraceType.Silent - ) - continue - } - - val howManyFit = currentSlot.billValue / smallerSlot.billValue - - if (smallerSlot.billCount() >= howManyFit - 1) { - // No reason to exchange yet, the smaller slot - // already has enough bills for most payments - trace( - "$padding x Enough bills", - type = TraceType.Silent - ) - continue - } - - val amount = Kin.fromKin(currentSlot.billValue) - - // Adjust the slot balance - decrement(AccountType.Bucket(currentSlot.type), kin = amount) - increment(AccountType.Bucket(smallerSlot.type), kin = amount) - - trace( - message = "$padding v Exchanging from ${currentSlot.type} to ${smallerSlot.type} $amount Kin", - type = TraceType.Silent - ) - - exchanges.add( - InternalExchange( - from = AccountType.Bucket(currentSlot.type), - to = AccountType.Bucket(smallerSlot.type), - kin = amount - ) - ) - - exchanges.addAll( - exchangeLargeToSmall(layer = layer + 1) - ) // Recursive - } - - return exchanges - } - - /// Recursive function to exchange small bills to larger bills (when - /// possible). - /// - /// For example, if we have 19 pennies or more, we should exchange excess - /// pennies for dimes. But if we only have 18 pennies or less, we - /// should not exchange any because we'd be unable to make a future - /// payment that has a $0.09 amount (there are some edge cases). - /// - - fun exchangeSmallToLarge(layer: Int = 0): List { - val padding = "-".repeat(layer + 1) + "|" - - val exchanges = mutableListOf() - - for (element in slots) { - - val currentSlot = element // Forwards - val largerSlot = slotUp(currentSlot.type) - - trace("$padding o Checking slot: ${currentSlot.type}") - - if (largerSlot == null) { - // We're at the largest denomination - // so we can't exchange anymore. - trace( - "$padding x Last slot", - type = TraceType.Silent - ) - break - } - - // First we need to check how many bills of the current type fit - // into the next slot. - - val howManyFit = largerSlot.billValue / currentSlot.billValue - val howManyWeHave = currentSlot.billCount() - val howManyToLeave = min(howManyFit - 1L, howManyWeHave) - - if (howManyWeHave < ((howManyFit * 2) - 1)) { - // We don't have enough bills to exchange, so we can't do - // anything in this slot at the moment. - trace( - "$padding x Not enough bills", - type = TraceType.Silent - ) - continue - } - - val howManyToExchange = (howManyWeHave - howManyToLeave) / howManyFit * howManyFit - val amount = Kin.fromKin(kin = howManyToExchange) * currentSlot.billValue - - val slotTransfers = mutableListOf() - - normalizeLargest(amount = amount) { partialAmount -> - slotTransfers.add( - InternalExchange( - from = AccountType.Bucket(currentSlot.type), - to = AccountType.Bucket(largerSlot.type), - kin = partialAmount - ) - ) - } - - // Adjust the slot balance - decrement(AccountType.Bucket(currentSlot.type), kin = amount) - increment(AccountType.Bucket(largerSlot.type), kin = amount) - - slotTransfers.forEach { transfer -> - trace( - message = "$padding v Exchanging from ${transfer.from} to {transfer.to!} {transfer.kin} Kin", - type = TraceType.Silent - ) - } - - exchanges.addAll( - slotTransfers - ) - - exchanges.addAll( - exchangeSmallToLarge(layer = layer + 1) - ) // Recursive - } - - return exchanges - } - - fun normalize(slotType: SlotType, amount: Kin, handler: (Kin) -> Unit) { - var howManyFit = amount.toKinTruncatingLong() / slotType.getBillValue() - while (howManyFit > 0) { - val billsToMove = min(howManyFit, 9) - val moveAmount = Kin.fromKin(slotType.getBillValue() * billsToMove) - - handler(moveAmount) - - howManyFit -= billsToMove - } - } - - fun normalizeLargest(amount: Kin, handler: (Kin) -> Unit) { - var remainingAmount = amount - - // Starting from largest denomination to the smallest - // we'll find how many 'bills' from each stack we need - for (i in 1..slots.size) { - val slot = slots[slots.size - i] // Backwards - - var howManyFit = remainingAmount.toKinTruncatingLong() / slot.billValue - while (howManyFit > 0) { - val billsToMove = min(howManyFit, 9) - val moveAmount = Kin.fromKin(kin = slot.billValue * billsToMove) - - handler(moveAmount) - - remainingAmount -= moveAmount - howManyFit -= billsToMove - } - } - } - - - /// This function sends money from the organizer to the outgoing - /// temporary account. It has to solve the interesting problem of - /// figuring out which denominations to use when making a payment. - /// - /// Unfortunately, this is actually a pretty hard - /// problem to solve optimally. - /// https://en.wikipedia.org/wiki/Change-making_problem - /// - /// We're going to use the following approach, which should be pretty - /// good most of the time but definitely has room for improvement. - /// Specifically, we may want to move from a dynamic programming - /// solution to a greedy solution in the future. - /// - /// Algorithm - /// - /// 1. Check the total balance to make sure we have enough to send. - /// - /// 2. Try using a naive approach where we send from the amounts - /// currently in the slots. This will fail if we don't have enough of - /// a particular bill to pay the amount. - /// - /// 3. If step 2 fails, start at the smallest denomination and move - /// upwards while adding everything along the way until we reach a - /// denomination that is larger than the remaining amount. Then split - /// and go backwards... (dynamic programming strategy) - /// - fun transfer(amount: Kin): List { - if (amount <= 0) { - throw OrganizerException.InvalidAmountException() - } - - if (amount > availableBalance) { - throw OrganizerException.InsufficientTrayBalanceException() - } - - val startState = this.copy() - - return try { - withdrawNaively(amount = amount) - } catch (e: OrganizerException) { - this.slots = startState.slots.map { it.copy() } - this.owner = startState.owner.copy() - this.incoming = startState.incoming.copy() - this.outgoing = startState.outgoing.copy() - withdrawDynamically(amount = amount) - } - } - - fun withdrawNaively(amount: Kin): List { - if (amount <= 0) { - throw OrganizerException.InvalidAmountException() - } - - val container = mutableListOf() - - var remainingAmount = amount - - // Starting from largest denomination to the smallest - // we'll find how many 'bills' from each stack we need - for (i in 1..slots.size) { - val slot = slots[slots.size - i] // Backwards - - if (slot.partialBalance <= 0) { - continue - } - - val howManyFit = remainingAmount.toKinTruncatingLong() / slot.billValue - - val maxAmount = Kin.fromKin(howManyFit * slot.billValue) - val howMuchToSend: Kin = if (slot.partialBalance < maxAmount) slot.partialBalance else maxAmount - - if (howMuchToSend > 0) { - if (slot.partialBalance < howMuchToSend) { - throw OrganizerException.InvalidSlotBalanceException() - } - - val sourceBucket = AccountType.Bucket(slot.type) - - normalize(slotType = slot.type, amount = howMuchToSend) { amountN -> - container.add( - InternalExchange( - from = sourceBucket, - to = AccountType.Outgoing, - kin = amountN - ) - ) - } - - decrement(sourceBucket, kin = howMuchToSend) - increment(AccountType.Outgoing, kin = howMuchToSend) - - remainingAmount -= howMuchToSend - } - } - - if (remainingAmount >= 1) { - throw OrganizerException.InvalidSlotBalanceException() - } - - return container - } - - fun withdrawDynamically(amount: Kin): List { - if (amount <= 0) { - throw OrganizerException.InvalidAmountException() - } - - if (amount > availableBalance) { - throw OrganizerException.InsufficientTrayBalanceException() - } - - val step = withdrawDynamicallyStep1(amount = amount) - val exchanges = withdrawDynamicallyStep2(step = step) - - return step.exchanges + exchanges - } - - /// This function assumes that the 'naive strategy' withdrawal was already - /// attempted. We'll iterate over the slots, from smallest to largest, drain - /// every slot up to the `amount`. Once a slot that is larger than the - /// remaining amount is reached, the function returns the index at which the - /// second step should resume. - /// - /// Returns the index that should be broken down in step 2. - /// - fun withdrawDynamicallyStep1(amount: Kin): InternalDynamicStep { - val container = mutableListOf() - var remaining = amount - - for (i in slots.indices) { - val currentSlot = slots[i] // Forwards - - if (currentSlot.partialBalance <= 0) { - // Try next slot - continue - } - - if (remaining.toKinValueDouble() < 1) { - // Sent it all - break - } - - if (remaining.toKinTruncatingLong() < currentSlot.billValue) { - // If there's a remaining amount and the current - // bill value is greater, we'll need to break the - // current slot bill down to lower slots - break - } - - val howManyFit = remaining.toKinTruncatingLong() / currentSlot.billValue - val maxAmount = howManyFit * currentSlot.billValue - val howMuchToSend = - min(currentSlot.partialBalance.toKinValueDouble(), maxAmount.toDouble()) - .let { Kin.fromKin(it) } - - if (howMuchToSend > 0) { - normalize(slotType = currentSlot.type, amount = howMuchToSend) { kinToSend -> - container.add( - InternalExchange( - from = AccountType.Bucket(currentSlot.type), - to = AccountType.Outgoing, - kin = kinToSend - ) - ) - } - - // Adjust the slot balance - decrement(AccountType.Bucket(currentSlot.type), kin = howMuchToSend) - increment(AccountType.Outgoing, kin = howMuchToSend) - - remaining -= howMuchToSend - } - } - - var index = slots.indexOfFirst { it.billValue > remaining.toKinTruncatingLong() && it.billCount() > 0 } - - // Only throw an error if there's a - // non-zero remaining amount, other - // wise the first step covered the - // total amount - if (index == -1 && remaining >= 1) { - throw OrganizerException.InvalidStepIndexException() - } - - if (index == -1) index = 0 - return InternalDynamicStep( - remaining = remaining, - index = index, - exchanges = container - ) - } - - fun withdrawDynamicallyStep2(step: InternalDynamicStep): List { - if (!(step.index > 0 && step.index < slots.size)) { - return listOf() - } - - if (step.remaining < 1) { - return listOf() - } - - val container = mutableListOf() - var remaining = step.remaining - - val current = slots[step.index] - val lower = slots[step.index - 1] - - if (current.billCount() < 1) { - throw OrganizerException.SlotAtIndexEmptyException() - } - - // Break the current slot into the lower - // slot and exchange all the way down - val initialSplitAmount = Kin.fromKin(kin = current.billValue) - container.add( - InternalExchange( - from = AccountType.Bucket(current.type), - to = AccountType.Bucket(lower.type), - kin = initialSplitAmount - ) - ) - - // Adjust the slot balance - decrement(type = AccountType.Bucket(current.type), kin = initialSplitAmount) - increment(type = AccountType.Bucket(lower.type), kin = initialSplitAmount) - - - for (i in (step.index-1 downTo 0)) { - val currentSlot = slots[i] - - // Split every slot down to the smallest - // to ensure we have enough bills in each - if (i > 0) { - val lowerSlot = slots[i - 1] - val splitAmount = Kin.fromKin(currentSlot.billValue) - - container.add( - InternalExchange( - from = AccountType.Bucket(currentSlot.type), - to = AccountType.Bucket(lowerSlot.type), - kin = splitAmount - ) - ) - - // Adjust the slot balance - decrement(AccountType.Bucket(currentSlot.type), splitAmount) - increment(AccountType.Bucket(lowerSlot.type), splitAmount) - } - - val howManyFit = remaining.toKinTruncatingLong() / currentSlot.billValue - val kinToSend = Kin.fromKin(howManyFit * currentSlot.billValue) - - if (howManyFit <= 0) { - continue - } - - if (howManyFit > currentSlot.billCount().toInt()) { - throw OrganizerException.InvalidSlotBalanceException() - } - - container.add( - InternalExchange( - from = AccountType.Bucket(currentSlot.type), - to = AccountType.Outgoing, - kin = kinToSend - ) - ) - - // Adjust the slot balance - decrement(AccountType.Bucket(currentSlot.type), kin = kinToSend) - increment(AccountType.Outgoing, kin = kinToSend) - - remaining -= kinToSend - } - - return container - } - - fun reportableRepresentation(): List { - return listOf( - string(named = "Primary ", partialAccount = owner), - string(named = "Incoming ", partialAccount = incoming), - string(named = "Outgoing ", partialAccount = outgoing), - string("1 ", slot = slot(SlotType.Bucket1)), - string("10 ", slot = slot(SlotType.Bucket10)), - string("100 ", slot = slot(SlotType.Bucket100)), - string("1k ", slot = slot(SlotType.Bucket1k)), - string("10k ", slot = slot(SlotType.Bucket10k)), - string("100k ", slot = slot(SlotType.Bucket100k)), - string("1m ", slot = slot(SlotType.Bucket1m)), - ) - } - - private fun string(named: String, partialAccount: PartialAccount): String { - return "$named ${partialAccount.getCluster().vaultPublicKey.base58().padded(44)}) ${partialAccount.partialBalance.description}" - } - - private fun string(named: String, slot: Slot): String { - return "$named ${slot.getCluster().vaultPublicKey.base58().padded(44)}) ${slot.partialBalance.description}" - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Tray - - if (slots != other.slots) return false - if (incoming != other.incoming) return false - if (outgoing != other.outgoing) return false - if (mnemonic != other.mnemonic) return false - - return true - } - - override fun hashCode(): Int { - var result = slots.hashCode() - result = 31 * result + incoming.hashCode() - result = 31 * result + outgoing.hashCode() - result = 31 * result + mnemonic.hashCode() - return result - } - - sealed class OrganizerException : Exception() { - class InvalidAmountException : OrganizerException() - class InsufficientTrayBalanceException : OrganizerException() - class InvalidSlotBalanceException : OrganizerException() - class InvalidStepIndexException : OrganizerException() - class SlotAtIndexEmptyException : OrganizerException() - } -} - -data class InternalExchange( - var from: AccountType, - var to: AccountType? = null, - var kin: Kin -) - -data class InternalDynamicStep( - var remaining: Kin, - var index: Int, - var exchanges: List -) - -data class InternalDeposit( - var to: SlotType, - var kin: Kin -) \ No newline at end of file diff --git a/services/code/src/main/java/com/getcode/utils/SignMessage.kt b/services/code/src/main/java/com/getcode/utils/SignMessage.kt deleted file mode 100644 index 8da7bb1b7..000000000 --- a/services/code/src/main/java/com/getcode/utils/SignMessage.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.getcode.utils - -import com.codeinc.gen.common.v1.Model -import com.getcode.ed25519.Ed25519 -import com.getcode.network.repository.toSignature -import com.google.protobuf.GeneratedMessageLite -import java.io.ByteArrayOutputStream - -fun , B : GeneratedMessageLite.Builder> GeneratedMessageLite.Builder.sign(owner: Ed25519.KeyPair): Model.Signature { - // dump message up until this point into a ByteArray - val bos = ByteArrayOutputStream() - this.buildPartial().writeTo(bos) - - /** - * sign message up to this point with owner and return as [com.codeinc.gen.common.v1.Model.Signature] - */ - return Ed25519.sign(bos.toByteArray(), owner).toSignature() -} diff --git a/services/flipchat/chat/.gitignore b/services/flipchat/chat/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/services/flipchat/chat/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/services/flipchat/chat/build.gradle.kts b/services/flipchat/chat/build.gradle.kts deleted file mode 100644 index 4608e35f7..000000000 --- a/services/flipchat/chat/build.gradle.kts +++ /dev/null @@ -1,96 +0,0 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - id(Plugins.android_library) - id(Plugins.kotlin_android) - id(Plugins.kotlin_kapt) - id(Plugins.kotlin_serialization) -} - -android { - namespace = "${Gradle.flipchatNamespace}.services.chat" - compileSdk = Android.compileSdkVersion - defaultConfig { - minSdk = Android.minSdkVersion - testInstrumentationRunner = Android.testInstrumentationRunner - - consumerProguardFiles("consumer-rules.pro") - - buildConfigField("String", "VERSION_NAME", "\"${Packaging.Flipchat.versionName}\"") - } - - buildFeatures { - buildConfig = true - } -} - -kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(Versions.java)) - } - - compilerOptions { - jvmTarget.set(JvmTarget.fromTarget(Versions.java)) - optIn.addAll( - "kotlin.time.ExperimentalTime", - "kotlin.ExperimentalUnsignedTypes", - "kotlin.RequiresOptIn" - ) - } -} - -dependencies { - implementation(project(":definitions:flipchat:models")) - implementation(project(":services:flipchat:core")) - api(project(":services:legacy-shared")) - implementation(project(":ui:resources")) - - implementation(Libs.kotlinx_coroutines_core) - implementation(Libs.kotlinx_serialization_json) - implementation(Libs.inject) - - implementation(platform(Libs.compose_bom)) - implementation(Libs.compose_ui) - - implementation(Libs.grpc_android) - implementation(Libs.grpc_okhttp) - implementation(Libs.grpc_kotlin) - implementation(Libs.androidx_lifecycle_runtime) - implementation(Libs.androidx_room_runtime) - implementation(Libs.androidx_room_ktx) - implementation(Libs.androidx_room_paging) - implementation(Libs.androidx_room_rxjava3) - implementation(Libs.okhttp) - implementation(Libs.mixpanel) - - implementation(platform(Libs.firebase_bom)) - implementation(Libs.firebase_crashlytics) - implementation(Libs.firebase_installations) - implementation(Libs.firebase_perf) - implementation(Libs.firebase_messaging) - - implementation(Libs.play_integrity) - - implementation(Libs.androidx_paging_runtime) - - kapt(Libs.androidx_room_compiler) - implementation(Libs.sqlcipher) - - api(Libs.google_play_billing_runtime) - api(Libs.google_play_billing_ktx) - - implementation(Libs.fingerprint_pro) - - implementation(Libs.lib_phone_number_google) - - androidTestImplementation(Libs.androidx_junit) - androidTestImplementation(Libs.junit) - androidTestImplementation(Libs.androidx_test_runner) - - implementation(Libs.hilt) - kapt(Libs.hilt_android_compiler) - kapt(Libs.hilt_compiler) - - implementation(Libs.timber) - implementation(Libs.bugsnag) -} diff --git a/services/flipchat/chat/consumer-rules.pro b/services/flipchat/chat/consumer-rules.pro deleted file mode 100644 index 5f642a47c..000000000 --- a/services/flipchat/chat/consumer-rules.pro +++ /dev/null @@ -1,6 +0,0 @@ -# Needed to keep generic signatures --keepattributes Signature - --keepclasseswithmembernames class * { - native ; -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/FcChatConfig.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/FcChatConfig.kt deleted file mode 100644 index 5c0610b17..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/FcChatConfig.kt +++ /dev/null @@ -1,9 +0,0 @@ -package xyz.flipchat.services - -import com.getcode.services.ChannelConfig -import xyz.flipchat.services.chat.BuildConfig - -internal data class FcChatConfig( - override val baseUrl: String = "chat.api.flipchat-infra.xyz", - override val userAgent: String = "Flipchat/Chat/Android/${BuildConfig.VERSION_NAME}", -): ChannelConfig diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/billing/BillingClient.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/billing/BillingClient.kt deleted file mode 100644 index 505542308..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/billing/BillingClient.kt +++ /dev/null @@ -1,68 +0,0 @@ -package xyz.flipchat.services.billing - -import android.app.Activity -import androidx.compose.runtime.staticCompositionLocalOf -import com.android.billingclient.api.BillingResult -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlin.time.Duration.Companion.seconds - -sealed interface IapPaymentEvent { - data class OnSuccess(val productId: String) : IapPaymentEvent - data object OnCancelled : IapPaymentEvent - data class OnError(val productId: String, val error: Throwable): IapPaymentEvent -} - -class IapPaymentError(val code: Int, override val message: String): Throwable(message) { - constructor(result: BillingResult): this(result.responseCode, result.debugMessage) -} - -enum class BillingClientState { - Disconnected, - Connecting, - Connected, - ConnectionLost, - Failed; - - fun canConnect() = this == Disconnected || this == ConnectionLost || this == Failed -} - -val LocalBillingClient = staticCompositionLocalOf { StubBillingClient } - -interface BillingClient { - val eventFlow: SharedFlow - val state: StateFlow - - fun connect() - fun disconnect() - fun hasPaidFor(product: IapProduct): Boolean - fun costOf(product: IapProduct): String - suspend fun purchase(activity: Activity, product: IapProduct) -} - -object StubBillingClient: BillingClient { - private val _eventFlow: MutableSharedFlow = MutableSharedFlow() - override val eventFlow: SharedFlow = _eventFlow.asSharedFlow() - - private val _stateFlow = MutableStateFlow(BillingClientState.Disconnected) - override val state: StateFlow = _stateFlow.asStateFlow() - - data class State( - val connected: Boolean = false, - val failedToConnect: Boolean = false, - ) - - override fun connect() = Unit - override fun disconnect() = Unit - override fun hasPaidFor(product: IapProduct): Boolean = false - override fun costOf(product: IapProduct): String = "NOT_DEFINED" - override suspend fun purchase(activity: Activity, product: IapProduct) { - delay(1.seconds) - _eventFlow.emit(IapPaymentEvent.OnSuccess(product.productId)) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/billing/GooglePlayBillingClient.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/billing/GooglePlayBillingClient.kt deleted file mode 100644 index 101e14773..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/billing/GooglePlayBillingClient.kt +++ /dev/null @@ -1,406 +0,0 @@ -package xyz.flipchat.services.billing - -import android.app.Activity -import android.content.Context -import android.os.Handler -import android.os.Looper -import com.android.billingclient.api.AcknowledgePurchaseParams -import com.android.billingclient.api.BillingClient.BillingResponseCode -import com.android.billingclient.api.BillingClientStateListener -import com.android.billingclient.api.BillingFlowParams -import com.android.billingclient.api.BillingResult -import com.android.billingclient.api.ConsumeParams -import com.android.billingclient.api.PendingPurchasesParams -import com.android.billingclient.api.ProductDetails -import com.android.billingclient.api.Purchase -import com.android.billingclient.api.Purchase.PurchaseState -import com.android.billingclient.api.PurchasesResponseListener -import com.android.billingclient.api.PurchasesUpdatedListener -import com.android.billingclient.api.QueryProductDetailsParams -import com.android.billingclient.api.QueryPurchasesParams -import com.android.billingclient.api.acknowledgePurchase -import com.android.billingclient.api.consumePurchase -import com.getcode.model.uuid -import com.getcode.utils.TraceType -import com.getcode.utils.trace -import com.google.common.collect.ImmutableList -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import xyz.flipchat.services.internal.network.repository.iap.InAppPurchaseRepository -import xyz.flipchat.services.user.UserManager -import kotlin.math.pow -import com.android.billingclient.api.BillingClient as GooglePlayBillingClient - - -class GooglePlayBillingClient( - @ApplicationContext context: Context, - private val userManager: UserManager, - private val purchaseRepository: InAppPurchaseRepository -) : BillingClient, PurchasesUpdatedListener { - - companion object { - private const val TAG = "IAP" - private val MAX_RETRY_ATTEMPTS = 5 - private var retryAttempt = 0 - private val baseDelayMillis = 1000L // Initial delay: 1 second - - } - - private val scope = CoroutineScope(Dispatchers.IO) - - private val _eventFlow: MutableSharedFlow = MutableSharedFlow() - override val eventFlow: SharedFlow = _eventFlow.asSharedFlow() - - private val _stateFlow = MutableStateFlow(BillingClientState.Disconnected) - override val state: StateFlow = _stateFlow.asStateFlow() - - data class State( - val connected: Boolean = false, - val failedToConnect: Boolean = false, - ) - - private val client = GooglePlayBillingClient.newBuilder(context) - .setListener(this) - .enablePendingPurchases( - PendingPurchasesParams.newBuilder() - .enableOneTimeProducts() - .build() - ) - .build() - - private val productDetails = mutableMapOf() - private val purchases = mutableMapOf() - - override fun onPurchasesUpdated( - billingResult: BillingResult, - purchases: MutableList? - ) { - printLog("onPurchasesUpdated c=${billingResult.responseCode} m=${billingResult.debugMessage}; p=${purchases?.count()}") - if (billingResult.responseCode == BillingResponseCode.OK && purchases != null) { - for (purchase in purchases) { - completePurchase(purchase) - } - } else if (billingResult.responseCode == BillingResponseCode.USER_CANCELED) { - // Handle an error caused by a user canceling the purchase flow. - scope.launch { _eventFlow.emit(IapPaymentEvent.OnCancelled) } - } - } - - override fun connect() { - if (_stateFlow.value.canConnect()) { - _stateFlow.update { BillingClientState.Connecting } - client.startConnection(clientStateListener) - } - } - - override fun disconnect() { - runCatching { - client.endConnection() - _stateFlow.update { BillingClientState.Disconnected } - } - } - - override fun hasPaidFor(product: IapProduct) = - purchases[product.productId] == PurchaseState.PURCHASED - - override fun costOf(product: IapProduct): String { - var details = productDetails[product.productId] - if (details == null) { - queryProduct(product) - details = productDetails[product.productId] - } - - if (details == null) { - scope.launch { - _eventFlow.emit( - IapPaymentEvent.OnError( - product.productId, - Throwable("Unable to resolve product details for ${product.productId}") - ) - ) - } - return " " - } - - return details.oneTimePurchaseOfferDetails?.formattedPrice ?: " " - } - - override suspend fun purchase(activity: Activity, product: IapProduct) { - var details = productDetails[product.productId] - if (details == null) { - queryProduct(product) - details = productDetails[product.productId] - } - - if (details == null) { - _eventFlow.emit( - IapPaymentEvent.OnError( - product.productId, - Throwable("Unable to resolve product details for ${product.productId}") - ) - ) - return - } - - val billingFlowParams = BillingFlowParams.newBuilder() - .setProductDetailsParamsList( - ImmutableList.of( - BillingFlowParams.ProductDetailsParams.newBuilder() - .setProductDetails(details) - .build() - ) - ) - .setObfuscatedAccountId(userManager.userId?.uuid.toString()) - .build() - - client.launchBillingFlow(activity, billingFlowParams) - } - - private fun completePurchase(item: Purchase) { - printLog("complete purchase ${item.orderId} ack=${item.isAcknowledged}") - if (!item.isAcknowledged) { - scope.launch { - printLog("onPurchaseComplete for ${item.purchaseToken}") - purchaseRepository.onPurchaseCompleted(item.purchaseToken) - .onSuccess { - acknowledgeOrConsume(item) - }.onFailure { - _eventFlow.emit( - IapPaymentEvent.OnError( - item.products.firstOrNull() ?: "NONE", - it - ) - ) - } - } - - } else { - val productId = item.products.first() - val product = IapProduct.entries.firstOrNull { it.productId == productId } - if (product != null) { - scope.launch { - _eventFlow.emit(IapPaymentEvent.OnSuccess(productId)) - } - } - } - - purchases[item.products.first()] = item.purchaseState - } - - private fun acknowledgeOrConsume(item: Purchase) { - printLog("ack or consume purchase") - val productId = item.products.first() - val product = IapProduct.entries.firstOrNull { it.productId == productId } - if (product != null) { - scope.launch { - if (product.isConsumable) { - printLog("consumable") - val consumeResult = withContext(Dispatchers.IO) { - client.consumePurchase( - ConsumeParams.newBuilder() - .setPurchaseToken(item.purchaseToken) - .build() - ) - } - - if (consumeResult.billingResult.responseCode == BillingResponseCode.OK) { - _eventFlow.emit(IapPaymentEvent.OnSuccess(productId)) - } else { - _eventFlow.emit( - IapPaymentEvent.OnError( - productId, - IapPaymentError(consumeResult.billingResult) - ) - ) - } - } else { - printLog("non-consumable") - val acknowledgeResult = withContext(Dispatchers.IO) { - client.acknowledgePurchase( - AcknowledgePurchaseParams.newBuilder() - .setPurchaseToken(item.purchaseToken) - .build() - ) - } - - if (acknowledgeResult.responseCode == BillingResponseCode.OK) { - _eventFlow.emit(IapPaymentEvent.OnSuccess(productId)) - } else { - _eventFlow.emit( - IapPaymentEvent.OnError( - productId, - IapPaymentError(acknowledgeResult) - ) - ) - } - } - } - } - } - - private fun queryProducts() { - IapProduct.entries.onEach { product -> queryProduct(product) } - } - - private fun queryProduct(product: IapProduct) { - val queryProductDetailsParams = QueryProductDetailsParams.newBuilder() - .setProductList( - ImmutableList.of( - QueryProductDetailsParams.Product.newBuilder() - .setProductId(product.productId) - .setProductType(GooglePlayBillingClient.ProductType.INAPP) - .build() - ) - ) - .build() - - client.queryProductDetailsAsync( - queryProductDetailsParams - ) { result, productDetailsList -> - printLog("products for ${product.productId} = ${productDetailsList.count()}") - if (productDetailsList.isNotEmpty()) { - productDetails[product.productId] = productDetailsList.first() - } - } - } - - private fun restorePurchases() { - val queryPurchasesParams = QueryPurchasesParams.newBuilder() - .setProductType(GooglePlayBillingClient.ProductType.INAPP) - .build() - - client.queryPurchasesAsync( - queryPurchasesParams, - restorePurchasesListener - ) - } - - private val restorePurchasesListener = PurchasesResponseListener { _, purchases -> - printLog("restore ${purchases.count()}") - purchases.onEach { completePurchase(it) } - } - - private val clientStateListener = object : BillingClientStateListener { - override fun onBillingSetupFinished( - billingResult: BillingResult - ) { - if (billingResult.responseCode == BillingResponseCode.OK) { - // Billing client connected successfully - printLog("connected!") - retryAttempt = 0 // Reset retry count - - _stateFlow.update { BillingClientState.Connected } - queryProducts() - restorePurchases() - } else { - _stateFlow.update { BillingClientState.Failed } - handleConnectionFailure(billingResult) - } - } - - override fun onBillingServiceDisconnected() { - printLog("connection lost") - _stateFlow.update { BillingClientState.ConnectionLost } - retryBillingConnection() - } - } - - private fun handleConnectionFailure(billingResult: BillingResult) { - when (billingResult.responseCode) { - BillingResponseCode.SERVICE_UNAVAILABLE -> { - trace( - tag = TAG, - message = "Billing Service is unavailable. Please check your network connection.", - type = TraceType.Silent - ) - retryBillingConnection() - } - - BillingResponseCode.SERVICE_DISCONNECTED -> { - trace( - tag = TAG, - message = "Billing Service disconnected. Retrying...", - type = TraceType.Silent - ) - retryBillingConnection() - } - - BillingResponseCode.BILLING_UNAVAILABLE -> { - trace( - tag = TAG, - message = "Billing is not available on this device. Ensure Play Store is installed.", - type = TraceType.Error - ) - } - - BillingResponseCode.ITEM_UNAVAILABLE -> { - trace( - tag = TAG, - message = "Requested item is not available.", - type = TraceType.Error - ) - } - - BillingResponseCode.ERROR -> { - trace( - tag = TAG, - message = "An unknown error occurred with billing: ${billingResult.debugMessage}", - type = TraceType.Error - ) - retryBillingConnection() - } - - BillingResponseCode.USER_CANCELED -> { - trace( - tag = TAG, - message = "User canceled the purchase flow.", - type = TraceType.Silent - ) - } - - else -> { - trace( - tag = TAG, - message = "Unhandled billing response: ${billingResult.responseCode}, ${billingResult.debugMessage}", - type = TraceType.Error - ) - retryBillingConnection() - } - } - } - - private fun retryBillingConnection() { - if (retryAttempt < MAX_RETRY_ATTEMPTS) { - val delayMillis = baseDelayMillis * (2.0.pow(retryAttempt)).toLong() - - Handler(Looper.getMainLooper()).postDelayed({ - retryAttempt++ - connect() - }, delayMillis) - - trace( - tag = TAG, - message = "Retrying connection: Attempt $retryAttempt after ${delayMillis}ms", - type = TraceType.Silent - ) - } else { - trace( - tag = TAG, - message = "Max retry attempts reached. Could not connect to billing service.", - type = TraceType.Error - ) - } - } - - private fun printLog(message: String) = println("GPBC $message") -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/billing/Products.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/billing/Products.kt deleted file mode 100644 index 3c573d75b..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/billing/Products.kt +++ /dev/null @@ -1,5 +0,0 @@ -package xyz.flipchat.services.billing - -enum class IapProduct(internal val productId: String, internal val isConsumable: Boolean) { - CreateAccount("com.flipchat.iap.createaccount", true) -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/ChatIdentifier.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/ChatIdentifier.kt deleted file mode 100644 index 250b0059a..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/ChatIdentifier.kt +++ /dev/null @@ -1,8 +0,0 @@ -package xyz.flipchat.services.data - -import com.getcode.model.ID - -sealed interface ChatIdentifier { - data class Id(val roomId: ID): ChatIdentifier - data class RoomNumber(val number: Long): ChatIdentifier -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/Member.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/Member.kt deleted file mode 100644 index fab193617..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/Member.kt +++ /dev/null @@ -1,24 +0,0 @@ -package xyz.flipchat.services.data - -import com.getcode.model.ID -import com.getcode.model.chat.Pointer -import com.getcode.model.social.user.SocialProfile -import kotlinx.serialization.Serializable - -@Serializable -data class Member( - val id: ID, - val isSelf: Boolean, - val isModerator: Boolean, - val isMuted: Boolean, - val isSpectator: Boolean, - val identity: MemberIdentity?, - val pointers: List, -) - -@Serializable -data class MemberIdentity( - val displayName: String, - val imageUrl: String?, - val socialProfiles: List, -) \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/PaymentTarget.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/PaymentTarget.kt deleted file mode 100644 index 8c9c4a807..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/PaymentTarget.kt +++ /dev/null @@ -1,7 +0,0 @@ -package xyz.flipchat.services.data - -import com.getcode.model.ID - -sealed interface PaymentTarget { - data class User(val id: ID): PaymentTarget -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/Room.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/Room.kt deleted file mode 100644 index d5f0456f1..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/Room.kt +++ /dev/null @@ -1,72 +0,0 @@ -package xyz.flipchat.services.data - -import com.getcode.model.ID -import com.getcode.model.Kin -import com.getcode.model.chat.ChatType -import com.getcode.utils.serializer.KinQuarksSerializer -import kotlinx.datetime.Instant -import kotlinx.serialization.Serializable - -@Serializable -data class RoomWithMemberCount( - val room: Room, - val members: Int -) - -data class RoomWithMembers( - val room: Room, - val members: List -) - -@Serializable -data class Room( - val id: ID, - val type: ChatType, - private val _title: String?, - val description: String?, - val ownerId: ID, - val roomNumber: Long, - private val canDisablePush: Boolean, - private val isPushEnabled: Boolean, - private val unread: Int, - private val moreUnread: Boolean, - @Serializable(with = KinQuarksSerializer::class) - val messagingFee: Kin, - private val lastActive: Long?, - val isOpen: Boolean, -) { - val title: String? - get() { - val providedTitle = _title - if (providedTitle != null) { - return providedTitle - } - return null - } - - val imageData: String? - get() { - // TODO: - return null - } - - val unreadCount: Int - get() { - return unread - } - - val hasMoreUnread: Boolean - get() { - return moreUnread - } - - val canMute: Boolean - get() = canDisablePush - - val isMuted: Boolean - get() = !isPushEnabled - - val lastActivity: Instant? - get() = lastActive?.let { Instant.fromEpochMilliseconds(it) } -} - diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/StartChatRequestType.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/StartChatRequestType.kt deleted file mode 100644 index 1d13606d8..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/StartChatRequestType.kt +++ /dev/null @@ -1,12 +0,0 @@ -package xyz.flipchat.services.data - -import com.getcode.model.ID - -interface StartChatRequestType { - data class TwoWay(val recipient: ID) : StartChatRequestType - data class Group( - val title: String? = null, - val recipients: List = emptyList(), - val paymentId: ID - ) : StartChatRequestType -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/metadata/JoinChatPaymentMetadata.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/metadata/JoinChatPaymentMetadata.kt deleted file mode 100644 index 671c23370..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/metadata/JoinChatPaymentMetadata.kt +++ /dev/null @@ -1,32 +0,0 @@ -package xyz.flipchat.services.data.metadata - -import com.codeinc.flipchat.gen.chat.v1.ChatService -import com.getcode.model.ID -import kotlinx.serialization.Serializable -import xyz.flipchat.services.internal.network.extensions.toChatId -import xyz.flipchat.services.internal.network.extensions.toUserId - -@Serializable -data class JoinChatPaymentMetadata( - val userId: ID, - val chatId: ID, -) { - companion object { - fun unerase(payload: ByteArray): JoinChatPaymentMetadata { - val proto = ChatService.JoinChatPaymentMetadata.parseFrom(payload) - return JoinChatPaymentMetadata( - chatId = proto.chatId.value.toList(), - userId = proto.userId.value.toList(), - ) - } - } -} - -// TODO: make this somehow generic -fun JoinChatPaymentMetadata.erased(): ByteArray = ChatService.JoinChatPaymentMetadata.newBuilder() - .setChatId(chatId.toChatId()) - .setUserId(userId.toUserId()) - .build().toByteArray() - -val JoinChatPaymentMetadata.typeUrl: String - get() = "type.googleapis.com/flipchat.chat.v1.JoinChatPaymentMetadata" \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/metadata/SendMessageAsListenerPaymentMetadata.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/metadata/SendMessageAsListenerPaymentMetadata.kt deleted file mode 100644 index 15ba33ed0..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/metadata/SendMessageAsListenerPaymentMetadata.kt +++ /dev/null @@ -1,32 +0,0 @@ -package xyz.flipchat.services.data.metadata - -import com.codeinc.flipchat.gen.messaging.v1.MessagingService -import com.getcode.model.ID -import kotlinx.serialization.Serializable -import xyz.flipchat.services.internal.network.extensions.toChatId -import xyz.flipchat.services.internal.network.extensions.toUserId - -@Serializable -data class SendMessageAsListenerPaymentMetadata( - val userId: ID, - val chatId: ID, -) { - companion object { - fun unerase(payload: ByteArray): SendMessageAsListenerPaymentMetadata { - val proto = MessagingService.SendMessageAsListenerPaymentMetadata.parseFrom(payload) - return SendMessageAsListenerPaymentMetadata( - chatId = proto.chatId.value.toList(), - userId = proto.userId.value.toList(), - ) - } - } -} - -// TODO: make this somehow generic -fun SendMessageAsListenerPaymentMetadata.erased(): ByteArray = MessagingService.SendMessageAsListenerPaymentMetadata.newBuilder() - .setChatId(chatId.toChatId()) - .setUserId(userId.toUserId()) - .build().toByteArray() - -val SendMessageAsListenerPaymentMetadata.typeUrl: String - get() = "type.googleapis.com/flipchat.messaging.v1.SendMessageAsListenerPaymentMetadata" \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/metadata/SendTipMessagePaymentMetadata.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/metadata/SendTipMessagePaymentMetadata.kt deleted file mode 100644 index 83bbbe779..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/metadata/SendTipMessagePaymentMetadata.kt +++ /dev/null @@ -1,35 +0,0 @@ -package xyz.flipchat.services.data.metadata - -import com.codeinc.flipchat.gen.messaging.v1.MessagingService -import com.getcode.model.ID -import kotlinx.serialization.Serializable -import xyz.flipchat.services.internal.network.extensions.toChatId -import xyz.flipchat.services.internal.network.extensions.toMessageId -import xyz.flipchat.services.internal.network.extensions.toUserId - -@Serializable -data class SendTipMessagePaymentMetadata( - val chatId: ID, - val messageId: ID, - val tipperId: ID, -) { - companion object { - fun unerase(payload: ByteArray): SendTipMessagePaymentMetadata { - val proto = MessagingService.SendTipMessagePaymentMetadata.parseFrom(payload) - return SendTipMessagePaymentMetadata( - chatId = proto.chatId.value.toList(), - tipperId = proto.tipperId.value.toList(), - messageId = proto.messageId.value.toList(), - ) - } - } -} - -fun SendTipMessagePaymentMetadata.erased(): ByteArray = MessagingService.SendTipMessagePaymentMetadata.newBuilder() - .setChatId(chatId.toChatId()) - .setMessageId(messageId.toMessageId()) - .setTipperId(tipperId.toUserId()) - .build().toByteArray() - -val SendTipMessagePaymentMetadata.typeUrl: String - get() = "type.googleapis.com/flipchat.messaging.v1.SendTipMessagePaymentMetadata" \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/metadata/StartGroupChatPaymentMetadata.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/metadata/StartGroupChatPaymentMetadata.kt deleted file mode 100644 index eec02d86b..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/data/metadata/StartGroupChatPaymentMetadata.kt +++ /dev/null @@ -1,27 +0,0 @@ -package xyz.flipchat.services.data.metadata - -import com.codeinc.flipchat.gen.chat.v1.ChatService -import com.getcode.model.ID -import kotlinx.serialization.Serializable -import xyz.flipchat.services.internal.network.extensions.toUserId - -@Serializable -data class StartGroupChatPaymentMetadata( - val userId: ID, -) { - companion object { - fun unerase(payload: ByteArray): StartGroupChatPaymentMetadata { - val proto = ChatService.StartGroupChatPaymentMetadata.parseFrom(payload) - return StartGroupChatPaymentMetadata( - userId = proto.userId.value.toList(), - ) - } - } -} - -fun StartGroupChatPaymentMetadata.erased(): ByteArray = ChatService.StartGroupChatPaymentMetadata.newBuilder() - .setUserId(userId.toUserId()) - .build().toByteArray() - -val StartGroupChatPaymentMetadata.typeUrl: String - get() = "type.googleapis.com/flipchat.chat.v1.StartGroupChatPaymentMetadata" \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/mapper/ConversationMessageMapper.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/mapper/ConversationMessageMapper.kt deleted file mode 100644 index a3b0f8eaa..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/mapper/ConversationMessageMapper.kt +++ /dev/null @@ -1,34 +0,0 @@ -package xyz.flipchat.services.domain.mapper - -import xyz.flipchat.services.domain.model.chat.ConversationMessage -import com.getcode.model.ID -import com.getcode.services.mapper.Mapper -import com.getcode.model.chat.ChatMessage -import com.getcode.utils.base58 -import javax.inject.Inject - -class ConversationMessageMapper @Inject constructor() : - Mapper, ConversationMessage> { - override fun map(from: Pair): ConversationMessage { - val (conversationId, message) = from - - val content = message.contents.first() - - return ConversationMessage( - idBase58 = message.id.base58, - conversationIdBase58 = conversationId.base58, - senderIdBase58 = message.senderId.base58, - dateMillis = message.dateMillis, - type = content.kind, - content = content.content, - sentOffStage = message.wasSentOffStage, - // deletions happen as a by-product of a received message with delete content type - deleted = false, - deletedByBase58 = null, - // replies happen as a by-product of a received message with reply content type - inReplyToBase58 = null, - // approvals (or rejections) happen as a by-product of a received message with review content type - isApproved = null, - ) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/mapper/RoomConversationMapper.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/mapper/RoomConversationMapper.kt deleted file mode 100644 index 758df05a7..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/mapper/RoomConversationMapper.kt +++ /dev/null @@ -1,27 +0,0 @@ -package xyz.flipchat.services.domain.mapper - -import xyz.flipchat.services.data.Room -import xyz.flipchat.services.domain.model.chat.Conversation -import com.getcode.services.mapper.Mapper -import com.getcode.utils.base58 -import javax.inject.Inject - -class RoomConversationMapper @Inject constructor() : Mapper { - override fun map(from: Room): Conversation { - return Conversation( - idBase58 = from.id.base58, - ownerIdBase58 = from.ownerId.base58, - title = from.title.orEmpty(), - description = from.description, - imageUri = from.imageData, - unreadCount = from.unreadCount, - hasMoreUnread = from.hasMoreUnread, - isMuted = from.isMuted, - canMute = from.canMute, - roomNumber = from.roomNumber, - messagingFee = from.messagingFee.quarks, - lastActivity = from.lastActivity?.toEpochMilliseconds(), - isOpen = from.isOpen - ) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/Conversation.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/Conversation.kt deleted file mode 100644 index 87129518d..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/Conversation.kt +++ /dev/null @@ -1,127 +0,0 @@ -package xyz.flipchat.services.domain.model.chat - -import androidx.room.ColumnInfo -import androidx.room.Embedded -import androidx.room.Entity -import androidx.room.Ignore -import androidx.room.PrimaryKey -import androidx.room.Relation -import com.getcode.model.ID -import com.getcode.model.Kin -import com.getcode.model.chat.MessageContent -import com.getcode.model.chat.MessageStatus -import com.getcode.utils.serializer.KinQuarksSerializer -import com.getcode.vendor.Base58 -import kotlinx.serialization.Serializable -import java.util.UUID - -@Serializable -@Entity(tableName = "conversations") -data class Conversation( - @PrimaryKey - val idBase58: String, - val ownerIdBase58: String?, - val title: String, - val description: String?, - @ColumnInfo(defaultValue = "0") - val roomNumber: Long, - val imageUri: String?, - val lastActivity: Long?, - val isMuted: Boolean, - @ColumnInfo(defaultValue = "true") - val canMute: Boolean, - val unreadCount: Int, - @ColumnInfo(name = "coverChargeQuarks") - val messagingFee: Long?, - @ColumnInfo(defaultValue = "false") - val hasMoreUnread: Boolean, - @ColumnInfo(defaultValue = "true") - val isOpen: Boolean, -) { - @Ignore - val id: ID = Base58.decode(idBase58).toList() - - @Ignore - val ownerId: ID? = ownerIdBase58?.let { Base58.decode(it).toList() } - - @Ignore - @Serializable(with = KinQuarksSerializer::class) - val coverCharge: Kin = messagingFee?.let { Kin.fromQuarks(messagingFee) } ?: Kin.fromQuarks(0) -} - -data class ConversationWithMembersAndLastPointers( - @Embedded val conversation: Conversation, - @Relation( - parentColumn = "idBase58", - entityColumn = "conversationIdBase58", - entity = ConversationMember::class - ) - val members: List, - @Relation( - parentColumn = "idBase58", - entityColumn = "conversationIdBase58", - entity = ConversationPointerCrossRef::class, - ) - val pointersCrossRef: List, -) { - val pointers: Map - get() { - return pointersCrossRef - .associateBy { it.status } - .mapKeys { it.value.messageId } - .mapValues { it.value.status } - } -} - -data class ConversationWithMembers( - @Embedded val conversation: Conversation, - @Relation( - parentColumn = "idBase58", - entityColumn = "conversationIdBase58" - ) - val members: List, -) - - -data class ConversationWithMembersAndLastMessage( - @Embedded val conversation: Conversation, - @Relation( - parentColumn = "idBase58", - entityColumn = "conversationIdBase58" - ) - val members: List, - @Relation( - parentColumn = "idBase58", - entityColumn = "conversationIdBase58", - entity = ConversationMessage::class, - projection = ["idBase58", "dateMillis", "senderIdBase58", "type", "content", "tipCount", "reactionCount", "isApproved", "sentOffStage"] - ) - val lastMessage: ConversationMessage? -) { - val id: ID - get() = conversation.id - val title: String - get() = conversation.title - val imageUri: String? - get() = conversation.imageUri - val lastActivity: Long? - get() = conversation.lastActivity - val isMuted: Boolean - get() = conversation.isMuted - val canChangeMuteState: Boolean - get() = conversation.canMute - val unreadCount: Int - get() = conversation.unreadCount - val hasMoreUnread: Boolean - get() = conversation.hasMoreUnread - - val ownerId: ID? - get() = conversation.ownerId - - val messageContentPreview: MessageContent? - get() = lastMessage?.let { MessageContent.fromData(it.type, it.content, false) } - - @Ignore - var pageIndex: Int? = null -} - diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/ConversationMember.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/ConversationMember.kt deleted file mode 100644 index 9fec88ec8..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/ConversationMember.kt +++ /dev/null @@ -1,84 +0,0 @@ -package xyz.flipchat.services.domain.model.chat - -import androidx.room.ColumnInfo -import androidx.room.Embedded -import androidx.room.Entity -import androidx.room.Ignore -import androidx.room.Index -import androidx.room.Relation -import com.getcode.model.ID -import com.getcode.model.social.user.XExtraData -import com.getcode.vendor.Base58 -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import xyz.flipchat.services.domain.model.profile.MemberSocialProfile -import xyz.flipchat.services.domain.model.people.FlipchatUser -import xyz.flipchat.services.domain.model.people.MemberPersonalInfo - -@Serializable -@Entity( - tableName = "members", - primaryKeys = ["memberIdBase58", "conversationIdBase58"], - indices = [ - Index(value = ["memberIdBase58"]), - Index(value = ["conversationIdBase58"]) - ] -) -data class ConversationMember( - val memberIdBase58: String, - val conversationIdBase58: String, - @ColumnInfo(defaultValue = "false") - val isHost: Boolean, // isModerator - @ColumnInfo(defaultValue = "false") - val isMuted: Boolean, - @ColumnInfo(defaultValue = "false") - val isFullMember: Boolean, -) { - @Ignore - val id: ID = Base58.decode(memberIdBase58).toList() - - @Ignore - val conversationId: ID = Base58.decode(conversationIdBase58).toList() -} - -data class ConversationMemberWithLinkedSocialProfiles( - @Embedded val member: ConversationMember?, - @Relation( - parentColumn = "memberIdBase58", - entityColumn = "userIdBase58", - projection = ["memberName", "imageUri", "isBlocked"], - entity = FlipchatUser::class - ) - private val personalInfo: MemberPersonalInfo?, - @Relation( - parentColumn = "memberIdBase58", - entityColumn = "memberIdBase58", - entity = MemberSocialProfile::class, - ) - val profiles: List -) { - val id: ID? - get() = member?.id - - val displayName: String? - get() { - return personalInfo?.memberName - } - - val imageUri: String? - get() { - return personalInfo?.imageUri - } - - val isBlocked: Boolean - get() = personalInfo?.isBlocked == true - - val isFullMember: Boolean - get() = member?.isFullMember == true - - val isHost: Boolean - get() = member?.isHost == true - - val isMuted: Boolean - get() = member?.isMuted == true -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/ConversationMessage.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/ConversationMessage.kt deleted file mode 100644 index 7683cf87b..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/ConversationMessage.kt +++ /dev/null @@ -1,219 +0,0 @@ -package xyz.flipchat.services.domain.model.chat - -import androidx.room.ColumnInfo -import androidx.room.Embedded -import androidx.room.Entity -import androidx.room.Ignore -import androidx.room.Index -import androidx.room.PrimaryKey -import androidx.room.Relation -import com.getcode.model.ID -import com.getcode.model.chat.MessageContent -import com.getcode.vendor.Base58 -import kotlinx.serialization.Serializable -import xyz.flipchat.services.domain.model.people.FlipchatUser -import xyz.flipchat.services.domain.model.people.MemberPersonalInfo -import xyz.flipchat.services.domain.model.profile.MemberSocialProfile - -@Serializable -@Entity( - tableName = "messages", - indices = [ - Index(value = ["conversationIdBase58"]), // For filtering by conversation ID - Index(value = ["senderIdBase58"]), // For joining on sender ID - Index(value = ["dateMillis"]), // For ordering by date - ] -) -data class ConversationMessage( - @PrimaryKey - val idBase58: String, - val conversationIdBase58: String, - @ColumnInfo(defaultValue = "") - val senderIdBase58: String, - val dateMillis: Long, - private val deleted: Boolean?, - private val deletedByBase58: String? = null, - val inReplyToBase58: String? = null, - @ColumnInfo(defaultValue = "false") - val sentOffStage: Boolean = false, - val isApproved: Boolean? = null, - @ColumnInfo(defaultValue = "0") - val tipCount: Int = 0, - @ColumnInfo(defaultValue = "0") - val reactionCount: Int = 0, - @ColumnInfo(defaultValue = "1") - val type: Int, - @ColumnInfo(defaultValue = "") - val content: String, -) { - fun getDeletedByBase58(): String? = deletedByBase58 - - @Ignore - val id: ID = Base58.decode(idBase58).toList() - - @Ignore - val conversationId: ID = Base58.decode(conversationIdBase58).toList() - - @Ignore - val senderId: ID = Base58.decode(senderIdBase58).toList() - - @Ignore - val deletedBy: ID? = deletedByBase58?.let { Base58.decode(deletedByBase58).toList() } - - @Ignore - val isDeleted: Boolean = deleted == true - - @Ignore - val inReplyTo: ID? = inReplyToBase58?.let { Base58.decode(it).toList() } -} - -data class ConversationMessageWithMemberAndReply( - @Embedded val message: ConversationMessage, - @Relation( - parentColumn = "senderIdBase58", - entityColumn = "memberIdBase58", - entity = ConversationMember::class, - ) - val member: ConversationMember?, -) - - -data class ConversationMessageWithMemberAndContent( - @Embedded val message: ConversationMessage, - - // Member information from members table - @Relation( - parentColumn = "senderIdBase58", - entityColumn = "memberIdBase58", - entity = ConversationMember::class - ) - val member: ConversationMember?, - - @Relation( - parentColumn = "senderIdBase58", - entityColumn = "userIdBase58", - projection = ["memberName", "imageUri", "isBlocked"], - entity = FlipchatUser::class, - ) - val personalInfo: MemberPersonalInfo? -) { - @Ignore - var socialProfiles: List = emptyList() - @Ignore - var contentEntity: MessageContent = MessageContent.Unknown(false) -} - -data class InflatedConversationMessage( - val pageIndex: Int = 0, // tracking for [PagingSource] refresh eky - val message: ConversationMessage, - val member: ConversationMemberWithLinkedSocialProfiles?, - val content: MessageContent, - val reply: ConversationMessageWithMemberAndContent?, - val tips: List, - val reactions: List -) - -data class MessageTipInfo( - @Embedded val tip: ConversationMessageTip, - @Relation( - parentColumn = "tipperIdBase58", - entityColumn = "memberIdBase58", - entity = ConversationMember::class, - ) - val tipper: ConversationMember?, - @Relation( - parentColumn = "tipperIdBase58", - entityColumn = "userIdBase58", - projection = ["memberName", "imageUri", "isBlocked"], - entity = FlipchatUser::class - ) - val personalInfo: MemberPersonalInfo?, - - @Relation( - parentColumn = "tipperIdBase58", - entityColumn = "memberIdBase58", - entity = MemberSocialProfile::class - ) - val socialProfiles: List, -) - -data class MessageReactionInfo( - @Embedded val reaction: ConversationMessageReaction, - @Relation( - parentColumn = "senderIdBase58", - entityColumn = "memberIdBase58", - entity = ConversationMember::class, - ) - val sender: ConversationMember?, - @Relation( - parentColumn = "senderIdBase58", - entityColumn = "userIdBase58", - projection = ["memberName", "imageUri", "isBlocked"], - entity = FlipchatUser::class - ) - val personalInfo: MemberPersonalInfo?, - - @Relation( - parentColumn = "senderIdBase58", - entityColumn = "memberIdBase58", - entity = MemberSocialProfile::class - ) - val socialProfiles: List, -) - -fun List.deletedMessages(selfId: ID?): List { - return mapNotNull { m -> - MessageContent.fromData( - type = m.type, - content = m.content, - isFromSelf = m.senderId == selfId, - ) as? MessageContent.DeletedMessage - } -} - -fun List.replies(selfId: ID?): List> { - return mapNotNull { m -> - val originalMessageId = (MessageContent.fromData( - type = m.type, - content = m.content, - isFromSelf = m.senderId == selfId, - ) as? MessageContent.Reply)?.originalMessageId ?: return@mapNotNull null - - m.id to originalMessageId - } -} - -fun List.tips(selfId: ID?): List> { - return mapNotNull { m -> - val tipContent = MessageContent.fromData( - type = m.type, - content = m.content, - isFromSelf = m.senderId == selfId, - ) as? MessageContent.MessageTip ?: return@mapNotNull null - - m.id to tipContent - } -} - -fun List.reactions(selfId: ID?): List> { - return mapNotNull { m -> - val reactionContent = MessageContent.fromData( - type = m.type, - content = m.content, - isFromSelf = m.senderId == selfId, - ) as? MessageContent.Reaction ?: return@mapNotNull null - - m.id to reactionContent - } -} - -fun List.reviews(selfId: ID?): List { - return mapNotNull { m -> - MessageContent.fromData( - type = m.type, - content = m.content, - isFromSelf = m.senderId == selfId, - ) as? MessageContent.MessageInReview ?: return@mapNotNull null - } -} - diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/ConversationMessageReaction.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/ConversationMessageReaction.kt deleted file mode 100644 index f40b90541..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/ConversationMessageReaction.kt +++ /dev/null @@ -1,37 +0,0 @@ -package xyz.flipchat.services.domain.model.chat - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.Ignore -import androidx.room.Index -import androidx.room.PrimaryKey -import com.getcode.model.ID -import com.getcode.vendor.Base58 -import kotlinx.serialization.Serializable - -@Serializable -@Entity( - tableName = "reactions", - indices = [ - Index(value = ["messageIdBase58", "senderIdBase58", "emoji"]), - ], -) -data class ConversationMessageReaction( - @PrimaryKey - val idBase58: String, - val messageIdBase58: String, - val senderIdBase58: String, - val emoji: String, - val deleted: Boolean = false, - @ColumnInfo(defaultValue = "0") - val sentAt: Long, -) { - @Ignore - val id: ID = Base58.decode(idBase58).toList() - - @Ignore - val messageId: ID = Base58.decode(messageIdBase58).toList() - - @Ignore - val senderId: ID = Base58.decode(senderIdBase58).toList() -} diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/ConversationMessageTip.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/ConversationMessageTip.kt deleted file mode 100644 index 62885292f..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/ConversationMessageTip.kt +++ /dev/null @@ -1,37 +0,0 @@ -package xyz.flipchat.services.domain.model.chat - -import androidx.room.Entity -import androidx.room.Ignore -import androidx.room.Index -import androidx.room.PrimaryKey -import com.getcode.model.ID -import com.getcode.model.KinAmount -import com.getcode.vendor.Base58 -import kotlinx.serialization.Serializable - -@Serializable -@Entity( - tableName = "tips", - indices = [ - Index(value = ["messageIdBase58"]), - ], -) -data class ConversationMessageTip( - @PrimaryKey - val idBase58: String, - val messageIdBase58: String, - val amount: Long, - val tipperIdBase58: String, -) { - @Ignore - val id: ID = Base58.decode(idBase58).toList() - - @Ignore - val messageId: ID = Base58.decode(messageIdBase58).toList() - - @Ignore - val tipperId: ID = Base58.decode(tipperIdBase58).toList() - - @Ignore - val kin: KinAmount = KinAmount.fromQuarks(amount) -} diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/Pointers.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/Pointers.kt deleted file mode 100644 index b22bdb68d..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/Pointers.kt +++ /dev/null @@ -1,26 +0,0 @@ -package xyz.flipchat.services.domain.model.chat - -import androidx.room.Entity -import androidx.room.Ignore -import com.getcode.model.ID -import com.getcode.model.chat.MessageStatus -import com.getcode.vendor.Base58 -import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient -import java.util.UUID - - -@Serializable -@Entity(tableName = "conversation_pointers", primaryKeys = ["conversationIdBase58", "status"]) -data class ConversationPointerCrossRef( - val conversationIdBase58: String, - val messageIdString: String, - val status: MessageStatus, -) { - @Ignore - val conversationId: ID = Base58.decode(conversationIdBase58).toList() - - @Ignore - @Transient - val messageId: UUID = UUID.fromString(messageIdString) -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/StreamMemberUpdate.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/StreamMemberUpdate.kt deleted file mode 100644 index 39a7fa43c..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/StreamMemberUpdate.kt +++ /dev/null @@ -1,76 +0,0 @@ -package xyz.flipchat.services.domain.model.chat - -import com.codeinc.flipchat.gen.chat.v1.ChatService -import com.getcode.model.ID -import com.getcode.model.uuid -import xyz.flipchat.services.data.Member -import xyz.flipchat.services.data.MemberIdentity - -sealed interface StreamMemberUpdate { - // Refreshes the state of the entire chat membership - data class Refresh(val members: List): StreamMemberUpdate { - override fun toString(): String { - return "FULL REFRESH:: ${members.count()}" - } - } - // Refreshes the state of an individual member in the chat - data class IndividualRefresh(val member: Member): StreamMemberUpdate { - override fun toString(): String { - return "INDIVIDUAL REFRESH:: ${member.id.uuid}" - } - } - // Member joined the chat via the JoinChat RPC - data class Joined(val member: Member): StreamMemberUpdate { - override fun toString(): String { - return "JOINED:: ${member.id.uuid}" - } - } - // Member left the chat via the LeaveChat RPC - data class Left(val memberId: ID): StreamMemberUpdate { - override fun toString(): String { - return "LEFT:: ${memberId.uuid}" - } - } - // Member was removed from the chat via the RemoveUser RPC - data class Removed(val memberId: ID, val removedBy: ID): StreamMemberUpdate { - override fun toString(): String { - return "REMOVED:: ${memberId.uuid} by ${removedBy.uuid}" - } - } - // Member was muted in the chat via the MuteUser RPC - data class Muted(val memberId: ID, val mutedBy: ID): StreamMemberUpdate { - override fun toString(): String { - return "MUTED:: ${memberId.uuid} by ${mutedBy.uuid}" - } - } - - // Member was promoted in the chat via the PromoteUser RPC - data class Promoted(val memberId: ID, val by: ID): StreamMemberUpdate { - override fun toString(): String { - return "PROMOTED:: ${memberId.uuid} by ${by.uuid}" - } - } - - // Member was demoted in the chat via the DemoteUser RPC - data class Demoted(val memberId: ID, val by: ID): StreamMemberUpdate { - override fun toString(): String { - return "DEMOTED:: ${memberId.uuid} by ${by.uuid}" - } - } - - data class IdentityChanged(val memberId: ID, val identity: MemberIdentity): StreamMemberUpdate { - override fun toString(): String { - return "IDENTITY:: ${memberId.uuid}" - } - } -} - -sealed interface StreamMetadataUpdate { - data class Refresh(val metadata: ChatService.Metadata): StreamMetadataUpdate - data class UnreadCount(val numUnread: Int, val hasMoreUnread: Boolean): StreamMetadataUpdate - data class DisplayName(val name: String): StreamMetadataUpdate - data class MessagingFee(val amount: Long): StreamMetadataUpdate - data class LastActivity(val timestamp: Long): StreamMetadataUpdate - data class OpenStatusChanged(val nowOpen: Boolean): StreamMetadataUpdate - data class Description(val description: String): StreamMetadataUpdate -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/db/ChatUpdate.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/db/ChatUpdate.kt deleted file mode 100644 index 331dc321a..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/chat/db/ChatUpdate.kt +++ /dev/null @@ -1,37 +0,0 @@ -package xyz.flipchat.services.domain.model.chat.db - -import com.getcode.model.ID -import xyz.flipchat.services.data.Member -import xyz.flipchat.services.data.MemberIdentity -import xyz.flipchat.services.domain.model.chat.Conversation -import xyz.flipchat.services.domain.model.chat.ConversationMessage - -data class ChatUpdate( - val metadata: List, - val message: ConversationMessage?, - val members: List, -) - -sealed interface ConversationMemberUpdate { - data class FullRefresh(val roomId: ID, val members: List): ConversationMemberUpdate - data class IndividualRefresh(val roomId: ID, val member: Member): ConversationMemberUpdate - data class Joined(val roomId: ID, val member: Member): ConversationMemberUpdate - data class Left(val roomId: ID, val memberId: ID): ConversationMemberUpdate - data class Removed(val roomId: ID, val memberId: ID, val removedBy: ID): ConversationMemberUpdate - data class Muted(val roomId: ID, val memberId: ID, val mutedBy: ID): ConversationMemberUpdate - data class Promoted(val roomId: ID, val memberId: ID, val by: ID): ConversationMemberUpdate - data class Demoted(val roomId: ID, val memberId: ID, val by: ID): ConversationMemberUpdate - data class IdentityChanged(val memberId: ID, val identity: MemberIdentity): ConversationMemberUpdate -} - -sealed interface ConversationUpdate { - data class Refresh(val conversation: Conversation): ConversationUpdate - data class UnreadCount(val roomId: ID, val numUnread: Int, val hasMoreUnread: Boolean): ConversationUpdate - data class DisplayName(val roomId: ID, val name: String): ConversationUpdate - data class CoverCharge(val roomId: ID, val amount: Long): ConversationUpdate - data class LastActivity(val roomId: ID, val timestamp: Long): ConversationUpdate - data class OpenStatus(val roomId: ID, val nowOpen: Boolean): ConversationUpdate - data class Description(val roomId: ID, val description: String): ConversationUpdate -} - - diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/people/FlipchatUser.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/people/FlipchatUser.kt deleted file mode 100644 index 73621bb42..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/people/FlipchatUser.kt +++ /dev/null @@ -1,48 +0,0 @@ -package xyz.flipchat.services.domain.model.people - -import androidx.room.Embedded -import androidx.room.Entity -import androidx.room.Ignore -import androidx.room.PrimaryKey -import androidx.room.Relation -import com.getcode.model.ID -import com.getcode.vendor.Base58 -import kotlinx.serialization.Serializable -import xyz.flipchat.services.domain.model.profile.MemberSocialProfile -import xyz.flipchat.services.internal.data.mapper.nullIfEmpty - -@Serializable -@Entity(tableName = "users",) -data class FlipchatUser( - @PrimaryKey - val userIdBase58: String, - val memberName: String?, - val imageUri: String?, - val isBlocked: Boolean? = null -) { - @Ignore - val id: ID = Base58.decode(userIdBase58).toList() -} - -data class FlipchatUserWithSocialProfiles( - @Embedded val user: FlipchatUser, - @Relation( - parentColumn = "userIdBase58", - entityColumn = "memberIdBase58", - entity = MemberSocialProfile::class, - ) - val profiles: List -) { - val imageData: Any - get() { - return profiles.firstOrNull()?.profileImageUrl.nullIfEmpty() - ?: user.imageUri.nullIfEmpty() - ?: user.id - } -} - -data class MemberPersonalInfo( - val memberName: String?, - val imageUri: String?, - val isBlocked: Boolean -) diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/profile/MemberSocialProfile.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/profile/MemberSocialProfile.kt deleted file mode 100644 index c2e9b7ee5..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/profile/MemberSocialProfile.kt +++ /dev/null @@ -1,34 +0,0 @@ -package xyz.flipchat.services.domain.model.profile - -import androidx.room.Entity -import androidx.room.PrimaryKey -import com.getcode.model.chat.LinkedSocialProfile - -@Entity( - tableName = "social_profiles" -) -data class MemberSocialProfile( - @PrimaryKey - val id: String, - val memberIdBase58: String, - val platformType: String, - val username: String, - val profileImageUrl: String?, - val verified: Boolean = false, - val extraData: String?, -) - -fun MemberSocialProfile.toLinked(): LinkedSocialProfile? { - return when (platformType) { - "x" -> { - LinkedSocialProfile( - platformType = platformType, - username = username, - profileImageUrl = profileImageUrl, - isVerifiedOnPlatform = verified, - rawMetadata = extraData - ) - } - else -> null - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/profile/UserProfile.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/profile/UserProfile.kt deleted file mode 100644 index 518fdb5ce..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/profile/UserProfile.kt +++ /dev/null @@ -1,8 +0,0 @@ -package xyz.flipchat.services.domain.model.profile - -import com.getcode.model.social.user.SocialProfile - -data class UserProfile( - val displayName: String, - val socialProfiles: List -) diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/query/QueryOptions.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/query/QueryOptions.kt deleted file mode 100644 index f40f700f9..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/domain/model/query/QueryOptions.kt +++ /dev/null @@ -1,9 +0,0 @@ -package xyz.flipchat.services.domain.model.query - -typealias PagingToken = List - -data class QueryOptions( - val limit: Int = 100, - val token: PagingToken? = null, - val descending: Boolean = true -) diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/extensions/Conversation.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/extensions/Conversation.kt deleted file mode 100644 index cd63f5738..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/extensions/Conversation.kt +++ /dev/null @@ -1,36 +0,0 @@ -package xyz.flipchat.services.extensions - -import com.getcode.util.resources.ResourceHelper -import xyz.flipchat.services.chat.R -import xyz.flipchat.services.data.Room -import xyz.flipchat.services.domain.model.chat.Conversation - -fun Conversation.titleOrFallback(resources: ResourceHelper): String { - return if (title.isEmpty()) { - resources.getString( - R.string.title_implicitRoomTitleWithoutPrefix, - roomNumber - ) - } else { - resources.getString( - R.string.title_explicitRoomTitle, - roomNumber, - title - ) - } -} - -fun Room.titleOrFallback(resources: ResourceHelper): String { - return if (title == null) { - resources.getString( - R.string.title_implicitRoomTitleWithoutPrefix, - roomNumber - ) - } else { - resources.getString( - R.string.title_explicitRoomTitle, - roomNumber, - title!! - ) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/annotations/ChatManagedChannel.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/annotations/ChatManagedChannel.kt deleted file mode 100644 index beacaca71..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/annotations/ChatManagedChannel.kt +++ /dev/null @@ -1,13 +0,0 @@ -package xyz.flipchat.services.internal.annotations - -import javax.inject.Qualifier - -@Qualifier -@Target( - AnnotationTarget.FUNCTION, - AnnotationTarget.PROPERTY_GETTER, - AnnotationTarget.PROPERTY_SETTER, - AnnotationTarget.VALUE_PARAMETER, - AnnotationTarget.FIELD -) -annotation class ChatManagedChannel \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/ChatMessageMapper.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/ChatMessageMapper.kt deleted file mode 100644 index 163dc2bb7..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/ChatMessageMapper.kt +++ /dev/null @@ -1,37 +0,0 @@ -package xyz.flipchat.services.internal.data.mapper - -import com.codeinc.flipchat.gen.messaging.v1.Model -import com.getcode.model.ID -import com.getcode.model.chat.ChatMessage -import com.getcode.model.chat.MessageContent -import com.getcode.model.uuid -import com.getcode.services.mapper.Mapper -import com.getcode.utils.timestamp -import xyz.flipchat.services.internal.protomapping.invoke -import javax.inject.Inject - -class ChatMessageMapper @Inject constructor(): Mapper, ChatMessage> { - override fun map(from: Pair): ChatMessage { - val (selfId, message) = from - val messageId = message.messageId.value.toByteArray().toList() - val messageSenderId = message.senderId.value.toByteArray().toList() - val isFromSelf = selfId == messageSenderId - - val timestamp = messageId.uuid?.timestamp ?: (message.ts.seconds * 1_000L) - return ChatMessage( - id = messageId, - senderId = messageSenderId, - isFromSelf = isFromSelf, - dateMillis = timestamp, - wasSentOffStage = message.wasSenderOffStage, - contents = message.contentList.mapNotNull { - MessageContent.invoke( - it, - messageSenderId, - timestamp, - isFromSelf - ) - }, - ) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/ConversationMemberMapper.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/ConversationMemberMapper.kt deleted file mode 100644 index f74e5c1f8..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/ConversationMemberMapper.kt +++ /dev/null @@ -1,21 +0,0 @@ -package xyz.flipchat.services.internal.data.mapper - -import com.getcode.model.ID -import com.getcode.services.mapper.Mapper -import com.getcode.utils.base58 -import xyz.flipchat.services.data.Member -import xyz.flipchat.services.domain.model.chat.ConversationMember -import javax.inject.Inject - -class ConversationMemberMapper @Inject constructor(): Mapper, ConversationMember> { - override fun map(from: Pair): ConversationMember { - val (conversationId, member) = from - return ConversationMember( - memberIdBase58 = member.id.base58, - conversationIdBase58 = conversationId.base58, - isHost = member.isModerator, - isMuted = member.isMuted, - isFullMember = !member.isSpectator, - ) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/LastMessageMapper.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/LastMessageMapper.kt deleted file mode 100644 index 2d6ba055e..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/LastMessageMapper.kt +++ /dev/null @@ -1,39 +0,0 @@ -package xyz.flipchat.services.internal.data.mapper - -import com.codeinc.flipchat.gen.messaging.v1.Model -import com.getcode.model.ID -import com.getcode.model.chat.ChatMessage -import com.getcode.model.chat.MessageContent -import com.getcode.model.uuid -import com.getcode.services.mapper.Mapper -import com.getcode.utils.timestamp -import xyz.flipchat.services.internal.protomapping.invoke -import javax.inject.Inject - -class LastMessageMapper @Inject constructor( -): Mapper, ChatMessage> { - override fun map(from: Pair): ChatMessage { - val (userId, message) = from - val messageId = message.messageId.value.toByteArray().toList() - val messageSenderId = message.senderId.value.toByteArray().toList() - val isFromSelf = userId == messageSenderId - - val timestamp = messageId.uuid?.timestamp ?: (message.ts.seconds * 1_000L) - - return ChatMessage( - id = messageId, - senderId = messageSenderId, - isFromSelf = isFromSelf, - wasSentOffStage = message.wasSenderOffStage, - dateMillis = timestamp, - contents = message.contentList.mapNotNull { - MessageContent.invoke( - it, - messageSenderId, - timestamp, - isFromSelf - ) - }, - ) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/MemberIdentityMapper.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/MemberIdentityMapper.kt deleted file mode 100644 index 1da7a4991..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/MemberIdentityMapper.kt +++ /dev/null @@ -1,21 +0,0 @@ -package xyz.flipchat.services.internal.data.mapper - -import com.codeinc.flipchat.gen.chat.v1.ChatService -import com.getcode.model.social.user.SocialProfile -import com.getcode.services.mapper.Mapper -import xyz.flipchat.services.data.MemberIdentity -import javax.inject.Inject - -class MemberIdentityMapper @Inject constructor( - private val socialProfileMapper: SocialProfileMapper -): Mapper { - override fun map(from: ChatService.MemberIdentity): MemberIdentity { - return MemberIdentity( - displayName = from.displayName, - imageUrl = from.profilePicUrl, - socialProfiles = from.socialProfilesList - .map { socialProfileMapper.map(it) } - .filterNot { it is SocialProfile.Unknown } - ) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/MemberMapper.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/MemberMapper.kt deleted file mode 100644 index 83a973b50..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/MemberMapper.kt +++ /dev/null @@ -1,24 +0,0 @@ -package xyz.flipchat.services.internal.data.mapper - -import com.codeinc.flipchat.gen.chat.v1.ChatService -import com.getcode.services.mapper.Mapper -import xyz.flipchat.services.data.Member -import javax.inject.Inject - -class MemberMapper @Inject constructor( - private val identityMapper: MemberIdentityMapper, - private val pointerModelMapper: PointerModelMapper, -): Mapper { - override fun map(from: ChatService.Member): Member { - val memberId = from.userId.value.toByteArray().toList() - return Member( - id = memberId, - isSelf = from.isSelf, - isModerator = from.hasModeratorPermission, - isMuted = from.isMuted, - isSpectator = !from.hasSendPermission, - identity = identityMapper.map(from.identity), - pointers = from.pointersList.mapNotNull { pointerModelMapper.map(memberId to it) } - ) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/MemberUpdateMapper.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/MemberUpdateMapper.kt deleted file mode 100644 index e95bde726..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/MemberUpdateMapper.kt +++ /dev/null @@ -1,76 +0,0 @@ -package xyz.flipchat.services.internal.data.mapper - -import com.codeinc.flipchat.gen.chat.v1.ChatService.MemberUpdate as ApiMemberUpdate -import com.getcode.services.mapper.Mapper -import xyz.flipchat.services.domain.model.chat.StreamMemberUpdate -import javax.inject.Inject - - -class MemberUpdateMapper @Inject constructor( - private val memberMapper: MemberMapper, - private val identityMapper: MemberIdentityMapper, -) : Mapper { - override fun map(from: ApiMemberUpdate): StreamMemberUpdate? { - val result = when (from.kindCase) { - ApiMemberUpdate.KindCase.FULL_REFRESH -> { - StreamMemberUpdate.Refresh(members = from.fullRefresh.membersList.map { - memberMapper.map( - it - ) - }) - } - - ApiMemberUpdate.KindCase.LEFT -> { - StreamMemberUpdate.Left(memberId = from.left.member.value.toList()) - } - - ApiMemberUpdate.KindCase.JOINED -> { - StreamMemberUpdate.Joined(member = memberMapper.map(from.joined.member)) - } - - ApiMemberUpdate.KindCase.INDIVIDUAL_REFRESH -> { - StreamMemberUpdate.IndividualRefresh(member = memberMapper.map(from.joined.member)) - } - - ApiMemberUpdate.KindCase.MUTED -> { - StreamMemberUpdate.Muted( - memberId = from.muted.member.value.toList(), - mutedBy = from.muted.mutedBy.value.toList() - ) - } - - ApiMemberUpdate.KindCase.REMOVED -> { - StreamMemberUpdate.Removed( - memberId = from.muted.member.value.toList(), - removedBy = from.muted.mutedBy.value.toList() - ) - } - - ApiMemberUpdate.KindCase.PROMOTED -> { - StreamMemberUpdate.Promoted( - memberId = from.promoted.member.value.toList(), - by = from.promoted.promotedBy.value.toList(), - ) - } - - ApiMemberUpdate.KindCase.DEMOTED -> { - StreamMemberUpdate.Demoted( - memberId = from.demoted.member.value.toList(), - by = from.demoted.demotedBy.value.toList(), - ) - } - - ApiMemberUpdate.KindCase.IDENTITY_CHANGED -> { - StreamMemberUpdate.IdentityChanged( - memberId = from.identityChanged.member.value.toList(), - identity = identityMapper.map(from.identityChanged.newIdentity), - ) - } - - ApiMemberUpdate.KindCase.KIND_NOT_SET -> null - else -> null - } - - return result - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/MetadataRoomMapper.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/MetadataRoomMapper.kt deleted file mode 100644 index bc794c4eb..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/MetadataRoomMapper.kt +++ /dev/null @@ -1,34 +0,0 @@ -package xyz.flipchat.services.internal.data.mapper - -import com.codeinc.flipchat.gen.chat.v1.ChatService -import com.codeinc.flipchat.gen.chat.v1.lastActivityOrNull -import com.codeinc.flipchat.gen.chat.v1.openStatusOrNull -import com.getcode.model.Kin -import com.getcode.model.chat.ChatType -import com.getcode.services.mapper.Mapper -import xyz.flipchat.services.data.Room -import javax.inject.Inject - -class MetadataRoomMapper @Inject constructor( -): Mapper { - override fun map(from: ChatService.Metadata): Room { - return Room( - id = from.chatId.value.toByteArray().toList(), - ownerId = from.owner.value.toByteArray().toList(), - _title = from.displayName.nullIfEmpty(), - description = from.description.nullIfEmpty(), - roomNumber = from.roomNumber, - type = ChatType.entries[from.type.ordinal], - unread = from.numUnread, - moreUnread = from.hasMoreUnread, - canDisablePush = from.canDisablePush, - isPushEnabled = from.isPushEnabled, - messagingFee = Kin.fromQuarks(from.messagingFee.quarks.ifZeroOrElse(200) { it / 100_000 }), - lastActive = from.lastActivityOrNull?.seconds?.times(1_000), - isOpen = from.openStatusOrNull?.isCurrentlyOpen ?: true, - ) - } -} - -internal fun Long.ifZeroOrElse(other: Long, block: (Long) -> Long) = takeIf { it > 0 }?.let(block) ?: other -fun String?.nullIfEmpty() = if (this?.isEmpty() == true) null else this \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/MetadataUpdateMapper.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/MetadataUpdateMapper.kt deleted file mode 100644 index 2f58e0d55..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/MetadataUpdateMapper.kt +++ /dev/null @@ -1,23 +0,0 @@ -package xyz.flipchat.services.internal.data.mapper - -import com.codeinc.flipchat.gen.chat.v1.ChatService -import com.codeinc.flipchat.gen.chat.v1.ChatService.MetadataUpdate as ApiMetadataUpdate -import com.getcode.services.mapper.Mapper -import xyz.flipchat.services.domain.model.chat.StreamMetadataUpdate -import javax.inject.Inject - -class MetadataUpdateMapper @Inject constructor(): Mapper { - override fun map(from: ApiMetadataUpdate): StreamMetadataUpdate? { - return when (from.kindCase) { - ChatService.MetadataUpdate.KindCase.FULL_REFRESH -> StreamMetadataUpdate.Refresh(from.fullRefresh.metadata) - ChatService.MetadataUpdate.KindCase.UNREAD_COUNT_CHANGED -> StreamMetadataUpdate.UnreadCount(from.unreadCountChanged.numUnread, from.unreadCountChanged.hasMoreUnread) - ChatService.MetadataUpdate.KindCase.DISPLAY_NAME_CHANGED -> StreamMetadataUpdate.DisplayName(from.displayNameChanged.newDisplayName) - ChatService.MetadataUpdate.KindCase.MESSAGING_FEE_CHANGED -> StreamMetadataUpdate.MessagingFee(from.messagingFeeChanged.newMessagingFee.quarks.ifZeroOrElse(200) { it / 100_000 }) - ChatService.MetadataUpdate.KindCase.LAST_ACTIVITY_CHANGED -> StreamMetadataUpdate.LastActivity(from.lastActivityChanged.newLastActivity.seconds * 1000L) - ChatService.MetadataUpdate.KindCase.OPEN_STATUS_CHANGED -> StreamMetadataUpdate.OpenStatusChanged(from.openStatusChanged.newOpenStatus.isCurrentlyOpen) - ChatService.MetadataUpdate.KindCase.DESCRIPTION_CHANGED -> StreamMetadataUpdate.Description(from.descriptionChanged.newDescription) - ChatService.MetadataUpdate.KindCase.KIND_NOT_SET -> null - else -> null - } - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/PointerMapper.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/PointerMapper.kt deleted file mode 100644 index 51e840bc5..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/PointerMapper.kt +++ /dev/null @@ -1,31 +0,0 @@ -package xyz.flipchat.services.internal.data.mapper - -import com.codeinc.flipchat.gen.messaging.v1.Model -import com.getcode.model.ID -import com.getcode.model.chat.MessageStatus -import com.getcode.model.chat.Pointer -import com.getcode.model.uuid -import com.getcode.services.mapper.Mapper -import javax.inject.Inject - -class PointerModelMapper @Inject constructor(): Mapper, Pointer?> { - - override fun map(from: Pair): Pointer? { - val (memberId, proto) = from - val status = when (proto.type) { - Model.Pointer.Type.SENT -> MessageStatus.Sent - Model.Pointer.Type.DELIVERED -> MessageStatus.Delivered - Model.Pointer.Type.READ -> MessageStatus.Read - else -> MessageStatus.Unknown - } - - val messageId = proto.value.value.toByteArray().toList().uuid ?: return null - - return when (status) { - MessageStatus.Sent -> Pointer.Sent(memberId, messageId) - MessageStatus.Delivered -> Pointer.Delivered(memberId, messageId) - MessageStatus.Read -> Pointer.Read(memberId, messageId) - MessageStatus.Unknown -> Pointer.Unknown(memberId) - } - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/RoomWithMemberCountMapper.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/RoomWithMemberCountMapper.kt deleted file mode 100644 index 9f0783ed8..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/RoomWithMemberCountMapper.kt +++ /dev/null @@ -1,17 +0,0 @@ -package xyz.flipchat.services.internal.data.mapper - -import com.getcode.services.mapper.Mapper -import xyz.flipchat.services.data.RoomWithMemberCount -import xyz.flipchat.services.internal.network.chat.GetOrJoinChatResponse -import javax.inject.Inject - -class RoomWithMemberCountMapper @Inject constructor( - private val roomMapper: MetadataRoomMapper, -) : Mapper { - override fun map(from: GetOrJoinChatResponse): RoomWithMemberCount { - return RoomWithMemberCount( - room = roomMapper.map(from.metadata), - members = from.members.count() - ) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/RoomWithMembersMapper.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/RoomWithMembersMapper.kt deleted file mode 100644 index b55418307..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/RoomWithMembersMapper.kt +++ /dev/null @@ -1,18 +0,0 @@ -package xyz.flipchat.services.internal.data.mapper - -import com.getcode.services.mapper.Mapper -import xyz.flipchat.services.data.RoomWithMembers -import xyz.flipchat.services.internal.network.chat.GetOrJoinChatResponse -import javax.inject.Inject - -class RoomWithMembersMapper @Inject constructor( - private val roomMapper: MetadataRoomMapper, - private val memberMapper: MemberMapper, -) : Mapper { - override fun map(from: GetOrJoinChatResponse): RoomWithMembers { - return RoomWithMembers( - room = roomMapper.map(from.metadata), - members = from.members.map { memberMapper.map(it) } - ) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/SocialProfileMapper.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/SocialProfileMapper.kt deleted file mode 100644 index fd73e1749..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/SocialProfileMapper.kt +++ /dev/null @@ -1,28 +0,0 @@ -package xyz.flipchat.services.internal.data.mapper - -import com.codeinc.flipchat.gen.profile.v1.Model -import com.getcode.model.social.user.SocialProfile -import com.getcode.services.mapper.Mapper -import javax.inject.Inject - -class SocialProfileMapper @Inject constructor(): Mapper { - override fun map(from: Model.SocialProfile): SocialProfile { - return when (from.typeCase) { - Model.SocialProfile.TypeCase.X -> with (from.x) { - val verificationType = SocialProfile.X.VerificationType.entries.getOrNull(verifiedTypeValue) - ?: SocialProfile.X.VerificationType.UNKNOWN - - SocialProfile.X( - id = id, - friendlyName = name, - username = username, - description = description, - _profilePicUrl = profilePicUrl, - followerCount = followerCount, - verificationType = verificationType - ) - } - else -> SocialProfile.Unknown - } - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/StreamMetadataUpdateMapper.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/StreamMetadataUpdateMapper.kt deleted file mode 100644 index 44085723a..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/StreamMetadataUpdateMapper.kt +++ /dev/null @@ -1,26 +0,0 @@ -package xyz.flipchat.services.internal.data.mapper - -import com.getcode.model.ID -import com.getcode.services.mapper.Mapper -import xyz.flipchat.services.domain.mapper.RoomConversationMapper -import xyz.flipchat.services.domain.model.chat.StreamMetadataUpdate -import xyz.flipchat.services.domain.model.chat.db.ConversationUpdate -import javax.inject.Inject - -class StreamMetadataUpdateMapper @Inject constructor( - private val metadataMapper: MetadataRoomMapper, - private val conversationMapper: RoomConversationMapper, -): Mapper, ConversationUpdate> { - override fun map(from: Pair): ConversationUpdate { - val (id, update) = from - return when (update) { - is StreamMetadataUpdate.MessagingFee -> ConversationUpdate.CoverCharge(id, update.amount) - is StreamMetadataUpdate.DisplayName -> ConversationUpdate.DisplayName(id, update.name) - is StreamMetadataUpdate.LastActivity -> ConversationUpdate.LastActivity(id, update.timestamp) - is StreamMetadataUpdate.Refresh -> ConversationUpdate.Refresh(conversationMapper.map(metadataMapper.map(update.metadata))) - is StreamMetadataUpdate.UnreadCount -> ConversationUpdate.UnreadCount(id, update.numUnread, update.hasMoreUnread) - is StreamMetadataUpdate.OpenStatusChanged -> ConversationUpdate.OpenStatus(id, update.nowOpen) - is StreamMetadataUpdate.Description -> ConversationUpdate.Description(id, update.description) - } - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/TypingMapper.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/TypingMapper.kt deleted file mode 100644 index 14a35c04b..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/TypingMapper.kt +++ /dev/null @@ -1,16 +0,0 @@ -package xyz.flipchat.services.internal.data.mapper - -import com.codeinc.flipchat.gen.messaging.v1.Model -import com.getcode.services.mapper.Mapper -import xyz.flipchat.services.internal.network.chat.IsTyping -import xyz.flipchat.services.internal.network.chat.TypingState -import javax.inject.Inject - -class TypingMapper @Inject constructor(): Mapper { - override fun map(from: Model.IsTyping): IsTyping { - return IsTyping( - userId = from.userId.value.toList(), - state = TypingState.entries.getOrElse(from.typingStateValue) { TypingState.Unknown } - ) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/UserFlagsMapper.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/UserFlagsMapper.kt deleted file mode 100644 index cad6d5744..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/UserFlagsMapper.kt +++ /dev/null @@ -1,26 +0,0 @@ -package xyz.flipchat.services.internal.data.mapper - -import com.codeinc.flipchat.gen.account.v1.AccountService -import com.getcode.model.Kin -import com.getcode.services.mapper.Mapper -import xyz.flipchat.services.user.UserFlags -import xyz.flipchat.services.internal.network.extensions.toPublicKey -import xyz.flipchat.services.user.TypingNotificationsConstraints -import javax.inject.Inject - -class UserFlagsMapper @Inject constructor(): Mapper { - override fun map(from: AccountService.UserFlags): UserFlags { - return UserFlags( - isStaff = from.isStaff, - isRegistered = from.isRegisteredAccount, - createCost = Kin.fromQuarks(from.startGroupFee.quarks.ifZeroOrElse(200) { it / 100_000 }), - feeDestination = from.feeDestination.toPublicKey(), - typingNotifications = TypingNotificationsConstraints( - canSendAtAll = from.canSendIsTypingNotifications, - canSendAsListener = from.canSendIsTypingNotificationsAsListener, - interval = from.isTypingNotificationInterval.seconds * 1_000, - timeout = from.isTypingNotificationTimeout.seconds * 1_000, - ) - ) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/UserMapper.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/UserMapper.kt deleted file mode 100644 index 8ba3d381c..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/UserMapper.kt +++ /dev/null @@ -1,17 +0,0 @@ -package xyz.flipchat.services.internal.data.mapper - -import com.getcode.services.mapper.Mapper -import com.getcode.utils.base58 -import xyz.flipchat.services.data.Member -import xyz.flipchat.services.domain.model.people.FlipchatUser -import javax.inject.Inject - -class UserMapper @Inject constructor(): Mapper { - override fun map(from: Member): FlipchatUser { - return FlipchatUser( - userIdBase58 = from.id.base58, - memberName = from.identity?.displayName, - imageUri = from.identity?.imageUrl, - ) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/UserProfileMapper.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/UserProfileMapper.kt deleted file mode 100644 index 0ee7606c8..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/data/mapper/UserProfileMapper.kt +++ /dev/null @@ -1,20 +0,0 @@ -package xyz.flipchat.services.internal.data.mapper - -import com.codeinc.flipchat.gen.profile.v1.Model -import com.getcode.model.social.user.SocialProfile -import com.getcode.services.mapper.Mapper -import xyz.flipchat.services.domain.model.profile.UserProfile -import javax.inject.Inject - -class UserProfileMapper @Inject constructor( - private val socialProfileMapper: SocialProfileMapper, -): Mapper { - override fun map(from: Model.UserProfile): UserProfile { - return UserProfile( - displayName = from.displayName, - socialProfiles = from.socialProfilesList - .map { socialProfileMapper.map(it) } - .filterNot { it is SocialProfile.Unknown } - ) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/db/ConversationDao.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/db/ConversationDao.kt deleted file mode 100644 index e652b9ea6..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/db/ConversationDao.kt +++ /dev/null @@ -1,236 +0,0 @@ -package xyz.flipchat.services.internal.db - -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import androidx.room.RewriteQueriesToDropUnusedColumns -import androidx.room.Transaction -import com.getcode.model.ID -import com.getcode.model.Kin -import com.getcode.utils.base58 -import kotlinx.coroutines.flow.Flow -import xyz.flipchat.services.domain.model.chat.Conversation -import xyz.flipchat.services.domain.model.chat.ConversationMessage -import xyz.flipchat.services.domain.model.chat.ConversationWithMembers -import xyz.flipchat.services.domain.model.chat.ConversationWithMembersAndLastMessage -import xyz.flipchat.services.domain.model.chat.ConversationWithMembersAndLastPointers - -@Dao -interface ConversationDao { - - @Query("SELECT * FROM conversations") - suspend fun getConversations(): List - suspend fun getConversationIds(): List { - return getConversations().map { it.id } - } - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsertConversations(vararg conversation: Conversation) - - @RewriteQueriesToDropUnusedColumns - @Query( - """ - SELECT * FROM conversations - WHERE roomNumber > 0 - ORDER BY lastActivity DESC - LIMIT :limit OFFSET :offset - """ - ) - suspend fun getPagedConversationsWithMembers(limit: Int, offset: Int): List - - suspend fun getPagedConversations(limit: Int, offset: Int): List { - return getPagedConversationsWithMembers(limit, offset) - .map { - val lastMessage = getLatestMessage(it.conversation.id) - ConversationWithMembersAndLastMessage( - conversation = it.conversation, - members = it.members, - lastMessage = lastMessage - ) - } - } - - @Query( - """ - WITH prioritized_messages AS ( - SELECT *, - CASE - WHEN type IN (1, 8) THEN 1 - ELSE 2 - END AS priority - FROM messages - WHERE conversationIdBase58 = :id AND type NOT IN (7) - ) - SELECT * - FROM prioritized_messages - ORDER BY priority, dateMillis DESC - LIMIT 1; - """ - ) - suspend fun getLatestMessage(id: String): ConversationMessage? - suspend fun getLatestMessage(id: ID): ConversationMessage? { - return getLatestMessage(id.base58) - } - - @RewriteQueriesToDropUnusedColumns - @Transaction - @Query( - """ - SELECT * FROM conversations AS c - LEFT JOIN members AS m ON c.idBase58 = m.conversationIdBase58 - LEFT JOIN conversation_pointers AS p ON c.idBase58 = p.conversationIdBase58 - LEFT JOIN users AS u ON m.memberIdBase58 = u.userIdBase58 - WHERE c.idBase58 = :id - """ - ) - fun observeConversation(id: String): Flow - - fun observeConversation(id: ID): Flow { - return observeConversation(id.base58) - } - - @RewriteQueriesToDropUnusedColumns - @Query("SELECT * FROM conversations WHERE idBase58 = :id") - suspend fun findConversation(id: String): ConversationWithMembersAndLastPointers? - - suspend fun findConversation(id: ID): ConversationWithMembersAndLastPointers? { - return findConversation(id.base58) - } - - @RewriteQueriesToDropUnusedColumns - @Query("SELECT * FROM conversations WHERE idBase58 = :id") - suspend fun findConversationRaw(id: String): Conversation? - - @RewriteQueriesToDropUnusedColumns - @Query("SELECT * FROM conversations WHERE roomNumber = :number") - suspend fun findConversationRaw(number: Long): Conversation? - - suspend fun findConversationRaw(id: ID): Conversation? { - return findConversationRaw(id.base58) - } - - @Query("SELECT * FROM conversations") - suspend fun queryConversations(): List - - @Query(""" - DELETE FROM conversations - WHERE idBase58 NOT IN ( - SELECT conversationIdBase58 - FROM members - WHERE idBase58 = :id -) - """) - suspend fun removeConversationsWhereNotMember(id: String) - suspend fun removeConversationsWhereNotMember(id: ID) { - removeConversationsWhereNotMember(id.base58) - } - - @Query(""" - SELECT EXISTS ( - SELECT 1 FROM conversations - WHERE idBase58 NOT IN ( - SELECT conversationIdBase58 - FROM members - WHERE idBase58 = :userId - ) AND idBase58 = :conversationId - ) - """) - suspend fun isUserMemberIn(userId: String, conversationId: String): Boolean - suspend fun isUserMemberIn(userId: ID, conversationId: ID): Boolean { - return isUserMemberIn(userId.base58, conversationId.base58) - } - - @Delete - fun deleteConversation(conversation: Conversation) - - @Query("DELETE FROM conversations WHERE idBase58 = :id") - suspend fun deleteConversationById(id: String) - - suspend fun deleteConversationById(id: ID) { - deleteConversationById(id.base58) - } - - suspend fun setDisplayName(id: String, displayName: String) { - val conversation = findConversation(id)?.conversation ?: return - upsertConversations(conversation.copy(title = displayName)) - } - - suspend fun setDisplayName(id: ID, displayName: String) { - setDisplayName(id.base58, displayName) - } - - suspend fun setDescription(id: String, description: String) { - val conversation = findConversation(id)?.conversation ?: return - upsertConversations(conversation.copy(description = description)) - } - - suspend fun setDescription(id: ID, description: String) { - setDescription(id.base58, description) - } - - @Query("DELETE FROM conversations WHERE idBase58 NOT IN (:chatIds)") - suspend fun purgeConversationsNotInByString(chatIds: List) - suspend fun purgeConversationsNotIn(chatIds: List) { - purgeConversationsNotInByString(chatIds.map { it.base58 }) - } - - @Query("DELETE FROM conversations") - fun clearConversations() - - @Query("SELECT unreadCount FROM conversations WHERE idBase58 = :conversationId") - suspend fun getUnreadCount(conversationId: String): Int? - suspend fun getUnreadCount(conversationId: ID): Int? { - return getUnreadCount(conversationId.base58) - } - - suspend fun resetUnreadCount(conversationId: String) { - val conversation = findConversation(conversationId)?.conversation ?: return - upsertConversations(conversation.copy(unreadCount = 0)) - } - - suspend fun resetUnreadCount(conversationId: ID) { - resetUnreadCount(conversationId.base58) - } - - @Query("UPDATE conversations SET coverChargeQuarks = :quarks WHERE idBase58 = :conversationId") - suspend fun updateMessagingFee(conversationId: String, quarks: Long) - suspend fun updateMessagingFee(conversationId: ID, quarks: Long) { - updateMessagingFee(conversationId.base58, quarks) - } - suspend fun updateMessagingFee(conversationId: ID, kin: Kin) { - updateMessagingFee(conversationId.base58, kin.toKinTruncatingLong()) - } - - @Query("UPDATE conversations SET isOpen = 1 WHERE idBase58 = :conversationId") - suspend fun enableChatInRoom(conversationId: String) - suspend fun enableChatInRoom(conversationId: ID) { - enableChatInRoom(conversationId.base58) - } - - @Query("UPDATE conversations SET isOpen = 0 WHERE idBase58 = :conversationId") - suspend fun disableChatInRoom(conversationId: String) - suspend fun disableChatInRoom(conversationId: ID) { - disableChatInRoom(conversationId.base58) - } - - @Query("UPDATE conversations SET isMuted = 1 WHERE idBase58 = :conversationId") - suspend fun muteChat(conversationId: String) - suspend fun muteChat(conversationId: ID) { - muteChat(conversationId.base58) - } - - @Query("UPDATE conversations SET isMuted = 0 WHERE idBase58 = :conversationId") - suspend fun unmuteChat(conversationId: String) - suspend fun unmuteChat(conversationId: ID) { - unmuteChat(conversationId.base58) - } - - @Transaction - @Query(""" - SELECT * FROM conversations - WHERE idBase58 = :conversationId - """) - suspend fun getConversationWithMembersAndLastMessage(conversationId: String): ConversationWithMembersAndLastMessage? -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/db/ConversationMemberDao.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/db/ConversationMemberDao.kt deleted file mode 100644 index f4275bb42..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/db/ConversationMemberDao.kt +++ /dev/null @@ -1,87 +0,0 @@ -package xyz.flipchat.services.internal.db - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import androidx.room.RewriteQueriesToDropUnusedColumns -import androidx.room.Transaction -import com.getcode.model.ID -import com.getcode.utils.base58 -import kotlinx.coroutines.flow.Flow -import xyz.flipchat.services.data.MemberIdentity -import xyz.flipchat.services.domain.model.chat.Conversation -import xyz.flipchat.services.domain.model.chat.ConversationMember -import xyz.flipchat.services.domain.model.chat.ConversationWithMembersAndLastPointers - -@Dao -interface ConversationMemberDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsertMembers(vararg members: ConversationMember) - - @RewriteQueriesToDropUnusedColumns - @Transaction - @Query( - """ - SELECT * FROM members - WHERE conversationIdBase58 = :id - """ - ) - fun observeMembersIn(id: String): Flow> - - fun observeMembersIn(id: ID): Flow> { - return observeMembersIn(id.base58) - } - - @Query("SELECT * FROM members WHERE memberIdBase58 = :memberId AND conversationIdBase58 = :conversationId") - suspend fun getMemberIn(memberId: String, conversationId: String): ConversationMember? - suspend fun getMemberIn(memberId: ID, conversationId: ID): ConversationMember? { - return getMemberIn(memberId.base58, conversationId.base58) - } - - @Query("DELETE FROM members WHERE conversationIdBase58 = :conversationId") - suspend fun removeMembersFrom(conversationId: String) - suspend fun removeMembersFrom(conversationId: ID) { - removeMembersFrom(conversationId.base58) - } - - @Query("DELETE FROM members WHERE memberIdBase58 = :memberId AND conversationIdBase58 = :conversationId") - suspend fun removeMemberFromConversation(memberId: String, conversationId: String) - suspend fun removeMemberFromConversation(memberId: ID, conversationId: ID) { - removeMemberFromConversation(memberId.base58, conversationId.base58) - } - - @Query("DELETE FROM members WHERE memberIdBase58 NOT IN (:memberIds) AND conversationIdBase58 = :conversationId") - suspend fun purgeMembersNotIn(conversationId: String, memberIds: List) - - suspend fun purgeMembersNotIn(conversationId: ID, memberIds: List) { - purgeMembersNotIn(conversationId.base58, memberIds) - } - - @Query("UPDATE members SET isMuted = 1 WHERE conversationIdBase58 = :conversationId AND memberIdBase58 = :memberId") - suspend fun muteMember(conversationId: String, memberId: String) - suspend fun muteMember(conversationId: ID, memberId: ID) { - muteMember(conversationId.base58, memberId.base58) - } - - @Query("UPDATE members SET isMuted = 0 WHERE conversationIdBase58 = :conversationId AND memberIdBase58 = :memberId") - suspend fun unmuteMember(conversationId: String, memberId: String) - suspend fun unmuteMember(conversationId: ID, memberId: ID) { - unmuteMember(conversationId.base58, memberId.base58) - } - - @Query("UPDATE members SET isFullMember = 1 WHERE conversationIdBase58 = :conversationId AND memberIdBase58 = :memberId") - suspend fun promoteMember(conversationId: String, memberId: String) - suspend fun promoteMember(conversationId: ID, memberId: ID) { - promoteMember(conversationId.base58, memberId.base58) - } - - @Query("UPDATE members SET isFullMember = 0 WHERE conversationIdBase58 = :conversationId AND memberIdBase58 = :memberId") - suspend fun demoteMember(conversationId: String, memberId: String) - suspend fun demoteMember(conversationId: ID, memberId: ID) { - demoteMember(conversationId.base58, memberId.base58) - } - - @Query("DELETE FROM members") - fun clearMembers() -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/db/ConversationMessageDao.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/db/ConversationMessageDao.kt deleted file mode 100644 index e889533b6..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/db/ConversationMessageDao.kt +++ /dev/null @@ -1,405 +0,0 @@ -package xyz.flipchat.services.internal.db - -import androidx.compose.ui.util.fastDistinctBy -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import androidx.room.RewriteQueriesToDropUnusedColumns -import androidx.room.Transaction -import com.getcode.model.ID -import com.getcode.model.chat.MessageContent -import com.getcode.model.uuid -import com.getcode.utils.base58 -import xyz.flipchat.services.domain.model.chat.ConversationMember -import xyz.flipchat.services.domain.model.chat.ConversationMemberWithLinkedSocialProfiles -import xyz.flipchat.services.domain.model.chat.ConversationMessage -import xyz.flipchat.services.domain.model.chat.ConversationMessageReaction -import xyz.flipchat.services.domain.model.chat.ConversationMessageTip -import xyz.flipchat.services.domain.model.chat.ConversationMessageWithMemberAndContent -import xyz.flipchat.services.domain.model.chat.ConversationMessageWithMemberAndReply -import xyz.flipchat.services.domain.model.chat.InflatedConversationMessage -import xyz.flipchat.services.domain.model.chat.MessageReactionInfo -import xyz.flipchat.services.domain.model.chat.MessageTipInfo -import xyz.flipchat.services.domain.model.chat.deletedMessages -import xyz.flipchat.services.domain.model.chat.reviews -import xyz.flipchat.services.domain.model.chat.reactions -import xyz.flipchat.services.domain.model.chat.replies -import xyz.flipchat.services.domain.model.chat.tips -import xyz.flipchat.services.domain.model.people.FlipchatUser -import xyz.flipchat.services.domain.model.people.MemberPersonalInfo -import xyz.flipchat.services.domain.model.profile.MemberSocialProfile - -@Dao -interface ConversationMessageDao { - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsertMessagesInternal(vararg message: ConversationMessage) - - @Transaction - suspend fun upsertMessages( - chatId: ID, - messages: List, - selfID: ID? - ) { - upsertMessagesInternal(*messages.toTypedArray()) - - val deletes = messages.deletedMessages(selfID) + getMessagesOfTypeInConversation( - conversationId = chatId, - type = MessageContent.getType(MessageContent.DeletedMessage::class) - ).deletedMessages(selfID) - - val replies = messages.replies(selfID) - val tips = messages.tips(selfID) - - val reactions = messages.reactions(selfID) - - // first review for a message in the stream wins - // so we sort and distinct by the [originalMessageId] and only change [isApproved] if - // not previously set - val reviews: List = messages.reviews(selfID) - .sortedBy { it.originalMessageId.uuid } - .fastDistinctBy { it.originalMessageId } - .filterNot { hasBeenReviewed(it.originalMessageId) } - - replies.onEach { (messageId, inReplyTo) -> - connectReply(messageId, inReplyTo) - } - - tips.onEach { - addTip(tipMessageId = it.first, tipContent = it.second) - } - - reactions.onEach { - addReaction(reactionMessageId = it.first, reactionContent = it.second) - } - - reviews.onEach { review -> - if (review.isApproved) { - approve(review.originalMessageId) - } else { - reject(review.originalMessageId) - } - } - - // deletes need to happen last to account for deleted reactions - deletes.onEach { - markDeleted(it.originalMessageId, it.messageDeleter) - removeReaction(it.originalMessageId) - } - } - - @RewriteQueriesToDropUnusedColumns - @Transaction - @Query( - """ - SELECT - messages.idBase58 AS idBase58, - messages.senderIdBase58 AS senderIdBase58, - messages.dateMillis AS dateMillis, - messages.conversationIdBase58 AS conversationIdBase58, - messages.type AS type, - messages.tipCount AS tipCount, - messages.reactionCount AS reactionCount, - messages.deleted AS deleted, - messages.deletedByBase58 AS deletedByBase58, - messages.inReplyToBase58 AS inReplyToBase58, - messages.isApproved AS isApproved, - messages.sentOffStage AS sentOffStage, - messages.content AS content, - members.memberIdBase58 AS memberIdBase58, - members.isHost AS isHost, - members.isFullMember AS isFullMember, - members.isMuted AS isMuted, - users.memberName AS memberName, - users.imageUri AS imageUri, - users.isBlocked AS isBlocked - FROM messages - - LEFT JOIN members ON messages.senderIdBase58 = members.memberIdBase58 - AND messages.conversationIdBase58 = members.conversationIdBase58 - AND members.conversationIdBase58 = :id - LEFT JOIN users ON messages.senderIdBase58 = users.userIdBase58 - LEFT JOIN tips ON messages.idBase58 = tips.messageIdBase58 - LEFT JOIN reactions ON messages.idBase58 = reactions.messageIdBase58 - -- RawText, Announcements, Replies, and Actionable Announcements -- - WHERE messages.conversationIdBase58 = :id AND type IN (1, 4, 8, 12) - GROUP BY messages.idBase58 - -- ID is a base58 encoded v7 UUID which is guaranteed lexigraphically in order -- - ORDER BY messages.idBase58 DESC - LIMIT :limit OFFSET :offset -""" - ) - suspend fun getPagedMessages(id: String, limit: Int, offset: Int): List - suspend fun getPagedMessages(id: ID, limit: Int, offset: Int): List { - return getPagedMessages(id.base58, limit, offset) - } - - @Query(""" - SELECT * - FROM members - WHERE members.memberIdBase58 = :memberId AND members.conversationIdBase58 = :conversationId - """) - suspend fun getMemberInternal(conversationId: String, memberId: String): ConversationMember? - suspend fun getMemberInternal(conversationId: ID, memberId: ID): ConversationMember? { - return getMemberInternal(conversationId.base58, memberId.base58) - } - - @Query("SELECT * FROM users WHERE userIdBase58 = :memberId") - suspend fun getMemberPersonalInfo(memberId: String): FlipchatUser? - suspend fun getMemberPersonalInfo(memberId: ID): FlipchatUser? { - return getMemberPersonalInfo(memberId.base58) - } - - @Query("SELECT * FROM social_profiles WHERE memberIdBase58 = :memberId") - suspend fun getUserSocialProfiles(memberId: String): List - suspend fun getUserSocialProfiles(memberId: ID): List { - return getUserSocialProfiles(memberId.base58) - } - - @Query("SELECT * FROM tips WHERE messageIdBase58 = :id") - suspend fun getTipsForMessage(id: String): List - suspend fun getTipsForMessage(id: ID): List { - return getTipsForMessage(id.base58) - } - - @Query("SELECT * FROM reactions WHERE messageIdBase58 = :id AND deleted = 0") - suspend fun getReactionsForMessage(id: String): List - suspend fun getReactionsForMessage(id: ID): List { - return getReactionsForMessage(id.base58) - } - - @Query("SELECT * FROM reactions WHERE idBase58 = :id") - suspend fun getReaction(id: String): ConversationMessageReaction? - suspend fun getReaction(id: ID): ConversationMessageReaction? { - return getReaction(id.base58) - } - - @Query("UPDATE reactions SET deleted = 1 WHERE idBase58 = :id") - suspend fun removeReactionInternal(id: String) - suspend fun removeReactionInternal(id: ID) { - removeReactionInternal(id.base58) - } - - suspend fun getPagedMessagesWithDetails(id: ID, limit: Int, offset: Int, selfId: ID?): List { - val messages = getPagedMessages(id.base58, limit, offset) - - return messages.map { - val content = MessageContent.fromData(it.message.type, it.message.content, isFromSelf = selfId == it.message.senderId) - val member = getMemberInternal(id, it.message.senderId) - val pii = getMemberPersonalInfo(it.message.senderId) - val profiles = getUserSocialProfiles(it.message.senderId) - val replyMessage = it.message.inReplyTo?.let { id -> getMessageById(id.base58) } - val replyContent = replyMessage?.let { rp -> - MessageContent.fromData(rp.message.type, rp.message.content, isFromSelf = rp.message.senderId == selfId) - } ?: MessageContent.Unknown(false) - - val replyMemberSocials = replyMessage?.member?.id?.let { id -> getUserSocialProfiles(id) }.orEmpty() - - val tips = getTipsForMessage(it.message.id) - val reactions = getReactionsForMessage(it.message.id) - InflatedConversationMessage( - message = it.message, - member = ConversationMemberWithLinkedSocialProfiles( - member = member, - personalInfo = pii?.let { - MemberPersonalInfo( - memberName = pii.memberName, - imageUri = pii.imageUri, - isBlocked = pii.isBlocked ?: false - ) - }, - profiles = profiles - ), - content = content, - reply = replyMessage?.apply { - socialProfiles = replyMemberSocials - contentEntity = replyContent - }, - tips = tips, - reactions = reactions - ) - } - } - - @Query("SELECT COUNT(*) FROM messages WHERE conversationIdBase58 = :conversationId") - suspend fun getTotalMessageCountFor(conversationId: String): Int - suspend fun getTotalMessageCountFor(conversationId: ID): Int { - return getTotalMessageCountFor(conversationId.base58) - } - - @Query("SELECT * FROM messages WHERE conversationIdBase58 = :conversationId") - suspend fun queryMessages(conversationId: String): List - - suspend fun queryMessages(conversationId: ID): List { - return queryMessages(conversationId.base58) - } - - @Query("SELECT * FROM messages WHERE conversationIdBase58 = :conversationId ORDER BY idBase58 DESC LIMIT 1") - suspend fun getNewestMessage(conversationId: String): ConversationMessage? - suspend fun getNewestMessage(conversationId: ID): ConversationMessage? { - return getNewestMessage(conversationId.base58) - } - - @Query("SELECT * FROM messages WHERE conversationIdBase58 = :conversationId ORDER BY idBase58 ASC LIMIT 1") - suspend fun getOldestMessage(conversationId: String): ConversationMessage? - suspend fun getOldestMessage(conversationId: ID): ConversationMessage? { - return getOldestMessage(conversationId.base58) - } - - @RewriteQueriesToDropUnusedColumns - @Transaction - @Query( - """ - SELECT messages.* - FROM messages - LEFT JOIN social_profiles ON messages.senderIdBase58 = social_profiles.memberIdBase58 - WHERE messages.idBase58 = :messageId - LIMIT 1 - """) - suspend fun getMessageById(messageId: String): ConversationMessageWithMemberAndContent? - - @Query(""" - SELECT * FROM messages - WHERE type = :type AND conversationIdBase58 = :conversationId - ORDER BY dateMillis DESC - """) - suspend fun getMessagesOfTypeInConversation(conversationId: String, type: Int): List - suspend fun getMessagesOfTypeInConversation(conversationId: ID, type: Int): List { - return getMessagesOfTypeInConversation(conversationId.base58, type) - } - - - @Query("DELETE FROM messages WHERE conversationIdBase58 = :conversationId") - suspend fun removeForConversation(conversationId: String) - - suspend fun removeForConversation(conversationId: ID) { - removeForConversation(conversationId.base58) - } - - @Query("UPDATE messages SET deleted = 1, deletedByBase58 = :by WHERE idBase58 = :messageId") - suspend fun markDeleted(messageId: String, by: String) - - suspend fun markDeleted(messageId: ID, by: ID) { - markDeleted(messageId.base58, by.base58) - } - - @Query("UPDATE messages SET inReplyToBase58 = :inReplyTo WHERE idBase58 = :messageId") - suspend fun connectReply(messageId: String, inReplyTo: String) - suspend fun connectReply(messageId: ID, inReplyTo: ID) { - connectReply(messageId.base58, inReplyTo.base58) - } - - @Query("UPDATE messages SET tipCount = tipCount + 1 WHERE idBase58 = :messageId") - suspend fun incrementTipCount(messageId: String) - suspend fun incrementTipCount(messageId: ID) { - incrementTipCount(messageId.base58) - } - - @Query("UPDATE messages SET reactionCount = reactionCount + 1 WHERE idBase58 = :messageId") - suspend fun incrementReactionCount(messageId: String) - suspend fun incrementReactionCount(messageId: ID) { - incrementTipCount(messageId.base58) - } - - @Query("UPDATE messages SET reactionCount = reactionCount - 1 WHERE idBase58 = :messageId") - suspend fun decrementReactionCount(messageId: String) - suspend fun decrementReactionCount(messageId: ID) { - decrementReactionCount(messageId.base58) - } - - @Transaction - suspend fun removeReaction(id: ID) { - removeReactionInternal(id) - } - - @Query(""" - WITH last_100 AS ( - SELECT emoji, sentAt - FROM reactions - WHERE senderIdBase58 = :senderId - ORDER BY sentAt DESC - LIMIT 100 - ), - grouped AS ( - SELECT emoji, MAX(sentAt) AS latestSentAt, COUNT(*) AS count - FROM last_100 - GROUP BY emoji - ) - SELECT r.emoji - FROM reactions r - INNER JOIN grouped ON r.emoji = grouped.emoji - AND r.sentAt = grouped.latestSentAt - WHERE r.senderIdBase58 = :senderId AND grouped.count >= 5 - GROUP BY r.emoji, grouped.count - ORDER BY grouped.count DESC, r.sentAt DESC - LIMIT 20; - """) - suspend fun getFrequentEmojis(senderId: String): List - suspend fun getFrequentEmojis(senderId: ID): List { - return getFrequentEmojis(senderId.base58) - } - - @Query("SELECT isApproved FROM messages WHERE idBase58 = :messageId AND isApproved IS NOT NULL") - suspend fun hasBeenReviewed(messageId: String): Boolean - suspend fun hasBeenReviewed(messageId: ID): Boolean { - return hasBeenReviewed(messageId.base58) - } - - @Query("UPDATE messages SET isApproved = 1 WHERE idBase58 = :messageId") - suspend fun approve(messageId: String) - suspend fun approve(messageId: ID) { - approve(messageId.base58) - } - - @Query("UPDATE messages SET isApproved = 0 WHERE idBase58 = :messageId") - suspend fun reject(messageId: String) - suspend fun reject(messageId: ID) { - reject(messageId.base58) - } - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun addTip(vararg tip: ConversationMessageTip) - - suspend fun addTip(tipMessageId: ID, tipContent: MessageContent.MessageTip) { - val tip = ConversationMessageTip( - idBase58 = tipMessageId.base58, - messageIdBase58 = tipContent.originalMessageId.base58, - amount = tipContent.amountInQuarks, - tipperIdBase58 = tipContent.tipperId.base58 - ) - - addTip(tip) - } - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun addReaction(vararg reaction: ConversationMessageReaction) - - suspend fun addReaction(reactionMessageId: ID, reactionContent: MessageContent.Reaction) { - val reaction = ConversationMessageReaction( - idBase58 = reactionMessageId.base58, - messageIdBase58 = reactionContent.originalMessageId.base58, - senderIdBase58 = reactionContent.senderId.base58, - emoji = reactionContent.emoji, - sentAt = reactionContent.sentAt - ) - - addReaction(reaction) - } - - @Query("DELETE FROM messages WHERE conversationIdBase58 NOT IN (:chatIds)") - suspend fun purgeMessagesNotInByString(chatIds: List) - - suspend fun purgeMessagesNotIn(chatIds: List) { - purgeMessagesNotInByString(chatIds.map { it.base58 }) - } - - @Query("DELETE FROM messages") - fun clearMessages() - - @Query("DELETE FROM messages WHERE conversationIdBase58 = :chatId") - suspend fun clearMessagesForChat(chatId: String) - suspend fun clearMessagesForChat(chatId: ID) { - clearMessagesForChat(chatId.base58) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/db/ConversationPointerDao.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/db/ConversationPointerDao.kt deleted file mode 100644 index e57c93244..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/db/ConversationPointerDao.kt +++ /dev/null @@ -1,41 +0,0 @@ -package xyz.flipchat.services.internal.db - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.getcode.model.ID -import com.getcode.model.chat.MessageStatus -import com.getcode.utils.base58 -import xyz.flipchat.services.domain.model.chat.ConversationPointerCrossRef -import java.util.UUID - -@Dao -interface ConversationPointerDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(crossRef: ConversationPointerCrossRef) - - suspend fun insert(conversationId: ID, messageId: UUID, status: MessageStatus) { - insert(ConversationPointerCrossRef(conversationId.base58, messageId.toString(), status)) - } - - @Query("SELECT * FROM conversation_pointers") - suspend fun queryPointers(): List - - @Query("DELETE FROM conversation_pointers WHERE conversationIdBase58 = :id") - suspend fun deletePointerForConversation(id: String) - - suspend fun deletePointerForConversation(id: ID) { - deletePointerForConversation(id.base58) - } - - @Query("DELETE FROM conversation_pointers WHERE conversationIdBase58 NOT IN (:chatIds)") - suspend fun purgePointersNoLongerNeededByString(chatIds: List) - - suspend fun purgePointersNoLongerNeeded(chatIds: List) { - purgePointersNoLongerNeededByString(chatIds.map { it.base58 }) - } - - @Query("DELETE FROM conversation_pointers") - suspend fun clearMapping() -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/db/MemberSocialProfileDao.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/db/MemberSocialProfileDao.kt deleted file mode 100644 index b632f0d40..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/db/MemberSocialProfileDao.kt +++ /dev/null @@ -1,65 +0,0 @@ -package xyz.flipchat.services.internal.db - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import androidx.room.Transaction -import com.getcode.model.ID -import com.getcode.model.social.user.SocialProfile -import com.getcode.model.social.user.XExtraData -import com.getcode.utils.base58 -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import xyz.flipchat.services.domain.model.profile.MemberSocialProfile - -@Dao -interface MemberSocialProfileDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsert(vararg profile: MemberSocialProfile) - - @Transaction - suspend fun upsert(memberId: ID, profile: SocialProfile) { - upsert(memberId, listOf(profile)) - } - - @Transaction - suspend fun upsert(vararg item: Pair>) { - item.onEach { (id, profiles) -> - upsert(memberId = id, profiles = profiles) - } - } - - @Transaction - suspend fun upsert(memberId: ID, profiles: List) { - val models = profiles.mapNotNull { profile -> - when (profile) { - is SocialProfile.Unknown -> null - is SocialProfile.X -> { - val metadata = XExtraData( - friendlyName = profile.friendlyName, - description = profile.description, - verificationType = profile.verificationType, - followerCount = profile.followerCount - ) - - MemberSocialProfile( - id = profile.id, - memberIdBase58 = memberId.base58, - username = profile.username, - profileImageUrl = profile.profilePicUrl, - platformType = "x", - verified = profile.verificationType != SocialProfile.X.VerificationType.NONE, - extraData = Json.encodeToString(metadata) - ) - } - } - } - - purgeSocialProfilesForMemberNotIn(models.map { it.id }, memberId.base58) - upsert(*models.toTypedArray()) - } - - @Query("DELETE FROM social_profiles WHERE id NOT IN (:profiles) AND memberIdBase58 = :memberId") - suspend fun purgeSocialProfilesForMemberNotIn(profiles: List, memberId: String) -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/db/UserDao.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/db/UserDao.kt deleted file mode 100644 index cdd98a2f7..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/db/UserDao.kt +++ /dev/null @@ -1,47 +0,0 @@ -package xyz.flipchat.services.internal.db - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.getcode.model.ID -import com.getcode.utils.base58 -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import xyz.flipchat.services.data.MemberIdentity -import xyz.flipchat.services.domain.model.people.FlipchatUser -import xyz.flipchat.services.domain.model.people.FlipchatUserWithSocialProfiles - -@Dao -interface UserDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun upsert(vararg users: FlipchatUser) - - @Query("UPDATE users SET isBlocked = 1 WHERE userIdBase58 = :userId") - suspend fun blockUser(userId: String) - suspend fun blockUser(userId: ID) { - blockUser(userId.base58) - } - - @Query("UPDATE users SET isBlocked = 0 WHERE userIdBase58 = :userId") - suspend fun unblockUser(userId: String) - suspend fun unblockUser(userId: ID) { - unblockUser(userId.base58) - } - - @Query("UPDATE users SET memberName = :displayName, imageUri = :profileImageUrl WHERE userIdBase58 = :memberId") - suspend fun updateIdentity(memberId: String, displayName: String, profileImageUrl: String?) - - suspend fun updateIdentity(memberId: ID, identity: MemberIdentity) { - updateIdentity(memberId.base58, identity.displayName, identity.imageUrl) - } - - @Query("SELECT * FROM users WHERE userIdBase58 IN (:userIds)") - fun getUsersFrom(userIds: List): Flow> - fun getUsersFromIds(userIds: List): Flow> { - return getUsersFrom(userIds.map { it.base58 }) - .map { users -> - users.sortedBy { userIds.indexOf(it.user.id) } - } - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/inject/FcChatModule.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/inject/FcChatModule.kt deleted file mode 100644 index 7bde49a8d..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/inject/FcChatModule.kt +++ /dev/null @@ -1,49 +0,0 @@ -package xyz.flipchat.services.internal.inject - -import android.content.Context -import com.getcode.services.utils.logging.LoggingClientInterceptor -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import io.grpc.ManagedChannel -import io.grpc.android.AndroidChannelBuilder -import org.kin.sdk.base.network.api.agora.OkHttpChannelBuilderForcedTls12 -import xyz.flipchat.services.FcChatConfig -import xyz.flipchat.services.chat.BuildConfig -import xyz.flipchat.services.internal.annotations.ChatManagedChannel -import java.util.concurrent.TimeUnit -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object FcChatModule { - - @Singleton - @Provides - fun providesChatServicesConfig(): FcChatConfig { - return FcChatConfig() - } - - @Singleton - @Provides - @ChatManagedChannel - fun provideManagedChannel( - @ApplicationContext context: Context, - config: FcChatConfig, - ): ManagedChannel { - return AndroidChannelBuilder - .usingBuilder(OkHttpChannelBuilderForcedTls12.forAddress(config.baseUrl, config.port)) - .context(context) - .userAgent(config.userAgent) - .keepAliveTime(config.keepAlive.inWholeMilliseconds, TimeUnit.MILLISECONDS) - .keepAliveTimeout(config.keepAliveTimeout.inWholeMilliseconds, TimeUnit.MILLISECONDS) - .apply { - if (BuildConfig.DEBUG) { - this.intercept(LoggingClientInterceptor()) - } - } - .build() - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/inject/RepositoryModule.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/inject/RepositoryModule.kt deleted file mode 100644 index 5d03357bd..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/inject/RepositoryModule.kt +++ /dev/null @@ -1,117 +0,0 @@ -package xyz.flipchat.services.internal.inject - -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import xyz.flipchat.services.domain.mapper.ConversationMessageMapper -import xyz.flipchat.services.domain.mapper.RoomConversationMapper -import xyz.flipchat.services.internal.data.mapper.ChatMessageMapper -import xyz.flipchat.services.internal.data.mapper.ConversationMemberMapper -import xyz.flipchat.services.internal.data.mapper.LastMessageMapper -import xyz.flipchat.services.internal.data.mapper.MemberUpdateMapper -import xyz.flipchat.services.internal.data.mapper.MetadataRoomMapper -import xyz.flipchat.services.internal.data.mapper.MetadataUpdateMapper -import xyz.flipchat.services.internal.data.mapper.UserProfileMapper -import xyz.flipchat.services.internal.data.mapper.RoomWithMembersMapper -import xyz.flipchat.services.internal.data.mapper.SocialProfileMapper -import xyz.flipchat.services.internal.data.mapper.StreamMetadataUpdateMapper -import xyz.flipchat.services.internal.data.mapper.TypingMapper -import xyz.flipchat.services.internal.data.mapper.UserFlagsMapper -import xyz.flipchat.services.internal.network.repository.accounts.AccountRepository -import xyz.flipchat.services.internal.network.repository.accounts.RealAccountRepository -import xyz.flipchat.services.internal.network.repository.chat.ChatRepository -import xyz.flipchat.services.internal.network.repository.chat.RealChatRepository -import xyz.flipchat.services.internal.network.repository.iap.InAppPurchaseRepository -import xyz.flipchat.services.internal.network.repository.iap.RealInAppPurchaseRepository -import xyz.flipchat.services.internal.network.repository.messaging.MessagingRepository -import xyz.flipchat.services.internal.network.repository.messaging.RealMessagingRepository -import xyz.flipchat.services.internal.network.repository.profile.ProfileRepository -import xyz.flipchat.services.internal.network.repository.profile.RealProfileRepository -import xyz.flipchat.services.internal.network.repository.push.PushRepository -import xyz.flipchat.services.internal.network.repository.push.RealPushRepository -import xyz.flipchat.services.internal.network.service.AccountService -import xyz.flipchat.services.internal.network.service.ChatService -import xyz.flipchat.services.internal.network.service.MessagingService -import xyz.flipchat.services.internal.network.service.ProfileService -import xyz.flipchat.services.internal.network.service.PurchaseService -import xyz.flipchat.services.internal.network.service.PushService -import xyz.flipchat.services.user.UserManager - -@Module -@InstallIn(SingletonComponent::class) -internal object RepositoryModule { - - @Provides - internal fun providesAccountRepository( - userManager: UserManager, - service: AccountService, - userFlagsMapper: UserFlagsMapper, - ): AccountRepository = RealAccountRepository(userManager, service, userFlagsMapper) - - @Provides - internal fun provideChatRepository( - userManager: UserManager, - service: ChatService, - roomMapper: MetadataRoomMapper, - conversationMapper: RoomConversationMapper, - roomWithMembersMapper: RoomWithMembersMapper, - memberUpdateMapper: MemberUpdateMapper, - metadataUpdateMapper: MetadataUpdateMapper, - streamMetadataUpdateMapper: StreamMetadataUpdateMapper, - conversationMemberMapper: ConversationMemberMapper, - messageMapper: LastMessageMapper, - messageWithContentMapper: ConversationMessageMapper, - ): ChatRepository = RealChatRepository( - userManager = userManager, - service = service, - roomMapper = roomMapper, - roomWithMembersMapper = roomWithMembersMapper, - memberUpdateMapper = memberUpdateMapper, - lastMessageMapper = messageMapper, - messageMapper = messageWithContentMapper, - metadataUpdateMapper = metadataUpdateMapper, - streamMetadataUpdateMapper = streamMetadataUpdateMapper - ) - - @Provides - internal fun providesInAppPurchaseRepository( - userManager: UserManager, - service: PurchaseService - ): InAppPurchaseRepository = RealInAppPurchaseRepository(userManager, service) - - @Provides - internal fun providesMessagingRepository( - userManager: UserManager, - service: MessagingService, - messageMapper: ChatMessageMapper, - lastMessageMapper: LastMessageMapper, - messageWithContentMapper: ConversationMessageMapper, - typingMapper: TypingMapper, - ): MessagingRepository = RealMessagingRepository( - userManager = userManager, - service = service, - chatMessageMapper = messageMapper, - lastMessageMapper = lastMessageMapper, - messageMapper = messageWithContentMapper, - typingMapper = typingMapper, - ) - - @Provides - internal fun providesProfileRepository( - userManager: UserManager, - service: ProfileService, - userProfileMapper: UserProfileMapper, - socialProfileMapper: SocialProfileMapper, - ): ProfileRepository = RealProfileRepository( - userManager = userManager, - service = service, - userProfileMapper = userProfileMapper, - socialProfileMapper = socialProfileMapper, - ) - - @Provides - internal fun providesPushRepository( - service: PushService - ): PushRepository = RealPushRepository(service = service) -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/api/AccountApi.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/api/AccountApi.kt deleted file mode 100644 index e67097e45..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/api/AccountApi.kt +++ /dev/null @@ -1,134 +0,0 @@ -package xyz.flipchat.services.internal.network.api - -import com.codeinc.flipchat.gen.account.v1.AccountGrpc -import com.codeinc.flipchat.gen.account.v1.AccountService -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.ID -import com.getcode.services.network.core.GrpcApi -import com.google.protobuf.Timestamp -import io.grpc.ManagedChannel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import xyz.flipchat.services.data.PaymentTarget -import xyz.flipchat.services.internal.annotations.ChatManagedChannel -import xyz.flipchat.services.internal.network.extensions.asPublicKey -import xyz.flipchat.services.internal.network.extensions.toUserId -import xyz.flipchat.services.internal.network.utils.authenticate -import xyz.flipchat.services.internal.network.utils.sign -import javax.inject.Inject - -class AccountApi @Inject constructor( - @ChatManagedChannel - managedChannel: ManagedChannel, -) : GrpcApi(managedChannel) { - - private val api = AccountGrpc.newStub(managedChannel).withWaitForReady() - - /** - * Register registers a new user, bound to the provided PublicKey. - * If the PublicKey is already in use, the previous user account is returned. - */ - fun register(owner: KeyPair, displayName: String?): Flow { - val builder = AccountService.RegisterRequest.newBuilder() - .setPublicKey(owner.asPublicKey()) - - if (displayName != null) { - builder.setDisplayName(displayName) - } - - val request = builder.apply { setSignature(sign(owner)) }.build() - - return api::register - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - fun login(owner: KeyPair): Flow { - val request = AccountService.LoginRequest.newBuilder() - .setTimestamp(Timestamp.newBuilder().setSeconds(System.currentTimeMillis() / 1_000)) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::login - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - /** - * Authorizes an additional PublicKey to an account. - */ - fun authorizePublicKey( - userId: ID, - owner: KeyPair, - newKeyPair: KeyPair, - ): Flow { - - val request = AccountService.AuthorizePublicKeyRequest.newBuilder() - .setPublicKey(newKeyPair.asPublicKey()) - .setUserId(userId.toUserId()) - .apply { setAuth(authenticate(owner)) } - .apply { setSignature(sign(newKeyPair)) } - .build() - - return api::authorizePublicKey - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - /** - * Revokes a public key from an account. - * - * There must be at least one public key per account. For now, any authorized public key - * may revoke another public key, but this may change in the future. - */ - fun revokePublicKey( - userId: ID, - owner: KeyPair, - keypair: KeyPair, - ): Flow { - - val request = AccountService.RevokePublicKeyRequest.newBuilder() - .setPublicKey(keypair.asPublicKey()) - .setUserId(userId.toUserId()) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::revokePublicKey - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - /** - * Gets the payment destination for a target - */ - fun getPaymentDestination( - target: PaymentTarget - ): Flow { - val builder = AccountService.GetPaymentDestinationRequest.newBuilder() - - when (target) { - is PaymentTarget.User -> builder.setUserId(target.id.toUserId()) - } - - val request = builder.build() - - return api::getPaymentDestination - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - fun getUserFlags( - userId: ID, - owner: KeyPair, - ): Flow { - val request = AccountService.GetUserFlagsRequest.newBuilder() - .setUserId(userId.toUserId()) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::getUserFlags - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/api/ChatApi.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/api/ChatApi.kt deleted file mode 100644 index d6520d811..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/api/ChatApi.kt +++ /dev/null @@ -1,435 +0,0 @@ -package xyz.flipchat.services.internal.network.api - -import com.codeinc.flipchat.gen.chat.v1.ChatGrpc -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.ID -import com.getcode.model.KinAmount -import com.getcode.services.network.core.GrpcApi -import io.grpc.ManagedChannel -import io.grpc.stub.StreamObserver -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import xyz.flipchat.services.data.ChatIdentifier -import xyz.flipchat.services.data.StartChatRequestType -import xyz.flipchat.services.domain.model.query.QueryOptions -import xyz.flipchat.services.internal.annotations.ChatManagedChannel -import xyz.flipchat.services.internal.network.extensions.toChatId -import xyz.flipchat.services.internal.network.extensions.toIntentId -import xyz.flipchat.services.internal.network.extensions.toMessageId -import xyz.flipchat.services.internal.network.extensions.toPagingToken -import xyz.flipchat.services.internal.network.extensions.toPaymentAmount -import xyz.flipchat.services.internal.network.extensions.toProto -import xyz.flipchat.services.internal.network.extensions.toUserId -import xyz.flipchat.services.internal.network.utils.authenticate -import javax.inject.Inject -import com.codeinc.flipchat.gen.chat.v1.ChatService as ChatServiceRpc - -class ChatApi @Inject constructor( - @ChatManagedChannel - managedChannel: ManagedChannel -) : GrpcApi(managedChannel) { - private val api = ChatGrpc.newStub(managedChannel).withWaitForReady() - - // StartChat starts a chat. The RPC call is idempotent and will use existing - // chats whenever applicable within the context of message routing. - fun startChat( - owner: KeyPair, - type: StartChatRequestType, - ): Flow { - val builder = ChatServiceRpc.StartChatRequest.newBuilder() - - with(builder) { - when (type) { - is StartChatRequestType.TwoWay -> setTwoWayChat( - ChatServiceRpc.StartChatRequest.StartTwoWayChatParameters.newBuilder() - .setOtherUserId(type.recipient.toUserId()) - ) - - is StartChatRequestType.Group -> { - val groupBuilder = - ChatServiceRpc.StartChatRequest.StartGroupChatParameters.newBuilder() - with(groupBuilder) { - type.recipients - .map { it.toUserId() } - .onEachIndexed { index, user -> setUsers(index, user) } - } - - if (type.title != null) { - groupBuilder.setDisplayName(type.title) - } - - groupBuilder.setPaymentIntent(type.paymentId.toIntentId()) - - setGroupChat(groupBuilder) - } - - else -> {} - } - } - - val request = builder.apply { setAuth(authenticate(owner)) }.build() - - return api::startChat - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - // GetChats gets the set of chats for an owner account using a paged API. - // This RPC is aware of all identities tied to the owner account. - fun getChats( - owner: KeyPair, - queryOptions: QueryOptions, - ): Flow { - val request = ChatServiceRpc.GetChatsRequest.newBuilder() - .setQueryOptions(queryOptions.toProto()) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::getChats - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - // GetChat returns the metadata for a specific chat. - fun getChat( - owner: KeyPair, - identifier: ChatIdentifier, - ): Flow { - - val builder = ChatServiceRpc.GetChatRequest.newBuilder() - when (identifier) { - is ChatIdentifier.Id -> builder.setChatId(identifier.roomId.toChatId()) - is ChatIdentifier.RoomNumber -> builder.setRoomNumber(identifier.number) - } - - builder.apply { setAuth(authenticate(owner)) } - - val request = builder.build() - - return api::getChat - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - // JoinChat joins a given chat. - fun joinChat( - owner: KeyPair, - identifier: ChatIdentifier, - paymentId: ID?, - ): Flow { - val builder = ChatServiceRpc.JoinChatRequest.newBuilder() - - if (paymentId != null) { - builder.setWithoutSendPermission(false) - builder.setPaymentIntent(paymentId.toIntentId()) - } else { - builder.setWithoutSendPermission(true) - } - - when (identifier) { - is ChatIdentifier.Id -> builder.setChatId(identifier.roomId.toChatId()) - is ChatIdentifier.RoomNumber -> builder.setRoomId(identifier.number) - } - - builder.apply { setAuth(authenticate(owner)) } - - val request = builder.build() - - return api::joinChat - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - // LeaveChat leaves a given chat. - fun leaveChat( - owner: KeyPair, - chatId: ID, - ): Flow { - val request = ChatServiceRpc.LeaveChatRequest.newBuilder() - .setChatId(chatId.toChatId()) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::leaveChat - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - // SetDisplayName sets a chat's display name. If the display name isn't allowed, - // then a set of alternate suggestions may be provided - fun setDisplayName( - owner: KeyPair, - chatId: ID, - displayName: String, - ): Flow { - val request = ChatServiceRpc.SetDisplayNameRequest.newBuilder() - .setChatId(chatId.toChatId()) - .setDisplayName(displayName) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::setDisplayName - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - // SetDescription sets a chat's description - fun setDescription( - owner: KeyPair, - chatId: ID, - description: String, - ): Flow { - val request = ChatServiceRpc.SetDescriptionRequest.newBuilder() - .setChatId(chatId.toChatId()) - .setDescription(description) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::setDescription - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - // MuteChat mutes a chat and disables push notifications - fun muteChat( - owner: KeyPair, - chatId: ID, - ): Flow { - val request = ChatServiceRpc.MuteChatRequest.newBuilder() - .setChatId(chatId.toChatId()) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::muteChat - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - // UnmuteChat unmutes a chat and enables push notifications - fun unmuteChat( - owner: KeyPair, - chatId: ID, - ): Flow { - val request = ChatServiceRpc.UnmuteChatRequest.newBuilder() - .setChatId(chatId.toChatId()) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::unmuteChat - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - // SetCoverCharge sets a chat's cover charge - // - // Deprecated: Use SetMessagingFee instead - fun setCoverCharge( - owner: KeyPair, - chatId: ID, - amount: KinAmount, - ): Flow { - val request = ChatServiceRpc.SetCoverChargeRequest.newBuilder() - .setChatId(chatId.toChatId()) - .setCoverCharge(amount.toPaymentAmount()) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::setCoverCharge - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - // SetMessagingFee sets a chat's messaging fee - fun setMessagingFee( - owner: KeyPair, - chatId: ID, - amount: KinAmount, - ): Flow { - val request = ChatServiceRpc.SetMessagingFeeRequest.newBuilder() - .setChatId(chatId.toChatId()) - .setMessagingFee(amount.toPaymentAmount()) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::setMessagingFee - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - // RemoveUser removes a user from a chat - fun removeUser( - owner: KeyPair, - chatId: ID, - userId: ID, - ): Flow { - val request = ChatServiceRpc.RemoveUserRequest.newBuilder() - .setChatId(chatId.toChatId()) - .setUserId(userId.toUserId()) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::removeUser - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - // ReportUser reports a user for a given message - fun reportUser( - owner: KeyPair, - userId: ID, - messageId: ID, - ): Flow { - val request = ChatServiceRpc.ReportUserRequest.newBuilder() - .setUserId(userId.toUserId()) - .setMessageId(messageId.toMessageId()) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::reportUser - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - // MuteUser mutes a user in the chat and removes their ability to send messages - fun muteUser( - owner: KeyPair, - chatId: ID, - userId: ID, - ): Flow { - val request = ChatServiceRpc.MuteUserRequest.newBuilder() - .setChatId(chatId.toChatId()) - .setUserId(userId.toUserId()) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::muteUser - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - // GetMemberUpdates gets member updates for a given chat - fun getMemberUpdates( - owner: KeyPair, - chatId: ID, - afterMember: ID?, - ): Flow { - val builder = ChatServiceRpc.GetMemberUpdatesRequest.newBuilder() - .setChatId(chatId.toChatId()) - - if (afterMember != null) { - builder.setPagingToken(afterMember.toPagingToken()) - } - - builder.apply { setAuth(authenticate(owner)) } - - val request = builder.build() - - - return api::getMemberUpdates - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - // PromoteUser promotes a user to an elevated permission state - fun promoteUser( - owner: KeyPair, - chatId: ID, - userId: ID, - enableSendPermission: Boolean = true, - ): Flow { - val request = ChatServiceRpc.PromoteUserRequest.newBuilder() - .setChatId(chatId.toChatId()) - .setUserId(userId.toUserId()) - .setEnableSendPermission(enableSendPermission) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::promoteUser - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - // DemoteUser demotes a user to a lower permission state - fun demoteUser( - owner: KeyPair, - chatId: ID, - userId: ID, - disableSendPermission: Boolean = true, - ): Flow { - val request = ChatServiceRpc.DemoteUserRequest.newBuilder() - .setChatId(chatId.toChatId()) - .setUserId(userId.toUserId()) - .setDisableSendPermission(disableSendPermission) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::demoteUser - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - // OpenChat opens a chat up for messaging across all members - fun openChat( - owner: KeyPair, - chatId: ID, - ): Flow { - val request = ChatServiceRpc.OpenChatRequest.newBuilder() - .setChatId(chatId.toChatId()) - .apply { setAuth(authenticate(owner)) } - .build() - - - return api::openChat - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - // CloseChat closes a chat up for messaging to just the chat owner - fun closeChat( - owner: KeyPair, - chatId: ID, - ): Flow { - val request = ChatServiceRpc.CloseChatRequest.newBuilder() - .setChatId(chatId.toChatId()) - .apply { setAuth(authenticate(owner)) } - .build() - - - return api::closeChat - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - // CheckDisplayName checks whether a chat display name passes moderation - fun checkDisplayName(name: String): Flow { - val request = ChatServiceRpc.CheckDisplayNameRequest.newBuilder() - .setDisplayName(name) - .build() - - return api::checkDisplayName - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - // StreamChatEvents streams all chat events for the requesting user. - // - // Chat events will include any update to a chat, including: - // 1. Metadata changes. - // 2. Membership changes. - // 3. Latest messages. - // - // The server will optionally filter out some events depending on load - // and chat type. For example, Broadcast chats will not receive latest - // messages. - // - // Clients should use GetMessages to backfill in any historical messages - // for a chat. It should be sufficient to rely on ChatEvents for some types - // of chats, but using StreamMessages provides a guarentee of message events - // for all chats. - fun streamEvents( - observer: StreamObserver - ): StreamObserver? { - return api.streamChatEvents(observer) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/api/MessagingApi.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/api/MessagingApi.kt deleted file mode 100644 index 56cb8d1b1..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/api/MessagingApi.kt +++ /dev/null @@ -1,187 +0,0 @@ -package xyz.flipchat.services.internal.network.api - -import com.codeinc.flipchat.gen.messaging.v1.MessagingGrpc -import com.codeinc.flipchat.gen.messaging.v1.MessagingService -import com.codeinc.flipchat.gen.messaging.v1.Model -import com.codeinc.flipchat.gen.messaging.v1.Model.Pointer -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.ID -import com.getcode.model.chat.MessageStatus -import com.getcode.services.model.chat.OutgoingMessageContent -import com.getcode.services.network.core.GrpcApi -import com.getcode.utils.toByteString -import io.grpc.ManagedChannel -import io.grpc.stub.StreamObserver -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import xyz.flipchat.services.domain.model.query.QueryOptions -import xyz.flipchat.services.internal.annotations.ChatManagedChannel -import xyz.flipchat.services.internal.network.chat.TypingState -import xyz.flipchat.services.internal.network.extensions.toChatId -import xyz.flipchat.services.internal.network.extensions.toIntentId -import xyz.flipchat.services.internal.network.extensions.toMessageId -import xyz.flipchat.services.internal.network.extensions.toPaymentAmount -import xyz.flipchat.services.internal.network.extensions.toProto -import xyz.flipchat.services.internal.network.extensions.toTypingState -import xyz.flipchat.services.internal.network.utils.authenticate -import javax.inject.Inject - -class MessagingApi @Inject constructor( - @ChatManagedChannel - managedChannel: ManagedChannel -) : GrpcApi(managedChannel) { - private val api = MessagingGrpc.newStub(managedChannel).withWaitForReady() - - /** - * gets the set of messages for a chat member using a paged API - */ - fun getMessages( - owner: KeyPair, - chatId: ID, - queryOptions: QueryOptions, - ): Flow { - val request = MessagingService.GetMessagesRequest.newBuilder() - .setChatId(chatId.toChatId()) - .setOptions(queryOptions.toProto()) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::getMessages - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - /** - * advances a pointer in message history for a chat member. - */ - fun advancePointer( - owner: KeyPair, - chatId: ID, - to: ID, - status: MessageStatus, - ): Flow { - val request = MessagingService.AdvancePointerRequest.newBuilder() - .setChatId(chatId.toChatId()) - .setPointer( - Pointer.newBuilder() - .setValue(Model.MessageId.newBuilder().setValue(to.toByteString())) - .setType( - when (status) { - MessageStatus.Sent -> Model.Pointer.Type.SENT - MessageStatus.Delivered -> Model.Pointer.Type.DELIVERED - MessageStatus.Read -> Model.Pointer.Type.READ - MessageStatus.Unknown -> Model.Pointer.Type.UNKNOWN - } - ) - ) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::advancePointer - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - /** - * sends a message to a chat. - */ - fun sendMessage( - owner: KeyPair, - chatId: ID, - content: OutgoingMessageContent, - observer: StreamObserver - ) { - val builder = MessagingService.SendMessageRequest.newBuilder() - .setChatId(chatId.toChatId()) - - val contentProto = when (content) { - is OutgoingMessageContent.Text -> Model.Content.newBuilder() - .setText(Model.TextContent.newBuilder().setText(content.text)) - - is OutgoingMessageContent.Reply -> { - Model.Content.newBuilder() - .setReply( - Model.ReplyContent.newBuilder() - .setOriginalMessageId(content.messageId.toMessageId()) - .setReplyText(content.text) - ) - } - - is OutgoingMessageContent.Reaction -> { - Model.Content.newBuilder() - .setReaction( - Model.ReactionContent.newBuilder() - .setOriginalMessageId(content.messageId.toMessageId()) - .setEmoji(content.emoji) - ) - } - is OutgoingMessageContent.Tip -> { - Model.Content.newBuilder() - .setTip( - Model.TipContent.newBuilder() - .setOriginalMessageId(content.messageId.toMessageId()) - .setTipAmount(content.amount.toPaymentAmount())) - - } - - is OutgoingMessageContent.DeleteRequest -> { - Model.Content.newBuilder() - .setDeleted( - Model.DeleteMessageContent.newBuilder() - .setOriginalMessageId(content.messageId.toMessageId()) - ) - } - } - - builder.addContent(contentProto) - - when (content) { - is OutgoingMessageContent.DeleteRequest -> Unit - is OutgoingMessageContent.Reaction -> { - content.intentId?.let { id -> - builder.setPaymentIntent(id.toIntentId()) - } - } - is OutgoingMessageContent.Reply -> { - content.intentId?.let { id -> - builder.setPaymentIntent(id.toIntentId()) - } - } - is OutgoingMessageContent.Text -> { - content.intentId?.let { id -> - builder.setPaymentIntent(id.toIntentId()) - } - } - is OutgoingMessageContent.Tip -> { - builder.setPaymentIntent(content.intentId.toIntentId()) - } - } - - val request = builder.apply { setAuth(authenticate(owner)) }.build() - - api.sendMessage(request, observer) - } - - fun notifyOfTypingState( - owner: KeyPair, - chatId: ID, - state: TypingState.UserEvent, - observer: StreamObserver - ) { - val request = MessagingService.NotifyIsTypingRequest.newBuilder() - .setChatId(chatId.toChatId()) - .setTypingState(state.toTypingState()) - .apply { setAuth(authenticate(owner)) } - .build() - - api.notifyIsTyping(request, observer) - } - - fun streamMessages( - observer: StreamObserver - ): StreamObserver? { - return api.streamMessages(observer) - } - -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/api/ProfileApi.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/api/ProfileApi.kt deleted file mode 100644 index 7acf473b3..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/api/ProfileApi.kt +++ /dev/null @@ -1,86 +0,0 @@ -package xyz.flipchat.services.internal.network.api - -import com.codeinc.flipchat.gen.profile.v1.ProfileGrpc -import com.codeinc.flipchat.gen.profile.v1.ProfileService -import com.codeinc.flipchat.gen.profile.v1.ProfileService.GetProfileRequest -import com.codeinc.flipchat.gen.profile.v1.ProfileService.LinkSocialAccountRequest -import com.codeinc.flipchat.gen.profile.v1.ProfileService.SetDisplayNameRequest -import com.codeinc.flipchat.gen.profile.v1.ProfileService.UnlinkSocialAccountRequest -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.ID -import com.getcode.services.model.profile.SocialAccountLinkRequest -import com.getcode.services.model.profile.SocialAccountUnlinkRequest -import com.getcode.services.network.core.GrpcApi -import io.grpc.ManagedChannel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import xyz.flipchat.services.internal.annotations.ChatManagedChannel -import xyz.flipchat.services.internal.network.extensions.linkingToken -import xyz.flipchat.services.internal.network.extensions.toUserId -import xyz.flipchat.services.internal.network.utils.authenticate -import javax.inject.Inject - -class ProfileApi @Inject constructor( - @ChatManagedChannel - managedChannel: ManagedChannel, -) : GrpcApi(managedChannel) { - - private val api = ProfileGrpc.newStub(managedChannel).withWaitForReady() - - fun getProfile(userId: ID): Flow { - val request = GetProfileRequest.newBuilder() - .setUserId(userId.toUserId()) - .build() - - return api::getProfile - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - fun setDisplayName( - owner: KeyPair, - displayName: String - ): Flow { - val request = SetDisplayNameRequest.newBuilder() - .setDisplayName(displayName) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::setDisplayName - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - fun linkSocialAccount( - owner: KeyPair, - request: SocialAccountLinkRequest, - ): Flow { - val apiRequest = LinkSocialAccountRequest.newBuilder() - .setLinkingToken(request.linkingToken()) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::linkSocialAccount - .callAsCancellableFlow(apiRequest) - .flowOn(Dispatchers.IO) - } - - fun unlinkSocialAccount( - owner: KeyPair, - request: SocialAccountUnlinkRequest, - ): Flow { - val builder = UnlinkSocialAccountRequest.newBuilder() - - when (request) { - is SocialAccountUnlinkRequest.X -> builder.setXUserId(request.userId) - } - - val apiRequest = builder.apply { setAuth(authenticate(owner)) }.build() - - return api::unlinkSocialAccount - .callAsCancellableFlow(apiRequest) - .flowOn(Dispatchers.IO) - } - -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/api/PurchaseApi.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/api/PurchaseApi.kt deleted file mode 100644 index 8acc84924..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/api/PurchaseApi.kt +++ /dev/null @@ -1,37 +0,0 @@ -package xyz.flipchat.services.internal.network.api - -import com.codeinc.flipchat.gen.common.v1.Common -import com.codeinc.flipchat.gen.iap.v1.IapGrpc -import com.codeinc.flipchat.gen.iap.v1.IapService -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.services.network.core.GrpcApi -import io.grpc.ManagedChannel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import xyz.flipchat.services.internal.annotations.ChatManagedChannel -import xyz.flipchat.services.internal.network.utils.authenticate -import javax.inject.Inject - -class PurchaseApi @Inject constructor( - @ChatManagedChannel - managedChannel: ManagedChannel, -) : GrpcApi(managedChannel) { - private val api = IapGrpc.newStub(managedChannel).withWaitForReady() - - // OnPurchaseCompleted is called when an IAP has been completed - fun onPurchaseCompleted( - owner: KeyPair, - receiptValue: String, - ): Flow { - val request = IapService.OnPurchaseCompletedRequest.newBuilder() - .setPlatform(Common.Platform.GOOGLE) - .setReceipt(IapService.Receipt.newBuilder().setValue(receiptValue)) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::onPurchaseCompleted - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/api/PushApi.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/api/PushApi.kt deleted file mode 100644 index aa71002bc..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/api/PushApi.kt +++ /dev/null @@ -1,72 +0,0 @@ -package xyz.flipchat.services.internal.network.api - -import com.codeinc.flipchat.gen.common.v1.Common -import com.codeinc.flipchat.gen.push.v1.PushGrpc -import com.codeinc.flipchat.gen.push.v1.PushService -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.services.network.core.GrpcApi -import io.grpc.ManagedChannel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import xyz.flipchat.services.internal.annotations.ChatManagedChannel -import xyz.flipchat.services.internal.network.utils.authenticate -import javax.inject.Inject - -class PushApi @Inject constructor( - @ChatManagedChannel - managedChannel: ManagedChannel, -) : GrpcApi(managedChannel) { - - private val api = PushGrpc.newStub(managedChannel).withWaitForReady() - - fun addToken( - owner: KeyPair, - token: String, - installationId: String? - ): Flow { - val request = - PushService.AddTokenRequest.newBuilder() - .setPushToken(token) - .setAppInstall(Common.AppInstallId.newBuilder().setValue(installationId)) - .setTokenType(PushService.TokenType.FCM_ANDROID) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::addToken - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - fun deleteToken( - owner: KeyPair, - token: String, - ): Flow { - val request = - PushService.DeleteTokenRequest.newBuilder() - .setPushToken(token) - .setTokenType(PushService.TokenType.FCM_ANDROID) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::deleteToken - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - fun deleteTokens( - owner: KeyPair, - installationId: String?, - ): Flow { - val request = - PushService.DeleteTokensRequest.newBuilder() - .setAppInstall(Common.AppInstallId.newBuilder().setValue(installationId)) - .apply { setAuth(authenticate(owner)) } - .build() - - return api::deleteTokens - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } -} - diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/chat/ChatStreamUpdate.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/chat/ChatStreamUpdate.kt deleted file mode 100644 index 26fa6e057..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/chat/ChatStreamUpdate.kt +++ /dev/null @@ -1,51 +0,0 @@ -package xyz.flipchat.services.internal.network.chat - -import com.codeinc.flipchat.gen.chat.v1.ChatService.MemberUpdate -import com.codeinc.flipchat.gen.chat.v1.ChatService.MetadataUpdate -import com.codeinc.flipchat.gen.chat.v1.isTypingOrNull -import com.codeinc.flipchat.gen.chat.v1.lastMessageOrNull -import com.codeinc.flipchat.gen.chat.v1.pointerOrNull -import com.codeinc.flipchat.gen.messaging.v1.Model -import com.codeinc.flipchat.gen.messaging.v1.Model.Pointer -import com.codeinc.flipchat.gen.messaging.v1.memberOrNull -import com.codeinc.flipchat.gen.messaging.v1.pointerOrNull -import com.getcode.model.ID -import com.codeinc.flipchat.gen.chat.v1.ChatService as ChatServiceRpc - -data class ChatStreamUpdate( - val id: ID, - val metadataUpdates: List, - val memberUpdates: List, - val lastMessage: Model.Message?, - val lastPointer: PointerUpdate?, - val isTyping: Boolean?, -) { - companion object { - operator fun invoke(proto: ChatServiceRpc.StreamChatEventsResponse.ChatUpdate?): ChatStreamUpdate? { - proto ?: return null - val chatId = proto.chatId.value.toByteArray().toList() - val lastMessage = proto.lastMessageOrNull - val lastPointer = proto.pointerOrNull?.let { - PointerUpdate( - it.memberOrNull?.value?.toByteArray()?.toList(), - it.pointerOrNull - ) - } - val isTyping = proto.isTypingOrNull - - return ChatStreamUpdate( - id = chatId, - metadataUpdates = proto.metadataUpdatesList, - lastMessage = lastMessage, - memberUpdates = proto.memberUpdatesList, - lastPointer = lastPointer, - isTyping = isTyping?.isTyping, - ) - } - } -} - -data class PointerUpdate( - val userId: ID?, - val pointer: Pointer?, -) diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/chat/GetOrJoinChatResponse.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/chat/GetOrJoinChatResponse.kt deleted file mode 100644 index b27009a04..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/chat/GetOrJoinChatResponse.kt +++ /dev/null @@ -1,8 +0,0 @@ -package xyz.flipchat.services.internal.network.chat - -import com.codeinc.flipchat.gen.chat.v1.ChatService as ChatServiceRpc - -data class GetOrJoinChatResponse( - val metadata: ChatServiceRpc.Metadata, - val members: List -) \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/chat/IsTyping.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/chat/IsTyping.kt deleted file mode 100644 index ec86ced27..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/chat/IsTyping.kt +++ /dev/null @@ -1,37 +0,0 @@ -package xyz.flipchat.services.internal.network.chat - -import com.getcode.model.ID - -data class IsTyping( - val userId: ID, - val state: TypingState -) { - val currentlyTyping: Boolean - get() = state is TypingState.Started || state is TypingState.Still -} - -sealed interface TypingState { - val ordinal: Int - sealed interface ChangeEvent: TypingState - sealed interface UserEvent: TypingState - - data object Unknown: TypingState { - override val ordinal: Int = 0 - } - data object Started: TypingState, ChangeEvent, UserEvent { - override val ordinal: Int = 1 - } - data object Still: TypingState, UserEvent { - override val ordinal: Int = 2 - } - data object Stopped: TypingState, ChangeEvent, UserEvent { - override val ordinal: Int = 3 - } - data object TimedOut: TypingState, ChangeEvent { - override val ordinal: Int = 4 - } - - companion object { - val entries: List = listOf(Unknown, Started, Still, Stopped, TimedOut) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/chat/MessageStreamUpdates.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/chat/MessageStreamUpdates.kt deleted file mode 100644 index b6dce7160..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/chat/MessageStreamUpdates.kt +++ /dev/null @@ -1,9 +0,0 @@ -package xyz.flipchat.services.internal.network.chat - -import com.codeinc.flipchat.gen.messaging.v1.Model - -sealed interface MessageStreamUpdate { - data class Messages(val data: List): MessageStreamUpdate - data class Pointers(val data: List): MessageStreamUpdate - data class Typing(val data: List): MessageStreamUpdate -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/extensions/Extensions.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/extensions/Extensions.kt deleted file mode 100644 index a3737353c..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/extensions/Extensions.kt +++ /dev/null @@ -1,82 +0,0 @@ -package xyz.flipchat.services.internal.network.extensions - -import com.codeinc.flipchat.gen.common.v1.Common -import com.codeinc.flipchat.gen.messaging.v1.Model -import com.codeinc.flipchat.gen.profile.v1.ProfileService -import com.codeinc.flipchat.gen.profile.v1.ProfileService.LinkSocialAccountRequest.LinkingToken.XLinkingToken -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.ID -import com.getcode.model.KinAmount -import com.getcode.services.model.profile.LinkingToken -import com.getcode.services.model.profile.SocialAccountLinkRequest -import com.getcode.solana.keys.PublicKey -import com.getcode.utils.toByteString -import xyz.flipchat.services.domain.model.query.PagingToken -import xyz.flipchat.services.domain.model.query.QueryOptions -import xyz.flipchat.services.internal.network.chat.TypingState - -internal fun ByteArray.toSignature(): Common.Signature { - return Common.Signature.newBuilder().setValue(this.toByteString()) - .build() -} - -internal fun KeyPair.asPublicKey(): Common.PublicKey { - return Common.PublicKey.newBuilder().setValue(this.publicKeyBytes.toByteString()).build() -} - -internal fun ID.toUserId(): Common.UserId { - return Common.UserId.newBuilder().setValue(toByteString()).build() -} - -internal fun ID.toMessageId(): Model.MessageId { - return Model.MessageId.newBuilder().setValue(toByteString()).build() -} - -internal fun ID.toChatId(): Common.ChatId { - return Common.ChatId.newBuilder().setValue(toByteString()).build() -} - -internal fun ID.toIntentId(): Common.IntentId { - return Common.IntentId.newBuilder().setValue(toByteString()).build() -} - -internal fun Common.PublicKey.toPublicKey(): PublicKey { - return PublicKey(this.value.toByteArray().toList()) -} - -internal fun KinAmount.toPaymentAmount(): Common.PaymentAmount { - return Common.PaymentAmount.newBuilder().setQuarks(this.kin.quarks).build() -} - -internal fun QueryOptions.toProto(): Common.QueryOptions { - return Common.QueryOptions.newBuilder() - .setPageSize(this@toProto.limit.toLong()) - .setOrder( - if (this@toProto.descending) Common.QueryOptions.Order.DESC - else Common.QueryOptions.Order.ASC - ).apply { - this@toProto.token?.let { - setPagingToken(it.toPagingToken()) - } - }.build() -} - -internal fun PagingToken.toPagingToken(): Common.PagingToken { - return Common.PagingToken.newBuilder().setValue(this.toByteString()).build() -} - -internal fun SocialAccountLinkRequest.linkingToken(): ProfileService.LinkSocialAccountRequest.LinkingToken { - val builder = ProfileService.LinkSocialAccountRequest.LinkingToken.newBuilder() - - when (this) { - is SocialAccountLinkRequest.X -> builder.setX( - XLinkingToken.newBuilder().setAccessToken(token) - ) - } - - return builder.build() -} - -internal fun TypingState.UserEvent.toTypingState(): Model.TypingState { - return Model.TypingState.forNumber(ordinal) -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/accounts/AccountRepository.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/accounts/AccountRepository.kt deleted file mode 100644 index 11c8b0d0d..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/accounts/AccountRepository.kt +++ /dev/null @@ -1,14 +0,0 @@ -package xyz.flipchat.services.internal.network.repository.accounts - -import com.getcode.model.ID -import com.getcode.solana.keys.PublicKey -import xyz.flipchat.services.data.PaymentTarget -import xyz.flipchat.services.user.UserFlags - -interface AccountRepository { - suspend fun createAccount(): Result - suspend fun register(displayName: String): Result - suspend fun login(): Result - suspend fun getPaymentDestination(target: PaymentTarget): Result - suspend fun getUserFlags(): Result -} diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/accounts/RealAccountRepository.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/accounts/RealAccountRepository.kt deleted file mode 100644 index ffb24eca1..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/accounts/RealAccountRepository.kt +++ /dev/null @@ -1,58 +0,0 @@ -package xyz.flipchat.services.internal.network.repository.accounts - -import com.getcode.model.ID -import com.getcode.solana.keys.PublicKey -import com.getcode.utils.ErrorUtils -import xyz.flipchat.services.data.PaymentTarget -import xyz.flipchat.services.user.UserFlags -import xyz.flipchat.services.internal.data.mapper.UserFlagsMapper -import xyz.flipchat.services.internal.network.service.AccountService -import xyz.flipchat.services.internal.network.service.RegisterError -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -internal class RealAccountRepository @Inject constructor( - private val userManager: UserManager, - private val service: AccountService, - private val userFlagsMapper: UserFlagsMapper, -) : AccountRepository { - - override suspend fun createAccount(): Result { - val owner = userManager.keyPair ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - return service.register( - owner = owner, - displayName = null - ).onFailure { ErrorUtils.handleError(it) } - } - - @Deprecated("Being replaced with a delayed account creation flow") - @Throws(RegisterError::class, IllegalStateException::class) - override suspend fun register(displayName: String): Result { - val owner = userManager.keyPair ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - return service.register( - owner = owner, - displayName = displayName - ).onFailure { ErrorUtils.handleError(it) } - } - - override suspend fun login(): Result { - val owner = userManager.keyPair ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - return service.login(owner) - .onFailure { ErrorUtils.handleError(it) } - } - - override suspend fun getPaymentDestination(target: PaymentTarget): Result { - return service.getPaymentDestination(target) - .onFailure { ErrorUtils.handleError(it) } - } - - override suspend fun getUserFlags(): Result { - val owner = userManager.keyPair ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - val userId = userManager.userId ?: return Result.failure(IllegalStateException("No userId found")) - return service.getUserFlags(owner, userId) - .map { userFlagsMapper.map(it) } - .onFailure { ErrorUtils.handleError(it) } - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/chat/ChatRepository.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/chat/ChatRepository.kt deleted file mode 100644 index 2255c54ba..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/chat/ChatRepository.kt +++ /dev/null @@ -1,54 +0,0 @@ -package xyz.flipchat.services.internal.network.repository.chat - -import com.getcode.model.ID -import com.getcode.model.KinAmount -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow -import xyz.flipchat.services.data.ChatIdentifier -import xyz.flipchat.services.data.Member -import xyz.flipchat.services.data.Room -import xyz.flipchat.services.data.RoomWithMembers -import xyz.flipchat.services.data.StartChatRequestType -import xyz.flipchat.services.domain.model.chat.StreamMemberUpdate -import xyz.flipchat.services.domain.model.chat.db.ChatUpdate -import xyz.flipchat.services.domain.model.query.QueryOptions - -interface ChatRepository { - suspend fun getChats( - queryOptions: QueryOptions = QueryOptions() - ): Result> - - suspend fun getChat(identifier: ChatIdentifier): Result - suspend fun getChatMembers(identifier: ChatIdentifier): Result> - suspend fun startChat(type: StartChatRequestType): Result - suspend fun joinChat( - identifier: ChatIdentifier, - paymentId: ID? = null, - ): Result - suspend fun getMemberUpdates(chatId: ID, afterMember: ID? = null): Result> - fun openEventStream(coroutineScope: CoroutineScope, onEvent: (ChatUpdate) -> Unit) - fun closeEventStream() - - // User actions - suspend fun leaveChat(chatId: ID): Result - - // Host controls - suspend fun checkDisplayName(displayName: String): Result - suspend fun setDisplayName(chatId: ID, displayName: String): Result - suspend fun setDescription(chatId: ID, description: String): Result - suspend fun mute(chatId: ID): Result - suspend fun unmute(chatId: ID): Result - @Deprecated("Replaced by setMessagingFee") - suspend fun setCoverCharge(chatId: ID, amount: KinAmount): Result - suspend fun setMessagingFee(chatId: ID, amount: KinAmount): Result - suspend fun promoteUser(chatId: ID, userId: ID): Result - suspend fun demoteUser(chatId: ID, userId: ID): Result - suspend fun enableChat(chatId: ID): Result - suspend fun disableChat(chatId: ID): Result - - // Self Defense Room Controls - suspend fun removeUser(chatId: ID, userId: ID): Result - suspend fun reportUserForMessage(userId: ID, messageId: ID): Result - suspend fun muteUser(chatId: ID, userId: ID): Result -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/chat/RealChatRepository.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/chat/RealChatRepository.kt deleted file mode 100644 index 9c80fb584..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/chat/RealChatRepository.kt +++ /dev/null @@ -1,393 +0,0 @@ -package xyz.flipchat.services.internal.network.repository.chat - -import com.getcode.model.ID -import com.getcode.model.KinAmount -import com.getcode.utils.ErrorUtils -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.withContext -import xyz.flipchat.services.data.ChatIdentifier -import xyz.flipchat.services.data.Member -import xyz.flipchat.services.data.Room -import xyz.flipchat.services.data.RoomWithMembers -import xyz.flipchat.services.data.StartChatRequestType -import xyz.flipchat.services.domain.mapper.ConversationMessageMapper -import xyz.flipchat.services.domain.mapper.RoomConversationMapper -import xyz.flipchat.services.domain.model.chat.StreamMemberUpdate -import xyz.flipchat.services.domain.model.chat.db.ChatUpdate -import xyz.flipchat.services.domain.model.chat.db.ConversationMemberUpdate -import xyz.flipchat.services.domain.model.query.QueryOptions -import xyz.flipchat.services.internal.data.mapper.ConversationMemberMapper -import xyz.flipchat.services.internal.data.mapper.LastMessageMapper -import xyz.flipchat.services.internal.data.mapper.MemberUpdateMapper -import xyz.flipchat.services.internal.data.mapper.MetadataRoomMapper -import xyz.flipchat.services.internal.data.mapper.MetadataUpdateMapper -import xyz.flipchat.services.internal.data.mapper.RoomWithMembersMapper -import xyz.flipchat.services.internal.data.mapper.StreamMetadataUpdateMapper -import xyz.flipchat.services.internal.network.chat.ChatStreamUpdate -import xyz.flipchat.services.internal.network.service.ChatHomeStreamReference -import xyz.flipchat.services.internal.network.service.ChatService -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -internal class RealChatRepository @Inject constructor( - private val userManager: UserManager, - private val service: ChatService, - private val roomMapper: MetadataRoomMapper, - private val roomWithMembersMapper: RoomWithMembersMapper, - private val metadataUpdateMapper: MetadataUpdateMapper, - private val streamMetadataUpdateMapper: StreamMetadataUpdateMapper, - private val memberUpdateMapper: MemberUpdateMapper, - private val lastMessageMapper: LastMessageMapper, - private val messageMapper: ConversationMessageMapper, -) : ChatRepository { - private var homeStreamReference: ChatHomeStreamReference? = null - - override suspend fun getChats( - queryOptions: QueryOptions, - ): Result> { - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.getChats(owner, queryOptions) - .map { it.map { meta -> roomMapper.map(meta) } } - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun getChat(identifier: ChatIdentifier): Result { - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.getChat(owner, identifier) - .map { roomWithMembersMapper.map(it) } - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun getChatMembers(identifier: ChatIdentifier): Result> { - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.getChat(owner, identifier) - .map { roomWithMembersMapper.map(it) } - .map { it.members } - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun startChat(type: StartChatRequestType): Result { - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.startChat(owner, type) - .map { roomWithMembersMapper.map(it) } - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun joinChat( - identifier: ChatIdentifier, - paymentId: ID? - ): Result { - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.joinChat(owner, identifier, paymentId) - .map { roomWithMembersMapper.map(it) } - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun leaveChat(chatId: ID): Result { - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.leaveChat(owner, chatId) - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun checkDisplayName(displayName: String): Result { - return withContext(Dispatchers.IO) { - service.checkDisplayName(displayName) - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun setDisplayName(chatId: ID, displayName: String): Result { - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.setDisplayName(owner, chatId, displayName) - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun setDescription(chatId: ID, description: String): Result { - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.setDescription(owner, chatId, description) - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun mute(chatId: ID): Result { - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.muteChat(owner, chatId) - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun unmute(chatId: ID): Result { - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.unmuteChat(owner, chatId) - .onFailure { ErrorUtils.handleError(it) } - } - } - - @Deprecated("Replaced by setMessagingFee") - override suspend fun setCoverCharge(chatId: ID, amount: KinAmount): Result { - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.setCoverCharge(owner, chatId, amount) - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun setMessagingFee(chatId: ID, amount: KinAmount): Result { - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.setMessagingFee(owner, chatId, amount) - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun enableChat(chatId: ID): Result { - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.enableChat(owner, chatId) - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun disableChat(chatId: ID): Result { - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.disableChat(owner, chatId) - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun getMemberUpdates(chatId: ID, afterMember: ID?): Result> { - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.getMemberUpdates(owner, chatId, afterMember) - .map { updates -> updates.mapNotNull { memberUpdateMapper.map(it) } } - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun promoteUser(chatId: ID, userId: ID): Result { - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.promoteUser(owner, chatId, userId) - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun demoteUser(chatId: ID, userId: ID): Result { - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.demoteUser(owner, chatId, userId) - .onFailure { ErrorUtils.handleError(it) } - } - } - -// override fun observeTyping(chatId: ID): Flow { -// return typingChats -// .map { chatId in it } -// } - - override fun openEventStream(coroutineScope: CoroutineScope, onEvent: (ChatUpdate) -> Unit) { - val owner = userManager.keyPair ?: throw IllegalStateException("No keypair found for owner") - val userId = userManager.userId ?: throw IllegalStateException("user not established") - if (homeStreamReference == null) { - homeStreamReference = service.openChatStream(coroutineScope, owner) { result -> - if (result.isSuccess) { - val data = result.getOrNull() ?: return@openChatStream - val updates = data.mapNotNull { ChatStreamUpdate.invoke(it) } - - updates.onEach { update -> -// // handle typing state changes -// if (update.isTyping != null) { -// if (update.isTyping) { -// _typingChats.update { it + listOf(update.id).toSet() } -// } else { -// _typingChats.update { it - listOf(update.id).toSet() } -// } -// } - - val memberUpdates = update.memberUpdates.map { memberUpdateMapper.map(it) } - - val streamMetadataUpdates = update.metadataUpdates.mapNotNull { metadataUpdateMapper.map(it) } - val metadataUpdates = streamMetadataUpdates.map { streamMetadataUpdateMapper.map(update.id to it) } - - // handle last message update - val message = if (userManager.openRoom != update.id) { - update.lastMessage?.let { - val chatId = update.id - val mapped = lastMessageMapper.map(userId to it) - messageMapper.map(chatId to mapped) - } - } else { - null - } - - val convoMemberUpdates = memberUpdates.mapNotNull { memberUpdate -> - when (memberUpdate) { - is StreamMemberUpdate.Refresh -> { - ConversationMemberUpdate.FullRefresh(update.id, memberUpdate.members) - } - - is StreamMemberUpdate.IndividualRefresh -> { - ConversationMemberUpdate.IndividualRefresh(update.id, memberUpdate.member) - } - - is StreamMemberUpdate.Joined -> { - ConversationMemberUpdate.Joined(update.id, memberUpdate.member) - } - - is StreamMemberUpdate.Left -> { - ConversationMemberUpdate.Left(update.id, memberUpdate.memberId) - } - - is StreamMemberUpdate.Muted -> { - ConversationMemberUpdate.Muted( - update.id, - memberUpdate.memberId, - memberUpdate.mutedBy - ) - } - - is StreamMemberUpdate.Removed -> { - ConversationMemberUpdate.Removed( - update.id, - memberUpdate.memberId, - memberUpdate.removedBy - ) - } - - is StreamMemberUpdate.Demoted -> { - ConversationMemberUpdate.Demoted( - update.id, - memberUpdate.memberId, - memberUpdate.by - ) - } - is StreamMemberUpdate.Promoted -> { - ConversationMemberUpdate.Promoted( - update.id, - memberUpdate.memberId, - memberUpdate.by - ) - } - is StreamMemberUpdate.IdentityChanged -> { - ConversationMemberUpdate.IdentityChanged( - memberUpdate.memberId, - memberUpdate.identity - ) - } - null -> null - } - } - - onEvent( - ChatUpdate( - metadata = metadataUpdates, - members = convoMemberUpdates, - message = message - ) - ) - } - } else { - result.exceptionOrNull()?.let { - ErrorUtils.handleError(it) - } - } - } - } - } - - override fun closeEventStream() { - homeStreamReference?.destroy() - homeStreamReference = null - } - - override suspend fun removeUser(chatId: ID, userId: ID): Result { - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - return withContext(Dispatchers.IO) { - service.removeUser(owner, chatId, userId) - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun reportUserForMessage( - userId: ID, - messageId: ID - ): Result { - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - return withContext(Dispatchers.IO) { - service.reportUser(owner, userId, messageId) - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun muteUser(chatId: ID, userId: ID): Result { - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - return withContext(Dispatchers.IO) { - service.muteUser(owner, chatId, userId) - .onFailure { ErrorUtils.handleError(it) } - } - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/iap/InAppPurchaseRepository.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/iap/InAppPurchaseRepository.kt deleted file mode 100644 index c33c2ffaa..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/iap/InAppPurchaseRepository.kt +++ /dev/null @@ -1,5 +0,0 @@ -package xyz.flipchat.services.internal.network.repository.iap - -interface InAppPurchaseRepository { - suspend fun onPurchaseCompleted(receipt: String): Result -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/iap/RealInAppPurchaseRepository.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/iap/RealInAppPurchaseRepository.kt deleted file mode 100644 index 7a2c0e7f0..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/iap/RealInAppPurchaseRepository.kt +++ /dev/null @@ -1,19 +0,0 @@ -package xyz.flipchat.services.internal.network.repository.iap - -import com.getcode.utils.ErrorUtils -import xyz.flipchat.services.internal.network.service.PurchaseService -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -internal class RealInAppPurchaseRepository @Inject constructor( - private val userManager: UserManager, - private val service: PurchaseService, -) : InAppPurchaseRepository { - override suspend fun onPurchaseCompleted(receipt: String): Result { - val owner = userManager.keyPair ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - return service.onPurchaseCompleted(owner, receipt) - .onFailure { ErrorUtils.handleError(it) } - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/messaging/MessagingRepository.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/messaging/MessagingRepository.kt deleted file mode 100644 index 5e5e2e10d..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/messaging/MessagingRepository.kt +++ /dev/null @@ -1,34 +0,0 @@ -package xyz.flipchat.services.internal.network.repository.messaging - -import com.getcode.model.ID -import com.getcode.model.chat.ChatMessage -import com.getcode.model.chat.MessageStatus -import com.getcode.services.model.chat.OutgoingMessageContent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import xyz.flipchat.services.domain.model.chat.ConversationMessage -import xyz.flipchat.services.domain.model.query.QueryOptions - -interface MessagingRepository { - suspend fun getMessages( - chatId: ID, - queryOptions: QueryOptions = QueryOptions(), - ): Result> - - suspend fun sendMessage(chatId: ID, content: OutgoingMessageContent): Result - suspend fun advancePointer(chatId: ID, messageId: ID, status: MessageStatus): Result - fun observeTyping(chatId: ID): Flow> - suspend fun onStartedTyping(chatId: ID): Result - suspend fun onStillTyping(chatId: ID): Result - suspend fun onStoppedTyping(chatId: ID): Result - fun openMessageStream( - coroutineScope: CoroutineScope, - chatId: ID, - onMessagesUpdated: (List) -> Unit, - ) - - fun closeMessageStream() - - // Self Defense Room Controls - suspend fun deleteMessage(chatId: ID, messageId: ID): Result -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/messaging/RealMessagingRepository.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/messaging/RealMessagingRepository.kt deleted file mode 100644 index 60cb7b541..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/messaging/RealMessagingRepository.kt +++ /dev/null @@ -1,193 +0,0 @@ -package xyz.flipchat.services.internal.network.repository.messaging - -import com.getcode.model.ID -import com.getcode.model.chat.ChatMessage -import com.getcode.model.chat.MessageStatus -import com.getcode.services.model.chat.OutgoingMessageContent -import com.getcode.utils.ErrorUtils -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext -import xyz.flipchat.services.domain.mapper.ConversationMessageMapper -import xyz.flipchat.services.domain.model.chat.ConversationMessage -import xyz.flipchat.services.domain.model.query.QueryOptions -import xyz.flipchat.services.internal.data.mapper.ChatMessageMapper -import xyz.flipchat.services.internal.data.mapper.LastMessageMapper -import xyz.flipchat.services.internal.data.mapper.TypingMapper -import xyz.flipchat.services.internal.network.chat.IsTyping -import xyz.flipchat.services.internal.network.chat.MessageStreamUpdate -import xyz.flipchat.services.internal.network.chat.TypingState -import xyz.flipchat.services.internal.network.service.ChatMessageStreamReference -import xyz.flipchat.services.internal.network.service.MessagingService -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject - -internal class RealMessagingRepository @Inject constructor( - private val userManager: UserManager, - private val service: MessagingService, - private val chatMessageMapper: ChatMessageMapper, - private val lastMessageMapper: LastMessageMapper, - private val messageMapper: ConversationMessageMapper, - private val typingMapper: TypingMapper, -): MessagingRepository { - private var messageStream: ChatMessageStreamReference? = null - - private val typingState = MutableStateFlow>(emptyList()) - - private fun handleTypingUpdates(updates: List) { - // Identify users who have stopped typing - val noLongerTyping = updates.filter { - it.state == TypingState.Stopped || it.state == TypingState.TimedOut - }.toSet() - - // Get current state - val currently = typingState.value - - // Remove users who stopped typing from current state, preserving order - val afterRemovals = currently.filter { current -> - noLongerTyping.none { it.userId == current.userId } - } - - // Process new typing updates (excluding stops/timeouts) - val newTypingUpdates = updates.filter { - it.state != TypingState.Stopped && it.state != TypingState.TimedOut - } - - // Update existing users and collect truly new users - val updatedList = afterRemovals.map { existing -> - // If there's a new update for this user, use it instead - newTypingUpdates.find { it.userId == existing.userId } ?: existing - } - - // Get users who are completely new (not in current state) - val trulyNewUpdates = newTypingUpdates.filter { update -> - afterRemovals.none { it.userId == update.userId } - } - - // Combine: updated existing users (in original order) + truly new users - typingState.value = updatedList + trulyNewUpdates - } - - override suspend fun getMessages( - chatId: ID, - queryOptions: QueryOptions, - ): Result> { - val owner = userManager.keyPair ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - val userId = userManager.userId ?: return Result.failure(IllegalStateException("No userId found for owner")) - - return withContext(Dispatchers.IO) { - service.getMessages(owner, chatId, queryOptions) - .map { it.map { meta -> chatMessageMapper.map(userId to meta) } } - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun sendMessage(chatId: ID, content: OutgoingMessageContent): Result { - val owner = userManager.keyPair ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - val userId = userManager.userId ?: return Result.failure(IllegalStateException("No userId found for owner")) - - return withContext(Dispatchers.IO) { - service.sendMessage(owner, chatId, content) - .map { lastMessageMapper.map(userId to it) } - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun deleteMessage(chatId: ID, messageId: ID): Result { - // this utilizes send message under the hood - val content = OutgoingMessageContent.DeleteRequest(messageId) - return sendMessage(chatId, content) - .map { Unit } - } - - override suspend fun advancePointer(chatId: ID, messageId: ID, status: MessageStatus): Result { - val owner = userManager.keyPair ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.advancePointer(owner, chatId, messageId, status) - .onFailure { ErrorUtils.handleError(it) } - } - } - - override fun observeTyping(chatId: ID): Flow> = typingState - .map { it.filterNot { s -> userManager.isSelf(s.userId) } } - .map { it.filter { i -> i.currentlyTyping } } - .map { list -> list.map { it.userId } } - - override suspend fun onStartedTyping(chatId: ID): Result { - val owner = userManager.keyPair ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.notifyOfTypingState(owner, chatId, TypingState.Started) - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun onStillTyping(chatId: ID): Result { - val owner = userManager.keyPair ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.notifyOfTypingState(owner, chatId, TypingState.Still) - .onFailure { ErrorUtils.handleError(it) } - } - } - - override suspend fun onStoppedTyping(chatId: ID): Result { - val owner = userManager.keyPair ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return withContext(Dispatchers.IO) { - service.notifyOfTypingState(owner, chatId, TypingState.Stopped) - .onFailure { ErrorUtils.handleError(it) } - } - } - - override fun openMessageStream( - coroutineScope: CoroutineScope, - chatId: ID, - onMessagesUpdated: (List) -> Unit, - ) { - val owner = userManager.keyPair ?: throw IllegalStateException("No ed25519 signature found for owner") - val userId = userManager.userId ?: throw IllegalStateException("No userId found for owner") - - if (messageStream == null) { - messageStream = service.openMessageStream( - scope = coroutineScope, - owner = owner, - chatId = chatId, - ) stream@{ result -> - if (result.isSuccess) { - val update = result.getOrNull() ?: return@stream - when (update) { - is MessageStreamUpdate.Messages -> { - val messages = update.data.map { lastMessageMapper.map(userId to it) } - val messagesWithContents = messages.map { messageMapper.map(chatId to it) } - - onMessagesUpdated(messagesWithContents) - } - is MessageStreamUpdate.Pointers -> Unit - is MessageStreamUpdate.Typing -> { - handleTypingUpdates(update.data.map { typingMapper.map(it) }) - } - } - } else { - result.exceptionOrNull()?.let { - ErrorUtils.handleError(it) - } - } - } - - messageStream?.onConnect = { - userManager.roomOpened(roomId = chatId) - } - } - } - - override fun closeMessageStream() { - userManager.roomClosed() - messageStream?.destroy() - messageStream = null - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/profile/ProfileRepository.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/profile/ProfileRepository.kt deleted file mode 100644 index e7836f237..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/profile/ProfileRepository.kt +++ /dev/null @@ -1,14 +0,0 @@ -package xyz.flipchat.services.internal.network.repository.profile - -import com.getcode.model.ID -import com.getcode.model.social.user.SocialProfile -import com.getcode.services.model.profile.SocialAccountLinkRequest -import com.getcode.services.model.profile.SocialAccountUnlinkRequest -import xyz.flipchat.services.domain.model.profile.UserProfile - -interface ProfileRepository { - suspend fun getProfile(userId: ID): Result - suspend fun setDisplayName(name: String): Result - suspend fun linkSocialAccount(request: SocialAccountLinkRequest): Result - suspend fun unlinkSocialAccount(request: SocialAccountUnlinkRequest): Result -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/profile/RealProfileRepository.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/profile/RealProfileRepository.kt deleted file mode 100644 index 106fb3576..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/profile/RealProfileRepository.kt +++ /dev/null @@ -1,48 +0,0 @@ -package xyz.flipchat.services.internal.network.repository.profile - -import com.getcode.model.ID -import com.getcode.model.social.user.SocialProfile -import com.getcode.services.model.profile.SocialAccountLinkRequest -import com.getcode.services.model.profile.SocialAccountUnlinkRequest -import com.getcode.utils.ErrorUtils -import xyz.flipchat.services.domain.model.profile.UserProfile -import xyz.flipchat.services.internal.data.mapper.SocialProfileMapper -import xyz.flipchat.services.internal.data.mapper.UserProfileMapper -import xyz.flipchat.services.internal.network.service.ProfileService -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject - -internal class RealProfileRepository @Inject constructor( - private val userManager: UserManager, - private val service: ProfileService, - private val userProfileMapper: UserProfileMapper, - private val socialProfileMapper: SocialProfileMapper -) : ProfileRepository { - override suspend fun getProfile(userId: ID): Result { - return service.getProfile(userId) - .map { userProfileMapper.map(it) } - .onFailure { ErrorUtils.handleError(it) } - } - - override suspend fun setDisplayName(name: String): Result { - val owner = userManager.keyPair ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return service.setDisplayName(owner, name) - .onFailure { ErrorUtils.handleError(it) } - } - - override suspend fun linkSocialAccount(request: SocialAccountLinkRequest): Result { - val owner = userManager.keyPair ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return service.linkSocialAccount(owner, request) - .map { socialProfileMapper.map(it) } - .onFailure { ErrorUtils.handleError(it) } - } - - override suspend fun unlinkSocialAccount(request: SocialAccountUnlinkRequest): Result { - val owner = userManager.keyPair ?: return Result.failure(IllegalStateException("No ed25519 signature found for owner")) - - return service.unlinkSocialAccount(owner, request) - .onFailure { ErrorUtils.handleError(it) } - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/push/PushRepository.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/push/PushRepository.kt deleted file mode 100644 index 56e1b7523..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/push/PushRepository.kt +++ /dev/null @@ -1,23 +0,0 @@ -package xyz.flipchat.services.internal.network.repository.push - -import com.getcode.ed25519.Ed25519 -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.ID - -interface PushRepository { - suspend fun addToken( - owner: KeyPair, - token: String, - installationId: String? - ): Result - - suspend fun deleteToken( - owner: KeyPair, - token: String - ): Result - - suspend fun deleteTokens( - owner: KeyPair, - installationId: String? - ): Result -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/push/RealPushRepository.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/push/RealPushRepository.kt deleted file mode 100644 index 79c956deb..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/repository/push/RealPushRepository.kt +++ /dev/null @@ -1,28 +0,0 @@ -package xyz.flipchat.services.internal.network.repository.push - -import com.getcode.ed25519.Ed25519 -import com.getcode.model.ID -import com.getcode.utils.ErrorUtils -import xyz.flipchat.services.internal.network.service.PushService -import javax.inject.Inject - -internal class RealPushRepository @Inject constructor( - private val service: PushService -) : PushRepository { - override suspend fun addToken( - owner: Ed25519.KeyPair, - token: String, - installationId: String? - ): Result { - return service.addToken(owner, token, installationId) - .onFailure { ErrorUtils.handleError(it) } - } - - override suspend fun deleteToken(owner: Ed25519.KeyPair, token: String): Result { - return service.deleteToken(owner, token) - } - - override suspend fun deleteTokens(owner: Ed25519.KeyPair, installationId: String?): Result { - return service.deleteTokens(owner, installationId) - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/service/AccountService.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/service/AccountService.kt deleted file mode 100644 index 9256a1540..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/service/AccountService.kt +++ /dev/null @@ -1,222 +0,0 @@ -package xyz.flipchat.services.internal.network.service - -import com.codeinc.flipchat.gen.account.v1.AccountService -import com.codeinc.flipchat.gen.account.v1.AccountService.LoginResponse -import com.codeinc.flipchat.gen.account.v1.AccountService.RegisterResponse -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.ID -import com.getcode.services.network.core.NetworkOracle -import com.getcode.solana.keys.PublicKey -import com.getcode.utils.CodeServerError -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import timber.log.Timber -import xyz.flipchat.services.data.PaymentTarget -import xyz.flipchat.services.internal.network.api.AccountApi -import com.getcode.utils.FlipchatServerError -import xyz.flipchat.services.internal.network.extensions.toPublicKey -import javax.inject.Inject - -internal class AccountService @Inject constructor( - private val api: AccountApi, - private val networkOracle: NetworkOracle, -) { - suspend fun register(owner: KeyPair, displayName: String?): Result { - return try { - networkOracle.managedRequest(api.register(owner, displayName)) - .map { response -> - when (response.result) { - RegisterResponse.Result.OK -> { - Result.success(response.userId.value.toByteArray().toList()) - } - - RegisterResponse.Result.INVALID_SIGNATURE -> { - val error = RegisterError.InvalidSignature(response.errorReason) - Timber.e(t = error) - Result.failure(error) - } - - RegisterResponse.Result.INVALID_DISPLAY_NAME -> { - val error = RegisterError.InvalidDisplayName(response.errorReason) - Timber.e(t = error) - Result.failure(error) - } - - RegisterResponse.Result.DENIED -> { - val error = RegisterError.Denied(response.errorReason) - Timber.e(t = error) - Result.failure(error) - } - - RegisterResponse.Result.UNRECOGNIZED -> { - val error = RegisterError.Unrecognized(response.errorReason) - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = RegisterError.Other("Failed to register") - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = RegisterError.Other( - "Road to greatness is bumpy. Apologies for the hiccup.", - cause = e - ) - Result.failure(error) - } - } - - suspend fun login(owner: KeyPair): Result { - return try { - networkOracle.managedRequest(api.login(owner)) - .map { response -> - when (response.result) { - LoginResponse.Result.OK -> { - Result.success(response.userId.value.toByteArray().toList()) - } - - LoginResponse.Result.UNRECOGNIZED -> { - val error = LoginError.Unrecognized("Failed to login") - Timber.e(t = error) - Result.failure(error) - } - - LoginResponse.Result.INVALID_TIMESTAMP -> { - val error = LoginError.InvalidTimestamp("Failed to login") - Timber.e(t = error) - Result.failure(error) - } - - LoginResponse.Result.DENIED -> { - val error = LoginError.Denied("Failed to login") - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = LoginError.Other("Failed to login") - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = RegisterError.Other( - "Road to greatness is bumpy. Apologies for the hiccup.", - cause = e - ) - Result.failure(error) - } - } - - suspend fun getPaymentDestination(target: PaymentTarget): Result { - return try { - networkOracle.managedRequest(api.getPaymentDestination(target)) - .map { response -> - when (response.result) { - AccountService.GetPaymentDestinationResponse.Result.OK -> Result.success( - response.paymentDestination.toPublicKey() - ) - - AccountService.GetPaymentDestinationResponse.Result.NOT_FOUND -> { - val error = GetPaymentDestinationError.NotFound() - Timber.e(t = error) - Result.failure(error) - } - - AccountService.GetPaymentDestinationResponse.Result.UNRECOGNIZED -> { - val error = GetPaymentDestinationError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = GetPaymentDestinationError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = GetPaymentDestinationError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun getUserFlags(owner: KeyPair, userId: ID): Result { - return try { - networkOracle.managedRequest(api.getUserFlags(owner = owner, userId = userId)) - .map { response -> - when (response.result) { - AccountService.GetUserFlagsResponse.Result.OK -> Result.success(response.userFlags) - AccountService.GetUserFlagsResponse.Result.DENIED -> { - val error = GetUserFlagsError.Denied() - Timber.e(t = error) - Result.failure(error) - } - - AccountService.GetUserFlagsResponse.Result.UNRECOGNIZED -> { - val error = GetUserFlagsError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = GetUserFlagsError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = GetUserFlagsError.Other(cause = e) - Result.failure(error) - } - } -} - -sealed class LoginError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - data class InvalidTimestamp(override val message: String) : LoginError(message) - data class NotFound(override val message: String) : LoginError(message) - data class Denied(override val message: String) : LoginError(message) - data class Unrecognized(override val message: String) : LoginError(message) - data class Other(override val message: String, override val cause: Throwable? = null) : - LoginError(message, cause) -} - -sealed class RegisterError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - data class InvalidSignature(override val message: String) : RegisterError(message) - data class InvalidDisplayName(override val message: String) : RegisterError(message) - data class Denied(override val message: String): RegisterError(message) - data class Unrecognized(override val message: String) : RegisterError(message) - data class Other(override val message: String, override val cause: Throwable? = null) : - RegisterError(message) -} - -sealed class GetPaymentDestinationError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Unrecognized : GetPaymentDestinationError() - class NotFound : GetPaymentDestinationError() - data class Other(override val cause: Throwable? = null) : GetPaymentDestinationError() -} - -sealed class GetUserFlagsError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Unrecognized : GetUserFlagsError() - class Denied : GetUserFlagsError() - data class Other(override val cause: Throwable? = null) : GetUserFlagsError() -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/service/ChatService.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/service/ChatService.kt deleted file mode 100644 index 04010bd9f..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/service/ChatService.kt +++ /dev/null @@ -1,1017 +0,0 @@ -package xyz.flipchat.services.internal.network.service - -import com.codeinc.flipchat.gen.chat.v1.ChatService -import com.codeinc.flipchat.gen.chat.v1.ChatService.MemberUpdate -import com.codeinc.flipchat.gen.common.v1.Common -import com.codeinc.flipchat.gen.chat.v1.ChatService as ChatServiceRpc -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.ID -import com.getcode.model.KinAmount -import com.getcode.services.network.core.NetworkOracle -import com.getcode.services.observers.BidirectionalStreamReference -import com.getcode.utils.CodeServerError -import com.getcode.utils.ErrorUtils -import com.getcode.utils.TraceType -import com.getcode.utils.trace -import com.google.protobuf.Timestamp -import io.grpc.Status -import io.grpc.StatusRuntimeException -import io.grpc.stub.StreamObserver -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import timber.log.Timber -import xyz.flipchat.services.data.ChatIdentifier -import xyz.flipchat.services.data.StartChatRequestType -import xyz.flipchat.services.domain.model.query.QueryOptions -import xyz.flipchat.services.internal.network.api.ChatApi -import xyz.flipchat.services.internal.network.chat.GetOrJoinChatResponse -import xyz.flipchat.services.internal.network.utils.authenticate -import xyz.flipchat.services.network.managedApiRequest -import javax.inject.Inject - -typealias ChatHomeStreamReference = BidirectionalStreamReference - - -internal class ChatService @Inject constructor( - private val api: ChatApi, - private val networkOracle: NetworkOracle, -) { - suspend fun getChats( - owner: KeyPair, - queryOptions: QueryOptions = QueryOptions() - ): Result> { - return try { - networkOracle.managedRequest( - api.getChats( - owner = owner, - queryOptions = queryOptions, - ) - ) - .map { response -> - when (response.result) { - ChatServiceRpc.GetChatsResponse.Result.OK -> { - Result.success(response.chatsList) - } - - ChatServiceRpc.GetChatsResponse.Result.UNRECOGNIZED -> { - val error = GetChatsError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = GetChatsError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = GetChatsError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun getChat( - owner: KeyPair, - identifier: ChatIdentifier, - ): Result { - return try { - networkOracle.managedRequest(api.getChat(owner, identifier)) - .map { response -> - when (response.result) { - ChatServiceRpc.GetChatResponse.Result.OK -> { - Result.success( - GetOrJoinChatResponse( - response.metadata, - response.membersList - ) - ) - } - - ChatServiceRpc.GetChatResponse.Result.UNRECOGNIZED -> { - val error = GetChatError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - - ChatServiceRpc.GetChatResponse.Result.NOT_FOUND -> { - val error = GetChatError.NotFound() - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = GetChatError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - e.printStackTrace() - val error = GetChatError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun startChat( - owner: KeyPair, - type: StartChatRequestType, - ): Result { - return try { - networkOracle.managedRequest(api.startChat(owner, type)) - .map { response -> - when (response.result) { - ChatServiceRpc.StartChatResponse.Result.OK -> { - Result.success( - GetOrJoinChatResponse( - response.chat, - response.membersList - ) - ) - } - - ChatServiceRpc.StartChatResponse.Result.DENIED -> { - val error = StartChatError.Denied() - Timber.e(t = error) - Result.failure(error) - } - - ChatServiceRpc.StartChatResponse.Result.USER_NOT_FOUND -> { - val error = StartChatError.UserNotFound() - Timber.e(t = error) - Result.failure(error) - } - - ChatServiceRpc.StartChatResponse.Result.UNRECOGNIZED -> { - val error = StartChatError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = StartChatError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - e.printStackTrace() - val error = StartChatError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun joinChat( - owner: KeyPair, - identifier: ChatIdentifier, - paymentId: ID?, - ): Result { - return try { - networkOracle.managedRequest(api.joinChat(owner, identifier, paymentId)) - .map { response -> - when (response.result) { - ChatServiceRpc.JoinChatResponse.Result.OK -> { - Result.success( - GetOrJoinChatResponse( - response.metadata, - response.membersList - ) - ) - } - - ChatServiceRpc.JoinChatResponse.Result.UNRECOGNIZED -> { - val error = JoinChatError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - - ChatServiceRpc.JoinChatResponse.Result.DENIED -> { - val error = JoinChatError.Denied() - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = JoinChatError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = JoinChatError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun leaveChat( - owner: KeyPair, - chatId: ID, - ): Result { - return try { - networkOracle.managedRequest(api.leaveChat(owner, chatId)) - .map { response -> - when (response.result) { - ChatServiceRpc.LeaveChatResponse.Result.OK -> { - Result.success(Unit) - } - - ChatServiceRpc.LeaveChatResponse.Result.UNRECOGNIZED -> { - val error = LeaveChatError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = LeaveChatError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = LeaveChatError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun setDisplayName( - owner: KeyPair, - chatId: ID, - displayName: String, - ): Result { - return try { - networkOracle.managedRequest(api.setDisplayName(owner, chatId, displayName)) - .map { response -> - when (response.result) { - ChatServiceRpc.SetDisplayNameResponse.Result.OK -> { - Result.success(Unit) - } - - ChatServiceRpc.SetDisplayNameResponse.Result.DENIED -> { - val error = SetRoomDisplayNameError.Denied() - Timber.e(t = error) - Result.failure(error) - } - - ChatServiceRpc.SetDisplayNameResponse.Result.CANT_SET -> { - val error = - SetRoomDisplayNameError.CantSet(response.alternateSuggestionsList.toList()) - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = SetRoomDisplayNameError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = SetRoomDisplayNameError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun setDescription( - owner: KeyPair, - chatId: ID, - description: String, - ): Result { - return try { - networkOracle.managedRequest(api.setDescription(owner, chatId, description)) - .map { response -> - when (response.result) { - ChatServiceRpc.SetDescriptionResponse.Result.OK -> { - Result.success(Unit) - } - - ChatServiceRpc.SetDescriptionResponse.Result.DENIED -> { - val error = SetRoomDescriptionError.Denied() - Timber.e(t = error) - Result.failure(error) - } - - ChatServiceRpc.SetDescriptionResponse.Result.CANT_SET -> { - val error = - SetRoomDescriptionError.CantSet() - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = SetRoomDescriptionError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = SetRoomDescriptionError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun muteChat(owner: KeyPair, chatId: ID): Result { - return try { - networkOracle.managedRequest(api.muteChat(owner, chatId)) - .map { response -> - when (response.result) { - ChatServiceRpc.MuteChatResponse.Result.OK -> { - Result.success(Unit) - } - - ChatServiceRpc.MuteChatResponse.Result.UNRECOGNIZED -> { - val error = MuteChatStateError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - - ChatServiceRpc.MuteChatResponse.Result.DENIED -> { - val error = MuteChatStateError.Denied() - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = MuteChatStateError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = MuteChatStateError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun unmuteChat(owner: KeyPair, chatId: ID): Result { - return try { - networkOracle.managedRequest(api.unmuteChat(owner, chatId)) - .map { response -> - when (response.result) { - ChatServiceRpc.UnmuteChatResponse.Result.OK -> { - Result.success(Unit) - } - - ChatServiceRpc.UnmuteChatResponse.Result.UNRECOGNIZED -> { - val error = MuteChatStateError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - - ChatServiceRpc.UnmuteChatResponse.Result.DENIED -> { - val error = MuteChatStateError.Denied() - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = MuteChatStateError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = MuteChatStateError.Other(cause = e) - Result.failure(error) - } - } - - @Deprecated("Replaced by setMessagingFee") - suspend fun setCoverCharge( - owner: KeyPair, - chatId: ID, - amount: KinAmount - ): Result { - return try { - networkOracle.managedRequest(api.setCoverCharge(owner, chatId, amount)) - .map { response -> - when (response.result) { - ChatServiceRpc.SetCoverChargeResponse.Result.OK -> { - Result.success(Unit) - } - - ChatServiceRpc.SetCoverChargeResponse.Result.UNRECOGNIZED -> { - val error = CoverChargeError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - - ChatServiceRpc.SetCoverChargeResponse.Result.DENIED -> { - val error = CoverChargeError.Denied() - Timber.e(t = error) - Result.failure(error) - } - - ChatServiceRpc.SetCoverChargeResponse.Result.CANT_SET -> { - val error = CoverChargeError.CantSet() - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = CoverChargeError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = MessagingFeeError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun setMessagingFee( - owner: KeyPair, - chatId: ID, - amount: KinAmount - ): Result { - return try { - networkOracle.managedRequest(api.setMessagingFee(owner, chatId, amount)) - .map { response -> - when (response.result) { - ChatServiceRpc.SetMessagingFeeResponse.Result.OK -> { - Result.success(Unit) - } - - ChatServiceRpc.SetMessagingFeeResponse.Result.UNRECOGNIZED -> { - val error = MessagingFeeError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - - ChatServiceRpc.SetMessagingFeeResponse.Result.DENIED -> { - val error = MessagingFeeError.Denied() - Timber.e(t = error) - Result.failure(error) - } - - ChatServiceRpc.SetMessagingFeeResponse.Result.CANT_SET -> { - val error = MessagingFeeError.CantSet() - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = MessagingFeeError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = MessagingFeeError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun getMemberUpdates( - owner: KeyPair, - chatId: ID, - afterMember: ID?, - ): Result> { - return try { - networkOracle.managedRequest(api.getMemberUpdates(owner, chatId, afterMember)) - .map { response -> - when (response.result) { - ChatServiceRpc.GetMemberUpdatesResponse.Result.OK -> { - Result.success(response.updatesList) - } - - ChatServiceRpc.GetMemberUpdatesResponse.Result.UNRECOGNIZED -> { - val error = GetMemberUpdateError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - - ChatServiceRpc.GetMemberUpdatesResponse.Result.NOT_FOUND -> { - val error = GetMemberUpdateError.NotFound() - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = GetMemberUpdateError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = GetMemberUpdateError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun removeUser( - owner: KeyPair, - chatId: ID, - userId: ID - ): Result { - return try { - networkOracle.managedRequest(api.removeUser(owner, chatId, userId)) - .map { response -> - when (response.result) { - ChatServiceRpc.RemoveUserResponse.Result.OK -> { - Result.success(Unit) - } - - ChatServiceRpc.RemoveUserResponse.Result.UNRECOGNIZED -> { - val error = RemoveUserError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - - ChatServiceRpc.RemoveUserResponse.Result.DENIED -> { - val error = RemoveUserError.Denied() - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = RemoveUserError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = RemoveUserError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun reportUser( - owner: KeyPair, - userId: ID, - messageId: ID - ): Result { - return try { - networkOracle.managedRequest(api.reportUser(owner, userId, messageId)) - .map { response -> - when (response.result) { - ChatServiceRpc.ReportUserResponse.Result.OK -> { - Result.success(Unit) - } - - ChatServiceRpc.ReportUserResponse.Result.UNRECOGNIZED -> { - val error = ReportUserError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = ReportUserError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = ReportUserError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun muteUser( - owner: KeyPair, - chatId: ID, - userId: ID, - ): Result { - return try { - networkOracle.managedRequest(api.muteUser(owner, chatId, userId)) - .map { response -> - when (response.result) { - ChatServiceRpc.MuteUserResponse.Result.OK -> { - Result.success(Unit) - } - - ChatServiceRpc.MuteUserResponse.Result.UNRECOGNIZED -> { - val error = MuteUserError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - - else -> { - val error = MuteUserError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = MuteUserError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun promoteUser( - owner: KeyPair, - chatId: ID, - userId: ID, - enableSendPermission: Boolean = true, - ): Result { - return networkOracle.managedApiRequest( - call = { api.promoteUser(owner, chatId, userId, enableSendPermission) }, - handleResponse = { response -> - when (response.result) { - ChatService.PromoteUserResponse.Result.OK -> Result.success(Unit) - ChatService.PromoteUserResponse.Result.NOT_REGISTERED -> Result.failure(PromoteUserError.NotRegistered()) - ChatService.PromoteUserResponse.Result.DENIED -> Result.failure(PromoteUserError.Denied()) - ChatService.PromoteUserResponse.Result.UNRECOGNIZED -> Result.failure(PromoteUserError.Unrecognized()) - else -> Result.failure(PromoteUserError.Other()) - } - }, - onOtherError = { cause -> - Result.failure(PromoteUserError.Other(cause)) - } - ) - } - - suspend fun demoteUser( - owner: KeyPair, - chatId: ID, - userId: ID, - disableSendPermission: Boolean = true, - ): Result { - return networkOracle.managedApiRequest( - call = { api.demoteUser(owner, chatId, userId, disableSendPermission) }, - handleResponse = { response -> - when (response.result) { - ChatService.DemoteUserResponse.Result.OK -> Result.success(Unit) - ChatService.DemoteUserResponse.Result.DENIED -> Result.failure(DemoteUserError.Denied()) - ChatService.DemoteUserResponse.Result.UNRECOGNIZED -> Result.failure(DemoteUserError.Unrecognized()) - else -> Result.failure(DemoteUserError.Other()) - } - }, - onOtherError = { cause -> - Result.failure(DemoteUserError.Other(cause)) - } - ) - } - - suspend fun enableChat( - owner: KeyPair, - chatId: ID, - ): Result { - return networkOracle.managedApiRequest( - call = { api.openChat(owner, chatId) }, - handleResponse = { response -> - when (response.result) { - ChatService.OpenChatResponse.Result.OK -> Result.success(Unit) - ChatService.OpenChatResponse.Result.DENIED -> Result.failure(OpenChatError.Denied()) - ChatService.OpenChatResponse.Result.UNRECOGNIZED -> Result.failure(OpenChatError.Unrecognized()) - else -> Result.failure(OpenChatError.Other()) - } - }, - onOtherError = { cause -> - Result.failure(OpenChatError.Other(cause = cause)) - } - ) - } - - suspend fun disableChat( - owner: KeyPair, - chatId: ID, - ): Result { - return networkOracle.managedApiRequest( - call = { api.closeChat(owner, chatId) }, - handleResponse = { response -> - when (response.result) { - ChatService.CloseChatResponse.Result.OK -> Result.success(Unit) - ChatService.CloseChatResponse.Result.DENIED -> Result.failure(CloseChatError.Denied()) - ChatService.CloseChatResponse.Result.UNRECOGNIZED -> Result.failure(CloseChatError.Unrecognized()) - else -> Result.failure(CloseChatError.Other()) - } - }, - onOtherError = { cause -> - Result.failure(CloseChatError.Other(cause = cause)) - } - ) - } - - suspend fun checkDisplayName(name: String): Result { - return networkOracle.managedApiRequest( - call = { api.checkDisplayName(name) }, - handleResponse = { response -> - when (response.result) { - ChatService.CheckDisplayNameResponse.Result.OK -> { - if (!response.isAllowed) { - return@managedApiRequest Result.failure(CheckDisplayNameError.CantSet()) - } - Result.success(Unit) - } - ChatService.CheckDisplayNameResponse.Result.UNRECOGNIZED -> Result.failure(CheckDisplayNameError.Unrecognized()) - else -> Result.failure(CheckDisplayNameError.Other()) - } - - }, - onOtherError = { cause -> - Result.failure(CheckDisplayNameError.Other(cause = cause)) - } - ) - } - - fun openChatStream( - scope: CoroutineScope, - owner: KeyPair, - onEvent: (Result>) -> Unit - ): ChatHomeStreamReference { - trace("Chat Opening stream.") - val streamReference = ChatHomeStreamReference(scope) - streamReference.retain() - streamReference.timeoutHandler = { - trace("Chat Stream timed out") - openChatStream( - owner = owner, - reference = streamReference, - onEvent = onEvent - ) - } - - openChatStream(owner, streamReference, onEvent) - - return streamReference - } - - private fun openChatStream( - owner: KeyPair, - reference: ChatHomeStreamReference, - onEvent: (Result>) -> Unit - ) { - try { - reference.cancel() - reference.stream = - api.streamEvents(object : StreamObserver { - override fun onNext(value: ChatServiceRpc.StreamChatEventsResponse?) { - val result = value?.typeCase - if (result == null) { - trace( - message = "Chat Stream Server sent empty message. This is unexpected.", - type = TraceType.Error - ) - return - } - - when (result) { - ChatServiceRpc.StreamChatEventsResponse.TypeCase.EVENTS -> { - onEvent(Result.success(value.events.updatesList)) - } - - ChatServiceRpc.StreamChatEventsResponse.TypeCase.PING -> { - val stream = reference.stream ?: return - val request = ChatServiceRpc.StreamChatEventsRequest.newBuilder() - .setPong( - Common.ClientPong.newBuilder() - .setTimestamp( - Timestamp.newBuilder() - .setSeconds(System.currentTimeMillis() / 1_000) - ) - ).build() - - reference.receivedPing(updatedTimeout = value.ping.pingDelay.seconds * 1_000L) - stream.onNext(request) - trace("Pong Chat Stream Server timestamp: ${value.ping.timestamp}") - } - - ChatServiceRpc.StreamChatEventsResponse.TypeCase.TYPE_NOT_SET -> Unit - ChatServiceRpc.StreamChatEventsResponse.TypeCase.ERROR -> { - trace( - type = TraceType.Error, - message = "Chat Stream hit a snag. ${value.error.code}" - ) - } - } - } - - override fun onError(t: Throwable?) { - val statusException = t as? StatusRuntimeException - if (statusException?.status?.code == Status.Code.UNAVAILABLE) { - trace("Chat Reconnecting keepalive stream...") - openChatStream( - owner, - reference, - onEvent - ) - } else { - trace( - "Chat Stream ${statusException?.status?.code?.name}", - error = statusException?.status?.cause - ) - } - } - - override fun onCompleted() { - - } - }) - - reference.coroutineScope.launch { - val request = ChatServiceRpc.StreamChatEventsRequest.newBuilder() - .setParams( - ChatServiceRpc.StreamChatEventsRequest.Params.newBuilder() - .setTs( - Timestamp.newBuilder() - .setSeconds(System.currentTimeMillis() / 1_000) - ) - .apply { setAuth(authenticate(owner)) } - .build() - ).build() - - reference.stream?.onNext(request) - trace("Chat Stream Initiating a connection...") - } - } catch (e: Exception) { - if (e is IllegalStateException && e.message == "call already half-closed") { - // ignore - } else { - ErrorUtils.handleError(e) - } - } - } -} - -sealed class StartChatError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class UserNotFound : StartChatError() - class Denied : StartChatError() - class Unrecognized : StartChatError() - data class Other(override val cause: Throwable? = null) : StartChatError(cause = cause) -} - -sealed class GetChatsError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Unrecognized : GetChatsError() - data class Other(override val cause: Throwable? = null) : GetChatsError(cause = cause) -} - -sealed class JoinChatError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Unrecognized : JoinChatError() - class Denied : JoinChatError() - data class Other(override val cause: Throwable? = null) : JoinChatError(cause = cause) -} - -sealed class LeaveChatError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Unrecognized : LeaveChatError() - data class Other(override val cause: Throwable? = null) : LeaveChatError(cause = cause) -} - -sealed class MuteChatStateError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Unrecognized : MuteChatStateError() - class Denied : MuteChatStateError() - data class Other(override val cause: Throwable? = null) : MuteChatStateError(cause = cause) -} - -sealed class SetRoomDisplayNameError( - open val alternateSuggestions: List = emptyList(), - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - data class CantSet(override val alternateSuggestions: List) : - SetRoomDisplayNameError(alternateSuggestions) - - class Denied : SetRoomDisplayNameError() - data class Other(override val cause: Throwable? = null) : SetRoomDisplayNameError(cause = cause) -} - -sealed class SetRoomDescriptionError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class CantSet : SetRoomDescriptionError() - class Denied : SetRoomDescriptionError() - data class Other(override val cause: Throwable? = null) : SetRoomDescriptionError(cause = cause) -} - -sealed class GetChatError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class NotFound : GetChatError() - class Unrecognized : GetChatError() - data class Other(override val cause: Throwable? = null) : GetChatError(cause = cause) -} - -sealed class CoverChargeError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Unrecognized : CoverChargeError() - class Denied : CoverChargeError() - class CantSet : CoverChargeError() - data class Other(override val cause: Throwable? = null) : CoverChargeError(cause = cause) -} - -sealed class MessagingFeeError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Unrecognized : MessagingFeeError() - class Denied : MessagingFeeError() - class CantSet : MessagingFeeError() - data class Other(override val cause: Throwable? = null) : MessagingFeeError(cause = cause) -} - -sealed class RemoveUserError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Unrecognized : RemoveUserError() - class Denied : RemoveUserError() - data class Other(override val cause: Throwable? = null) : RemoveUserError(cause = cause) -} - -sealed class GetMemberUpdateError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Unrecognized : GetMemberUpdateError() - class NotFound : GetMemberUpdateError() - data class Other(override val cause: Throwable? = null) : GetMemberUpdateError(cause = cause) -} - -sealed class PromoteUserError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Denied : PromoteUserError() - class Unrecognized : PromoteUserError() - class NotRegistered: PromoteUserError() - data class Other(override val cause: Throwable? = null) : PromoteUserError(cause = cause) -} - -sealed class DemoteUserError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Denied : DemoteUserError() - class Unrecognized : DemoteUserError() - data class Other(override val cause: Throwable? = null) : DemoteUserError(cause = cause) -} - -sealed class OpenChatError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Denied : OpenChatError() - class Unrecognized : OpenChatError() - data class Other(override val cause: Throwable? = null) : OpenChatError(cause = cause) -} - -sealed class CloseChatError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Denied : CloseChatError() - class Unrecognized : CloseChatError() - data class Other(override val cause: Throwable? = null) : CloseChatError(cause = cause) -} - -sealed class CheckDisplayNameError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class CantSet: CheckDisplayNameError() - class Unrecognized : CheckDisplayNameError() - data class Other(override val cause: Throwable? = null) : CheckDisplayNameError(cause = cause) -} - - -sealed class MuteUserError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Unrecognized : MuteUserError() - data class Other(override val cause: Throwable? = null) : MuteUserError(cause = cause) -} - -sealed class ReportUserError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Unrecognized : ReportUserError() - data class Other(override val cause: Throwable? = null) : ReportUserError(cause = cause) -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/service/MessagingService.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/service/MessagingService.kt deleted file mode 100644 index 09a4fd581..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/service/MessagingService.kt +++ /dev/null @@ -1,405 +0,0 @@ -package xyz.flipchat.services.internal.network.service - -import com.codeinc.flipchat.gen.common.v1.Common -import com.codeinc.flipchat.gen.messaging.v1.MessagingService -import com.codeinc.flipchat.gen.messaging.v1.MessagingService.AdvancePointerResponse -import com.codeinc.flipchat.gen.messaging.v1.MessagingService.GetMessagesResponse -import com.codeinc.flipchat.gen.messaging.v1.MessagingService.StreamMessagesRequest.Params -import com.codeinc.flipchat.gen.messaging.v1.Model -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.ID -import com.getcode.model.chat.MessageStatus -import com.getcode.model.description -import com.getcode.services.model.chat.OutgoingMessageContent -import com.getcode.services.network.core.NetworkOracle -import com.getcode.services.observers.BidirectionalStreamReference -import com.getcode.utils.CodeServerError -import com.getcode.utils.ErrorUtils -import com.getcode.utils.TraceType -import com.getcode.utils.trace -import com.google.protobuf.Timestamp -import io.grpc.Status -import io.grpc.StatusRuntimeException -import io.grpc.stub.StreamObserver -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import xyz.flipchat.services.domain.model.query.QueryOptions -import xyz.flipchat.services.internal.network.api.MessagingApi -import com.getcode.utils.FlipchatServerError -import xyz.flipchat.services.internal.network.chat.MessageStreamUpdate -import xyz.flipchat.services.internal.network.chat.TypingState -import xyz.flipchat.services.internal.network.extensions.toChatId -import xyz.flipchat.services.internal.network.utils.authenticate -import javax.inject.Inject -import kotlin.coroutines.resume - -typealias ChatMessageStreamReference = BidirectionalStreamReference - - -internal class MessagingService @Inject constructor( - private val api: MessagingApi, - private val networkOracle: NetworkOracle, -) { - suspend fun getMessages( - owner: KeyPair, - chatId: ID, - queryOptions: QueryOptions, - ): Result> { - return try { - networkOracle.managedRequest( - api.getMessages(owner = owner, chatId = chatId, queryOptions = queryOptions) - ).map { response -> - when (response.result) { - GetMessagesResponse.Result.OK -> { - Result.success(response.messagesList) - } - - GetMessagesResponse.Result.UNRECOGNIZED -> { - val error = GetMessagesError.Unrecognized() - Result.failure(error) - } - - GetMessagesResponse.Result.DENIED -> { - val error = GetMessagesError.Denied() - Result.failure(error) - } - - else -> { - val error = GetMessagesError.Other() - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = GetMessagesError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun sendMessage( - owner: KeyPair, - chatId: ID, - content: OutgoingMessageContent, - ): Result = suspendCancellableCoroutine { cont -> - try { - api.sendMessage( - owner = owner, - chatId = chatId, - content = content, - observer = object : StreamObserver { - override fun onNext(value: MessagingService.SendMessageResponse?) { - val requestResult = value?.result - if (requestResult == null) { - trace( - message = "Messaging SendMessage Server returned empty message. This is unexpected.", - type = TraceType.Error - ) - return - } - - val result = when (requestResult) { - MessagingService.SendMessageResponse.Result.OK -> { - trace("Chat message sent =: ${value.message.messageId.value.toList().description}") - Result.success(value.message) - } - - MessagingService.SendMessageResponse.Result.DENIED -> { - val error = SendMessageError.Denied() - Result.failure(error) - } - - MessagingService.SendMessageResponse.Result.UNRECOGNIZED -> { - val error = SendMessageError.Unrecognized() - Result.failure(error) - } - - else -> { - val error = SendMessageError.Other() - Result.failure(error) - } - } - - cont.resume(result) - } - - override fun onError(t: Throwable?) { - val error = SendMessageError.Other(t) - cont.resume(Result.failure(error)) - } - - override fun onCompleted() = Unit - } - ) - } catch (e: Exception) { - ErrorUtils.handleError(e) - cont.resume(Result.failure(e)) - } - } - - suspend fun advancePointer( - owner: KeyPair, - chatId: ID, - to: ID, - status: MessageStatus, - ): Result { - return try { - networkOracle.managedRequest(api.advancePointer(owner, chatId, to, status)) - .map { response -> - when (response.result) { - AdvancePointerResponse.Result.OK -> { - Result.success(Unit) - } - - AdvancePointerResponse.Result.UNRECOGNIZED -> { - val error = AdvancePointerError.Unrecognized() - Result.failure(error) - } - - AdvancePointerResponse.Result.DENIED -> { - val error = AdvancePointerError.Denied() - Result.failure(error) - } - - else -> { - val error = AdvancePointerError.Other() - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = AdvancePointerError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun notifyOfTypingState( - owner: KeyPair, - chatId: ID, - state: TypingState.UserEvent - ): Result = suspendCancellableCoroutine { cont -> - try { - api.notifyOfTypingState( - owner, - chatId, - state, - observer = object : StreamObserver { - override fun onNext(value: MessagingService.NotifyIsTypingResponse?) { - val requestResult = value?.result - if (requestResult == null) { - trace( - message = "Messaging NotifyTyping Server returned empty message. This is unexpected.", - type = TraceType.Error - ) - return - } - - val result = when (requestResult) { - MessagingService.NotifyIsTypingResponse.Result.OK -> Result.success(Unit) - MessagingService.NotifyIsTypingResponse.Result.DENIED -> { - val error = TypingChangeError.Denied() - Result.failure(error) - } - - MessagingService.NotifyIsTypingResponse.Result.UNRECOGNIZED -> { - val error = TypingChangeError.Unrecognized() - Result.failure(error) - } - - else -> { - val error = TypingChangeError.Other() - Result.failure(error) - } - } - - cont.resume(result) - } - - override fun onError(t: Throwable?) { - val error = TypingChangeError.Other(cause = t) - cont.resume(Result.failure(error)) - } - - override fun onCompleted() = Unit - } - ) - } catch (e: Exception) { - val error = TypingChangeError.Other(cause = e) - cont.resume(Result.failure(error)) - } - } - - fun openMessageStream( - scope: CoroutineScope, - owner: KeyPair, - chatId: ID, - onEvent: (Result) -> Unit - ): ChatMessageStreamReference { - trace("Message Opening stream.") - val streamReference = ChatMessageStreamReference(scope) - streamReference.retain() - streamReference.timeoutHandler = { - trace("Message Stream timed out") - openMessageStream( - owner = owner, - chatId = chatId, - reference = streamReference, - onEvent = onEvent - ) - } - - openMessageStream(owner, chatId, streamReference, onEvent) - - return streamReference - } - - private fun openMessageStream( - owner: KeyPair, - chatId: ID, - reference: ChatMessageStreamReference, - onEvent: (Result) -> Unit - ) { - try { - reference.cancel() - reference.stream = - api.streamMessages(object : StreamObserver { - override fun onNext(value: MessagingService.StreamMessagesResponse?) { - val result = value?.typeCase - if (result == null) { - trace( - message = "Message Stream Server sent empty message. This is unexpected.", - type = TraceType.Error - ) - return - } - - when (result) { - MessagingService.StreamMessagesResponse.TypeCase.MESSAGES -> { - onEvent( - Result.success( - MessageStreamUpdate.Messages(value.messages.messagesList) - ) - ) - } - - MessagingService.StreamMessagesResponse.TypeCase.POINTER_UPDATES -> { - onEvent( - Result.success( - MessageStreamUpdate.Pointers(value.pointerUpdates.pointerUpdatesList) - ) - ) - } - MessagingService.StreamMessagesResponse.TypeCase.IS_TYPING_NOTIFICATIONS -> { - onEvent( - Result.success( - MessageStreamUpdate.Typing(value.isTypingNotifications.isTypingNotificationsList) - ) - ) - } - - MessagingService.StreamMessagesResponse.TypeCase.PING -> { - val stream = reference.stream ?: return - val request = MessagingService.StreamMessagesRequest.newBuilder() - .setParams(Params.newBuilder().setChatId(chatId.toChatId())) - .setPong( - Common.ClientPong.newBuilder() - .setTimestamp( - Timestamp.newBuilder() - .setSeconds(System.currentTimeMillis() / 1_000) - ) - ).build() - - reference.receivedPing(updatedTimeout = value.ping.pingDelay.seconds * 1_000L) - stream.onNext(request) - trace("Pong Message Stream Server timestamp: ${value.ping.timestamp}") - } - - MessagingService.StreamMessagesResponse.TypeCase.TYPE_NOT_SET -> Unit - MessagingService.StreamMessagesResponse.TypeCase.ERROR -> { - trace( - type = TraceType.Error, - message = "Message Stream hit a snag. ${value.error.code}" - ) - } - } - } - - override fun onError(t: Throwable?) { - val statusException = t as? StatusRuntimeException - if (statusException?.status?.code == Status.Code.UNAVAILABLE) { - trace("Message Stream Reconnecting keepalive stream...") - openMessageStream( - owner, - chatId, - reference, - onEvent - ) - } else { - t?.printStackTrace() - } - } - - override fun onCompleted() { - - } - }) - - reference.coroutineScope.launch { - val request = MessagingService.StreamMessagesRequest.newBuilder() - .setParams( - Params.newBuilder() - .setChatId(chatId.toChatId()) - .apply { setAuth(authenticate(owner)) } - ).build() - - reference.stream?.onNext(request) - trace("Message Stream Initiating a connection...") - } - } catch (e: Exception) { - if (e is IllegalStateException && e.message == "call already half-closed") { - // ignore - } else { - ErrorUtils.handleError(e) - } - } - } -} - -sealed class GetMessagesError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Unrecognized : GetMessagesError() - class Denied : GetMessagesError() - data class Other(override val cause: Throwable? = null) : GetMessagesError(cause = cause) -} - -sealed class AdvancePointerError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Unrecognized : AdvancePointerError() - class Denied : AdvancePointerError() - data class Other(override val cause: Throwable? = null) : AdvancePointerError(cause = cause) -} - -sealed class TypingChangeError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Unrecognized : AdvancePointerError() - class Denied : AdvancePointerError() - data class Other(override val cause: Throwable? = null) : AdvancePointerError(cause = cause) -} - -sealed class SendMessageError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Unrecognized : GetMessagesError() - class Denied : GetMessagesError() - class InvalidContentType : GetMessagesError() - data class Other(override val cause: Throwable? = null) : GetMessagesError(cause = cause) -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/service/ProfileService.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/service/ProfileService.kt deleted file mode 100644 index 607f9ea95..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/service/ProfileService.kt +++ /dev/null @@ -1,164 +0,0 @@ -package xyz.flipchat.services.internal.network.service - -import com.codeinc.flipchat.gen.profile.v1.Model -import com.codeinc.flipchat.gen.profile.v1.ProfileService -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.ID -import com.getcode.services.model.profile.SocialAccountLinkRequest -import com.getcode.services.model.profile.SocialAccountUnlinkRequest -import com.getcode.services.network.core.NetworkOracle -import com.getcode.utils.CodeServerError -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import timber.log.Timber -import xyz.flipchat.services.internal.network.api.ProfileApi -import com.getcode.utils.FlipchatServerError -import xyz.flipchat.services.network.managedApiRequest -import javax.inject.Inject - -internal class ProfileService @Inject constructor( - private val api: ProfileApi, - private val networkOracle: NetworkOracle, -) { - suspend fun getProfile(userId: ID): Result { - return try { - networkOracle.managedRequest(api.getProfile(userId)) - .map { - when (it.result) { - ProfileService.GetProfileResponse.Result.OK -> Result.success(it.userProfile) - ProfileService.GetProfileResponse.Result.NOT_FOUND -> { - val error = GetProfileError.NotFound() - Timber.e(t = error) - Result.failure(error) - } - ProfileService.GetProfileResponse.Result.UNRECOGNIZED -> { - val error = GetProfileError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - else -> { - val error = GetProfileError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = GetProfileError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun setDisplayName(owner: KeyPair, displayName: String): Result { - return try { - networkOracle.managedRequest(api.setDisplayName(owner, displayName)) - .map { - when (it.result) { - ProfileService.SetDisplayNameResponse.Result.OK -> Result.success(Unit) - ProfileService.SetDisplayNameResponse.Result.INVALID_DISPLAY_NAME -> { - val error = SetUserDisplayNameError.InvalidDisplayName() - Timber.e(t = error) - Result.failure(error) - } - ProfileService.SetDisplayNameResponse.Result.UNRECOGNIZED -> { - val error = SetUserDisplayNameError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - else -> { - val error = SetUserDisplayNameError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = SetUserDisplayNameError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun linkSocialAccount( - owner: KeyPair, - request: SocialAccountLinkRequest - ): Result { - return networkOracle.managedApiRequest( - call = { api.linkSocialAccount(owner, request) }, - handleResponse = { response -> - when (response.result) { - ProfileService.LinkSocialAccountResponse.Result.OK -> Result.success(response.socialProfile) - ProfileService.LinkSocialAccountResponse.Result.INVALID_LINKING_TOKEN -> Result.failure(LinkSocialAccountError.InvalidLinkingToken()) - ProfileService.LinkSocialAccountResponse.Result.EXISTING_LINK -> Result.failure(LinkSocialAccountError.ExistingLink()) - ProfileService.LinkSocialAccountResponse.Result.DENIED -> Result.failure(LinkSocialAccountError.Denied()) - ProfileService.LinkSocialAccountResponse.Result.UNRECOGNIZED -> Result.failure(LinkSocialAccountError.Unrecognized()) - else -> { - Result.failure(LinkSocialAccountError.Other()) - } - } - }, - onOtherError = { cause -> - Result.failure(LinkSocialAccountError.Other(cause = cause)) - } - ) - } - - suspend fun unlinkSocialAccount( - owner: KeyPair, - request: SocialAccountUnlinkRequest, - ): Result { - return networkOracle.managedApiRequest( - call = { api.unlinkSocialAccount(owner, request) }, - handleResponse = { response -> - when (response.result) { - ProfileService.UnlinkSocialAccountResponse.Result.OK -> Result.success(Unit) - ProfileService.UnlinkSocialAccountResponse.Result.DENIED -> Result.failure(UnlinkSocialAccountError.Denied()) - ProfileService.UnlinkSocialAccountResponse.Result.UNRECOGNIZED -> Result.failure(UnlinkSocialAccountError.Unrecognized()) - else -> { - Result.failure(UnlinkSocialAccountError.Other()) - } - } - }, - onOtherError = { cause -> - Result.failure(UnlinkSocialAccountError.Other(cause = cause)) - } - ) - } -} - -sealed class GetProfileError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class NotFound : GetProfileError() - class Unrecognized : GetProfileError() - data class Other(override val cause: Throwable? = null) : GetProfileError(cause = cause) -} - -sealed class SetUserDisplayNameError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class InvalidDisplayName : SetUserDisplayNameError() - class Unrecognized : SetUserDisplayNameError() - data class Other(override val cause: Throwable? = null) : SetUserDisplayNameError(cause = cause) -} - -sealed class LinkSocialAccountError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class InvalidLinkingToken : LinkSocialAccountError() - class ExistingLink : LinkSocialAccountError() - class Denied : LinkSocialAccountError() - class Unrecognized : LinkSocialAccountError() - data class Other(override val cause: Throwable? = null) : LinkSocialAccountError(cause = cause) -} - -sealed class UnlinkSocialAccountError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Denied : UnlinkSocialAccountError() - class Unrecognized : UnlinkSocialAccountError() - data class Other(override val cause: Throwable? = null) : UnlinkSocialAccountError(cause = cause) -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/service/PurchaseService.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/service/PurchaseService.kt deleted file mode 100644 index 4cacf08aa..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/service/PurchaseService.kt +++ /dev/null @@ -1,62 +0,0 @@ -package xyz.flipchat.services.internal.network.service - -import com.codeinc.flipchat.gen.iap.v1.IapService -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.services.network.core.NetworkOracle -import com.getcode.utils.CodeServerError -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import timber.log.Timber -import xyz.flipchat.services.internal.network.api.PurchaseApi -import com.getcode.utils.FlipchatServerError -import javax.inject.Inject - -internal class PurchaseService @Inject constructor( - private val api: PurchaseApi, - private val networkOracle: NetworkOracle, -) { - suspend fun onPurchaseCompleted(owner: KeyPair, receipt: String): Result { - return try { - networkOracle.managedRequest(api.onPurchaseCompleted(owner, receipt)) - .map { response -> - when (response.result) { - IapService.OnPurchaseCompletedResponse.Result.OK -> Result.success(Unit) - IapService.OnPurchaseCompletedResponse.Result.DENIED -> { - val error = PurchaseAckError.Denied() - Timber.e(t = error) - Result.failure(error) - } - IapService.OnPurchaseCompletedResponse.Result.INVALID_RECEIPT -> { - val error = PurchaseAckError.InvalidReceipt() - Timber.e(t = error) - Result.failure(error) - } - IapService.OnPurchaseCompletedResponse.Result.UNRECOGNIZED -> { - val error = PurchaseAckError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - else -> { - val error = PurchaseAckError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = PurchaseAckError.Other(e) - Timber.e(t = error) - Result.failure(error) - } - } -} - -sealed class PurchaseAckError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Unrecognized : PurchaseAckError() - class Denied : PurchaseAckError() - class InvalidReceipt: PurchaseAckError() - data class Other(override val cause: Throwable? = null) : PurchaseAckError(cause = cause) -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/service/PushService.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/service/PushService.kt deleted file mode 100644 index 9d59f5d3b..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/service/PushService.kt +++ /dev/null @@ -1,122 +0,0 @@ -package xyz.flipchat.services.internal.network.service - -import com.codeinc.flipchat.gen.push.v1.PushService -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.ID -import com.getcode.services.network.core.NetworkOracle -import com.getcode.utils.CodeServerError -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import timber.log.Timber -import xyz.flipchat.services.internal.network.api.PushApi -import com.getcode.utils.FlipchatServerError -import javax.inject.Inject - -internal class PushService @Inject constructor( - private val api: PushApi, - private val networkOracle: NetworkOracle, -) { - suspend fun addToken( - owner: KeyPair, - token: String, - installationId: String? - ): Result { - return try { - networkOracle.managedRequest(api.addToken(owner, token, installationId)) - .map { - when (it.result) { - PushService.AddTokenResponse.Result.OK -> Result.success(Unit) - PushService.AddTokenResponse.Result.INVALID_PUSH_TOKEN -> { - val error = AddTokenError.InvalidPushToken() - Timber.e(t = error) - Result.failure(error) - } - PushService.AddTokenResponse.Result.UNRECOGNIZED -> { - val error = AddTokenError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - else -> { - val error = AddTokenError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = AddTokenError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun deleteToken( - owner: KeyPair, - token: String, - ): Result { - return try { - networkOracle.managedRequest(api.deleteToken(owner, token)) - .map { - when (it.result) { - PushService.DeleteTokenResponse.Result.OK -> Result.success(Unit) - PushService.DeleteTokenResponse.Result.UNRECOGNIZED -> { - val error = DeleteTokenError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - else -> { - val error = DeleteTokenError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = DeleteTokenError.Other(cause = e) - Result.failure(error) - } - } - - suspend fun deleteTokens( - owner: KeyPair, - installationId: String?, - ): Result { - return try { - networkOracle.managedRequest(api.deleteTokens(owner, installationId)) - .map { - when (it.result) { - PushService.DeleteTokensResponse.Result.OK -> Result.success(Unit) - PushService.DeleteTokensResponse.Result.UNRECOGNIZED -> { - val error = DeleteTokenError.Unrecognized() - Timber.e(t = error) - Result.failure(error) - } - else -> { - val error = DeleteTokenError.Other() - Timber.e(t = error) - Result.failure(error) - } - } - }.first() - } catch (e: Exception) { - val error = DeleteTokenError.Other(cause = e) - Result.failure(error) - } - } -} - -sealed class AddTokenError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class InvalidPushToken : AddTokenError() - class Unrecognized : AddTokenError() - data class Other(override val cause: Throwable? = null) : AddTokenError(cause = cause) -} - -sealed class DeleteTokenError( - override val message: String? = null, - override val cause: Throwable? = null -) : CodeServerError(message, cause) { - class Unrecognized : DeleteTokenError() - data class Other(override val cause: Throwable? = null) : DeleteTokenError(cause = cause) -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/utils/AuthenticateMessage.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/utils/AuthenticateMessage.kt deleted file mode 100644 index c60db6441..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/utils/AuthenticateMessage.kt +++ /dev/null @@ -1,29 +0,0 @@ -package xyz.flipchat.services.internal.network.utils - -import com.codeinc.flipchat.gen.common.v1.Common -import com.getcode.ed25519.Ed25519 -import com.google.protobuf.GeneratedMessageLite -import xyz.flipchat.services.internal.network.extensions.asPublicKey -import xyz.flipchat.services.internal.network.extensions.toSignature -import java.io.ByteArrayOutputStream - -internal fun , B : GeneratedMessageLite.Builder> GeneratedMessageLite.Builder.authenticate(owner: Ed25519.KeyPair): Common.Auth { - // dump message up until this point into a ByteArray - val bos = ByteArrayOutputStream() - this.buildPartial().writeTo(bos) - - /** - * sign message up to this point with owner and convert to [com.codeinc.flipchat.gen.common.v1.Signature] - */ - val signature = Ed25519.sign(bos.toByteArray(), owner).toSignature() - // build Auth.Keypair sub model - val keyPairModel = Common.Auth.KeyPair.newBuilder() - .setPubKey(owner.asPublicKey()) - .apply { setSignature(signature) } - .build() - - // return Auth model - return Common.Auth.newBuilder() - .setKeyPair(keyPairModel) - .build() -} diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/utils/SignMessage.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/utils/SignMessage.kt deleted file mode 100644 index c4bf711c0..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/network/utils/SignMessage.kt +++ /dev/null @@ -1,18 +0,0 @@ -package xyz.flipchat.services.internal.network.utils - -import com.codeinc.flipchat.gen.common.v1.Common -import com.getcode.ed25519.Ed25519 -import com.google.protobuf.GeneratedMessageLite -import xyz.flipchat.services.internal.network.extensions.toSignature -import java.io.ByteArrayOutputStream - -internal fun , B : GeneratedMessageLite.Builder> GeneratedMessageLite.Builder.sign(owner: Ed25519.KeyPair): Common.Signature { - // dump message up until this point into a ByteArray - val bos = ByteArrayOutputStream() - this.buildPartial().writeTo(bos) - - /** - * sign message up to this point with owner and return as [com.codeinc.flipchat.gen.common.v1.Signature] - */ - return Ed25519.sign(bos.toByteArray(), owner).toSignature() -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/protomapping/MessageContent.kt b/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/protomapping/MessageContent.kt deleted file mode 100644 index d4da42ddf..000000000 --- a/services/flipchat/chat/src/main/kotlin/xyz/flipchat/services/internal/protomapping/MessageContent.kt +++ /dev/null @@ -1,66 +0,0 @@ -package xyz.flipchat.services.internal.protomapping - -import com.codeinc.flipchat.gen.messaging.v1.Model -import com.getcode.model.ID -import com.getcode.model.chat.AnnouncementAction -import com.getcode.model.chat.MessageContent -import xyz.flipchat.services.internal.data.mapper.ifZeroOrElse - -operator fun MessageContent.Companion.invoke( - proto: Model.Content, - senderId: ID, - date: Long, - isFromSelf: Boolean = false, -): MessageContent? { - return when (proto.typeCase) { - Model.Content.TypeCase.LOCALIZED_ANNOUNCEMENT -> MessageContent.Announcement( - isFromSelf = isFromSelf, - value = proto.localizedAnnouncement.keyOrText - ) - - Model.Content.TypeCase.ACTIONABLE_ANNOUNCEMENT -> MessageContent.ActionableAnnouncement( - isFromSelf = isFromSelf, - keyOrText = proto.actionableAnnouncement.keyOrText, - action = when (proto.actionableAnnouncement.action.typeCase) { - Model.ActionableAnnouncementContent.Action.TypeCase.SHARE_ROOM_LINK -> AnnouncementAction.Share - Model.ActionableAnnouncementContent.Action.TypeCase.TYPE_NOT_SET, - null -> AnnouncementAction.Unknown - } - ) - - Model.Content.TypeCase.TEXT -> MessageContent.RawText( - isFromSelf = isFromSelf, - value = proto.text.text - ) - - Model.Content.TypeCase.REACTION -> MessageContent.Reaction( - emoji = proto.reaction.emoji, - originalMessageId = proto.reaction.originalMessageId.value.toList(), - senderId = senderId, - sentAt = date, - isFromSelf = isFromSelf - ) - - Model.Content.TypeCase.REPLY -> MessageContent.Reply( - text = proto.reply.replyText, - originalMessageId = proto.reply.originalMessageId.value.toList(), - isFromSelf = isFromSelf - ) - - Model.Content.TypeCase.DELETED -> MessageContent.DeletedMessage( - originalMessageId = proto.deleted.originalMessageId.value.toList(), - messageDeleter = senderId, - isFromSelf = isFromSelf - ) - - Model.Content.TypeCase.TIP -> MessageContent.MessageTip( - originalMessageId = proto.tip.originalMessageId.value.toList(), - tipperId = senderId, - isFromSelf = isFromSelf, - amountInQuarks = proto.tip.tipAmount.quarks / 100_000 - ) - - Model.Content.TypeCase.TYPE_NOT_SET -> return null - else -> return null - } -} \ No newline at end of file diff --git a/services/flipchat/chat/src/main/res/values/strings.xml b/services/flipchat/chat/src/main/res/values/strings.xml deleted file mode 100644 index f38e17280..000000000 --- a/services/flipchat/chat/src/main/res/values/strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - Room #%1$s - #%1$s - #%1$s: %2$s - \ No newline at end of file diff --git a/services/flipchat/core/.gitignore b/services/flipchat/core/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/services/flipchat/core/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/services/flipchat/core/build.gradle.kts b/services/flipchat/core/build.gradle.kts deleted file mode 100644 index 21c87aff6..000000000 --- a/services/flipchat/core/build.gradle.kts +++ /dev/null @@ -1,104 +0,0 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - id(Plugins.android_library) - id(Plugins.kotlin_android) - id(Plugins.kotlin_kapt) - id(Plugins.kotlin_serialization) -} - -android { - namespace = "${Gradle.flipchatNamespace}.services.core" - compileSdk = Android.compileSdkVersion - defaultConfig { - minSdk = Android.minSdkVersion - testInstrumentationRunner = Android.testInstrumentationRunner - - consumerProguardFiles("consumer-rules.pro") - - buildConfigField("String", "VERSION_NAME", "\"${Packaging.Flipchat.versionName}\"") - - buildConfigField("Boolean", "NOTIFY_ERRORS", "false") - buildConfigField( - "String", - "GOOGLE_CLOUD_PROJECT_NUMBER", - "\"${tryReadProperty(rootProject.rootDir, "GOOGLE_CLOUD_PROJECT_NUMBER", "-1L")}\"" - ) - - buildConfigField( - "String", - "FINGERPRINT_API_KEY", - "\"${tryReadProperty(rootProject.rootDir, "FINGERPRINT_API_KEY")}\"" - ) - } - - buildFeatures { - buildConfig = true - } -} - -kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(Versions.java)) - } - - compilerOptions { - jvmTarget.set(JvmTarget.fromTarget(Versions.java)) - optIn.addAll( - "kotlin.time.ExperimentalTime", - "kotlin.ExperimentalUnsignedTypes", - "kotlin.RequiresOptIn" - ) - } -} - -dependencies { - implementation(project(":definitions:flipchat-vm:models")) - api(project(":services:legacy-shared")) - api(project(":libs:models")) - api(project(":libs:crypto:kin")) - api(project(":libs:crypto:solana")) - api(project(":libs:currency")) - implementation(project(":ui:resources")) - - api(project(":libs:analytics")) - - implementation(Libs.kotlinx_coroutines_core) - implementation(Libs.kotlinx_serialization_json) - implementation(Libs.inject) - - implementation(Libs.grpc_android) - implementation(Libs.grpc_okhttp) - implementation(Libs.grpc_kotlin) - implementation(Libs.androidx_lifecycle_runtime) - implementation(Libs.okhttp) - implementation(Libs.mixpanel) - - implementation(platform(Libs.firebase_bom)) - implementation(Libs.firebase_crashlytics) - implementation(Libs.firebase_installations) - implementation(Libs.firebase_perf) - implementation(Libs.firebase_messaging) - - implementation(Libs.play_integrity) - - implementation(Libs.androidx_paging_runtime) - - kapt(Libs.androidx_room_compiler) - implementation(Libs.sqlcipher) - - implementation(Libs.fingerprint_pro) - - implementation(Libs.lib_phone_number_google) - - androidTestImplementation(Libs.androidx_junit) - androidTestImplementation(Libs.junit) - androidTestImplementation(Libs.androidx_test_runner) - - implementation(Libs.hilt) - kapt(Libs.hilt_android_compiler) - kapt(Libs.hilt_compiler) - - implementation(Libs.timber) - implementation(Libs.bugsnag) -} diff --git a/services/flipchat/core/consumer-rules.pro b/services/flipchat/core/consumer-rules.pro deleted file mode 100644 index 5f642a47c..000000000 --- a/services/flipchat/core/consumer-rules.pro +++ /dev/null @@ -1,6 +0,0 @@ -# Needed to keep generic signatures --keepattributes Signature - --keepclasseswithmembernames class * { - native ; -} \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/generator/Generator.kt b/services/flipchat/core/src/main/kotlin/com/getcode/generator/Generator.kt deleted file mode 100644 index e21ce8993..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/generator/Generator.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.getcode.generator - -interface Generator { - fun generate(predicate: D): R -} \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/generator/MnemonicGenerator.kt b/services/flipchat/core/src/main/kotlin/com/getcode/generator/MnemonicGenerator.kt deleted file mode 100644 index f2ca12d11..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/generator/MnemonicGenerator.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.getcode.generator - -import com.getcode.crypt.MnemonicPhrase -import com.getcode.services.utils.Base58String -import com.getcode.services.utils.Base64String -import javax.inject.Inject - -class MnemonicGenerator @Inject constructor( -): Generator { - - override fun generate(predicate: Base64String): MnemonicPhrase { - return MnemonicPhrase.fromEntropyB64(predicate) - } - - fun generateFromBase58(predicate: Base58String): MnemonicPhrase { - return MnemonicPhrase.fromEntropyB58(predicate) - } -} \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/generator/OrganizerGenerator.kt b/services/flipchat/core/src/main/kotlin/com/getcode/generator/OrganizerGenerator.kt deleted file mode 100644 index f2333ef54..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/generator/OrganizerGenerator.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.getcode.generator - -import com.getcode.crypt.MnemonicPhrase -import com.getcode.solana.organizer.Organizer -import javax.inject.Inject - -class OrganizerGenerator @Inject constructor(): Generator { - - override fun generate(predicate: MnemonicPhrase): Organizer { - return Organizer.newInstance(predicate) - } -} \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/model/AccountInfo.kt b/services/flipchat/core/src/main/kotlin/com/getcode/model/AccountInfo.kt deleted file mode 100644 index e74014d5c..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/model/AccountInfo.kt +++ /dev/null @@ -1,268 +0,0 @@ -package com.getcode.model - -import com.codeinc.gen.account.v1.CodeAccountService as AccountService -import com.getcode.solana.organizer.AccountType - -data class AccountInfo ( - /// The account's derivation index for applicable account types. When this field - /// doesn't apply, a zero value is provided. - var index: Int, - - /// The type of token account, which infers its intended use. - var accountType: AccountType, - - /// The token account's address - var address: com.getcode.solana.keys.PublicKey, - - /// The owner of the token account, which can also be thought of as a parent - /// account that links to one or more token accounts. This is provided when - /// available. - var owner: com.getcode.solana.keys.PublicKey?, - - /// The token account's authority, which has access to moving funds for the - /// account. This can be the owner account under certain circumstances (eg. - /// ATA, primary account). This is provided when available. - var authority: com.getcode.solana.keys.PublicKey?, - - /// The source of truth for the balance calculation. - var balanceSource: BalanceSource, - - /// The Kin balance in quarks, as observed by Code. This may not reflect the - /// value on the blockchain and could be non-zero even if the account hasn't - /// been created. Use balance_source to determine how this value was calculated. - var balance: Kin, - - /// The state of the account as it pertains to Code's ability to manage funds. - var managementState: ManagementState, - - /// The state of the account on the blockchain. - var blockchainState: BlockchainState, - - /// Whether an account is claimed. This only applies to relevant account types - /// (eg. REMOTE_SEND_GIFT_CARD). - var claimState: ClaimState, - - /// For temporary incoming accounts only. Flag indicates whether client must - /// actively try rotating it by issuing a ReceivePayments intent. In general, - /// clients should wait as long as possible until this flag is true or requiring - /// the funds to send their next payment. - var mustRotate: Boolean, - - /// For account types used as an intermediary for sending money between two - /// users (eg. REMOTE_SEND_GIFT_CARD), this represents the original exchange - /// data used to fund the account. Over time, this value will become stale: - /// 1. Exchange rates will fluctuate, so the total fiat amount will differ. - /// 2. External entities can deposit additional funds into the account, so - /// the balance, in quarks, may be greater than the original quark value. - /// 3. The balance could have been received, so the total balance can show - /// as zero. - var originalKinAmount: KinAmount?, - - /// The relationship with a third party that this account has established with. - /// This only applies to relevant account types (eg. RELATIONSHIP). - var relationship: Relationship?, - - // Time the account was created, if available. For Code accounts, this is - // the time of intent submission. Otherwise, for external accounts, it is - // the time created on the blockchain. - var createdAt: Long?, - - ) { - companion object { - fun newInstance(info: AccountService.TokenAccountInfo): AccountInfo? { - val accountType = AccountType.newInstance(info.accountType, info.relationship) ?: return null - val address = - com.getcode.solana.keys.PublicKey(info.address.value.toByteArray().toList()) - val balanceSource = BalanceSource.getInstance(info.balanceSource) ?: return null - - val managementState = ManagementState.getInstance(info.managementState) ?: return null - val blockchainState = BlockchainState.getInstance(info.blockchainState) ?: return null - val claimState = ClaimState.getInstance(info.claimState) ?: return null - - val owner = com.getcode.solana.keys.PublicKey(info.owner.value.toByteArray().toList()) - val authority = - com.getcode.solana.keys.PublicKey(info.authority.value.toByteArray().toList()) - - val originalCurrency = CurrencyCode.tryValueOf(info.originalExchangeData.currency) - - val originalKinAmount = originalCurrency?.let { - KinAmount.newInstance( - kin = Kin(info.originalExchangeData.quarks), - rate = Rate( - fx = info.originalExchangeData.exchangeRate, - currency = originalCurrency - ) - ) - } - - val relationship = Domain.from(info.relationship.domain.value) - ?.let { Relationship(it) } - - return AccountInfo( - index = info.index.toInt(), - accountType = accountType, - address = address, - owner = owner, - authority = authority, - balanceSource = balanceSource, - balance = Kin(info.balance), - managementState = managementState, - blockchainState = blockchainState, - claimState = claimState, - mustRotate = info.mustRotate, - originalKinAmount = originalKinAmount, - relationship = relationship, - createdAt = info.createdAt.seconds * 1000L - ) - } - } - - enum class ManagementState { - /// The state of the account is unknown. This may be returned when the - /// data source is unstable and a reliable state cannot be determined. - Unknown, - - /// Code does not maintain a management state and won't move funds for this - /// account. - None, - - /// The account is in the process of transitioning to the LOCKED state. - Locking, - - /// The account's funds are locked and Code has co-signing authority. - Locked, - - /// The account is in the process of transitioning to the UNLOCKED state. - Unlocking, - - /// The account's funds are unlocked and Code no longer has co-signing - /// authority. The account must transition to the LOCKED state to have - /// management capabilities. - Unlocked, - - /// The account is in the process of transitioning to the CLOSED state. - Closing, - - /// The account has been closed and doesn't exist on the blockchain. - /// Subsequently, it also has a zero balance. - Closed; - - companion object { - fun getInstance(state: AccountService.TokenAccountInfo.ManagementState): ManagementState? { - return when (state) { - AccountService.TokenAccountInfo.ManagementState.MANAGEMENT_STATE_UNKNOWN -> Unknown - AccountService.TokenAccountInfo.ManagementState.MANAGEMENT_STATE_NONE -> None - AccountService.TokenAccountInfo.ManagementState.MANAGEMENT_STATE_LOCKING -> Locking - AccountService.TokenAccountInfo.ManagementState.MANAGEMENT_STATE_LOCKED -> Locked - AccountService.TokenAccountInfo.ManagementState.MANAGEMENT_STATE_UNLOCKING -> Unlocking - AccountService.TokenAccountInfo.ManagementState.MANAGEMENT_STATE_UNLOCKED -> Unlocked - AccountService.TokenAccountInfo.ManagementState.MANAGEMENT_STATE_CLOSING -> Closing - AccountService.TokenAccountInfo.ManagementState.MANAGEMENT_STATE_CLOSED -> Closed - AccountService.TokenAccountInfo.ManagementState.UNRECOGNIZED -> null - } - - } - } - } - - enum class BlockchainState { - /// The state of the account is unknown. This may be returned when the - /// data source is unstable and a reliable state cannot be determined. - Unknown, - - /// The account does not exist on the blockchain. - DoesntExist, - - /// The account is created and exists on the blockchain. - Exists; - - companion object { - fun getInstance(state: AccountService.TokenAccountInfo.BlockchainState): BlockchainState? { - return when (state) { - AccountService.TokenAccountInfo.BlockchainState.BLOCKCHAIN_STATE_UNKNOWN -> Unknown - AccountService.TokenAccountInfo.BlockchainState.BLOCKCHAIN_STATE_DOES_NOT_EXIST -> DoesntExist - AccountService.TokenAccountInfo.BlockchainState.BLOCKCHAIN_STATE_EXISTS -> Exists - AccountService.TokenAccountInfo.BlockchainState.UNRECOGNIZED -> null - } - } - } - } - - enum class ClaimState { - /// could not be fetched by server. - Unknown, - - /// The account has not yet been claimed. - NotClaimed, - - /// The account is claimed. Attempting to claim it will fail. - Claimed, - - /// The account hasn't been claimed, but is expired. Funds will move - /// back to the issuer. Attempting to claim it will fail. - Expired; - - companion object { - fun getInstance(state: AccountService.TokenAccountInfo.ClaimState): ClaimState? { - return when (state) { - AccountService.TokenAccountInfo.ClaimState.CLAIM_STATE_UNKNOWN -> Unknown - AccountService.TokenAccountInfo.ClaimState.CLAIM_STATE_NOT_CLAIMED -> NotClaimed - AccountService.TokenAccountInfo.ClaimState.CLAIM_STATE_CLAIMED -> Claimed - AccountService.TokenAccountInfo.ClaimState.CLAIM_STATE_EXPIRED -> Expired - AccountService.TokenAccountInfo.ClaimState.UNRECOGNIZED -> null - } - } - } - } - - enum class BalanceSource { - /// The account's balance could not be determined. This may be returned when - /// the data source is unstable and a reliable balance cannot be determined. - Unknown, - - /// The account's balance was fetched directly from a finalized state on the - /// blockchain. - Blockchain, - - /// The account's balance was calculated using cached values in Code. Accuracy - /// is only guaranteed when management_state is LOCKED. - Cache; - - companion object { - fun getInstance(source: AccountService.TokenAccountInfo.BalanceSource): BalanceSource? { - return when (source) { - AccountService.TokenAccountInfo.BalanceSource.BALANCE_SOURCE_UNKNOWN -> Unknown - AccountService.TokenAccountInfo.BalanceSource.BALANCE_SOURCE_BLOCKCHAIN -> Blockchain - AccountService.TokenAccountInfo.BalanceSource.BALANCE_SOURCE_CACHE -> Cache - AccountService.TokenAccountInfo.BalanceSource.UNRECOGNIZED -> null - } - } - - } - } - - data class Relationship(val domain: Domain) -} - -val AccountInfo.displayName: String - get() = when (val type = accountType) { - is AccountType.Bucket -> type.type.name.replace("Bucket", "") - AccountType.Incoming -> "Incoming $index" - AccountType.Outgoing -> "Outgoing $index" - AccountType.Primary -> "Primary" - AccountType.RemoteSend -> "Remote Send" - is AccountType.Relationship -> type.domain.relationshipHost - AccountType.Swap -> "Swap (USDC)" -} - -// An account is deemed unuseable in Code if the management -// state for said account is no longer `locked`. Some accounts may -// be allowed to operated in an 'unlocked' or another state -val AccountInfo.unusable: Boolean - get() = if (managementState == AccountInfo.ManagementState.None) { - // If the account is not managed - // by Code, it is always useable - false - } else { - managementState != AccountInfo.ManagementState.Locked - } diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/model/RelationshipBox.kt b/services/flipchat/core/src/main/kotlin/com/getcode/model/RelationshipBox.kt deleted file mode 100644 index 19c77304b..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/model/RelationshipBox.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.getcode.model - -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.Relationship -import okhttp3.internal.toImmutableMap -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class RelationshipBox @Inject constructor() { - private val _publicKeys = mutableMapOf() - val publicKeys - get() = _publicKeys.toImmutableMap() - - private val _domains = mutableMapOf() - val domains - get() = _domains.toImmutableMap() - - - fun relationships(largestFirst: Boolean = false): List { - return _domains.values.sortedWith { a, b -> - val comparisonResult = a.partialBalance.compareTo(b.partialBalance) - if (largestFirst) -comparisonResult else comparisonResult - } - } - fun relationshipWith(publicKey: PublicKey) = _publicKeys[publicKey] - fun relationshipWith(domain: Domain) = _domains[domain.relationshipHost] - - fun insert(relationship: Relationship) { - _publicKeys[relationship.getCluster().vaultPublicKey] = relationship - _domains[relationship.domain.relationshipHost] = relationship - } -} \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/model/extensions/AssociatedTokenAccount.kt b/services/flipchat/core/src/main/kotlin/com/getcode/model/extensions/AssociatedTokenAccount.kt deleted file mode 100644 index 25207b8c1..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/model/extensions/AssociatedTokenAccount.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.getcode.model.extensions - -import com.getcode.solana.keys.AssociatedTokenAccount -import com.getcode.solana.keys.Mint -import com.getcode.solana.keys.PublicKey - -fun AssociatedTokenAccount.Companion.newInstance( - owner: PublicKey, - mint: Mint -): AssociatedTokenAccount { - return AssociatedTokenAccount( - owner = owner, - ata = PublicKey.deriveAssociatedAccount(owner = owner, mint = mint) - ) -} \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/model/extensions/MemoProgram_Memo.kt b/services/flipchat/core/src/main/kotlin/com/getcode/model/extensions/MemoProgram_Memo.kt deleted file mode 100644 index a05af2ca2..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/model/extensions/MemoProgram_Memo.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.getcode.model.extensions - -import com.getcode.model.SocialUser -import com.getcode.solana.instructions.programs.MemoProgram_Memo - -fun MemoProgram_Memo.Companion.newInstance(tipMetadata: SocialUser): MemoProgram_Memo { - val memo = "tip:${tipMetadata.platform}:${tipMetadata.username}" - - return MemoProgram_Memo( - memo.toByteArray().toList() - ) -} diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/model/extensions/PreSwapStateAccount.kt b/services/flipchat/core/src/main/kotlin/com/getcode/model/extensions/PreSwapStateAccount.kt deleted file mode 100644 index 58768c104..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/model/extensions/PreSwapStateAccount.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.getcode.model.extensions - -import com.getcode.solana.keys.PreSwapStateAccount -import com.getcode.solana.keys.PublicKey - -fun PreSwapStateAccount.Companion.newInstance( - owner: PublicKey, - source: PublicKey, - destination: PublicKey, - nonce: PublicKey -): PreSwapStateAccount { - return PreSwapStateAccount( - owner = owner, - state = PublicKey.derivePreSwapState(source, destination, nonce) - ) -} \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/model/extensions/PublicKey.kt b/services/flipchat/core/src/main/kotlin/com/getcode/model/extensions/PublicKey.kt deleted file mode 100644 index bfaea0f2e..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/model/extensions/PublicKey.kt +++ /dev/null @@ -1,198 +0,0 @@ -package com.getcode.model.extensions - -import com.getcode.crypt.Sha256Hash -import com.getcode.ed25519.Ed25519 -import com.getcode.model.Kin -import com.getcode.solana.builder.vmTimeAuthority -import com.getcode.solana.instructions.programs.* -import com.getcode.solana.keys.Hash -import com.getcode.solana.keys.Key32.Companion.splitter -import com.getcode.solana.keys.Key32.Companion.subsidizer -import com.getcode.solana.keys.ProgramDerivedAccount -import com.getcode.solana.keys.PublicKey -import org.kin.sdk.base.tools.longToByteArray -import java.io.ByteArrayOutputStream -import java.io.IOException - -fun PublicKey.Companion.deriveAssociatedAccount(owner: PublicKey, mint: PublicKey): ProgramDerivedAccount { - return findProgramAddress( - seeds = listOf(owner.bytes.toByteArray(), TokenProgram.address.bytes.toByteArray(), mint.bytes.toByteArray()), - programId = AssociatedTokenProgram.address, - ) -} - -fun PublicKey.Companion.deriveTimelockStateAccount( - owner: PublicKey, - lockout: Long -): ProgramDerivedAccount { - val seeds: List = listOf( - "timelock_state".toByteArray(Charsets.UTF_8), - kin.bytes.toByteArray(), - vmTimeAuthority.bytes.toByteArray(), - owner.bytes.toByteArray(), - byteArrayOf(lockout.toByte()) - ) - - return findProgramAddress( - seeds = seeds, - programId = TimelockProgram.address, - ) -} - -fun PublicKey.Companion.deriveTimelockVaultAccount( - stateAccount: PublicKey, - version: Long -): ProgramDerivedAccount { - val seeds: List = listOf( - "timelock_vault".toByteArray(Charsets.UTF_8), - stateAccount.bytes.toByteArray(), - byteArrayOf(version.toByte()) - ) - - return findProgramAddress( - seeds = seeds, - programId = TimelockProgram.address, - ) -} - -fun PublicKey.Companion.deriveLegacyTimelockStateAccount( - owner: PublicKey, - lockout: Long -): ProgramDerivedAccount { - val nonce = SystemProgram.address - val version = byteArrayOf(1) - val pdaPadding = SystemProgram.address - - val seeds: List = listOf( - "timelock_state".toByteArray(Charsets.UTF_8), - version, - kin.bytes.toByteArray(), - subsidizer.bytes.toByteArray(), - nonce.bytes.toByteArray(), - owner.bytes.toByteArray(), - lockout.longToByteArray(), - pdaPadding.bytes.toByteArray() - ) - - return findProgramAddress( - seeds = seeds, - programId = TimelockProgram.legacyAddress, - ) -} - -fun PublicKey.Companion.deriveLegacyTimelockVaultAccount( - stateAccount: PublicKey -): ProgramDerivedAccount { - val seeds: List = listOf( - "timelock_vault".toByteArray(Charsets.UTF_8), - stateAccount.bytes.toByteArray(), - byteArrayOf(0) - ) - - return findProgramAddress( - seeds = seeds, - programId = TimelockProgram.legacyAddress, - ) -} - -/// FindProgramAddress mirrors the implementation of the Solana SDK's FindProgramAddress. Its primary -/// use case (for Kin and Agora) is for deriving associated accounts. -/// -/// Reference: https://github.com/solana-labs/solana/blob/5548e599fe4920b71766e0ad1d121755ce9c63d5/sdk/program/src/pubkey.rs#L234 -/// -fun PublicKey.Companion.findProgramAddress( - seeds: List, - programId: PublicKey -): ProgramDerivedAccount { - for (i in 0..255) { - val bumpValue = 255 - i - try { - val publicKey = deriveProgramAddress(programId, listOf(*seeds.toTypedArray(), byteArrayOf(bumpValue.toByte()))) - return ProgramDerivedAccount(publicKey, bumpValue) - } catch (e: RuntimeException) { - //no-op - } - } - - throw Exception("Unable to find a viable program address nonce") -} - -/// CreateProgramAddress mirrors the implementation of the Solana SDK's CreateProgramAddress. -/// -/// ProgramAddresses are public keys that _do not_ lie on the ed25519 curve to ensure that -/// there is no associated private key. In the event that the program and seed parameters -/// result in a valid public key, ErrInvalidPublicKey is returned. -/// -/// Reference: https://github.com/solana-labs/solana/blob/5548e599fe4920b71766e0ad1d121755ce9c63d5/sdk/program/src/pubkey.rs#L158 -/// -fun PublicKey.Companion.deriveProgramAddress(programId: PublicKey, seeds: List): PublicKey { - fun PublicKey.Companion.getMaxSeeds() = 16 - - val buffer = ByteArrayOutputStream() - require(seeds.size < getMaxSeeds()) { "Max seed size exceeded" } - - for (seed in seeds) { - try { - buffer.write(seed) - } catch (e: IOException) { - throw RuntimeException(e) - } - } - try { - buffer.write(programId.bytes.toByteArray()) - buffer.write("ProgramDerivedAddress".toByteArray()) - } catch (e: IOException) { - throw RuntimeException(e) - } - val hash = Sha256Hash.hash(buffer.toByteArray()) - - val publicKey = PublicKey(hash.toList()) - - // Following the Solana SDK, we want to _reject_ the generated public key - // if it's a valid compressed EdwardsPoint (on the curve). - // - if (Ed25519.onCurve(publicKey.bytes.toByteArray())) { - throw RuntimeException("Invalid seeds, address must fall off the curve") - } - - return PublicKey(hash.toList()) -} - -fun PublicKey.Companion.deriveCommitmentStateAccount(treasury: PublicKey, recentRoot: Hash, transcript: Hash, destination: PublicKey, amount: Kin): ProgramDerivedAccount { - return findProgramAddress( - programId = splitter, - seeds = listOf( - "commitment_state".toByteArray(Charsets.UTF_8), - treasury.bytes.toByteArray(), - recentRoot.bytes.toByteArray(), - transcript.bytes.toByteArray(), - destination.bytes.toByteArray(), - amount.quarks.longToByteArray() - ) - ) -} - -fun PublicKey.Companion.deriveCommitmentVaultAccount(treasury: PublicKey, commitmentState: PublicKey): ProgramDerivedAccount { - return findProgramAddress( - programId = splitter, - seeds = listOf( - "commitment_vault".toByteArray(Charsets.UTF_8), - treasury.bytes.toByteArray(), - commitmentState.bytes.toByteArray() - ) - ) -} - -fun PublicKey.Companion.derivePreSwapState( - source: PublicKey, destination: PublicKey, nonce: PublicKey -): ProgramDerivedAccount { - return findProgramAddress( - programId = SwapValidatorProgram.address, - seeds = listOf( - "pre_swap_state".toByteArray(Charsets.UTF_8), - source.bytes.toByteArray(), - destination.bytes.toByteArray(), - nonce.bytes.toByteArray(), - ) - ) -} \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/model/extensions/SplitterCommitmentAccounts.kt b/services/flipchat/core/src/main/kotlin/com/getcode/model/extensions/SplitterCommitmentAccounts.kt deleted file mode 100644 index 3ce695768..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/model/extensions/SplitterCommitmentAccounts.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.getcode.model.extensions - -import com.getcode.model.Kin -import com.getcode.solana.keys.Hash -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.SplitterCommitmentAccounts -import com.getcode.solana.keys.SplitterTranscript -import com.getcode.solana.organizer.AccountCluster - -fun SplitterCommitmentAccounts.Companion.newInstance( - source: AccountCluster, - destination: PublicKey, - amount: Kin, - treasury: PublicKey, - recentRoot: Hash, - intentId: PublicKey, - actionId: Int -): SplitterCommitmentAccounts { - val transcript = SplitterTranscript( - intentId = intentId, - actionId = actionId, - amount = amount, - source = source.vaultPublicKey, - destination = destination - ) - - return newInstance( - treasury = treasury, - destination = destination, - recentRoot = recentRoot, - transcript = transcript.transcriptHash, - amount = amount - ) -} - -fun SplitterCommitmentAccounts.Companion.newInstance( - treasury: PublicKey, - destination: PublicKey, - recentRoot: Hash, - transcript: Hash, - amount: Kin -): SplitterCommitmentAccounts { - val state = PublicKey.deriveCommitmentStateAccount( - treasury = treasury, - recentRoot = recentRoot, - transcript = transcript, - destination = destination, - amount = amount - ) - - val vault = PublicKey.deriveCommitmentVaultAccount( - treasury = treasury, - commitmentState = state.publicKey - ) - - return SplitterCommitmentAccounts( - treasury = treasury, - destination = destination, - recentRoot = recentRoot, - transcript = transcript, - state = state, - vault = vault, - ) -} \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/model/extensions/TimelockDerivedAccounts.kt b/services/flipchat/core/src/main/kotlin/com/getcode/model/extensions/TimelockDerivedAccounts.kt deleted file mode 100644 index 08e09159f..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/model/extensions/TimelockDerivedAccounts.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.getcode.model.extensions - -import com.getcode.solana.keys.ProgramDerivedAccount -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.TimelockDerivedAccounts - -fun TimelockDerivedAccounts.Companion.newInstance(owner: PublicKey, legacy: Boolean = false): TimelockDerivedAccounts { - val state: ProgramDerivedAccount - val vault: ProgramDerivedAccount - - if (legacy) { - state = - PublicKey.deriveLegacyTimelockStateAccount(owner = owner, lockout = 1_814_400) - vault = PublicKey.deriveLegacyTimelockVaultAccount(stateAccount = state.publicKey) - } else { - state = PublicKey.deriveTimelockStateAccount(owner = owner, lockout = lockoutInDays) - vault = PublicKey.deriveTimelockVaultAccount( - stateAccount = state.publicKey, - version = dataVersion - ) - } - - return TimelockDerivedAccounts( - owner = owner, - state = state, - vault = vault - ) -} \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/model/intents/SwapConfigParameters.kt b/services/flipchat/core/src/main/kotlin/com/getcode/model/intents/SwapConfigParameters.kt deleted file mode 100644 index 8a066a1d6..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/model/intents/SwapConfigParameters.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.common.v1.CodeModel.InstructionAccount -import com.codeinc.gen.transaction.v2.CodeTransactionService -import com.getcode.model.toHash -import com.getcode.model.toPublicKey -import com.getcode.solana.keys.AccountMeta -import com.getcode.solana.keys.Hash -import com.getcode.solana.keys.PublicKey -import com.google.protobuf.ByteString - -data class SwapConfigParameters( - val payer: PublicKey, - val swapProgram: PublicKey, - val nonce: PublicKey, - val blockHash: Hash, - val maxToSend: Long, - val minToReceive: Long, - val computeUnitLimit: Int, - val computeUnitPrice: Long, - val swapAccounts: List, - val swapData: ByteString, -) { - companion object { - operator fun invoke(proto: CodeTransactionService.SwapResponse.ServerParameters): SwapConfigParameters? { - return runCatching { - val payer = proto.payer.value.toByteArray().toPublicKey() - val swapProgram = proto.swapProgram.value.toByteArray().toPublicKey() - val nonce = proto.nonce.value.toByteArray().toPublicKey() - val blockHash = proto.recentBlockhash.value.toByteArray().toHash() - - SwapConfigParameters( - payer = payer, - swapProgram = swapProgram, - nonce = nonce, - blockHash = blockHash, - maxToSend = proto.maxToSend, - minToReceive = proto.minToReceive, - computeUnitLimit = proto.computeUnitLimit, - computeUnitPrice = proto.computeUnitPrice, - swapAccounts = proto.swapIxnAccountsList.mapNotNull { it.meta() }, - swapData = proto.swapIxnData - ) - }.getOrNull() - } - } -} - -private fun InstructionAccount.meta(): AccountMeta? = runCatching { - val publicKey = PublicKey(account.value.toList()) - AccountMeta( - publicKey = publicKey, - isSigner = isSigner, - isWritable = isWritable, - isPayer = false, - isProgram = false - ) -}.getOrNull() \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/solana/builder/TransactionBuilder.kt b/services/flipchat/core/src/main/kotlin/com/getcode/solana/builder/TransactionBuilder.kt deleted file mode 100644 index 9972b86bf..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/solana/builder/TransactionBuilder.kt +++ /dev/null @@ -1,223 +0,0 @@ -package com.getcode.solana.builder - -import com.getcode.solana.keys.Hash -import com.getcode.model.Kin -import com.getcode.model.extensions.newInstance -import com.getcode.model.intents.SwapConfigParameters -import com.getcode.services.model.ExtendedMetadata -import com.getcode.solana.Instruction -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.TransferType -import com.getcode.solana.keys.description -import com.getcode.solana.instructions.programs.* -import com.getcode.solana.keys.Key32.Companion.mock -import com.getcode.solana.keys.Key32.Companion.subsidizer -import com.getcode.solana.keys.PreSwapStateAccount -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.TimelockDerivedAccounts -import com.getcode.solana.organizer.AccountCluster -import com.getcode.vendor.Base58 -import timber.log.Timber - -object TransactionBuilder { - - - fun transfer( - timelockDerivedAccounts: TimelockDerivedAccounts, - destination: PublicKey, - amount: Kin, - nonce: PublicKey, - recentBlockhash: Hash, - kreIndex: Int - ): SolanaTransaction { - return SolanaTransaction.newInstance( - payer = subsidizer, - recentBlockhash = recentBlockhash, - instructions = listOf( - SystemProgram_AdvanceNonce( - nonce = nonce, - authority = subsidizer - ).instruction(), - - MemoProgram_Memo.newInstance( - transferType = TransferType.p2p, - kreIndex = kreIndex - ).instruction(), - - TimelockProgram_TransferWithAuthority( - timelock = timelockDerivedAccounts.state.publicKey, - vault = timelockDerivedAccounts.vault.publicKey, - vaultOwner = timelockDerivedAccounts.owner, - timeAuthority = vmTimeAuthority, - destination = destination, - payer = subsidizer, - bump = timelockDerivedAccounts.state.bump.toByte(), - quarks = amount.quarks - ).instruction(), - ) - ) - } - - @Deprecated("No longer exists in VM") - fun closeDormantAccount( - authority: PublicKey, - timelockDerivedAccounts: TimelockDerivedAccounts, - destination: PublicKey, - nonce: PublicKey, - recentBlockhash: Hash, - kreIndex: Int, - legacy: Boolean = false, - metadata: ExtendedMetadata? = null, - ): SolanaTransaction { - val instructions = mutableListOf() - - instructions.add(SystemProgram_AdvanceNonce(nonce = nonce, authority = subsidizer).instruction()) - instructions.add( - MemoProgram_Memo.newInstance( - transferType = TransferType.p2p, - kreIndex = kreIndex - ).instruction(), - ) - - when (metadata) { - is ExtendedMetadata.Tip -> { - instructions.add(MemoProgram_Memo.newInstance(metadata.socialUser).instruction()) - } - else -> Unit - } - - instructions.addAll( - listOf( - TimelockProgram_RevokeLockWithAuthority( - timelock = timelockDerivedAccounts.state.publicKey, - vault = timelockDerivedAccounts.vault.publicKey, - closeAuthority = subsidizer, - payer = subsidizer, - bump = timelockDerivedAccounts.state.bump.toByte(), - legacy = legacy - ).instruction(), - - TimelockProgram_DeactivateLock( - timelock = timelockDerivedAccounts.state.publicKey, - vaultOwner = authority, - payer = subsidizer, - bump = timelockDerivedAccounts.state.bump.toByte(), - legacy = legacy - ).instruction(), - - TimelockProgram_Withdraw( - timelock = timelockDerivedAccounts.state.publicKey, - vault = timelockDerivedAccounts.vault.publicKey, - vaultOwner = authority, - destination = destination, - payer = subsidizer, - bump = timelockDerivedAccounts.state.bump.toByte(), - legacy = legacy - ).instruction(), - - TimelockProgram_CloseAccounts( - timelock = timelockDerivedAccounts.state.publicKey, - vault = timelockDerivedAccounts.vault.publicKey, - closeAuthority = subsidizer, - payer = subsidizer, - bump = timelockDerivedAccounts.state.bump.toByte(), - legacy = legacy, - ).instruction() - ), - ) - - return SolanaTransaction.newInstance( - payer = subsidizer, - recentBlockhash = recentBlockhash, - instructions = instructions, - ) - } - - - // Swap performs an on-chain swap. The high-level flow mirrors SubmitIntent - // closely. However, due to the time-sensitive nature and unreliability of - // swaps, they do not fit within the broader intent system. This results in - // a few key differences: - // * Transactions are submitted on a best-effort basis outside of the Code - // Sequencer within the RPC handler - // * Balance changes are applied after the transaction has finalized - // * Transactions use recent blockhashes over a nonce - // - // The transaction will have the following instruction format: - // 1. ComputeBudget::SetComputeUnitLimit - // 2. ComputeBudget::SetComputeUnitPrice - // 3. SwapValidator::PreSwap - // 4. Dynamic swap instruction - // 5. SwapValidator::PostSwap - // - // Note: Currently limited to swapping USDC to Kin. - // Note: Kin is deposited into the primary account. - // - fun swap( - fromUsdc: AccountCluster, - toPrimary: PublicKey, - parameters: SwapConfigParameters - ): SolanaTransaction { - val payer = parameters.payer - val destination = toPrimary - - val stateAccount = PreSwapStateAccount.newInstance( - owner = mock, - source = fromUsdc.vaultPublicKey, - destination = destination, - nonce = parameters.nonce - ) - - Timber.d("swap accounts=${parameters.swapAccounts.map { it.description }}") - val remainingAccounts = parameters.swapAccounts.filter { - (it.isSigner || it.isWritable) && - (it.publicKey != fromUsdc.authorityPublicKey && - it.publicKey != fromUsdc.vaultPublicKey && - it.publicKey != destination) - } - - return SolanaTransaction.newInstance( - payer = payer, - recentBlockhash = parameters.blockHash, - instructions = listOf( - ComputeBudgetProgram_SetComputeUnitLimit( - limit = parameters.computeUnitLimit, - bump = stateAccount.state.bump.toByte(), - ).instruction(), - - ComputeBudgetProgram_SetComputeUnitPrice( - microLamports = parameters.computeUnitPrice, - bump = stateAccount.state.bump.toByte(), - ).instruction(), - - SwapValidatorProgram_PreSwap( - preSwapState = stateAccount.state.publicKey, - user = fromUsdc.authorityPublicKey, - source = fromUsdc.vaultPublicKey, - destination = destination, - nonce = parameters.nonce, - payer = payer, - remainingAccounts = remainingAccounts, - ).instruction(), - - Instruction( - program = parameters.swapProgram, - accounts = parameters.swapAccounts, - data = parameters.swapData.toList(), - ), - - SwapValidatorProgram_PostSwap( - stateBump = stateAccount.state.bump.toByte(), - maxToSend = parameters.maxToSend, - minToReceive = parameters.minToReceive, - preSwapState = stateAccount.state.publicKey, - source = fromUsdc.vaultPublicKey, - destination = destination, - payer = payer, - ).instruction() - ) - ) - } -} - -val vmTimeAuthority = PublicKey(Base58.decode("f1ipC31qd2u88MjNYp1T4Cc7rnWfM9ivYpTV1Z8FHnD").toList()) \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/AccountCluster.kt b/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/AccountCluster.kt deleted file mode 100644 index d902dc27d..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/AccountCluster.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.getcode.solana.organizer - -import com.getcode.crypt.DerivedKey -import com.getcode.crypt.MnemonicPhrase -import com.getcode.model.extensions.newInstance -import com.getcode.model.toPublicKey -import com.getcode.solana.keys.AssociatedTokenAccount -import com.getcode.solana.keys.Mint -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.TimelockDerivedAccounts - - -class AccountCluster( - val index: Int, - val authority: DerivedKey, - val derivation: Derivation -) { - - val authorityPublicKey: PublicKey - get() = authority.keyPair.publicKeyBytes.toPublicKey() - - val vaultPublicKey: PublicKey - get() = when (derivation) { - is Derivation.Timelock -> timelock!!.vault.publicKey - is Derivation.Usdc -> ata!!.ata.publicKey - } - sealed interface Kind { - data object Timelock: Kind - data object Usdc: Kind - } - - sealed interface Derivation { - data class Timelock(val accounts: TimelockDerivedAccounts): Derivation - data class Usdc(val account: AssociatedTokenAccount): Derivation - } - - val timelock: TimelockDerivedAccounts? - get() = (derivation as? Derivation.Timelock)?.accounts - - val ata: AssociatedTokenAccount? - get() = (derivation as? Derivation.Usdc)?.account - - companion object { - fun newInstanceLazy(authority: DerivedKey, index: Int = 0, kind: Kind, legacy: Boolean = false): Lazy { - return lazy { newInstance(authority, index, kind, legacy) } - } - - fun newInstance(authority: DerivedKey, index: Int = 0, kind: Kind, legacy: Boolean = false): AccountCluster { - return AccountCluster( - index = index, - authority = authority, - derivation = when (kind) { - Kind.Timelock -> Derivation.Timelock( - TimelockDerivedAccounts.newInstance( - owner = PublicKey(authority.keyPair.publicKeyBytes.toList()), - legacy = legacy - ) - ) - Kind.Usdc -> { - Derivation.Usdc( - AssociatedTokenAccount.newInstance( - owner = authority.keyPair.publicKeyBytes.toPublicKey(), - mint = Mint.usd - ) - ) - } - }, - ) - } - - fun using(type: AccountType, index: Int, mnemonic: MnemonicPhrase): AccountCluster { - return newInstance( - index = index, - authority = DerivedKey.derive( - path = type.getDerivationPath(index), - mnemonic = mnemonic - ), - kind = Kind.Timelock - ) - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as AccountCluster - - if (authority != other.authority) return false - if (derivation != other.derivation) return false - - return true - } - - override fun hashCode(): Int { - var result = authority.hashCode() - result = 31 * result + derivation.hashCode() - return result - } - -} \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/AccountType.kt b/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/AccountType.kt deleted file mode 100644 index ed38448a8..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/AccountType.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.getcode.solana.organizer - -import com.codeinc.gen.common.v1.CodeModel as Model -import com.getcode.model.Domain - -sealed interface AccountType { - data object Primary : AccountType - data object Incoming : AccountType - data object Outgoing : AccountType - data class Bucket(val type: SlotType) : AccountType - data object RemoteSend: AccountType - - data class Relationship(val domain: Domain): AccountType - - data object Swap: AccountType - - fun sortOrder() = when (this) { - Primary -> 0 - Incoming -> 1 - Outgoing -> 2 - is Bucket -> { - when (type) { - SlotType.Bucket1 -> 3 - SlotType.Bucket10 -> 4 - SlotType.Bucket100 -> 5 - SlotType.Bucket1k -> 6 - SlotType.Bucket10k -> 7 - SlotType.Bucket100k -> 8 - SlotType.Bucket1m -> 9 - } - } - Swap -> 10 - is Relationship -> 11 - RemoteSend -> 12 - } - - fun getDerivationPath(index: Int): com.getcode.crypt.DerivePath { - return when (this) { - Primary -> com.getcode.crypt.DerivePath.primary - Incoming -> com.getcode.crypt.DerivePath.getBucketIncoming(index) - Outgoing -> com.getcode.crypt.DerivePath.getBucketOutgoing(index) - is Bucket -> type.getDerivationPath() - RemoteSend -> { - // Remote send accounts are standard Solana accounts - // and should use a standard derivation path that - // would be compatible with other 3rd party wallets - com.getcode.crypt.DerivePath.primary - } - is Relationship -> com.getcode.crypt.DerivePath.relationship(domain) - Swap -> com.getcode.crypt.DerivePath.swap - } - } - - fun getAccountType(): Model.AccountType { - return when (this) { - Primary -> Model.AccountType.PRIMARY - Incoming -> Model.AccountType.TEMPORARY_INCOMING - Outgoing -> Model.AccountType.TEMPORARY_OUTGOING - is Bucket -> { - when (this.type) { - SlotType.Bucket1 -> Model.AccountType.BUCKET_1_KIN - SlotType.Bucket10 -> Model.AccountType.BUCKET_10_KIN - SlotType.Bucket100 -> Model.AccountType.BUCKET_100_KIN - SlotType.Bucket1k -> Model.AccountType.BUCKET_1_000_KIN - SlotType.Bucket10k -> Model.AccountType.BUCKET_10_000_KIN - SlotType.Bucket100k -> Model.AccountType.BUCKET_100_000_KIN - SlotType.Bucket1m -> Model.AccountType.BUCKET_1_000_000_KIN - } - } - RemoteSend -> Model.AccountType.REMOTE_SEND_GIFT_CARD - is Relationship -> Model.AccountType.RELATIONSHIP - Swap -> Model.AccountType.SWAP - } - } - - companion object { - fun newInstance(accountType: Model.AccountType, relationship: Model.Relationship? = null): AccountType? { - return when (accountType) { - Model.AccountType.PRIMARY -> Primary - Model.AccountType.TEMPORARY_INCOMING -> Incoming - Model.AccountType.TEMPORARY_OUTGOING -> Outgoing - Model.AccountType.BUCKET_1_KIN -> Bucket(SlotType.Bucket1) - Model.AccountType.BUCKET_10_KIN -> Bucket(SlotType.Bucket10) - Model.AccountType.BUCKET_100_KIN -> Bucket(SlotType.Bucket100) - Model.AccountType.BUCKET_1_000_KIN -> Bucket(SlotType.Bucket1k) - Model.AccountType.BUCKET_10_000_KIN -> Bucket(SlotType.Bucket10k) - Model.AccountType.BUCKET_100_000_KIN -> Bucket(SlotType.Bucket100k) - Model.AccountType.BUCKET_1_000_000_KIN -> Bucket(SlotType.Bucket1m) - Model.AccountType.UNKNOWN -> null - Model.AccountType.LEGACY_PRIMARY_2022 -> Primary - Model.AccountType.REMOTE_SEND_GIFT_CARD -> RemoteSend - Model.AccountType.UNRECOGNIZED -> null - Model.AccountType.RELATIONSHIP -> { - val domain = Domain.from(relationship?.domain?.value) ?: return null - Relationship(domain) - } - Model.AccountType.SWAP -> Swap - } - } - } -} \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/GiftCardAccount.kt b/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/GiftCardAccount.kt deleted file mode 100644 index 77a307e96..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/GiftCardAccount.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.getcode.solana.organizer - -import com.getcode.crypt.DerivePath -import com.getcode.crypt.DerivedKey -import com.getcode.crypt.MnemonicPhrase - -class GiftCardAccount( - val mnemonicPhrase: MnemonicPhrase, - val cluster: AccountCluster -) { - companion object { - fun newInstance(mnemonicPhrase: MnemonicPhrase? = null): GiftCardAccount { - val phrase = mnemonicPhrase ?: MnemonicPhrase.generate() - - return GiftCardAccount( - mnemonicPhrase = phrase, - cluster = AccountCluster.newInstance( - authority = DerivedKey.derive( - path = DerivePath.primary, - mnemonic = phrase, - ), - kind = AccountCluster.Kind.Timelock, - ) - ) - } - } -} \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/Organizer.kt b/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/Organizer.kt deleted file mode 100644 index 4570a61a5..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/Organizer.kt +++ /dev/null @@ -1,162 +0,0 @@ -package com.getcode.solana.organizer - -import com.getcode.crypt.MnemonicPhrase -import com.getcode.model.AccountInfo -import com.getcode.model.Domain -import com.getcode.model.Kin -import com.getcode.model.unusable -import com.getcode.solana.keys.* -import com.getcode.utils.TraceType -import com.getcode.utils.getPublicKeyBase58 -import com.getcode.utils.trace -import timber.log.Timber - -class Organizer( - val tray: Tray, - val mnemonic: MnemonicPhrase, - private var accountInfos: Map = mapOf(), -) { - val slotsBalance get() = tray.slotsBalance - val availableBalance get() = tray.availableBalance - val availableDepositBalance get() = tray.owner.partialBalance - val availableIncomingBalance get() = tray.incoming.partialBalance - val availableRelationshipBalance get() = tray.availableRelationshipBalance - val ownerKeyPair get() = tray.owner.getCluster().authority.keyPair - val swapKeyPair get() = tray.swap.getCluster().authority.keyPair - val swapDepositAddress get() = swapKeyPair.getPublicKeyBase58() - val primaryVault get() = tray.owner.getCluster().vaultPublicKey - val incomingVault get() = tray.incoming.getCluster().vaultPublicKey - - val isUnuseable: Boolean get() = accountInfos.any { it.value.unusable } - - val createdAtMillis: Long? get() = accountInfos.mapNotNull { it.value.createdAt }.minOrNull() - - val isUnlocked: Boolean - get() = accountInfos.values.any { info -> - info.managementState != AccountInfo.ManagementState.Locked - } - - fun set(tray: Tray) { - this.tray.slots = tray.slots - this.tray.owner = tray.owner - this.tray.incoming = tray.incoming - this.tray.outgoing = tray.outgoing - this.tray.mnemonic = tray.mnemonic - } - - fun setBalances(balances: Map) { - tray.setBalances(balances) - } - - fun allAccounts() = tray.allAccounts() - - fun info(accountType: AccountType): AccountInfo? { - val account = tray.cluster(accountType).vaultPublicKey - return accountInfos[account] - } - - fun setAccountInfo(infos: Map) { - this.accountInfos = infos - tray.createRelationships(infos) - propagateBalances() - - trace( - tag = "Organizer", - message = "Fetched account infos", - type = TraceType.Process, - metadata = { - "tray" to tray.reportableRepresentation() - } - ) - } - - fun getAccountInfo() = accountInfos - - val buckets: List - get() = accountInfos.values.toList().sortedBy { it.accountType.sortOrder() } - - fun propagateBalances() { - val balances = mutableMapOf() - com.getcode.utils.timedTrace("propagate balances") { - for ((vaultPublicKey, info) in accountInfos) { - if (tray.publicKey(info.accountType) == vaultPublicKey) { - balances[info.accountType] = info.balance - } else { - // The public key above doesn't match any accounts - // that the Tray is aware of. If we're dealing with - // temp I/O accounts then we likely just need to - // update the index and try again - when (info.accountType) { - AccountType.Incoming, AccountType.Outgoing -> { - // Update the index - tray.setIndex(info.index, accountType = info.accountType) - Timber.i("Updating ${info.accountType} index to: ${info.index}") - - // Ensure that the account matches - if (tray.publicKey(info.accountType) != vaultPublicKey) { - Timber.i("Indexed account mismatch. This isn't suppose to happen.") - continue - } - balances[info.accountType] = info.balance - } - - AccountType.Primary, - is AccountType.Bucket, - AccountType.RemoteSend, - is AccountType.Relationship, - AccountType.Swap -> { - Timber.i("Non-indexed account mismatch. Account doesn't match server-provided account. Something is definitely wrong") - } - } - } - } - } - - setBalances(balances) - } - - fun relationshipFor(domain: Domain): Relationship? { - return tray.relationships.relationshipWith(domain) - } - - fun relationshipsLargestFirst(): List { - return tray.relationships.relationships(largestFirst = true).also { - Timber.d("relationships=${it.joinToString { it.domain.urlString }}") - } - } - - companion object { - fun newInstance( - mnemonic: MnemonicPhrase - ): Organizer { - val tray = Tray.newInstance(mnemonic) - return Organizer( - mnemonic = mnemonic, - tray = tray, - ) - } - } -} - -enum class Denomination { - ones, - tens, - hundreds, - thousands, - tenThousands, - hundredThousands, - millions; - - val derivationPath: com.getcode.crypt.DerivePath - get() { - return when (this) { - ones -> com.getcode.crypt.DerivePath.bucket1 - tens -> com.getcode.crypt.DerivePath.bucket10 - hundreds -> com.getcode.crypt.DerivePath.bucket100 - thousands -> com.getcode.crypt.DerivePath.bucket1k - tenThousands -> com.getcode.crypt.DerivePath.bucket10k - hundredThousands -> com.getcode.crypt.DerivePath.bucket100k - millions -> com.getcode.crypt.DerivePath.bucket1m - } - } -} diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/PartialAccount.kt b/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/PartialAccount.kt deleted file mode 100644 index 4922ba973..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/PartialAccount.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.getcode.solana.organizer - -import com.getcode.model.Kin - -data class PartialAccount(val cluster: Lazy, var partialBalance: Kin = Kin.fromKin(0)) { - fun getCluster() = cluster.value -} \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/Relationship.kt b/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/Relationship.kt deleted file mode 100644 index 11ac4cf27..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/Relationship.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.getcode.solana.organizer - -import com.getcode.crypt.DerivePath -import com.getcode.crypt.DerivedKey -import com.getcode.crypt.MnemonicPhrase -import com.getcode.model.Domain -import com.getcode.model.Kin - -class Relationship( - val domain: Domain, - val mnemonic: MnemonicPhrase, - var partialBalance: Kin = Kin.fromKin(0), -) { - private lateinit var cluster: Lazy - - fun getCluster() = cluster.value - - companion object { - fun newInstance( - domain: Domain, - mnemonic: MnemonicPhrase, - partialKinBalance: Kin = Kin.fromKin(0), - ): Relationship { - val cluster = AccountCluster.newInstanceLazy( - DerivedKey.derive( - path = DerivePath.relationship(domain), - mnemonic = mnemonic - ), - kind = AccountCluster.Kind.Timelock, - ) - - return Relationship( - domain = domain, - mnemonic = mnemonic, - partialBalance = partialKinBalance - ).apply { - this.cluster = cluster - } - } - } -} \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/Slot.kt b/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/Slot.kt deleted file mode 100644 index 712c01329..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/Slot.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.getcode.solana.organizer - -import com.getcode.crypt.DerivePath -import com.getcode.crypt.DerivedKey -import com.getcode.crypt.MnemonicPhrase -import com.getcode.model.Kin - - -data class Slot( - var partialBalance: Kin, - val type: SlotType, - private val cluster: Lazy -) { - val billValue: Int = type.getBillValue() - - fun billCount(): Long { - return (partialBalance.toKinValueDouble() / type.getBillValue()).toLong() - } - - fun getCluster() = cluster.value - - companion object { - fun newInstance( - partialBalance: Kin = Kin.fromQuarks(0), - type: SlotType, - mnemonic: MnemonicPhrase - ): Slot { - return Slot( - partialBalance = partialBalance, - type = type, - cluster = lazy { - AccountCluster.newInstance( - DerivedKey.derive( - type.getDerivationPath(), - mnemonic - ), - kind = AccountCluster.Kind.Timelock, - ) - } - ) - } - } -} - -enum class SlotType { - Bucket1, - Bucket10, - Bucket100, - Bucket1k, - Bucket10k, - Bucket100k, - Bucket1m; -} - -fun SlotType.getBillValue(): Int = - when (this.ordinal) { - SlotType.Bucket1.ordinal -> 1 - SlotType.Bucket10.ordinal -> 10 - SlotType.Bucket100.ordinal -> 100 - SlotType.Bucket1k.ordinal -> 1_000 - SlotType.Bucket10k.ordinal -> 10_000 - SlotType.Bucket100k.ordinal -> 100_000 - SlotType.Bucket1m.ordinal -> 1_000_000 - else -> throw IllegalStateException() - } - -fun SlotType.getDerivationPath(): DerivePath = - when (this.ordinal) { - SlotType.Bucket1.ordinal -> DerivePath.bucket1 - SlotType.Bucket10.ordinal -> DerivePath.bucket10 - SlotType.Bucket100.ordinal -> DerivePath.bucket100 - SlotType.Bucket1k.ordinal -> DerivePath.bucket1k - SlotType.Bucket10k.ordinal -> DerivePath.bucket10k - SlotType.Bucket100k.ordinal -> DerivePath.bucket100k - SlotType.Bucket1m.ordinal -> DerivePath.bucket1m - else -> null - }.let { it ?: throw IllegalStateException() } diff --git a/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/Tray.kt b/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/Tray.kt deleted file mode 100644 index e3b59e37d..000000000 --- a/services/flipchat/core/src/main/kotlin/com/getcode/solana/organizer/Tray.kt +++ /dev/null @@ -1,957 +0,0 @@ -package com.getcode.solana.organizer - -import com.getcode.crypt.DerivePath -import com.getcode.crypt.DerivedKey -import com.getcode.crypt.MnemonicPhrase -import com.getcode.model.AccountInfo -import com.getcode.model.Domain -import com.getcode.model.Kin -import com.getcode.model.RelationshipBox -import com.getcode.model.description -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.base58 -import com.getcode.utils.TraceType -import com.getcode.services.utils.padded -import com.getcode.utils.trace -import kotlin.math.min - -class Tray( - var slots: List, - var owner: PartialAccount, - var swap: PartialAccount, - var incoming: PartialAccount, - var outgoing: PartialAccount, - var mnemonic: MnemonicPhrase -) { - var slotsBalance: Kin = Kin.fromKin(0) - get() = slots.map { it.partialBalance }.reduce { acc, slot -> acc + slot } - private set - - var availableBalance: Kin = Kin.fromKin(0) - get() = slotsBalance + availableDepositBalance + availableIncomingBalance + availableRelationshipBalance - private set - - private val availableDepositBalance: Kin - get() = owner.partialBalance - - private val availableIncomingBalance: Kin - get() = incoming.partialBalance - - var relationships = RelationshipBox() - internal set - - var availableRelationshipBalance: Kin = Kin.fromKin(0) - get() = relationships.publicKeys.values.map { it.partialBalance } - .reduceOrNull { acc, slot -> acc + slot } ?: Kin.fromKin(0) - private set - - fun slot(type: SlotType): Slot { - return slots.first { it.type == type } - } - - fun slotDown(type: SlotType): Slot? { - val index = slots.indexOfFirst { it.type == type } - if (index > 0) { - return slots[index - 1] - } - return null - } - - fun slotUp(type: SlotType): Slot? { - val index = slots.indexOfFirst { it.type == type } - if (index < slots.size - 1) { - return slots[index + 1] - } - return null - } - - fun increment(type: AccountType, kin: Kin) { - when (type) { - AccountType.Primary -> owner.partialBalance += kin - AccountType.Incoming -> incoming.partialBalance += kin - AccountType.Outgoing -> outgoing.partialBalance += kin - is AccountType.Bucket -> slots[type.type.ordinal].partialBalance += kin - AccountType.RemoteSend -> throw IllegalStateException("Remote send account unsupported") - is AccountType.Relationship -> { - val relationship = relationships.relationshipWith(type.domain) - ?: throw IllegalStateException("Relationship for ${type.domain.relationshipHost}) not found in ${relationships.domains}") - relationships.insert( - relationship.apply { - partialBalance += kin - } - ) - } - - AccountType.Swap -> swap.partialBalance += kin - } - } - - fun decrement(type: AccountType, kin: Kin) { - when (type) { - AccountType.Primary -> owner.partialBalance -= kin - AccountType.Incoming -> incoming.partialBalance -= kin - AccountType.Outgoing -> outgoing.partialBalance -= kin - is AccountType.Bucket -> slots[type.type.ordinal].partialBalance -= kin - AccountType.RemoteSend -> throw IllegalStateException("Remote send account unsupported") - is AccountType.Relationship -> { - val relationship = relationships.relationshipWith(type.domain) - ?: throw IllegalStateException("Relationship for ${type.domain.relationshipHost}) not found in ${relationships.domains}") - relationships.insert( - relationship.apply { - partialBalance -= kin - } - ) - } - AccountType.Swap -> swap.partialBalance -= kin - } - } - - fun setBalances(balances: Map) { - owner.partialBalance = balances[AccountType.Primary] ?: owner.partialBalance - incoming.partialBalance = balances[AccountType.Incoming] ?: incoming.partialBalance - outgoing.partialBalance = balances[AccountType.Outgoing] ?: outgoing.partialBalance - - slots[0].partialBalance = balances[AccountType.Bucket(SlotType.Bucket1)] ?: slots[0].partialBalance - slots[1].partialBalance = balances[AccountType.Bucket(SlotType.Bucket10)] ?: slots[1].partialBalance - slots[2].partialBalance = balances[AccountType.Bucket(SlotType.Bucket100)] ?: slots[2].partialBalance - slots[3].partialBalance = balances[AccountType.Bucket(SlotType.Bucket1k)] ?: slots[3].partialBalance - slots[4].partialBalance = balances[AccountType.Bucket(SlotType.Bucket10k)] ?: slots[4].partialBalance - slots[5].partialBalance = balances[AccountType.Bucket(SlotType.Bucket100k)] ?: slots[5].partialBalance - slots[6].partialBalance = balances[AccountType.Bucket(SlotType.Bucket1m)] ?: slots[6].partialBalance - - balances.filter { (type, _) -> type is AccountType.Relationship } - .mapNotNull { (type, amount) -> - val relationshipType = type as? AccountType.Relationship ?: return@mapNotNull null - relationshipType to amount - } - .onEach { (relationship, amount) -> - val domain = relationship.domain - setBalance(domain, amount) - } - } - - private fun setBalance(domain: Domain, balance: Kin) { - val relationship = relationships.relationshipWith(domain) ?: return - relationships.insert(relationship.apply { - partialBalance = balance - }) - } - - fun partialBalance(type: AccountType): Kin { - return when (type) { - is AccountType.Primary -> owner.partialBalance - is AccountType.Incoming -> incoming.partialBalance - is AccountType.Outgoing -> outgoing.partialBalance - is AccountType.Bucket -> slot(type.type).partialBalance - AccountType.RemoteSend -> throw IllegalStateException("Remote send account unsupported") - is AccountType.Relationship -> { - val relationship = relationships.relationshipWith(type.domain) - ?: throw IllegalStateException("Relationship for ${type.domain.relationshipHost}) not found in ${relationships.domains}") - - return relationship.partialBalance - } - - AccountType.Swap -> swap.partialBalance - } - } - - fun createRelationships(accountInfos: Map) { - val domains= accountInfos - .mapNotNull { it.value.relationship?.domain } - - domains.onEach { createRelationship(it) } - } - - fun createRelationship(domain: Domain): Relationship { - val relationship = Relationship.newInstance(domain, mnemonic) - relationships.insert(relationship) - return relationship - } - - fun incrementIncoming() { - setIndex(incoming.getCluster().index + 1, AccountType.Incoming) - } - - fun incrementOutgoing() { - setIndex(outgoing.getCluster().index + 1, AccountType.Outgoing) - } - - fun setIndex(index: Int, accountType: AccountType) { - when (accountType) { - AccountType.Incoming -> { - incoming = PartialAccount(cluster = incoming(index, mnemonic)) - } - AccountType.Outgoing -> { - outgoing = PartialAccount(cluster = outgoing(index, mnemonic)) - } - - is AccountType.Bucket, - AccountType.Primary, - is AccountType.Relationship, - AccountType.RemoteSend, - AccountType.Swap -> { - throw IllegalStateException() - } - } - } - - fun allAccounts(): List> { - return listOf( - Pair(AccountType.Primary, owner.getCluster()), - Pair(AccountType.Incoming, incoming.getCluster()), - Pair(AccountType.Outgoing, outgoing.getCluster()), - *slots.map { Pair(AccountType.Bucket(it.type), it.getCluster()) }.toTypedArray(), - ) - } - - fun publicKey(accountType: AccountType): PublicKey { - return cluster(accountType).vaultPublicKey - } - - fun cluster(accountType: AccountType): AccountCluster { - return when (accountType) { - AccountType.Primary -> owner.getCluster() - AccountType.Incoming -> incoming.getCluster() - AccountType.Outgoing -> outgoing.getCluster() - is AccountType.Bucket -> slot(accountType.type).getCluster() - AccountType.RemoteSend -> throw IllegalStateException("Remote send account unsupported") - is AccountType.Relationship -> { - relationships.relationshipWith(domain = accountType.domain)!!.getCluster() - } - - AccountType.Swap -> swap.getCluster() - } - } - - fun copy(): Tray { - return Tray( - slots = slots.map { it.copy() }, - owner = owner.copy(), - swap = swap.copy(), - incoming = incoming.copy(), - outgoing = outgoing.copy(), - mnemonic = mnemonic, - ).apply tray@{ - this@tray.relationships = this@Tray.relationships - } - } - - companion object { - fun newInstance( - mnemonic: MnemonicPhrase - ): Tray { - return Tray( - mnemonic = mnemonic, - slots = listOf( - Slot.newInstance( - type = SlotType.Bucket1, - mnemonic = mnemonic - ), - Slot.newInstance( - type = SlotType.Bucket10, - mnemonic = mnemonic - ), - Slot.newInstance( - type = SlotType.Bucket100, - mnemonic = mnemonic - ), - Slot.newInstance( - type = SlotType.Bucket1k, - mnemonic = mnemonic - ), - Slot.newInstance( - type = SlotType.Bucket10k, - mnemonic = mnemonic - ), - Slot.newInstance( - type = SlotType.Bucket100k, - mnemonic = mnemonic - ), - Slot.newInstance( - type = SlotType.Bucket1m, - mnemonic = mnemonic - ), - ), - incoming = PartialAccount(incoming(0, mnemonic)), - outgoing = PartialAccount(outgoing( 0, mnemonic)), - owner = PartialAccount( - cluster = AccountCluster.newInstanceLazy( - authority = DerivedKey.derive(DerivePath.primary, mnemonic), - kind = AccountCluster.Kind.Timelock, - ) - ), - swap = PartialAccount( - cluster = AccountCluster.newInstanceLazy( - authority = DerivedKey.derive(DerivePath.swap, mnemonic), - kind = AccountCluster.Kind.Usdc, - ) - ) - ) - } - - fun incoming(index: Int, mnemonic: MnemonicPhrase): Lazy { - return lazy { - AccountCluster.newInstance( - authority = DerivedKey.derive( - DerivePath.getBucketIncoming(index), - mnemonic - ), - index = index, - kind = AccountCluster.Kind.Timelock, - ) - } - } - - fun outgoing(index: Int, mnemonic: MnemonicPhrase): Lazy { - return lazy { - AccountCluster.newInstance( - authority = DerivedKey.derive( - DerivePath.getBucketOutgoing(index), - mnemonic - ), - index = index, - kind = AccountCluster.Kind.Timelock, - ) - } - } - } - - - // MARK: - Redistribute - - - /// Redistribute the bills in the organizer to ensure there are no gaps - /// in consecutive slots. - /// - /// For example, avoid this: - /// ---------------------------------------------------------------- - /// | slot 0 | slot 1 | slot 2 | slot 3 | slot 4 | slot 5 | slot 6 | - /// ---------------------------------------------------------------- - /// | 1 | 0 | 10 | 10 | 0 | 0 | 0 | = 1,101 - /// ^---------^--- not optimal - /// - /// Instead, we want this: - /// ---------------------------------------------------------------- - /// | slot 0 | slot 1 | slot 2 | slot 3 | slot 4 | slot 5 | slot 6 | - /// ---------------------------------------------------------------- - /// | 11 | 9 | 9 | 10 | 0 | 0 | 0 | = 1,101 - /// ^---------^--------┘ split the 10 downwards - /// - /// The examples above both have the same total balance, but the second - /// example should allow for more efficient payments later down the line. - /// - /// We also try to limit the number of bills in each slot as a secondary - /// goal. This is done by recursively exchanging large bills for smaller - /// bills and vice versa with rules around how many of each denomination - /// to keep. Typically, you never need more than 9 pennies to make any - /// payment. - /// - /// Algorithm: - /// -------------------------------------------------------------------- - /// 1) First we take large bills and exchange them for smaller bills one - /// at a time. We do this recursively until we can't exchange any more - /// large bills to small ones. This spreads out our total balance over - /// as many slots as possible. - /// - /// 2) Then we take smaller bills and exchange them for larger bills if - /// we have more than needed in any slot. This reduces the number of - /// bills we have in total. - /// - /// This algorithm guarantees that we will never have gaps (zero balance) - /// between consecutive slots (e.g. 1000, 0, 10, 1). - /// --------------------------------------------------------------------- - /// - /// TODO: this algorithm could be optimized to reduce the number of - /// transactions - fun redistribute(): List { - val exchanges = mutableListOf() - - exchanges.addAll( - exchangeLargeToSmall() - ) - - exchanges.addAll( - exchangeSmallToLarge() - ) - - return exchanges - } - - fun receive(receivingAccount: AccountType, amount: Kin): List { - if (partialBalance(receivingAccount) < amount) throw OrganizerException.InvalidSlotBalanceException() - - val container = mutableListOf() - - var remainingAmount = amount - - for (i in (slots.size - 1 downTo 0)) { - val currentSlot = slots[i] - - val howManyFit: Int = (remainingAmount.toKinValueDouble() / currentSlot.billValue).toInt() - if (howManyFit > 0) { - val amountToDeposit = Kin.fromKin(howManyFit * currentSlot.billValue) - - normalize(slotType = currentSlot.type, amount = amountToDeposit) { subAmount -> - container.add( - InternalExchange( - from = receivingAccount, - to = AccountType.Bucket(currentSlot.type), - kin = subAmount - ) - ) - } - - decrement(receivingAccount, amountToDeposit) - increment(AccountType.Bucket(currentSlot.type), amountToDeposit) - - remainingAmount -= amountToDeposit - } - } - - return container - } - - /// Recursive function to exchange large bills to smaller bills (when - /// possible). For example, if we have dimes but no pennies, we should - /// break a dime into pennies. - /// - - fun exchangeLargeToSmall(layer: Int = 0): List { - val padding = "-".repeat(layer + 1) + "|" - - val exchanges = mutableListOf() - - for (i in 1..slots.size) { - val currentSlot = slots[slots.size - i] // Backwards - val smallerSlot = slotDown(currentSlot.type) - - trace( - "$padding o Checking slot: ${currentSlot.type}", - type = TraceType.Silent - ) - - if (smallerSlot == null) { - // We're at the lowest denomination - // so we can't exchange anymore. - trace( - "$padding x Last slot", - type = TraceType.Silent - ) - break - } - - if (currentSlot.billCount() <= 0) { - // Nothing to exchange, the current slot is empty. - trace( - "$padding x Empty", - type = TraceType.Silent - ) - continue - } - - val howManyFit = currentSlot.billValue / smallerSlot.billValue - - if (smallerSlot.billCount() >= howManyFit - 1) { - // No reason to exchange yet, the smaller slot - // already has enough bills for most payments - trace( - "$padding x Enough bills", - type = TraceType.Silent - ) - continue - } - - val amount = Kin.fromKin(currentSlot.billValue) - - // Adjust the slot balance - decrement(AccountType.Bucket(currentSlot.type), kin = amount) - increment(AccountType.Bucket(smallerSlot.type), kin = amount) - - trace( - message = "$padding v Exchanging from ${currentSlot.type} to ${smallerSlot.type} $amount Kin", - type = TraceType.Silent - ) - - exchanges.add( - InternalExchange( - from = AccountType.Bucket(currentSlot.type), - to = AccountType.Bucket(smallerSlot.type), - kin = amount - ) - ) - - exchanges.addAll( - exchangeLargeToSmall(layer = layer + 1) - ) // Recursive - } - - return exchanges - } - - /// Recursive function to exchange small bills to larger bills (when - /// possible). - /// - /// For example, if we have 19 pennies or more, we should exchange excess - /// pennies for dimes. But if we only have 18 pennies or less, we - /// should not exchange any because we'd be unable to make a future - /// payment that has a $0.09 amount (there are some edge cases). - /// - - fun exchangeSmallToLarge(layer: Int = 0): List { - val padding = "-".repeat(layer + 1) + "|" - - val exchanges = mutableListOf() - - for (element in slots) { - - val currentSlot = element // Forwards - val largerSlot = slotUp(currentSlot.type) - - trace("$padding o Checking slot: ${currentSlot.type}") - - if (largerSlot == null) { - // We're at the largest denomination - // so we can't exchange anymore. - trace( - "$padding x Last slot", - type = TraceType.Silent - ) - break - } - - // First we need to check how many bills of the current type fit - // into the next slot. - - val howManyFit = largerSlot.billValue / currentSlot.billValue - val howManyWeHave = currentSlot.billCount() - val howManyToLeave = min(howManyFit - 1L, howManyWeHave) - - if (howManyWeHave < ((howManyFit * 2) - 1)) { - // We don't have enough bills to exchange, so we can't do - // anything in this slot at the moment. - trace( - "$padding x Not enough bills", - type = TraceType.Silent - ) - continue - } - - val howManyToExchange = (howManyWeHave - howManyToLeave) / howManyFit * howManyFit - val amount = Kin.fromKin(kin = howManyToExchange) * currentSlot.billValue - - val slotTransfers = mutableListOf() - - normalizeLargest(amount = amount) { partialAmount -> - slotTransfers.add( - InternalExchange( - from = AccountType.Bucket(currentSlot.type), - to = AccountType.Bucket(largerSlot.type), - kin = partialAmount - ) - ) - } - - // Adjust the slot balance - decrement(AccountType.Bucket(currentSlot.type), kin = amount) - increment(AccountType.Bucket(largerSlot.type), kin = amount) - - slotTransfers.forEach { transfer -> - trace( - message = "$padding v Exchanging from ${transfer.from} to {transfer.to!} {transfer.kin} Kin", - type = TraceType.Silent - ) - } - - exchanges.addAll( - slotTransfers - ) - - exchanges.addAll( - exchangeSmallToLarge(layer = layer + 1) - ) // Recursive - } - - return exchanges - } - - fun normalize(slotType: SlotType, amount: Kin, handler: (Kin) -> Unit) { - var howManyFit = amount.toKinTruncatingLong() / slotType.getBillValue() - while (howManyFit > 0) { - val billsToMove = min(howManyFit, 9) - val moveAmount = Kin.fromKin(slotType.getBillValue() * billsToMove) - - handler(moveAmount) - - howManyFit -= billsToMove - } - } - - fun normalizeLargest(amount: Kin, handler: (Kin) -> Unit) { - var remainingAmount = amount - - // Starting from largest denomination to the smallest - // we'll find how many 'bills' from each stack we need - for (i in 1..slots.size) { - val slot = slots[slots.size - i] // Backwards - - var howManyFit = remainingAmount.toKinTruncatingLong() / slot.billValue - while (howManyFit > 0) { - val billsToMove = min(howManyFit, 9) - val moveAmount = Kin.fromKin(kin = slot.billValue * billsToMove) - - handler(moveAmount) - - remainingAmount -= moveAmount - howManyFit -= billsToMove - } - } - } - - - /// This function sends money from the organizer to the outgoing - /// temporary account. It has to solve the interesting problem of - /// figuring out which denominations to use when making a payment. - /// - /// Unfortunately, this is actually a pretty hard - /// problem to solve optimally. - /// https://en.wikipedia.org/wiki/Change-making_problem - /// - /// We're going to use the following approach, which should be pretty - /// good most of the time but definitely has room for improvement. - /// Specifically, we may want to move from a dynamic programming - /// solution to a greedy solution in the future. - /// - /// Algorithm - /// - /// 1. Check the total balance to make sure we have enough to send. - /// - /// 2. Try using a naive approach where we send from the amounts - /// currently in the slots. This will fail if we don't have enough of - /// a particular bill to pay the amount. - /// - /// 3. If step 2 fails, start at the smallest denomination and move - /// upwards while adding everything along the way until we reach a - /// denomination that is larger than the remaining amount. Then split - /// and go backwards... (dynamic programming strategy) - /// - fun transfer(amount: Kin): List { - if (amount <= 0) { - throw OrganizerException.InvalidAmountException() - } - - if (amount > availableBalance) { - throw OrganizerException.InsufficientTrayBalanceException() - } - - val startState = this.copy() - - return try { - withdrawNaively(amount = amount) - } catch (e: OrganizerException) { - this.slots = startState.slots.map { it.copy() } - this.owner = startState.owner.copy() - this.incoming = startState.incoming.copy() - this.outgoing = startState.outgoing.copy() - withdrawDynamically(amount = amount) - } - } - - fun withdrawNaively(amount: Kin): List { - if (amount <= 0) { - throw OrganizerException.InvalidAmountException() - } - - val container = mutableListOf() - - var remainingAmount = amount - - // Starting from largest denomination to the smallest - // we'll find how many 'bills' from each stack we need - for (i in 1..slots.size) { - val slot = slots[slots.size - i] // Backwards - - if (slot.partialBalance <= 0) { - continue - } - - val howManyFit = remainingAmount.toKinTruncatingLong() / slot.billValue - - val maxAmount = Kin.fromKin(howManyFit * slot.billValue) - val howMuchToSend: Kin = if (slot.partialBalance < maxAmount) slot.partialBalance else maxAmount - - if (howMuchToSend > 0) { - if (slot.partialBalance < howMuchToSend) { - throw OrganizerException.InvalidSlotBalanceException() - } - - val sourceBucket = AccountType.Bucket(slot.type) - - normalize(slotType = slot.type, amount = howMuchToSend) { amountN -> - container.add( - InternalExchange( - from = sourceBucket, - to = AccountType.Outgoing, - kin = amountN - ) - ) - } - - decrement(sourceBucket, kin = howMuchToSend) - increment(AccountType.Outgoing, kin = howMuchToSend) - - remainingAmount -= howMuchToSend - } - } - - if (remainingAmount >= 1) { - throw OrganizerException.InvalidSlotBalanceException() - } - - return container - } - - fun withdrawDynamically(amount: Kin): List { - if (amount <= 0) { - throw OrganizerException.InvalidAmountException() - } - - if (amount > availableBalance) { - throw OrganizerException.InsufficientTrayBalanceException() - } - - val step = withdrawDynamicallyStep1(amount = amount) - val exchanges = withdrawDynamicallyStep2(step = step) - - return step.exchanges + exchanges - } - - /// This function assumes that the 'naive strategy' withdrawal was already - /// attempted. We'll iterate over the slots, from smallest to largest, drain - /// every slot up to the `amount`. Once a slot that is larger than the - /// remaining amount is reached, the function returns the index at which the - /// second step should resume. - /// - /// Returns the index that should be broken down in step 2. - /// - fun withdrawDynamicallyStep1(amount: Kin): InternalDynamicStep { - val container = mutableListOf() - var remaining = amount - - for (i in slots.indices) { - val currentSlot = slots[i] // Forwards - - if (currentSlot.partialBalance <= 0) { - // Try next slot - continue - } - - if (remaining.toKinValueDouble() < 1) { - // Sent it all - break - } - - if (remaining.toKinTruncatingLong() < currentSlot.billValue) { - // If there's a remaining amount and the current - // bill value is greater, we'll need to break the - // current slot bill down to lower slots - break - } - - val howManyFit = remaining.toKinTruncatingLong() / currentSlot.billValue - val maxAmount = howManyFit * currentSlot.billValue - val howMuchToSend = - min(currentSlot.partialBalance.toKinValueDouble(), maxAmount.toDouble()) - .let { Kin.fromKin(it) } - - if (howMuchToSend > 0) { - normalize(slotType = currentSlot.type, amount = howMuchToSend) { kinToSend -> - container.add( - InternalExchange( - from = AccountType.Bucket(currentSlot.type), - to = AccountType.Outgoing, - kin = kinToSend - ) - ) - } - - // Adjust the slot balance - decrement(AccountType.Bucket(currentSlot.type), kin = howMuchToSend) - increment(AccountType.Outgoing, kin = howMuchToSend) - - remaining -= howMuchToSend - } - } - - var index = slots.indexOfFirst { it.billValue > remaining.toKinTruncatingLong() && it.billCount() > 0 } - - // Only throw an error if there's a - // non-zero remaining amount, other - // wise the first step covered the - // total amount - if (index == -1 && remaining >= 1) { - throw OrganizerException.InvalidStepIndexException() - } - - if (index == -1) index = 0 - return InternalDynamicStep( - remaining = remaining, - index = index, - exchanges = container - ) - } - - fun withdrawDynamicallyStep2(step: InternalDynamicStep): List { - if (!(step.index > 0 && step.index < slots.size)) { - return listOf() - } - - if (step.remaining < 1) { - return listOf() - } - - val container = mutableListOf() - var remaining = step.remaining - - val current = slots[step.index] - val lower = slots[step.index - 1] - - if (current.billCount() < 1) { - throw OrganizerException.SlotAtIndexEmptyException() - } - - // Break the current slot into the lower - // slot and exchange all the way down - val initialSplitAmount = Kin.fromKin(kin = current.billValue) - container.add( - InternalExchange( - from = AccountType.Bucket(current.type), - to = AccountType.Bucket(lower.type), - kin = initialSplitAmount - ) - ) - - // Adjust the slot balance - decrement(type = AccountType.Bucket(current.type), kin = initialSplitAmount) - increment(type = AccountType.Bucket(lower.type), kin = initialSplitAmount) - - - for (i in (step.index-1 downTo 0)) { - val currentSlot = slots[i] - - // Split every slot down to the smallest - // to ensure we have enough bills in each - if (i > 0) { - val lowerSlot = slots[i - 1] - val splitAmount = Kin.fromKin(currentSlot.billValue) - - container.add( - InternalExchange( - from = AccountType.Bucket(currentSlot.type), - to = AccountType.Bucket(lowerSlot.type), - kin = splitAmount - ) - ) - - // Adjust the slot balance - decrement(AccountType.Bucket(currentSlot.type), splitAmount) - increment(AccountType.Bucket(lowerSlot.type), splitAmount) - } - - val howManyFit = remaining.toKinTruncatingLong() / currentSlot.billValue - val kinToSend = Kin.fromKin(howManyFit * currentSlot.billValue) - - if (howManyFit <= 0) { - continue - } - - if (howManyFit > currentSlot.billCount().toInt()) { - throw OrganizerException.InvalidSlotBalanceException() - } - - container.add( - InternalExchange( - from = AccountType.Bucket(currentSlot.type), - to = AccountType.Outgoing, - kin = kinToSend - ) - ) - - // Adjust the slot balance - decrement(AccountType.Bucket(currentSlot.type), kin = kinToSend) - increment(AccountType.Outgoing, kin = kinToSend) - - remaining -= kinToSend - } - - return container - } - - fun reportableRepresentation(): List { - return listOf( - string(named = "Primary ", partialAccount = owner), - string(named = "Incoming ", partialAccount = incoming), - string(named = "Outgoing ", partialAccount = outgoing), - string("1 ", slot = slot(SlotType.Bucket1)), - string("10 ", slot = slot(SlotType.Bucket10)), - string("100 ", slot = slot(SlotType.Bucket100)), - string("1k ", slot = slot(SlotType.Bucket1k)), - string("10k ", slot = slot(SlotType.Bucket10k)), - string("100k ", slot = slot(SlotType.Bucket100k)), - string("1m ", slot = slot(SlotType.Bucket1m)), - ) - } - - private fun string(named: String, partialAccount: PartialAccount): String { - return "$named ${partialAccount.getCluster().vaultPublicKey.base58().padded(44)}) ${partialAccount.partialBalance.description}" - } - - private fun string(named: String, slot: Slot): String { - return "$named ${slot.getCluster().vaultPublicKey.base58().padded(44)}) ${slot.partialBalance.description}" - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Tray - - if (slots != other.slots) return false - if (incoming != other.incoming) return false - if (outgoing != other.outgoing) return false - if (mnemonic != other.mnemonic) return false - - return true - } - - override fun hashCode(): Int { - var result = slots.hashCode() - result = 31 * result + incoming.hashCode() - result = 31 * result + outgoing.hashCode() - result = 31 * result + mnemonic.hashCode() - return result - } - - sealed class OrganizerException : Exception() { - class InvalidAmountException : OrganizerException() - class InsufficientTrayBalanceException : OrganizerException() - class InvalidSlotBalanceException : OrganizerException() - class InvalidStepIndexException : OrganizerException() - class SlotAtIndexEmptyException : OrganizerException() - } -} - -data class InternalExchange( - var from: AccountType, - var to: AccountType? = null, - var kin: Kin -) - -data class InternalDynamicStep( - var remaining: Kin, - var index: Int, - var exchanges: List -) - -data class InternalDeposit( - var to: SlotType, - var kin: Kin -) \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/xyz/flipchat/services/analytics/FlipchatAnalyticsService.kt b/services/flipchat/core/src/main/kotlin/xyz/flipchat/services/analytics/FlipchatAnalyticsService.kt deleted file mode 100644 index bfc06306d..000000000 --- a/services/flipchat/core/src/main/kotlin/xyz/flipchat/services/analytics/FlipchatAnalyticsService.kt +++ /dev/null @@ -1,28 +0,0 @@ -package xyz.flipchat.services.analytics - -import com.getcode.libs.analytics.AnalyticsService -import com.getcode.libs.analytics.AppAction -import com.getcode.libs.analytics.AppActionSource -import com.mixpanel.android.mpmetrics.MixpanelAPI -import javax.inject.Inject - - -interface FlipchatAnalyticsService : AnalyticsService { -} - -class FlipchatAnalyticsManager @Inject constructor( - private val mixpanelAPI: MixpanelAPI -) : FlipchatAnalyticsService { - override fun onAppStart() { - } - - override fun onAppStarted() { - } - - override fun unintentionalLogout() { - } - - override fun action(action: AppAction, source: AppActionSource?) { - } - -} \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/xyz/flipchat/services/network/ManagedApiRequest.kt b/services/flipchat/core/src/main/kotlin/xyz/flipchat/services/network/ManagedApiRequest.kt deleted file mode 100644 index 1d5ca4ae1..000000000 --- a/services/flipchat/core/src/main/kotlin/xyz/flipchat/services/network/ManagedApiRequest.kt +++ /dev/null @@ -1,21 +0,0 @@ -package xyz.flipchat.services.network - -import com.getcode.services.network.core.DEFAULT_STREAM_TIMEOUT -import com.getcode.services.network.core.NetworkOracle -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map - -suspend fun NetworkOracle.managedApiRequest( - call: () -> Flow, - timeout: Long = DEFAULT_STREAM_TIMEOUT, - handleResponse: (ResponseType) -> Result, - onOtherError: (Exception) -> Result -): Result { - return try { - managedRequest(call(), timeout) - .map { response -> handleResponse(response) }.first() - } catch (e: Exception) { - onOtherError(e) - } -} \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/xyz/flipchat/services/user/UserFlags.kt b/services/flipchat/core/src/main/kotlin/xyz/flipchat/services/user/UserFlags.kt deleted file mode 100644 index a4bd03006..000000000 --- a/services/flipchat/core/src/main/kotlin/xyz/flipchat/services/user/UserFlags.kt +++ /dev/null @@ -1,24 +0,0 @@ -package xyz.flipchat.services.user - -import com.getcode.model.Kin -import com.getcode.solana.keys.PublicKey - -data class UserFlags( - val isStaff: Boolean, - val createCost: Kin, - val feeDestination: PublicKey, - val isRegistered: Boolean, - val typingNotifications: TypingNotificationsConstraints, -) - -data class TypingNotificationsConstraints( - // Can this user call NotifyIsTyping at all? - val canSendAtAll: Boolean, - // Can this user call NotifyIsTyping in chats where they are a listener? - val canSendAsListener: Boolean, - // Interval between calling NotifyIsTyping - val interval: Long, - // Client-side timeout for when they haven't seen an IsTyping event from a user. - // After this timeout has elapsed, client should assume the user has stopped typing. - val timeout: Long, -) \ No newline at end of file diff --git a/services/flipchat/core/src/main/kotlin/xyz/flipchat/services/user/UserManager.kt b/services/flipchat/core/src/main/kotlin/xyz/flipchat/services/user/UserManager.kt deleted file mode 100644 index d316fafe1..000000000 --- a/services/flipchat/core/src/main/kotlin/xyz/flipchat/services/user/UserManager.kt +++ /dev/null @@ -1,198 +0,0 @@ -package xyz.flipchat.services.user - -import com.bugsnag.android.Bugsnag -import com.getcode.crypt.DerivedKey -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.generator.OrganizerGenerator -import com.getcode.model.ID -import com.getcode.model.social.user.SocialProfile -import com.getcode.model.uuid -import com.getcode.services.manager.MnemonicManager -import com.getcode.solana.organizer.Organizer -import com.getcode.utils.FormatUtils -import com.mixpanel.android.mpmetrics.MixpanelAPI -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import xyz.flipchat.services.core.BuildConfig -import javax.inject.Inject -import javax.inject.Singleton - -sealed interface AuthState { - data object Unknown : AuthState - data object Unregistered : AuthState - data object LoggedInAwaitingUser : AuthState - data object LoggedIn : AuthState - data object LoggedOut : AuthState - - fun canOpenChatStream() = this is Unregistered || this is LoggedIn -} - -@Singleton -class UserManager @Inject constructor( - private val mnemonicManager: MnemonicManager, - private val organizerGenerator: OrganizerGenerator, - private val mixpanelAPI: MixpanelAPI, -) { - private val _state: MutableStateFlow = MutableStateFlow(State()) - val state: StateFlow - get() = _state.asStateFlow() - - val entropy: String? - get() = _state.value.entropy - - val keyPair: KeyPair? - get() = _state.value.keyPair - - val userId: ID? - get() = _state.value.userId - - val organizer: Organizer? - get() = _state.value.organizer - - val displayName: String? - get() = _state.value.displayName - - val userFlags: UserFlags? - get() = _state.value.flags - - val openRoom: ID? - get() = _state.value.openRoom - - val authState: AuthState - get() = _state.value.authState - - val socialProfiles: List - get() = _state.value.linkedSocialProfiles - - data class State( - val authState: AuthState = AuthState.Unknown, - val entropy: String? = null, - val keyPair: KeyPair? = null, - val userId: ID? = null, - val displayName: String? = null, - val organizer: Organizer? = null, - val flags: UserFlags? = null, - val isTimelockUnlocked: Boolean = false, - val openRoom: ID? = null, - val linkedSocialProfiles: List = emptyList() - ) - - fun establish(entropy: String) { - val mnemonic = mnemonicManager.fromEntropyBase64(entropy) - val authority = DerivedKey.derive(com.getcode.crypt.DerivePath.primary, mnemonic) - val organizer = organizerGenerator.generate(mnemonic) - _state.update { - it.copy( - entropy = entropy, - keyPair = authority.keyPair, - organizer = organizer - ) - } - } - - fun set(userId: ID) { - _state.update { - it.copy(userId = userId) - } - associate() - } - - fun set(displayName: String) { - _state.update { - it.copy( - displayName = displayName - ) - } - associate() - } - - fun setSocialProfiles(socialProfiles: List) { - _state.update { - it.copy(linkedSocialProfiles = socialProfiles) - } - } - - fun set(organizer: Organizer) { - _state.update { - it.copy(organizer = organizer) - } - } - - fun set(userFlags: UserFlags?) { - _state.update { - it.copy( - flags = userFlags, - authState = if (userFlags?.isRegistered == true) AuthState.LoggedIn else AuthState.Unregistered - ) - } - associate() - } - - fun set(authState: AuthState) { - _state.update { it.copy(authState = authState) } - } - - fun roomOpened(roomId: ID) { - _state.update { - it.copy(openRoom = roomId) - } - } - - fun roomClosed() { - _state.update { - it.copy(openRoom = null) - } - } - - fun didDetectUnlockedAccount() { - _state.update { - if (!it.isTimelockUnlocked) { - it.copy(isTimelockUnlocked = true) - } else { - it - } - } - } - - fun isSelf(id: ID?) = userId == id - - private fun associate() { - if (!BuildConfig.DEBUG) { - val distinctId = userId?.uuid?.toString() - if (Bugsnag.isStarted()) { - Bugsnag.setUser(distinctId, null, displayName) - userFlags?.let { flags -> - Bugsnag.addMetadata( - /* section = */ "userflags", - /* value = */ mapOf( - "isStaff" to flags.isStaff, - "isRegistered" to flags.isRegistered, - "createCost" to FormatUtils.formatWholeRoundDown(flags.createCost.toKinValueDouble()) - ) - ) - } - } - - mixpanelAPI.identify(distinctId) - } - } - - fun clear() { - _state.update { - it.copy( - authState = AuthState.LoggedOut, - entropy = null, - keyPair = null, - userId = emptyList(), - organizer = null, - flags = null, - openRoom = null, - linkedSocialProfiles = emptyList(), - isTimelockUnlocked = false, - ) - } - } - -} \ No newline at end of file diff --git a/services/flipchat/payments/.gitignore b/services/flipchat/payments/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/services/flipchat/payments/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/services/flipchat/payments/build.gradle.kts b/services/flipchat/payments/build.gradle.kts deleted file mode 100644 index cd27aa0b4..000000000 --- a/services/flipchat/payments/build.gradle.kts +++ /dev/null @@ -1,103 +0,0 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - id(Plugins.android_library) - id(Plugins.kotlin_android) - id(Plugins.kotlin_kapt) - id(Plugins.kotlin_serialization) -} - -android { - namespace = "${Gradle.flipchatNamespace}.services.payments" - compileSdk = Android.compileSdkVersion - defaultConfig { - minSdk = Android.minSdkVersion - testInstrumentationRunner = Android.testInstrumentationRunner - - consumerProguardFiles("consumer-rules.pro") - - buildConfigField("String", "VERSION_NAME", "\"${Packaging.Flipchat.versionName}\"") - - javaCompileOptions { - annotationProcessorOptions { - arguments += mapOf("room.schemaLocation" to "$projectDir/schemas") - } - } - } - - buildFeatures { - buildConfig = true - } -} - -kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(Versions.java)) - } - - compilerOptions { - jvmTarget.set(JvmTarget.fromTarget(Versions.java)) - optIn.addAll( - "kotlin.time.ExperimentalTime", - "kotlin.ExperimentalUnsignedTypes", - "kotlin.RequiresOptIn" - ) - } -} - -dependencies { - implementation(project(":definitions:flipchat-vm:models")) - implementation(project(":services:flipchat:core")) - api(project(":services:legacy-shared")) - api(project(":libs:crypto:solana")) - implementation(project(":ui:resources")) - - implementation(project(":libs:messaging")) - implementation(project(":libs:requests")) - - implementation(platform(Libs.compose_bom)) - implementation(Libs.compose_ui) - - implementation(Libs.kotlinx_coroutines_core) - implementation(Libs.kotlinx_serialization_json) - implementation(Libs.inject) - - implementation(Libs.grpc_android) - implementation(Libs.grpc_okhttp) - implementation(Libs.grpc_kotlin) - implementation(Libs.androidx_lifecycle_runtime) - implementation(Libs.androidx_room_runtime) - implementation(Libs.androidx_room_ktx) - implementation(Libs.androidx_room_paging) - implementation(Libs.androidx_room_rxjava3) - implementation(Libs.okhttp) - implementation(Libs.mixpanel) - - implementation(platform(Libs.firebase_bom)) - implementation(Libs.firebase_crashlytics) - implementation(Libs.firebase_installations) - implementation(Libs.firebase_perf) - implementation(Libs.firebase_messaging) - - implementation(Libs.play_integrity) - - implementation(Libs.androidx_paging_runtime) - - kapt(Libs.androidx_room_compiler) - implementation(Libs.sqlcipher) - - implementation(Libs.fingerprint_pro) - - implementation(Libs.lib_phone_number_google) - - androidTestImplementation(Libs.androidx_junit) - androidTestImplementation(Libs.junit) - androidTestImplementation(Libs.androidx_test_runner) - - implementation(Libs.hilt) - kapt(Libs.hilt_android_compiler) - kapt(Libs.hilt_compiler) - - implementation(Libs.timber) - implementation(Libs.bugsnag) -} diff --git a/services/flipchat/payments/consumer-rules.pro b/services/flipchat/payments/consumer-rules.pro deleted file mode 100644 index 5f642a47c..000000000 --- a/services/flipchat/payments/consumer-rules.pro +++ /dev/null @@ -1,6 +0,0 @@ -# Needed to keep generic signatures --keepattributes Signature - --keepclasseswithmembernames class * { - native ; -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/AndroidManifest.xml b/services/flipchat/payments/src/main/AndroidManifest.xml deleted file mode 100644 index 8b1db4200..000000000 --- a/services/flipchat/payments/src/main/AndroidManifest.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/db/ExchangeDao.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/db/ExchangeDao.kt deleted file mode 100644 index 407b2700c..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/db/ExchangeDao.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.getcode.db - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.getcode.model.Rate -import com.getcode.model.ExchangeRate -import kotlinx.coroutines.flow.Flow - -@Dao -interface ExchangeDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(rates: List, syncedAt: Long) { - insert(*rates.map { ExchangeRate(it.fx, it.currency, syncedAt) }.toTypedArray()) - } - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(vararg rate: ExchangeRate) - - @Query("SELECT * FROM exchangeData") - fun observeRates(): Flow> - - @Query("SELECT * FROM exchangeData") - suspend fun query(): List - - @Query("DELETE FROM exchangeData") - fun clear() -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/generator/GiftCardGenerator.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/generator/GiftCardGenerator.kt deleted file mode 100644 index 45ce169ee..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/generator/GiftCardGenerator.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.getcode.generator - -import com.getcode.crypt.MnemonicPhrase -import com.getcode.solana.organizer.GiftCardAccount -import javax.inject.Inject - -class GiftCardGenerator @Inject constructor( -): Generator { - override fun generate(predicate: MnemonicPhrase?): GiftCardAccount { - return GiftCardAccount.newInstance(predicate) - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/inject/CodeProxyApiModule.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/inject/CodeProxyApiModule.kt deleted file mode 100644 index bfbc0bedc..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/inject/CodeProxyApiModule.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.getcode.inject - -import com.getcode.network.exchange.CodeExchange -import com.getcode.network.exchange.Exchange -import com.getcode.network.repository.BalanceRepository -import com.getcode.network.service.CurrencyService -import com.getcode.services.db.CurrencyProvider -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object CodeProxyApiModule { - - @Singleton - @Provides - fun providesExchange( - currencyService: CurrencyService, - currencyProvider: CurrencyProvider, - ): Exchange = CodeExchange( - currencyService = currencyService, - preferredCurrency = { currencyProvider.preferredCurrency() }, - defaultCurrency = { currencyProvider.defaultCurrency() } - ) - - @Singleton - @Provides - fun provideBalanceRepository( - ): BalanceRepository { - return BalanceRepository() - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/manager/GiftCardManager.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/manager/GiftCardManager.kt deleted file mode 100644 index 2ee1c5e41..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/manager/GiftCardManager.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.getcode.manager - -import com.getcode.crypt.MnemonicPhrase -import com.getcode.generator.GiftCardGenerator -import com.getcode.solana.organizer.GiftCardAccount -import javax.inject.Inject - -class GiftCardManager @Inject constructor( - private val generator: GiftCardGenerator -) { - fun createGiftCard(mnemonic: MnemonicPhrase? = null): GiftCardAccount { - return generator.generate(mnemonic) - } - - fun getEntropy(giftCard: GiftCardAccount): String { - return giftCard.mnemonicPhrase.getBase58EncodedEntropy() - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/manager/ModalManager.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/manager/ModalManager.kt deleted file mode 100644 index b6e1f9889..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/manager/ModalManager.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.getcode.manager - -import androidx.annotation.DrawableRes -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import java.util.UUID - -object ModalManager { - data class Message( - @DrawableRes - val icon: Int? = null, - val title: String, - val subtitle: String = "", - val positiveText: String, - val negativeText: String? = null, - val tertiaryText: String? = null, - val onPositive: () -> Unit, - val onNegative: () -> Unit = {}, - val onTertiary: () -> Unit = {}, - val onClose: (actionType: ActionType?) -> Unit = {}, - val type: MessageType = MessageType.DEFAULT, -// val isDismissibleByTouchOutside: Boolean = true, - val isDismissibleByBackButton: Boolean = true, - val timeoutSeconds: Int? = null, - val id: Long = UUID.randomUUID().mostSignificantBits, - ) - - private val _messages: MutableStateFlow> = MutableStateFlow( - listOf() - ) - val messages: StateFlow> get() = _messages.asStateFlow() - - fun showMessage(message: Message) { - _messages.update { currentMessages -> - currentMessages + message - } - } - - fun setMessageShown(messageId: Long) { - _messages.update { currentMessages -> - currentMessages.filterNot { it.id == messageId } - } - } - - fun clear() = _messages.update { listOf() } - - fun clearByType(type: MessageType) = _messages.update { it.filterNot { m -> m.type == type } } - - enum class MessageType { DEFAULT } - - enum class ActionType { - Positive, - Negative, - Tertiary - } - -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/AirdropType.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/AirdropType.kt deleted file mode 100644 index 19185c5d1..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/AirdropType.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.getcode.model - - -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService - -enum class AirdropType { - Unknown, - GiveFirstKin, - GetFirstKin; - - companion object { - fun getInstance(airdropType: TransactionService.AirdropType): AirdropType? { - return when (airdropType) { - TransactionService.AirdropType.UNKNOWN -> Unknown - TransactionService.AirdropType.GIVE_FIRST_KIN -> GiveFirstKin - TransactionService.AirdropType.GET_FIRST_KIN -> GetFirstKin - TransactionService.AirdropType.UNRECOGNIZED -> null - } - } - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/ClientSignature.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/ClientSignature.kt deleted file mode 100644 index bcb751ce8..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/ClientSignature.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.getcode.model - -import com.getcode.solana.keys.Signature - -data class ClientSignature( - val transaction: Signature, - val signature: Signature -) diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/CurrencyRate.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/CurrencyRate.kt deleted file mode 100644 index a31b89562..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/CurrencyRate.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.getcode.model - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity -data class CurrencyRate( - @PrimaryKey val id: String, - val rate: Double -) \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/ExchangeRate.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/ExchangeRate.kt deleted file mode 100644 index 4795a4508..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/ExchangeRate.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.getcode.model - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import kotlinx.serialization.Serializable - - -@Serializable -@Entity(tableName = "exchangeData") -data class ExchangeRate( - @ColumnInfo(name = "fiat") - val fx: Double, - @PrimaryKey - val currency: CurrencyCode, - @ColumnInfo(name = "synced_at") - val synced: Long, -) - -fun KinAmount.Companion.fromProtoExchangeData(exchangeData: TransactionService.ExchangeData): KinAmount { - return fromFiatAmount( - kin = Kin(exchangeData.quarks), - fiat = exchangeData.nativeAmount, - fx = exchangeData.exchangeRate, - currencyCode = CurrencyCode.tryValueOf(exchangeData.currency)!! - ) -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/FaqItem.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/FaqItem.kt deleted file mode 100644 index 1ed2ea70b..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/FaqItem.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.getcode.model - -import androidx.room.Entity -import androidx.room.PrimaryKey - - -@Entity -data class FaqItem( - @PrimaryKey(autoGenerate = true) val uid: Int? = null, - val question: String, - val answer: String - ) \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/GiftCard.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/GiftCard.kt deleted file mode 100644 index 54f9bb1e8..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/GiftCard.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.getcode.model - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity -data class GiftCard( - @PrimaryKey val key: String, //base58 - val entropy: String, //base58 - val amount: Long, - val date: Long -) \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/Intent.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/Intent.kt deleted file mode 100644 index cd453ceab..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/Intent.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.getcode.model - -sealed class Intent { - data class Transfer( - val id: List, - val amount: KinAmount, - val source: List, - val destination: SendDestination - ) : Intent() - - data class CreateTokenAccount( - val id: List, - val owner: List - ) : Intent() -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/IntentMetadata.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/IntentMetadata.kt deleted file mode 100644 index 8819907ab..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/IntentMetadata.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.getcode.model - -import com.getcode.solana.keys.PublicKey -import com.getcode.utils.toByteString -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService - -sealed class IntentMetadata { - data object OpenAccounts : IntentMetadata() - data class SendPrivatePayment(val metadata: PaymentMetadata) : IntentMetadata() - data class SendPublicPayment(val metadata: PaymentMetadata) : IntentMetadata() - data object ReceivePaymentsPrivately : IntentMetadata() - data class ReceivePaymentsPublicly(val metadata: PaymentMetadata) : IntentMetadata() - data object UpgradePrivacy : IntentMetadata() - - companion object { - fun newInstance(metadata: TransactionService.Metadata): IntentMetadata? { - return when (metadata.typeCase) { - TransactionService.Metadata.TypeCase.OPEN_ACCOUNTS -> OpenAccounts - TransactionService.Metadata.TypeCase.RECEIVE_PAYMENTS_PRIVATELY -> ReceivePaymentsPrivately - TransactionService.Metadata.TypeCase.RECEIVE_PAYMENTS_PUBLICLY -> { - getPaymentMetadata( - metadata.receivePaymentsPublicly.exchangeData.currency, - metadata.receivePaymentsPublicly.exchangeData.quarks, - metadata.receivePaymentsPublicly.exchangeData.exchangeRate, - false, - null - )?.let { ReceivePaymentsPublicly(it) } - } - TransactionService.Metadata.TypeCase.UPGRADE_PRIVACY -> UpgradePrivacy - TransactionService.Metadata.TypeCase.SEND_PRIVATE_PAYMENT -> { - getPaymentMetadata( - metadata.sendPrivatePayment.exchangeData.currency, - metadata.sendPrivatePayment.exchangeData.quarks, - metadata.sendPrivatePayment.exchangeData.exchangeRate, - metadata.sendPrivatePayment.isTip, - metadata.sendPrivatePayment.destination.toByteArray() - )?.let { SendPrivatePayment(it) } - } - TransactionService.Metadata.TypeCase.SEND_PUBLIC_PAYMENT -> { - getPaymentMetadata( - metadata.sendPublicPayment.exchangeData.currency, - metadata.sendPublicPayment.exchangeData.quarks, - metadata.sendPublicPayment.exchangeData.exchangeRate, - false, - metadata.sendPrivatePayment.destination.toByteArray() - )?.let { SendPublicPayment(it) } - } - else -> null - } - } - - private fun getPaymentMetadata( - currencyString: String, - quarks: Long, - exchangeRate: Double, - isTip: Boolean, - destination: ByteArray?, - ): PaymentMetadata? { - val currency = CurrencyCode.tryValueOf(currencyString.uppercase()) - ?: return null - - return PaymentMetadata( - amount = KinAmount.newInstance( - kin = Kin(quarks), - rate = Rate( - fx = exchangeRate, - currency = currency - ) - ), - isTip = isTip, - destination = destination?.let { PublicKey.fromByteString(it.toByteString()) }, - ) - } - } -} - -data class PaymentMetadata( - val amount: KinAmount, - val isTip: Boolean, - val destination: PublicKey? -) \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/KinCode.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/KinCode.kt deleted file mode 100644 index 7d6e329b4..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/KinCode.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.getcode.model - -import org.kin.sdk.base.models.QuarkAmount -import org.kin.sdk.base.models.solana.read -import org.kin.sdk.base.tools.byteArrayToLong -import org.kin.sdk.base.tools.longToByteArray -import org.kin.sdk.base.tools.toByteArray -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.util.UUID - - -data class KinCode(val type: Type = Type.None, val amount: QuarkAmount, val nonce: Nonce) { - - sealed class Type(val value: Int) { - object Unknown : Type(-1) - object None : Type(0) - - companion object { - @JvmStatic - fun from(value: Int): Type { - return when (value) { - 0 -> None - else -> Unknown - } - } - - } - } - - /** - * Only 11 Bytes LSB observed - */ - data class Nonce(val value: ByteArray) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Nonce) return false - - if (!value.contentEquals(other.value)) return false - - return true - } - - override fun hashCode(): Int { - return value.contentHashCode() - } - - companion object { - fun random(): Nonce { - return Nonce(UUID.randomUUID().toByteArray()) - } - } - } - - /** - * 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ - * |T | Amount | Nonce | - * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ - */ - fun encode(): ByteArray { - return with(ByteArrayOutputStream()) { - write(byteArrayOf(type.value.toByte())) - write(amount.value.longToByteArray()) - write(nonce.value, 0, 11) - toByteArray() - } - } - - companion object { - @JvmStatic - fun decode(bytes: ByteArray): KinCode { - return with(ByteArrayInputStream(bytes)) { - val type = Type.from(read()) - val amount = QuarkAmount(read(8).byteArrayToLong()) - val nonce = Nonce(read(11)) - KinCode(type, amount, nonce) - } - } - } -} diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/Limits.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/Limits.kt deleted file mode 100644 index 352207298..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/Limits.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.getcode.model - -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import com.codeinc.gen.transaction.v2.CodeTransactionService.BuyModuleLimit -import com.codeinc.gen.transaction.v2.CodeTransactionService.DepositLimit -import kotlin.time.Duration.Companion.hours - -data class Limits( - // Date from which the limits are computed - val sinceDate : Long, - - // Date at which the limits were fetched - val fetchDate: Long, - - // Maximum quarks that may be deposited at any time. Server will guarantee - // this threshold will be below enforced dollar value limits, while also - // ensuring sufficient funds are available for a full organizer that supports - // max payment sends. Total dollar value limits may be spread across many deposits. - val maxDeposit: Kin, - - // Remaining send limits keyed by currency - private val sendLimits: Map, - // Buy limits keyed by currency - private val buyLimits: Map, - - ) { - val isStale: Boolean - get() { - val now = System.currentTimeMillis() - return now - fetchDate > 1.hours.inWholeMilliseconds - } - - fun sendLimitFor(currencyCode: CurrencyCode) : SendLimit? { - return sendLimits[currencyCode] - } - - fun buyLimitFor(currencyCode: CurrencyCode): BuyLimit? { - return buyLimits[currencyCode] - } - - companion object { - fun newInstance( - sinceDate: Long, - fetchDate: Long, - sendLimits: Map, - buyLimits: Map, - deposits: DepositLimit, - ): Limits { - val sends = sendLimits - .mapNotNull { (k, v) -> - val code = CurrencyCode.tryValueOf(k) ?: return@mapNotNull null - val limit = SendLimit( - nextTransaction = v.nextTransaction.toDouble(), - maxPerDay = v.maxPerDay.toDouble(), - maxPerTransaction = v.maxPerTransaction.toDouble(), - ) - - code to limit - }.toMap() - - val buys = buyLimits - .mapValues { (_, v) -> - BuyLimit( - min = v.minPerTransaction.toDouble(), - max = v.maxPerTransaction.toDouble() - ) - } - .mapNotNull { (k, limit) -> - val code = CurrencyCode.tryValueOf(k) ?: return@mapNotNull null - code to limit - }.toMap() - - return Limits( - sinceDate = sinceDate, - fetchDate = fetchDate, - sendLimits = sends, - buyLimits = buys, - maxDeposit = Kin.fromQuarks(deposits.maxQuarks) - ) - } - } -} - -data class SendLimit( - val nextTransaction: Double, - val maxPerTransaction: Double, - val maxPerDay: Double -) { - companion object { - val Zero = SendLimit(0.0, 0.0, 0.0) - } -} - -data class BuyLimit(val min: Double, val max: Double) { - companion object { - val Zero = BuyLimit(0.0, 0.0) - } -} diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/PaymentRequest.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/PaymentRequest.kt deleted file mode 100644 index 54a92a4d5..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/PaymentRequest.kt +++ /dev/null @@ -1,199 +0,0 @@ -package com.getcode.model - -import com.codeinc.gen.messaging.v1.CodeMessagingService as MessagingService -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.Signature - -data class StreamMessage(val id: List, val kind: Kind) { - sealed interface Kind { - class ReceiveRequestKind(val receiveRequest: ReceiveRequest): Kind - class PaymentRequestKind(val paymentRequest: PaymentRequest) : Kind - class AirdropKind(val airdrop: Airdrop) : Kind - data class LoginRequestKind(val loginRequest: LoginRequest): Kind - } - - val receiveRequest: ReceiveRequest? = (kind as? Kind.ReceiveRequestKind)?.receiveRequest - val paymentRequest: PaymentRequest? = (kind as? Kind.PaymentRequestKind)?.paymentRequest - val loginRequest: LoginRequest? = (kind as? Kind.LoginRequestKind)?.loginRequest - val airdrop: Airdrop? = (kind as? Kind.AirdropKind)?.airdrop - - companion object { - fun getInstance(message: MessagingService.Message): StreamMessage? { - val kind: Kind = when (message.kindCase) { - MessagingService.Message.KindCase.REQUEST_TO_GRAB_BILL -> { - val account = - PublicKey( - message.requestToGrabBill.requestorAccount.value.toByteArray().toList() - ) - val signature = - Signature( - message.sendMessageRequestSignature.value.toByteArray().toList() - ) - - Kind.PaymentRequestKind( - PaymentRequest( - account = account, - signature = signature - ) - ) - } - MessagingService.Message.KindCase.REQUEST_TO_RECEIVE_BILL -> { - val request = message.requestToReceiveBill - val exchangeData = request.exchangeDataCase - val account = PublicKey( - request.requestorAccount.value.toByteArray().toList() - ) - val signature = Signature( - message.sendMessageRequestSignature.value.toByteArray().toList() - ) - - val domain: Domain? - val verifier: PublicKey? - if (request.hasDomain()) { - val validDomain = Domain.from(request.domain.value) ?: return null - val validVerifier = PublicKey( - request.verifier.value.toByteArray().toList() - ) - - domain = validDomain - verifier = validVerifier - } else { - domain = null - verifier = null - } - - val requestData = when (exchangeData) { - MessagingService.RequestToReceiveBill.ExchangeDataCase.EXACT -> { - val data = request.exact - val currency = CurrencyCode.tryValueOf(data.currency) ?: return null - - val additionalFees = request.additionalFeesList.mapNotNull { - val destination = PublicKey( - it.destination.value.toByteArray().toList() - ) - Fee(destination = destination, it.feeBps) - } - - ReceiveRequest( - account = account, - signature = signature, - amount = ReceiveRequest.Amount.Exact( - value = KinAmount.newInstance( - kin = Kin(data.quarks), - rate = Rate( - fx = data.exchangeRate, - currency = currency - ) - ) - ), - domain = domain, - verifier = verifier, - additionalFees = additionalFees, - ) - } - MessagingService.RequestToReceiveBill.ExchangeDataCase.PARTIAL -> { - val data = request.partial - val currency = CurrencyCode.tryValueOf(data.currency) ?: return null - - - val additionalFees = request.additionalFeesList.mapNotNull { - val destination = PublicKey( - it.destination.value.toByteArray().toList() - ) - Fee(destination = destination, it.feeBps) - } - - ReceiveRequest( - account = account, - signature = signature, - amount = ReceiveRequest.Amount.Partial( - value = Fiat(currency, data.nativeAmount) - ), - domain = domain, - verifier = verifier, - additionalFees - ) - } - else -> return null - } - - Kind.ReceiveRequestKind(requestData) - } - MessagingService.Message.KindCase.AIRDROP_RECEIVED -> { - val type = AirdropType.getInstance(message.airdropReceived.airdropType) - ?: return null - val currency = CurrencyCode.tryValueOf(message.airdropReceived.exchangeData.currency) - ?: return null - - Kind.AirdropKind( - Airdrop( - type = type, - date = message.airdropReceived.timestamp.seconds, - kinAmount = KinAmount.newInstance( - kin = Kin(message.airdropReceived.exchangeData.quarks), - rate = Rate( - fx = message.airdropReceived.exchangeData.exchangeRate, - currency = currency - ) - ) - ) - ) - return null - } - - MessagingService.Message.KindCase.REQUEST_TO_LOGIN -> { - val request = message.requestToLogin - val domain = request.domain?.let { Domain.from(it.value) } ?: return null - val verifier = PublicKey( - request.verifier.value.toByteArray().toList() - ) - val rendezvous = PublicKey( - request.rendezvousKey.toByteArray().toList() - ) - val signature = Signature( - request.signature.value.toByteArray().toList() - ) - - Kind.LoginRequestKind( - LoginRequest(domain, verifier, rendezvous, signature) - ) - } - else -> return null - } - return StreamMessage( - id = message.id.value.toByteArray().toList(), - kind = kind - ) - } - } -} - -data class ReceiveRequest( - val account: PublicKey, - val signature: Signature, - val amount: Amount, - val domain: Domain?, - val verifier: PublicKey?, - val additionalFees: List -) { - sealed interface Amount { - data class Exact(val value: KinAmount): Amount - data class Partial(val value: Fiat): Amount - } -} - -data class PaymentRequest(val account: PublicKey, val signature: Signature) - -data class LoginRequest( - val domain: Domain, - val verifier: PublicKey, - val rendezous: PublicKey, - val signature: Signature, -) - -data class Airdrop(val type: AirdropType, val date: Long, val kinAmount: KinAmount) - -data class Fee( - val destination: PublicKey, - val bps: Int, -) \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/SendDestination.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/SendDestination.kt deleted file mode 100644 index ac195e34c..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/SendDestination.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.getcode.model - -sealed class SendDestination { - data class SendDestinationPublicKey( - val publicKey: List, - ) : SendDestination() - - data class SendDestinationPhone( - val phoneNumber: String - ) : SendDestination() -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/StreamEvent.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/StreamEvent.kt deleted file mode 100644 index cdecbb7b8..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/StreamEvent.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.getcode.model - -sealed class StreamEvent(val id: String) { - class SimulationEvent( - id: String, - val isFailed: Boolean, - val rendezvousKey: ByteArray, - val exchangeCurrency: String?, - val exchangeRate: Double?, - val amountNative: Double?, - val kin: Kin?, - val region: String? - ) : StreamEvent(id) -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/UpgradeableIntent.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/UpgradeableIntent.kt deleted file mode 100644 index fd16cec6e..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/UpgradeableIntent.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.getcode.model - -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import com.getcode.solana.keys.PublicKey - -class UpgradeableIntent( - val id: PublicKey, - val actions: List, -) { - companion object { - fun newInstance(proto: TransactionService.UpgradeableIntent): UpgradeableIntent { - val intentId = PublicKey(proto.id.value.toByteArray().toList()) - - val actions = proto.actionsList.map { - UpgradeablePrivateAction.newInstance(it) - } - - return UpgradeableIntent(intentId, actions) - } - - } - -} diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/UpgradeablePrivateAction.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/UpgradeablePrivateAction.kt deleted file mode 100644 index f587b16b4..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/UpgradeablePrivateAction.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.getcode.model - -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.instructions.programs.SystemProgram_AdvanceNonce -import com.getcode.solana.instructions.programs.TimelockProgram_TransferWithAuthority -import com.getcode.solana.keys.Hash -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.Signature -import com.getcode.solana.organizer.AccountType - -class UpgradeablePrivateAction( - val id: Int, - val transactionBlob: List, - val clientSignature: Signature, - val sourceAccountType: AccountType, - val sourceDerivationIndex: Long, - val originalDestination: PublicKey, - val originalAmount: Kin, - val treasury: PublicKey, - val recentRoot: Hash, - - val transaction: SolanaTransaction, - val originalNonce: PublicKey, - val originalCommitment: PublicKey, - val originalRecentBlockhash: Hash -) { - companion object { - fun newInstance( - id: Int, - transactionBlob: List, - clientSignature: Signature, - sourceAccountType: AccountType, - sourceDerivationIndex: Long, - originalDestination: PublicKey, - originalAmount: Kin, - treasury: PublicKey, - recentRoot: Hash - ): UpgradeablePrivateAction { - val transaction: SolanaTransaction = SolanaTransaction.fromList(transactionBlob) - ?: throw UpgradeablePrivateActionException.FailedToParseTransactionException() - - val nonceInstruction = - transaction.findInstruction(SystemProgram_AdvanceNonce::newInstance) - ?: throw UpgradeablePrivateActionException.MissingOriginalNonceException() - - val transferInstruction = - transaction.findInstruction( - TimelockProgram_TransferWithAuthority::newInstance - ) - ?: throw UpgradeablePrivateActionException.MissingOriginalCommitmentException() - - - return UpgradeablePrivateAction( - id = id, - transactionBlob = transactionBlob, - clientSignature = clientSignature, - sourceAccountType = sourceAccountType, - sourceDerivationIndex = sourceDerivationIndex, - originalDestination = originalDestination, - originalAmount = originalAmount, - treasury = treasury, - recentRoot = recentRoot, - transaction = transaction, - originalNonce = nonceInstruction.nonce, - originalCommitment = transferInstruction.destination, - originalRecentBlockhash = transaction.recentBlockhash - ) - } - - fun newInstance(proto: TransactionService.UpgradeableIntent.UpgradeablePrivateAction): UpgradeablePrivateAction { - val signature = Signature( - proto.clientSignature.value.toByteArray().toList() - ) - val accountType = - AccountType.newInstance(proto.sourceAccountType) - ?: throw UpgradeablePrivateActionException.DeserializationFailedException() - val originalDestination = - PublicKey( - proto.originalDestination.value.toByteArray().toList() - ) - val treasury = - PublicKey(proto.treasury.value.toByteArray().toList()) - val recentRoot = - Hash(proto.recentRoot.value.toByteArray().toList()) - - return newInstance( - id = proto.actionId, - transactionBlob = proto.transactionBlob.value.toByteArray().toList(), - clientSignature = signature, - sourceAccountType = accountType, - sourceDerivationIndex = proto.sourceDerivationIndex, - originalDestination = originalDestination, - originalAmount = Kin.fromQuarks(proto.originalAmount), - treasury = treasury, - recentRoot = recentRoot - ) - } - } - - sealed class UpgradeablePrivateActionException : Exception() { - class MissingOriginalNonceException : UpgradeablePrivateActionException() - class MissingOriginalCommitmentException : UpgradeablePrivateActionException() - class FailedToParseTransactionException : UpgradeablePrivateActionException() - class DeserializationFailedException : UpgradeablePrivateActionException() - } -} diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentCreateAccounts.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentCreateAccounts.kt deleted file mode 100644 index f5479b347..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentCreateAccounts.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import com.getcode.ed25519.Ed25519 -import com.getcode.model.intents.actions.ActionOpenAccount -import com.getcode.model.intents.actions.ActionType -import com.getcode.model.toPublicKey -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.Organizer - -class IntentCreateAccounts( - override val id: PublicKey, - override val actionGroup: ActionGroup, - private val organizer: Organizer, -) : IntentType() { - - override fun metadata(): TransactionService.Metadata { - return TransactionService.Metadata.newBuilder() - .setOpenAccounts(TransactionService.OpenAccountsMetadata.getDefaultInstance()) - .build() - } - - - companion object { - fun newInstance(organizer: Organizer): IntentCreateAccounts { - val actionsList = mutableListOf().apply { - organizer.allAccounts().map { pair -> - val (type, cluster) = pair - ActionOpenAccount.newInstance( - owner = organizer.tray.owner.getCluster().authority.keyPair.publicKeyBytes.toPublicKey(), - type = type, - accountCluster = cluster - ).let { this.add(it) } - } - } - - return IntentCreateAccounts( - id = Ed25519.createKeyPair().publicKeyBytes.toPublicKey(), - organizer = organizer, - actionGroup = ActionGroup().apply { - actions = actionsList - } - ) - - } - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentDeposit.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentDeposit.kt deleted file mode 100644 index 82aa15348..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentDeposit.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import com.getcode.model.Kin -import com.getcode.model.generate -import com.getcode.model.intents.actions.ActionTransfer -import com.getcode.network.repository.toSolanaAccount -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Tray - -class IntentDeposit( - override val id: PublicKey, - private val organizer: Organizer, - private val amount: Kin, - private val source: AccountType, - val resultTray: Tray, - - override val actionGroup: ActionGroup, -) : IntentType() { - override fun metadata(): TransactionService.Metadata { - return TransactionService.Metadata.newBuilder() - .setReceivePaymentsPrivately( - TransactionService.ReceivePaymentsPrivatelyMetadata.newBuilder() - .setSource(organizer.tray.cluster(source).vaultPublicKey.bytes.toSolanaAccount()) - .setQuarks(amount.quarks) - .setIsDeposit(true) - ) - .build() - } - - companion object { - fun newInstance( - source: AccountType, - organizer: Organizer, - amount: Kin - ): IntentDeposit { - val intentId = PublicKey.generate() - val currentTray = organizer.tray.copy() - val startSlotBalance = currentTray.slotsBalance - - // 1. Move all funds from the primary - // account to appropriate slots - - val transfers = currentTray.receive(receivingAccount = source, amount = amount).map { transfer -> - ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyTransfer, - intentId = intentId, - amount = transfer.kin, - source = currentTray.cluster(transfer.from), - destination = currentTray.cluster(transfer.to!!).vaultPublicKey - ) - } - - // 2. Redistribute the funds to prepare for - // future transfers - - val redistributes = currentTray.redistribute().map { exchange -> - ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyExchange, - intentId = intentId, - amount = exchange.kin, - source = currentTray.cluster(exchange.from), - destination = currentTray.cluster(exchange.to!!).vaultPublicKey // Exchanges always provide destination accounts - ) - } - - val endSlotBalance = currentTray.slotsBalance - - // Ensure that balances are consistent - // with what we expect these action to do - if (endSlotBalance - startSlotBalance != amount) { - throw IntentReceive.Companion.IntentReceiveException.BalanceMismatchException() - } - - val group = ActionGroup().apply { - actions = listOf( - *transfers.toTypedArray(), - *redistributes.toTypedArray() - ) - } - - return IntentDeposit( - id = intentId, - source = source, - organizer = organizer, - amount = amount, - actionGroup = group, - resultTray = currentTray - ) - } - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentEstablishRelationship.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentEstablishRelationship.kt deleted file mode 100644 index 70e0219cf..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentEstablishRelationship.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.common.v1.CodeModel as Model -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import com.getcode.model.Domain -import com.getcode.model.generate -import com.getcode.model.intents.actions.ActionOpenAccount -import com.getcode.model.toPublicKey -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Relationship -import com.getcode.solana.organizer.Tray - -class IntentEstablishRelationship( - override val id: PublicKey, - override val actionGroup: ActionGroup, - val organizer: Organizer, - val domain: Domain, - val resultTray: Tray, - val relationship: Relationship, -): IntentType() { - override fun metadata(): TransactionService.Metadata { - return TransactionService.Metadata.newBuilder() - .setEstablishRelationship( - TransactionService.EstablishRelationshipMetadata.newBuilder() - .setRelationship( - Model.Relationship.newBuilder() - .setDomain(Model.Domain.newBuilder().setValue(domain.relationshipHost)) - ) - ) - .build() - } - - companion object { - fun newInstance(organizer: Organizer, domain: Domain): IntentEstablishRelationship { - val id = PublicKey.generate() - val currentTray = organizer.tray.copy() - - val relationship = currentTray.createRelationship(domain) - - val ownerKey = currentTray.owner.getCluster().authority.keyPair.publicKeyBytes.toPublicKey() - val actionOpenAccount = ActionOpenAccount.newInstance( - owner = ownerKey, - type = AccountType.Relationship(domain), - accountCluster = relationship.getCluster() - ) - - return IntentEstablishRelationship( - id = id, - organizer = organizer, - domain = domain, - actionGroup = ActionGroup().apply { - actions = listOf(actionOpenAccount) - }, - resultTray = currentTray, - relationship = relationship, - ) - } - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentPrivateTransfer.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentPrivateTransfer.kt deleted file mode 100644 index c2cc11ddf..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentPrivateTransfer.kt +++ /dev/null @@ -1,232 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.chat.v2.ChatService -import com.getcode.model.Fee -import com.getcode.model.Kin -import com.getcode.model.KinAmount -import com.getcode.model.chat.Platform -import com.getcode.model.intents.actions.ActionFeePayment -import com.getcode.model.intents.actions.ActionOpenAccount -import com.getcode.model.intents.actions.ActionTransfer -import com.getcode.model.intents.actions.ActionWithdraw -import com.getcode.model.toPublicKey -import com.getcode.network.repository.toSolanaAccount -import com.getcode.services.model.ExtendedMetadata -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Tray -import timber.log.Timber -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService - -class IntentPrivateTransfer( - override val id: PublicKey, - private val organizer: Organizer, - private val destination: PublicKey, - // Amount requested to transfer - private val grossAmount: KinAmount, - // Amount after fees are paid - private val netAmount: KinAmount, - private val fee: Kin, - private val additionalFees: List, - private val isWithdrawal: Boolean, - private val metadata: ExtendedMetadata?, - val resultTray: Tray, - - override val actionGroup: ActionGroup, -) : IntentType() { - override fun metadata(): TransactionService.Metadata { - return TransactionService.Metadata.newBuilder() - .setSendPrivatePayment( - TransactionService.SendPrivatePaymentMetadata.newBuilder().apply { - setDestination(this@IntentPrivateTransfer.destination.bytes.toSolanaAccount()) - setIsWithdrawal(this@IntentPrivateTransfer.isWithdrawal) - setExchangeData( - TransactionService.ExchangeData.newBuilder() - .setQuarks(grossAmount.kin.quarks) - .setCurrency(grossAmount.rate.currency.name.lowercase()) - .setExchangeRate(grossAmount.rate.fx) - .setNativeAmount(grossAmount.fiat) - ) - - when (metadata) { - is ExtendedMetadata.Tip -> { - setIsTip(true) - setTippedUser(TransactionService.TippedUser.newBuilder() - .setPlatformValue(when (Platform.named(metadata.socialUser.platform)) { - Platform.Unknown -> ChatService.Platform.UNKNOWN_PLATFORM_VALUE - Platform.Twitter -> ChatService.Platform.TWITTER_VALUE - }) - .setUsername(metadata.socialUser.username) - ) - } - else -> Unit - } - } - ) - .build() - } - - companion object { - fun newInstance( - rendezvousKey: PublicKey, - organizer: Organizer, - destination: PublicKey, - amount: KinAmount, - fee: Kin, - additionalFees: List, - isWithdrawal: Boolean, - metadata: ExtendedMetadata?, - ): IntentPrivateTransfer { - if (fee > amount.kin) { - throw IntentPrivateTransferException.InvalidFeeException() - } - - // Compute all the fees that will be - // paid out of this transaction - val concreteFees = additionalFees.map { - val _fee = amount.kin.calculateFee(it.bps) - _fee to it.destination - } - - var netKin = amount.kin - fee - - // Apply the fee to the gross amount - concreteFees.onEach { (fee, destination) -> - netKin -= fee - } - - val netAmount = KinAmount.newInstance(kin = netKin, rate = amount.rate) - - val currentTray = organizer.tray.copy() - val startBalance = currentTray.availableBalance - - // 1. Move all funds from bucket accounts into the - // outgoing account and prepare to transfer - - val transfers = currentTray.transfer(amount = amount.kin).map { transfer -> - val sourceCluster = currentTray.cluster(transfer.from) - - // If the transfer is to another bucket, it's an internal - // exchange. Otherwise, it is considered a transfer. - if (transfer.to is AccountType.Bucket) { - ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyExchange, - intentId = rendezvousKey, - amount = transfer.kin, - source = sourceCluster, - destination = currentTray.slot((transfer.to as AccountType.Bucket).type).getCluster().vaultPublicKey - ) - } else { - ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyTransfer, - intentId = rendezvousKey, - amount = transfer.kin, - source = sourceCluster, - destination = currentTray.outgoing.getCluster().vaultPublicKey - ) - } - } - - val feePayments = mutableListOf() - - // Code Fee - if (fee > 0) { - feePayments.add( - ActionFeePayment.newInstance( - kind = ActionFeePayment.Kind.Code, - cluster = currentTray.outgoing.getCluster(), - amount = fee - ) - ) - } - - concreteFees.onEach { (feeAmount, destination) -> - feePayments.add( - ActionFeePayment.newInstance( - kind = ActionFeePayment.Kind.ThirdParty(destination), - cluster = currentTray.outgoing.getCluster(), - amount = feeAmount, - ) - ) - } - - // 2. Transfer all collected funds from the temp - // outgoing account to the destination account - - val outgoing = ActionWithdraw.newInstance( - kind = ActionWithdraw.Kind.NoPrivacyWithdraw(netAmount.kin), - cluster = currentTray.outgoing.getCluster(), - destination = destination, - metadata = metadata - ) - - // 3. Redistribute the funds to optimize for a - // subsequent payment out of the buckets - - val redistributes = currentTray.redistribute().map { exchange -> - ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyExchange, - intentId = rendezvousKey, - amount = exchange.kin, - source = currentTray.cluster(exchange.from), - destination = currentTray.cluster(exchange.to!!).vaultPublicKey - // Exchanges always provide destination accounts - ) - } - - // 4. Rotate the outgoing account - - currentTray.incrementOutgoing() - val newOutgoing = currentTray.outgoing - - val rotation = listOf( - ActionOpenAccount.newInstance( - owner = organizer.tray.owner.getCluster().authority.keyPair.publicKeyBytes.toPublicKey(), - type = AccountType.Outgoing, - accountCluster = newOutgoing.getCluster() - ), - ) - - val endBalance = currentTray.availableBalance - - if (startBalance - endBalance != amount.kin) { - Timber.e( - "Expected: ${amount.kin}; actual = ${startBalance - endBalance}; " + - "difference: ${startBalance.quarks - currentTray.availableBalance.quarks - amount.kin.quarks}" - ) - throw IntentPrivateTransferException.BalanceMismatchException() - } - - val group = ActionGroup() - - group.actions += transfers - group.actions += listOf( - *feePayments.toTypedArray(), - outgoing, - *redistributes.toTypedArray(), - *rotation.toTypedArray() - ) - - return IntentPrivateTransfer( - id = rendezvousKey, - organizer = organizer, - destination = destination, - grossAmount = amount, - netAmount = netAmount, - fee = fee, - additionalFees = additionalFees, - isWithdrawal = isWithdrawal, - metadata = metadata, - actionGroup = group, - resultTray = currentTray, - ) - - } - } -} - -sealed class IntentPrivateTransferException: Exception() { - class BalanceMismatchException: IntentPrivateTransferException() - class InvalidFeeException: IntentPrivateTransferException() -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentPublicPayment.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentPublicPayment.kt deleted file mode 100644 index 979bf763f..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentPublicPayment.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.transaction.v2.CodeTransactionService -import com.codeinc.gen.transaction.v2.CodeTransactionService.ExtendedPaymentMetadata -import com.getcode.model.KinAmount -import com.getcode.model.generate -import com.getcode.model.intents.actions.ActionTransfer -import com.getcode.network.repository.toSolanaAccount -import com.getcode.services.model.ExtendedMetadata -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.AccountCluster -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Tray -import com.getcode.utils.toByteString - -class IntentPublicPayment( - override val id: PublicKey, - private val organizer: Organizer, - private val sourceCluster: AccountCluster, - private val destination: PublicKey, - private val amount: KinAmount, - val resultTray: Tray, - val metadata: ExtendedMetadata? = null, - - override val actionGroup: ActionGroup, -) : IntentType() { - override fun metadata(): CodeTransactionService.Metadata { - val pubPayBuilder = CodeTransactionService.SendPublicPaymentMetadata.newBuilder() - .setSource(sourceCluster.vaultPublicKey.bytes.toSolanaAccount()) - .setDestination(destination.bytes.toSolanaAccount()) - .setIsWithdrawal(true) - .setExchangeData( - CodeTransactionService.ExchangeData.newBuilder() - .setQuarks(amount.kin.quarks) - .setCurrency(amount.rate.currency.name.lowercase()) - .setExchangeRate(amount.rate.fx) - .setNativeAmount(amount.fiat) - ) - - if (metadata != null) { - when (metadata) { - is ExtendedMetadata.Any -> pubPayBuilder.setExtendedMetadata( - ExtendedPaymentMetadata.newBuilder() - .setValue(com.google.protobuf.Any.newBuilder() - .setTypeUrl(metadata.typeUrl) - .setValue(metadata.data.toByteString()))) - else -> Unit - } - } - - return CodeTransactionService.Metadata.newBuilder() - .setSendPublicPayment(pubPayBuilder.build()) - .build() - } - - companion object { - fun newInstance( - organizer: Organizer, - source: AccountType, - destination: PublicKey, - amount: KinAmount, - extendedMetadata: ExtendedMetadata? = null, - ): IntentPublicPayment { - val id = PublicKey.generate() - val currentTray = organizer.tray.copy() - val sourceCluster = organizer.tray.cluster(source) - - // 1. Transfer all funds in the primary account - // directly to the destination. This is a public - // transfer so no buckets involved and no rotation - // required. - - val transfer = ActionTransfer.newInstance( - kind = ActionTransfer.Kind.NoPrivacyTransfer, - intentId = id, - amount = amount.kin, - source = sourceCluster, - destination = destination - ) - - currentTray.decrement(source, kin = amount.kin) - - return IntentPublicPayment( - id = id, - organizer = organizer, - sourceCluster = sourceCluster, - destination = destination, - amount = amount, - actionGroup = ActionGroup().apply { - actions = listOf(transfer) - }, - resultTray = currentTray, - metadata = extendedMetadata - ) - } - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentPublicTransfer.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentPublicTransfer.kt deleted file mode 100644 index 02bcc27f7..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentPublicTransfer.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.getcode.model.intents - -import com.getcode.model.KinAmount -import com.getcode.model.generate -import com.getcode.model.intents.actions.ActionTransfer -import com.getcode.network.repository.toSolanaAccount -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.AccountCluster -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Tray -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService - -class IntentPublicTransfer( - override val id: PublicKey, - private val organizer: Organizer, - private val sourceCluster: AccountCluster, - private val destination: PublicKey, - private val amount: KinAmount, - - val resultTray: Tray, - - override val actionGroup: ActionGroup, -) : IntentType() { - override fun metadata(): TransactionService.Metadata { - return TransactionService.Metadata.newBuilder() - .setSendPublicPayment( - TransactionService.SendPublicPaymentMetadata.newBuilder() - .setSource(sourceCluster.vaultPublicKey.bytes.toSolanaAccount()) - .setDestination(destination.bytes.toSolanaAccount()) - .setIsWithdrawal(true) - .setExchangeData( - TransactionService.ExchangeData.newBuilder() - .setQuarks(amount.kin.quarks) - .setCurrency(amount.rate.currency.name.lowercase()) - .setExchangeRate(amount.rate.fx) - .setNativeAmount(amount.fiat) - ) - ) - .build() - } - - sealed interface Destination { - data class Local(val accountType: AccountType): Destination - data class External(val publicKey: PublicKey): Destination - } - - companion object { - fun newInstance( - organizer: Organizer, - source: AccountType, - destination: Destination, - amount: KinAmount, - ): IntentPublicTransfer { - val id = PublicKey.generate() - val currentTray = organizer.tray.copy() - val sourceCluster = organizer.tray.cluster(source) - - - val target = when (destination) { - is Destination.External -> destination.publicKey - is Destination.Local -> organizer.tray.cluster(destination.accountType).vaultPublicKey - } - - // 1. Transfer all funds in the primary account - // directly to the destination. This is a public - // transfer so no buckets involved and no rotation - // required. - - val transfer = ActionTransfer.newInstance( - kind = ActionTransfer.Kind.NoPrivacyTransfer, - intentId = id, - amount = amount.kin, - source = sourceCluster, - destination = target - ) - - currentTray.decrement(source, kin = amount.kin) - - // If moving funds to an already known account - // we should update the balance accordingly - if (destination is Destination.Local) { - currentTray.increment(destination.accountType, amount.kin) - } - - return IntentPublicTransfer( - id = id, - organizer = organizer, - sourceCluster = sourceCluster, - destination = target, - amount = amount, - actionGroup = ActionGroup().apply { - actions = listOf(transfer) - }, - resultTray = currentTray, - ) - } - } -} - -sealed class IntentPublicTransferException: Exception() { - class BalanceMismatchException: IntentPublicTransferException() -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentReceive.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentReceive.kt deleted file mode 100644 index e0f0fe846..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentReceive.kt +++ /dev/null @@ -1,115 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import com.getcode.model.Kin -import com.getcode.model.generate -import com.getcode.model.intents.actions.* -import com.getcode.model.toPublicKey -import com.getcode.network.repository.toSolanaAccount -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.Tray - -class IntentReceive( - override val id: PublicKey, - private val organizer: Organizer, - private val amount: Kin, - - val resultTray: Tray, - - override val actionGroup: ActionGroup, -) : IntentType() { - override fun metadata(): TransactionService.Metadata { - return TransactionService.Metadata.newBuilder() - .setReceivePaymentsPrivately( - TransactionService.ReceivePaymentsPrivatelyMetadata.newBuilder() - .setSource(organizer.tray.incoming.getCluster().vaultPublicKey.bytes.toSolanaAccount()) - .setQuarks(amount.quarks) - .setIsDeposit(false) - ) - .build() - } - - companion object { - fun newInstance( - organizer: Organizer, - amount: Kin - ): IntentReceive { - val intentId = PublicKey.generate() - val currentTray = organizer.tray.copy() - val startBalance = currentTray.availableBalance - - // 1. Move all funds from the incoming - // account to appropriate slots - - val transfers = currentTray.receive(AccountType.Incoming, amount = amount).map { transfer -> - ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyTransfer, - intentId = intentId, - amount = transfer.kin, - source = currentTray.cluster(transfer.from), - destination = - currentTray.cluster(transfer.to!!).vaultPublicKey - ) - } - - // 2. Redistribute the funds to prepare for - // future transfers - - val redistributes = currentTray.redistribute().map { exchange -> - ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyExchange, - intentId = intentId, - amount = exchange.kin, - source = currentTray.cluster(exchange.from), - destination = - currentTray.cluster(exchange.to!!).vaultPublicKey - // Exchanges always provide destination accounts - ) - } - - // 3. Rotate incoming account - - currentTray.incrementIncoming() - val newIncoming = currentTray.incoming - - val rotation = mutableListOf( - ActionOpenAccount.newInstance( - owner = organizer.tray.owner.getCluster().authority.keyPair.publicKeyBytes.toPublicKey(), - type = AccountType.Incoming, - accountCluster = newIncoming.getCluster() - ) - ) - - val endBalance = currentTray.availableBalance - - // We're just moving funds from incoming - // account to buckets, the balance - // shouldn't change - if (endBalance != startBalance) { - throw IntentReceiveException.BalanceMismatchException() - } - - val group = ActionGroup().apply { - actions = listOf( - *transfers.toTypedArray(), - *redistributes.toTypedArray(), - *rotation.toTypedArray() - ) - } - - return IntentReceive( - id = intentId, - organizer = organizer, - amount = amount, - actionGroup = group, - resultTray = currentTray, - ) - } - - sealed class IntentReceiveException : Exception() { - class BalanceMismatchException : IntentReceiveException() - } - } -} diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentRemoteReceive.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentRemoteReceive.kt deleted file mode 100644 index 286ea42d1..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentRemoteReceive.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import com.getcode.model.Kin -import com.getcode.model.generate -import com.getcode.model.intents.actions.ActionWithdraw -import com.getcode.network.repository.toSolanaAccount -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.GiftCardAccount -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Tray - -class IntentRemoteReceive( - override val id: PublicKey, - private val organizer: Organizer, - private val giftCard: GiftCardAccount, - private val amount: Kin, - private val isVoidingGiftCard: Boolean, - - val resultTray: Tray, - - override val actionGroup: ActionGroup, -) : IntentType() { - override fun metadata(): TransactionService.Metadata { - return TransactionService.Metadata.newBuilder() - .setReceivePaymentsPublicly( - TransactionService.ReceivePaymentsPubliclyMetadata.newBuilder() - .setSource(giftCard.cluster.vaultPublicKey.bytes.toSolanaAccount()) - .setQuarks(amount.quarks) - .setIsRemoteSend(true) - .setIsIssuerVoidingGiftCard(isVoidingGiftCard) - ) - .build() - } - - companion object { - fun newInstance( - organizer: Organizer, - giftCard: GiftCardAccount, - amount: Kin, - isVoidingGiftCard: Boolean - ): IntentRemoteReceive { - val intentId = PublicKey.generate() - val currentTray = organizer.tray.copy() - val startBalance = currentTray.availableBalance - - val giftCardWithdraw = ActionWithdraw.newInstance( - kind = ActionWithdraw.Kind.NoPrivacyWithdraw(amount), - cluster = giftCard.cluster, - destination = organizer.incomingVault - ) - - currentTray.increment(AccountType.Incoming, amount) - - val endBalance = currentTray.availableBalance - if (endBalance - startBalance != amount) { - throw IntentRemoteReceiveException.BalanceMismatchException() - } - - return IntentRemoteReceive( - id = intentId, - organizer = organizer, - giftCard = giftCard, - amount = amount, - actionGroup = ActionGroup().apply { - actions = listOf(giftCardWithdraw) - }, - resultTray = currentTray, - isVoidingGiftCard = isVoidingGiftCard - ) - } - } - - sealed class IntentRemoteReceiveException : Exception() { - class BalanceMismatchException : IntentRemoteReceiveException() - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentRemoteSend.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentRemoteSend.kt deleted file mode 100644 index 5a49ab868..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentRemoteSend.kt +++ /dev/null @@ -1,163 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import com.getcode.model.KinAmount -import com.getcode.model.intents.actions.ActionOpenAccount -import com.getcode.model.intents.actions.ActionTransfer -import com.getcode.model.intents.actions.ActionWithdraw -import com.getcode.model.toPublicKey -import com.getcode.network.repository.toSolanaAccount -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.GiftCardAccount -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Tray -import timber.log.Timber - -class IntentRemoteSend( - override val id: PublicKey, - private val organizer: Organizer, - private val giftCard: GiftCardAccount, - private val amount: KinAmount, - - val resultTray: Tray, - - override val actionGroup: ActionGroup, -) : IntentType() { - override fun metadata(): TransactionService.Metadata { - return TransactionService.Metadata.newBuilder() - .setSendPrivatePayment( - TransactionService.SendPrivatePaymentMetadata.newBuilder() - .setDestination(giftCard.cluster.vaultPublicKey.bytes.toSolanaAccount()) - .setIsWithdrawal(false) - .setIsRemoteSend(true) - .setExchangeData( - TransactionService.ExchangeData.newBuilder() - .setQuarks(amount.kin.quarks) - .setCurrency(amount.rate.currency.name.lowercase()) - .setExchangeRate(amount.rate.fx) - .setNativeAmount(amount.fiat) - ) - ) - .build() - } - - companion object { - fun newInstance( - rendezvousKey: PublicKey, - organizer: Organizer, - giftCard: GiftCardAccount, - amount: KinAmount - ): IntentRemoteSend { - val currentTray = organizer.tray.copy() - val startBalance = currentTray.availableBalance - - // 1. Open gift card account - - val openGiftCard = ActionOpenAccount.newInstance( - owner = giftCard.cluster.authority.keyPair.publicKeyBytes.toPublicKey(), - type = AccountType.RemoteSend, - accountCluster = giftCard.cluster - ) - - // 2. Move all funds from bucket accounts into the - // outgoing account and prepare to transfer - - val transfers = currentTray.transfer(amount = amount.kin).map { transfer -> - val sourceCluster = currentTray.cluster(transfer.from) - - // If the transfer is to another bucket, it's an internal - // exchange. Otherwise, it is considered a transfer. - if (transfer.to is AccountType.Bucket) { - ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyExchange, - intentId = rendezvousKey, - amount = transfer.kin, - source = sourceCluster, - destination = currentTray.slot((transfer.to as AccountType.Bucket).type).getCluster().vaultPublicKey - ) - } else { - ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyTransfer, - intentId = rendezvousKey, - amount = transfer.kin, - source = sourceCluster, - destination = currentTray.outgoing.getCluster().vaultPublicKey - ) - } - } - - // 3. Transfer all collected funds from the temp - // outgoing account to the destination account - - val outgoing = ActionWithdraw.newInstance( - kind = ActionWithdraw.Kind.NoPrivacyWithdraw(amount.kin), - cluster = currentTray.outgoing.getCluster(), - destination = giftCard.cluster.vaultPublicKey - ) - - // 4. Redistribute the funds to optimize for a - // subsequent payment out of the buckets - - val redistributes = currentTray.redistribute().map { exchange -> - ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyExchange, - intentId = rendezvousKey, - amount = exchange.kin, - source = currentTray.cluster(exchange.from), - destination = currentTray.cluster(exchange.to!!).vaultPublicKey - // Exchanges always provide destination accounts - ) - } - - // 5. Rotate the outgoing account - - currentTray.incrementOutgoing() - val newOutgoing = currentTray.outgoing - - val rotation = listOf( - ActionOpenAccount.newInstance( - owner = currentTray.owner.getCluster().authority.keyPair.publicKeyBytes.toPublicKey(), - type = AccountType.Outgoing, - accountCluster = newOutgoing.getCluster() - ), - ) - - // 6. Close gift card account - - val endBalance = currentTray.availableBalance - - if (startBalance - endBalance != amount.kin) { - Timber.e( - "Expected: ${amount.kin}; actual = ${startBalance - endBalance}; " + - "difference: ${startBalance.quarks - currentTray.availableBalance.quarks - amount.kin.quarks}" - ) - throw IntentRemoteSendException.BalanceMismatchException() - } - - val group = ActionGroup().apply { - actions = listOf( - openGiftCard, - *transfers.toTypedArray(), - outgoing, - *redistributes.toTypedArray(), - *rotation.toTypedArray(), - ) - } - - return IntentRemoteSend( - id = rendezvousKey, - organizer = organizer, - giftCard = giftCard, - amount = amount, - actionGroup = group, - resultTray = currentTray, - ) - - } - } - - sealed class IntentRemoteSendException : Exception() { - class BalanceMismatchException : IntentRemoteSendException() - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentType.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentType.kt deleted file mode 100644 index 8aa5d1934..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentType.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import com.getcode.ed25519.Ed25519 -import com.getcode.model.intents.actions.ActionType -import com.getcode.model.intents.actions.numberActions -import com.getcode.network.repository.* -import com.getcode.solana.Message -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.keys.Hash -import com.getcode.solana.keys.PublicKey -import com.getcode.utils.sign - -abstract class IntentType { - abstract val id: PublicKey - abstract val actionGroup: ActionGroup - - fun getActions() = actionGroup.actions - fun getAction(index: Int) = getActions()[index] - - fun apply(parameters: List) { - if (parameters.size != actionGroup.actions.size) { - throw Exception(Error.InvalidParameterCount.name) - } - - parameters.forEachIndexed { index, parameter -> - if (actionGroup.actions[index].id != parameter.actionId) { - throw Exception(Error.ActionParameterMismatch.name) - } - actionGroup.actions[index].serverParameter = parameter - } - } - - fun vixnHash(): Hash { - return actionGroup.actions.flatMap { it.compactMessages() } - .flatMap { it.toList() } - .take(32).let { Hash(it) } - } - - fun transaction(): SolanaTransaction { - val message = actionGroup.actions.flatMap { it.transactions() }.map { it.message } - .let { Message.newInstance(it.map { it.encode().toList() }.flatten()) }!! - val sigs = actionGroup.actions.flatMap { it.signatures() } - - return SolanaTransaction(message, sigs) - } - - fun signatures(): List = - actionGroup.actions.map { it.signatures().firstOrNull() }.mapNotNull { it } - - abstract fun metadata(): TransactionService.Metadata - - fun requestToSubmitSignatures(): TransactionService.SubmitIntentRequest { - return TransactionService.SubmitIntentRequest.newBuilder() - .setSubmitSignatures( - TransactionService.SubmitIntentRequest.SubmitSignatures.newBuilder() - .addAllSignatures(signatures().map { it.bytes.toByteArray().toSignature() }) - ) - .build() - } - - fun requestToSubmitActions(owner: Ed25519.KeyPair, deviceToken: String? = null): TransactionService.SubmitIntentRequest { - val submitActionsBuilder = TransactionService.SubmitIntentRequest.SubmitActions.newBuilder() - submitActionsBuilder.owner = owner.publicKeyBytes.toSolanaAccount() - submitActionsBuilder.id = id.toIntentId() - submitActionsBuilder.metadata = metadata() - submitActionsBuilder.addAllActions(actionGroup.actions.map { it.action() }) - - submitActionsBuilder.signature = submitActionsBuilder.sign(owner) - - return TransactionService.SubmitIntentRequest.newBuilder() - .setSubmitActions(submitActionsBuilder) - .build() - } - - enum class Error { - InvalidParameterCount, - ActionParameterMismatch - } -} - -class ActionGroup { - var actions: List = listOf() - set(value) { - field = value.numberActions() - } -} - -sealed interface CompactMessageArgs { - - data class Transfer( - val source: PublicKey, - val destination: PublicKey, - val amountInQuarks: Long, - val nonce: PublicKey, - val nonceValue: Hash, - ): CompactMessageArgs -} -typealias CompactMessage = ByteArray \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentUpgradePrivacy.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentUpgradePrivacy.kt deleted file mode 100644 index 9dbaa8367..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/IntentUpgradePrivacy.kt +++ /dev/null @@ -1,149 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import com.getcode.crypt.MnemonicPhrase -import com.getcode.model.Kin -import com.getcode.model.UpgradeableIntent -import com.getcode.model.extensions.newInstance -import com.getcode.model.intents.actions.ActionPrivacyUpgrade -import com.getcode.model.intents.actions.ActionTransfer -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.Signature -import com.getcode.solana.keys.SplitterCommitmentAccounts -import com.getcode.solana.organizer.AccountCluster - -class IntentUpgradePrivacy( - override val id: PublicKey, - override val actionGroup: ActionGroup, -) : IntentType() { - override fun metadata(): TransactionService.Metadata { - return TransactionService.Metadata.newBuilder() - .setUpgradePrivacy(TransactionService.UpgradePrivacyMetadata.getDefaultInstance()) - .build() - } - - companion object { - fun newInstance( - mnemonic: MnemonicPhrase, - upgradeableIntent: UpgradeableIntent - ): IntentUpgradePrivacy { - val actionsMapped = upgradeableIntent.actions.map { upgradeableAction -> - val actionAmount = upgradeableAction.originalAmount - val originalDestination = upgradeableAction.originalDestination - val treasury = upgradeableAction.treasury - val recentRoot = upgradeableAction.recentRoot - val originalNonce = upgradeableAction.originalNonce - val originalRecentBlockhash = upgradeableAction.originalRecentBlockhash - - val sourceCluster = AccountCluster.using( - type = upgradeableAction.sourceAccountType, - index = upgradeableAction.sourceDerivationIndex.toInt(), - mnemonic = mnemonic - ) - - // Validate the server isn't malicious and is providing - // the original details of the transaction - validate( - transactionData = upgradeableAction.transactionBlob, - clientSignature = upgradeableAction.clientSignature, - intentId = upgradeableIntent.id, - actionId = upgradeableAction.id, - amount = actionAmount, - source = sourceCluster, - destination = originalDestination, - originalNonce = originalNonce, - treasury = treasury, - recentRoot = recentRoot - ) - - // We have to derive the original commitment accounts because - // we'll need to verify whether the commitment state account - // is part of the merkle tree provided by server paramaeters - val originalSplitterAccounts = SplitterCommitmentAccounts.newInstance( - source = sourceCluster, - destination = originalDestination, - amount = actionAmount, - treasury = treasury, - recentRoot = recentRoot, - intentId = upgradeableIntent.id, - actionId = upgradeableAction.id - ) - - ActionPrivacyUpgrade.newInstance( - source = sourceCluster, - originalActionID = upgradeableAction.id, - originalCommitmentStateAccount = originalSplitterAccounts.state.publicKey, - originalAmount = actionAmount, - originalNonce = originalNonce, - originalRecentBlockhash = originalRecentBlockhash, - treasury = treasury - ) - } - - return IntentUpgradePrivacy( - id = upgradeableIntent.id, - actionGroup = ActionGroup().apply { this.actions = actionsMapped } - ) - } - - - fun validate( - transactionData: List, - clientSignature: Signature, - intentId: PublicKey, - actionId: Int, - amount: Kin, - source: AccountCluster, - destination: PublicKey, - originalNonce: PublicKey, - treasury: PublicKey, - recentRoot: com.getcode.solana.keys.Hash - ) { - val transaction = SolanaTransaction.fromList(transactionData) - ?: throw IntentUpgradePrivacyException.FailedToParseTransactionException() - - val originalTransfer = ActionTransfer.newInstance( - kind = ActionTransfer.Kind.TempPrivacyTransfer, - intentId = intentId, - amount = amount, - source = source, - destination = destination - ) - - originalTransfer.id = actionId - originalTransfer.serverParameter = ServerParameter( - actionId = actionId, - parameter = ServerParameter.Parameter.TempPrivacy( - treasury = treasury, - recentRoot = recentRoot - ), - configs = listOf( - ServerParameter.Config( - nonce = originalNonce, - blockhash = transaction.recentBlockhash - ) - ), - ) - - val originalTransaction = originalTransfer.transactions()[0] - - if (originalTransaction.encode() != transactionData) { - throw IntentUpgradePrivacyException.TransactionMismatchException() - } - - // (Optional) Reach into transaction and make sure the source is the same - val signature = originalTransaction.sign(source.authority.keyPair).firstOrNull() - - if (signature != clientSignature) { - throw IntentUpgradePrivacyException.SignatureMismatchException() - } - } - } -} - -sealed class IntentUpgradePrivacyException : Exception() { - class FailedToParseTransactionException : IntentUpgradePrivacyException() - class TransactionMismatchException : IntentUpgradePrivacyException() - class SignatureMismatchException : IntentUpgradePrivacyException() -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/ServerParameter.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/ServerParameter.kt deleted file mode 100644 index 806df60a2..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/ServerParameter.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import com.getcode.model.Kin -import com.getcode.model.toHash -import com.getcode.model.toPublicKey -import com.getcode.solana.keys.Hash -import com.getcode.solana.keys.PublicKey - -class ServerParameter( - val actionId: Int, - val parameter: Parameter?, - val configs: List -) { - data class Config(val nonce: PublicKey, val blockhash: Hash) - - sealed class Parameter { - data class TempPrivacy(val treasury: PublicKey, val recentRoot: Hash): Parameter() - data class PermanentPrivacyUpgrade( - val newCommitment: PublicKey, - val newCommitmentTranscript: Hash, - val newCommitmentDestination: PublicKey, - val newCommitmentAmount: Kin, - val merkleRoot: Hash, - val merkleProof: List, - ): Parameter() - - data class FeePayment(val publicKey: PublicKey): Parameter() - - companion object { - fun newInstance(proto: TransactionService.ServerParameter): Parameter? { - return when (proto.typeCase) { - TransactionService.ServerParameter.TypeCase.TEMPORARY_PRIVACY_TRANSFER -> { - val param = proto.temporaryPrivacyTransfer - val treasury = PublicKey( - param.treasury.value.toByteArray().toList() - ) - val recentRoot = Hash( - param.recentRoot.value.toByteArray().toList() - ) - return TempPrivacy(treasury, recentRoot) - } - TransactionService.ServerParameter.TypeCase.TEMPORARY_PRIVACY_EXCHANGE -> { - val param = proto.temporaryPrivacyExchange - val treasury = PublicKey( - param.treasury.value.toByteArray().toList() - ) - val recentRoot = Hash( - param.recentRoot.value.toByteArray().toList() - ) - return TempPrivacy(treasury, recentRoot) - } - TransactionService.ServerParameter.TypeCase.PERMANENT_PRIVACY_UPGRADE -> { - val param = proto.permanentPrivacyUpgrade - val newCommitment = PublicKey( - param.newCommitment.value.toByteArray().toList() - ) - val newCommitmentTranscript = Hash( - param.newCommitmentTranscript.value.toByteArray().toList() - ) - val newCommitmentDestination = PublicKey( - param.newCommitmentDestination.value.toByteArray().toList() - ) - val merkleRoot = Hash( - param.merkleRoot.value.toByteArray().toList() - ) - - val merkleProof = param.merkleProofList.map { - Hash(it.value.toByteArray().toList()) - } - - PermanentPrivacyUpgrade( - newCommitment = newCommitment, - newCommitmentTranscript = newCommitmentTranscript, - newCommitmentDestination = newCommitmentDestination, - newCommitmentAmount = Kin.fromQuarks(quarks = param.newCommitmentAmount), - merkleRoot = merkleRoot, - merkleProof = merkleProof - ) - } - TransactionService.ServerParameter.TypeCase.FEE_PAYMENT -> { - val param = proto.feePayment - - // PublicKey will be `nil` for .thirdParty fee payments - val optionalDestination = PublicKey( - param.codeDestination.value.toByteArray().toList() - ) - FeePayment(optionalDestination) - } - TransactionService.ServerParameter.TypeCase.OPEN_ACCOUNT, - TransactionService.ServerParameter.TypeCase.NO_PRIVACY_WITHDRAW, - TransactionService.ServerParameter.TypeCase.TYPE_NOT_SET, - TransactionService.ServerParameter.TypeCase.NO_PRIVACY_TRANSFER -> null - } - } - - } - } - - companion object { - fun newInstance(proto: TransactionService.ServerParameter): ServerParameter { - return ServerParameter( - actionId = proto.actionId, - parameter = Parameter.newInstance(proto), - configs = proto.noncesList.map { - Config( - nonce = it.nonce.value.toByteArray().toPublicKey(), - blockhash = it.blockhash.value.toByteArray().toHash() - ) - } - ) - } - } -} - - diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/SwapIntent.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/SwapIntent.kt deleted file mode 100644 index 68d1068f6..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/SwapIntent.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.getcode.model.intents - -import com.codeinc.gen.transaction.v2.CodeTransactionService.SwapRequest -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.generate -import com.getcode.network.repository.toSignature -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.builder.TransactionBuilder -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.Signature -import com.getcode.solana.organizer.AccountCluster -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.Organizer -import java.lang.IllegalStateException - -class SwapIntent( - val id: PublicKey, - val organizer: Organizer, - val owner: KeyPair, - val swapCluster: AccountCluster, -) { - - var parameters: SwapConfigParameters? = null - - fun sign(parameters: SwapConfigParameters): List { - val transaction = transaction(parameters) - return transaction.sign(organizer.swapKeyPair) - } - - fun transaction(parameters: SwapConfigParameters): SolanaTransaction { - return TransactionBuilder.swap( - fromUsdc = swapCluster, - toPrimary = organizer.primaryVault, - parameters = parameters - ) - } - - companion object { - fun newInstance(organizer: Organizer): SwapIntent { - return SwapIntent( - id = PublicKey.generate(), - organizer = organizer, - owner = organizer.ownerKeyPair, - swapCluster = organizer.tray.cluster(AccountType.Swap), - ) - } - } -} - -fun SwapIntent.requestToSubmitSignatures(): SwapRequest? = runCatching { - parameters ?: throw IllegalStateException("Missing swap parameters") - - return@runCatching SwapRequest.newBuilder() - .setSubmitSignature( - SwapRequest.SubmitSignature.newBuilder() - .setSignature(sign(parameters!!).first().bytes.toByteArray().toSignature()) - .build() - ).build() -}.getOrNull() \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/actions/ActionFeePayment.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/actions/ActionFeePayment.kt deleted file mode 100644 index 7760e229b..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/actions/ActionFeePayment.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.getcode.model.intents.actions - -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import com.codeinc.gen.transaction.v2.CodeTransactionService.FeePaymentAction -import com.getcode.ed25519.Ed25519 -import com.getcode.model.Kin -import com.getcode.model.intents.ServerParameter -import com.getcode.network.repository.toSolanaAccount -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.builder.TransactionBuilder -import com.getcode.solana.organizer.AccountCluster - -class ActionFeePayment( - override var id: Int, - override var serverParameter: ServerParameter? = null, - override val signer: Ed25519.KeyPair? = null, - val kind: Kind, - val cluster: AccountCluster, - val amount: Kin, - val configCountRequirement: Int = 1, -): ActionType() { - - sealed interface Kind { - val codeType: Int - data object Code: Kind { - override val codeType: Int = 0 - } - data class ThirdParty(val destination: com.getcode.solana.keys.PublicKey): Kind { - override val codeType: Int = 1 - } - } - - override fun transactions(): List { - val configs = serverParameter?.configs ?: return emptyList() - - val timelock = cluster.timelock ?: return emptyList() - - val destination: com.getcode.solana.keys.PublicKey = when (kind) { - Kind.Code -> { - (serverParameter?.parameter as? ServerParameter.Parameter.FeePayment)?.publicKey ?: return emptyList() - } - is Kind.ThirdParty -> kind.destination - } - - return configs.map { config -> - TransactionBuilder.transfer( - timelockDerivedAccounts = timelock, - destination = destination, - amount = amount, - nonce = config.nonce, - recentBlockhash = config.blockhash, - kreIndex = kreIndex - ) - } - } - - override fun action(): TransactionService.Action { - return TransactionService.Action.newBuilder() - .setId(id) - .setFeePayment( - FeePaymentAction.newBuilder() - .setTypeValue(kind.codeType) - .setAuthority(cluster.authority.keyPair.publicKeyBytes.toSolanaAccount()) - .setSource(cluster.vaultPublicKey.bytes.toSolanaAccount()) - .setAmount(amount.quarks) - .apply { - if (kind is Kind.ThirdParty) { - setDestination(kind.destination.bytes.toSolanaAccount()) - } - } - .build() - ).build() - } - - companion object { - fun newInstance(kind: Kind, cluster: AccountCluster, amount: Kin): ActionFeePayment { - return ActionFeePayment( - id = 0, - signer = cluster.authority.keyPair, - cluster = cluster, - kind = kind, - amount = amount - ) - } - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/actions/ActionOpenAccount.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/actions/ActionOpenAccount.kt deleted file mode 100644 index 49704d19a..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/actions/ActionOpenAccount.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.getcode.model.intents.actions - -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import com.getcode.ed25519.Ed25519 -import com.getcode.model.intents.ServerParameter -import com.getcode.network.repository.toSolanaAccount -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.organizer.AccountCluster -import com.getcode.solana.organizer.AccountType -import com.getcode.utils.sign - -class ActionOpenAccount( - override var id: Int, - override var serverParameter: ServerParameter? = null, - override val signer: Ed25519.KeyPair?, - val owner: com.getcode.solana.keys.PublicKey, - val type: AccountType, - val accountCluster: AccountCluster -) : ActionType() { - //static let configCountRequirement: Int = 0 - - override fun transactions(): List = listOf() - - override fun action(): TransactionService.Action { - return TransactionService.Action.newBuilder() - .apply { - this.id = - this@ActionOpenAccount.id - this.openAccount = TransactionService.OpenAccountAction.newBuilder().apply { - this.index = - this@ActionOpenAccount.accountCluster.index.toLong() - this.owner = - this@ActionOpenAccount.owner.bytes.toSolanaAccount() - this.accountType = - this@ActionOpenAccount.type.getAccountType() - this.authority = - this@ActionOpenAccount.accountCluster.authority.keyPair.publicKeyBytes.toSolanaAccount() - this.token = - this@ActionOpenAccount.accountCluster.vaultPublicKey - .bytes.toSolanaAccount() - this.authoritySignature = - this.sign(accountCluster.authority.keyPair) - }.build() - } - .build() - } - - companion object { - fun newInstance( - owner: com.getcode.solana.keys.PublicKey, - type: AccountType, - accountCluster: AccountCluster - ): ActionOpenAccount { - return ActionOpenAccount( - id = 0, - owner = owner, - type = type, - accountCluster = accountCluster, - signer = null - ) - } - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/actions/ActionPrivacyUpgrade.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/actions/ActionPrivacyUpgrade.kt deleted file mode 100644 index 1aea727f7..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/actions/ActionPrivacyUpgrade.kt +++ /dev/null @@ -1,127 +0,0 @@ -package com.getcode.model.intents.actions - -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import com.getcode.ed25519.Ed25519 -import com.getcode.model.Kin -import com.getcode.model.extensions.newInstance -import com.getcode.model.intents.ServerParameter -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.builder.TransactionBuilder -import com.getcode.solana.keys.verifyContained -import com.getcode.solana.organizer.AccountCluster -import timber.log.Timber - -class ActionPrivacyUpgrade( - override var id: Int, - override var serverParameter: ServerParameter? = null, - override val signer: Ed25519.KeyPair?, - - var source: AccountCluster, - var originalActionID: Int, - var originalCommitmentStateAccount: com.getcode.solana.keys.PublicKey, - var originalAmount: Kin, - var originalNonce: com.getcode.solana.keys.PublicKey, - var originalRecentBlockhash: com.getcode.solana.keys.Hash, - var treasury: com.getcode.solana.keys.PublicKey -) : ActionType() { - val configCountRequirement: Int = 1 - - override fun transactions(): List { - serverParameter ?: throw ActionPrivacyUpgradeException.MissingServerParameterException() - - val privacyUpgrade = serverParameter?.parameter - if (privacyUpgrade !is ServerParameter.Parameter.PermanentPrivacyUpgrade) { - throw ActionPrivacyUpgradeException.MissingPrivacyUpgradeParameterException() - } - - /// Validate the merkle proof and ensure that the original commitment - /// accounts exist in the merkle tree provided by the server via the - /// `merkleRoot` and `merkleProof` params - - val leaf = originalCommitmentStateAccount - - val isProofValid = leaf.verifyContained( - privacyUpgrade.merkleRoot, - privacyUpgrade.merkleProof - ) - - Timber.i("isProofValid: $isProofValid") - - if (!isProofValid) { - throw ActionPrivacyUpgradeException.InvalidMerkleProofException() - } - - val timelock = source.timelock ?: throw ActionPrivacyUpgradeException.InvalidSourceException() - - // Server may provide the nonce and recentBlockhash and - // it may match the original but we shouldn't trust it. - // We'll user the original nonce and recentBlockhash that - // the original transaction used. - - val splitterAccounts = com.getcode.solana.keys.SplitterCommitmentAccounts.newInstance( - treasury = treasury, - destination = privacyUpgrade.newCommitmentDestination, - recentRoot = privacyUpgrade.merkleRoot, - transcript = privacyUpgrade.newCommitmentTranscript, - amount = privacyUpgrade.newCommitmentAmount - ) - - val transaction = TransactionBuilder.transfer( - timelockDerivedAccounts = timelock, - destination = splitterAccounts.vault.publicKey, - amount = originalAmount, - nonce = originalNonce, - recentBlockhash = originalRecentBlockhash, - kreIndex = kreIndex - ) - - return listOf(transaction) - - } - - override fun action(): TransactionService.Action { - return TransactionService.Action.newBuilder() - .apply { - this.id = - this@ActionPrivacyUpgrade.id - this.permanentPrivacyUpgrade = - TransactionService.PermanentPrivacyUpgradeAction.newBuilder().apply { - this.actionId = - this@ActionPrivacyUpgrade.originalActionID - }.build() - } - .build() - } - - companion object { - fun newInstance( - source: AccountCluster, - originalActionID: Int, - originalCommitmentStateAccount: com.getcode.solana.keys.PublicKey, - originalAmount: Kin, - originalNonce: com.getcode.solana.keys.PublicKey, - originalRecentBlockhash: com.getcode.solana.keys.Hash, - treasury: com.getcode.solana.keys.PublicKey - ): ActionPrivacyUpgrade { - return ActionPrivacyUpgrade( - id = 0, - signer = source.authority.keyPair, - source = source, - - originalActionID = originalActionID, - originalCommitmentStateAccount = originalCommitmentStateAccount, - originalAmount = originalAmount, - originalNonce = originalNonce, - originalRecentBlockhash = originalRecentBlockhash, - treasury = treasury - ) - } - } -} - -sealed class ActionPrivacyUpgradeException : Exception() { - class MissingServerParameterException : ActionPrivacyUpgradeException() - class MissingPrivacyUpgradeParameterException : ActionPrivacyUpgradeException() - class InvalidMerkleProofException : ActionPrivacyUpgradeException() - class InvalidSourceException: ActionPrivacyUpgradeException() -} diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/actions/ActionTransfer.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/actions/ActionTransfer.kt deleted file mode 100644 index 8a5562741..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/actions/ActionTransfer.kt +++ /dev/null @@ -1,160 +0,0 @@ -package com.getcode.model.intents.actions - -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import com.getcode.ed25519.Ed25519 -import com.getcode.model.Kin -import com.getcode.model.extensions.newInstance -import com.getcode.model.intents.CompactMessageArgs -import com.getcode.model.intents.ServerParameter -import com.getcode.model.intents.actions.ActionTransfer.Kind.* -import com.getcode.network.repository.toSolanaAccount -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.builder.TransactionBuilder -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.AccountCluster - -class ActionTransfer( - override var id: Int, - override var serverParameter: ServerParameter? = null, - override val signer: Ed25519.KeyPair? = null, - - val kind: Kind, - val intentId: PublicKey, - val amount: Kin, - val source: AccountCluster, - val destination: PublicKey, -) : ActionType() { - - override fun transactions(): List { - val serverParameter = serverParameter ?: return emptyList() - val timelock = source.timelock ?: return emptyList() - - val tempPrivacyParameter = serverParameter.parameter - - val resolvedDestination: PublicKey = if (tempPrivacyParameter is ServerParameter.Parameter.TempPrivacy) { - val splitterAccounts = com.getcode.solana.keys.SplitterCommitmentAccounts.newInstance( - source = source, - destination = destination, - amount = amount, - treasury = tempPrivacyParameter.treasury, - recentRoot = tempPrivacyParameter.recentRoot, - intentId = intentId, - actionId = id - ) - - splitterAccounts.vault.publicKey - } else { - destination - } - - return serverParameter.configs.map { config -> - TransactionBuilder.transfer( - timelockDerivedAccounts = timelock, - destination = resolvedDestination, - amount = amount, - nonce = config.nonce, - recentBlockhash = config.blockhash, - kreIndex = kreIndex - ) - } - } - - override fun compactMessageArgs(): List { - val configs = serverParameter?.configs ?: return emptyList() - return configs.map { - - val amountInQuarks = amount.quarks - val nonceAccount = it.nonce - val nonceValue = it.blockhash - - CompactMessageArgs.Transfer( - source = source.vaultPublicKey, - destination = destination, - amountInQuarks = amountInQuarks, - nonce = nonceAccount, - nonceValue = nonceValue - ) - } - } - - - override fun action(): TransactionService.Action { - return TransactionService.Action.newBuilder() - .apply { - this.id = - this@ActionTransfer.id - - when (kind) { - TempPrivacyTransfer -> { - this.temporaryPrivacyTransfer = - TransactionService.TemporaryPrivacyTransferAction.newBuilder().apply { - this.source = - this@ActionTransfer.source.vaultPublicKey.bytes.toSolanaAccount() - this.destination = - this@ActionTransfer.destination.bytes.toSolanaAccount() - this.authority = - this@ActionTransfer.source.authority.keyPair.publicKeyBytes.toSolanaAccount() - this.amount = - this@ActionTransfer.amount.quarks - }.build() - } - TempPrivacyExchange -> { - this.temporaryPrivacyExchange = - TransactionService.TemporaryPrivacyExchangeAction.newBuilder().apply { - this.source = - this@ActionTransfer.source.vaultPublicKey.bytes.toSolanaAccount() - this.destination = - this@ActionTransfer.destination.bytes.toSolanaAccount() - this.authority = - this@ActionTransfer.source.authority.keyPair.publicKeyBytes.toSolanaAccount() - this.amount = - this@ActionTransfer.amount.quarks - }.build() - } - NoPrivacyTransfer -> { - this.noPrivacyTransfer = - TransactionService.NoPrivacyTransferAction.newBuilder().apply { - this.source = - this@ActionTransfer.source.vaultPublicKey.bytes.toSolanaAccount() - this.destination = - this@ActionTransfer.destination.bytes.toSolanaAccount() - this.authority = - this@ActionTransfer.source.authority.keyPair.publicKeyBytes.toSolanaAccount() - this.amount = - this@ActionTransfer.amount.quarks - }.build() - } - } - }.build() - - - } - - companion object { - fun newInstance( - kind: Kind, - intentId: PublicKey, - amount: Kin, - source: AccountCluster, - destination: PublicKey - ): ActionTransfer { - return ActionTransfer( - id = 0, - signer = source.authority.keyPair, - kind = kind, - intentId = intentId, - amount = amount, - source = source, - destination = destination - ) - } - - const val configCountRequirement: Int = 1 - } - - enum class Kind { - TempPrivacyTransfer, - TempPrivacyExchange, - NoPrivacyTransfer, - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/actions/ActionType.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/actions/ActionType.kt deleted file mode 100644 index 8f3d28268..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/actions/ActionType.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.getcode.model.intents.actions - -import com.getcode.crypt.Sha256Hash -import com.getcode.ed25519.Ed25519 -import com.getcode.model.intents.CompactMessage -import com.getcode.model.intents.CompactMessageArgs -import com.getcode.model.intents.ServerParameter -import com.getcode.solana.SolanaTransaction -import com.getcode.utils.toByteArray -import org.kin.sdk.base.models.toUTF8Bytes -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService - -abstract class ActionType { - abstract var id: Int - abstract var serverParameter: ServerParameter? - abstract val signer: Ed25519.KeyPair? - - //abstract var configCountRequirement: Int - - abstract fun transactions(): List - open fun compactMessageArgs(): List = emptyList() - fun compactMessages(): List { - return compactMessageArgs().map { args -> - when (args) { - is CompactMessageArgs.Transfer -> { - - val data = mutableListOf() - data.addAll("transfer".toUTF8Bytes().toList()) - data.addAll(args.source.bytes) - data.addAll(args.destination.bytes) - data.addAll(args.amountInQuarks.toByteArray().toList()) - data.addAll(args.nonce.bytes) - data.addAll(args.nonceValue.bytes) - - Sha256Hash.hash(data.toByteArray()) - } - } - } - } - - fun signatures(): List { - return signer?.let { s -> - compactMessages().map { - com.getcode.solana.keys.Signature(Ed25519.sign(it, s).toList()) } - }.orEmpty() - } - - abstract fun action(): TransactionService.Action - - companion object { - const val kreIndex: Int = 268 - } -} - -fun List.numberActions(): List { - return this.mapIndexed { index, _ -> - this[index].apply { this.id = index } - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/actions/ActionWithdraw.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/actions/ActionWithdraw.kt deleted file mode 100644 index 9843c7679..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/intents/actions/ActionWithdraw.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.getcode.model.intents.actions - -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import com.getcode.ed25519.Ed25519 -import com.getcode.model.Kin -import com.getcode.model.intents.ServerParameter -import com.getcode.model.toPublicKey -import com.getcode.network.repository.toSolanaAccount -import com.getcode.services.model.ExtendedMetadata -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.builder.TransactionBuilder -import com.getcode.solana.organizer.AccountCluster - -class ActionWithdraw( - override var id: Int, - override var serverParameter: ServerParameter? = null, - override val signer: Ed25519.KeyPair?, - - val kind: Kind, - - val cluster: AccountCluster, - val destination: com.getcode.solana.keys.PublicKey, - val legacy: Boolean, - val metadata: ExtendedMetadata? = null, -) : ActionType() { - - override fun transactions(): List { - val timelock = cluster.timelock ?: return emptyList() - return serverParameter?.configs?.map { config -> - TransactionBuilder.closeDormantAccount( - authority = cluster.authority.keyPair.publicKeyBytes.toPublicKey(), - timelockDerivedAccounts = timelock, - destination = destination, - nonce = config.nonce, - recentBlockhash = config.blockhash, - kreIndex = kreIndex, - legacy = legacy, - metadata = metadata, - ) - }.orEmpty() - } - - override fun action(): TransactionService.Action { - return TransactionService.Action.newBuilder() - .apply { - this.id = - this@ActionWithdraw.id - - when (kind) { - is Kind.NoPrivacyWithdraw -> { - this.noPrivacyWithdraw = - TransactionService.NoPrivacyWithdrawAction.newBuilder().apply { - this.authority = - this@ActionWithdraw.cluster.authority.keyPair.publicKeyBytes.toSolanaAccount() - this.source = - this@ActionWithdraw.cluster.vaultPublicKey.bytes.toSolanaAccount() - this.destination = - this@ActionWithdraw.destination.bytes.toSolanaAccount() - this.amount = - this@ActionWithdraw.kind.amount.quarks - this.shouldClose = - true - }.build() - } - } - }.build() - } - - companion object { - fun newInstance( - kind: Kind, - cluster: AccountCluster, - destination: com.getcode.solana.keys.PublicKey, - legacy: Boolean = false, - metadata: ExtendedMetadata? = null, - ): ActionWithdraw { - return ActionWithdraw( - id = 0, - signer = cluster.authority.keyPair, - - kind = kind, - cluster = cluster, - destination = destination, - legacy = legacy, - metadata = metadata - ) - } - - const val configCountRequirement: Int = 1 - } - - sealed class Kind { - data class NoPrivacyWithdraw(val amount: Kin): Kind() - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/protomapping/Platform.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/protomapping/Platform.kt deleted file mode 100644 index 5fa7b3c80..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/protomapping/Platform.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.getcode.model.protomapping - -import com.codeinc.gen.chat.v2.ChatService -import com.getcode.model.chat.Platform -import com.getcode.model.chat.Platform.Unknown -import com.getcode.model.chat.Platform.entries - -operator fun Platform.Companion.invoke(proto: ChatService.Platform): Platform { - return runCatching { entries[proto.ordinal] }.getOrNull() ?: Unknown -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/protomapping/Pointer.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/protomapping/Pointer.kt deleted file mode 100644 index d78cd17eb..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/protomapping/Pointer.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.getcode.model.protomapping - -import com.codeinc.gen.chat.v2.ChatService -import com.getcode.model.chat.Pointer -import com.getcode.model.uuid - -operator fun Pointer.Companion.invoke(proto: ChatService.Pointer): Pointer { - val memberId = proto.memberId.value.toList() - val messageId = proto.value.value.toList().uuid ?: return Pointer.Unknown(memberId) - - return when (proto.type) { - ChatService.PointerType.UNKNOWN_POINTER_TYPE -> Pointer.Unknown(proto.memberId.value.toList()) - ChatService.PointerType.READ -> Pointer.Read(memberId, messageId) - ChatService.PointerType.DELIVERED -> Pointer.Delivered(memberId, messageId) - ChatService.PointerType.SENT -> Pointer.Sent(memberId, messageId) - ChatService.PointerType.UNRECOGNIZED -> Pointer.Unknown(memberId) - else -> Pointer.Unknown(memberId) - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/protomapping/Reference.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/protomapping/Reference.kt deleted file mode 100644 index ccb8faefc..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/protomapping/Reference.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.getcode.model.protomapping - -import com.codeinc.gen.chat.v2.ChatService.ExchangeDataContent -import com.codeinc.gen.chat.v2.ChatService.ExchangeDataContent.ReferenceCase -import com.getcode.model.chat.Reference -import com.getcode.model.chat.Reference.IntentId -import com.getcode.model.chat.Reference.NoneSet -import com.getcode.model.chat.Reference.Signature - -operator fun Reference.Companion.invoke(proto: ExchangeDataContent): Reference { - return when (proto.referenceCase) { - ReferenceCase.INTENT -> IntentId(proto.intent.value.toByteArray().toList()) - ReferenceCase.SIGNATURE -> Signature(proto.signature.value.toByteArray().toList()) - ReferenceCase.REFERENCE_NOT_SET -> NoneSet - null -> NoneSet - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/protomapping/TwitterUser.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/protomapping/TwitterUser.kt deleted file mode 100644 index af654cb05..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/protomapping/TwitterUser.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.getcode.model.protomapping - -import com.codeinc.gen.user.v1.CodeIdentityService as IdentityService -import com.getcode.model.CurrencyCode -import com.getcode.model.Fiat -import com.getcode.model.TwitterUser -import com.getcode.model.TwitterUser.VerificationStatus -import com.getcode.solana.keys.PublicKey - -operator fun TwitterUser.Companion.invoke(proto: IdentityService.TwitterUser): TwitterUser? { - val avatarUrl = proto.profilePicUrl - - val tipAddress = runCatching { PublicKey.fromByteString(proto.tipAddress.value) }.getOrNull() ?: return null - - return TwitterUser( - username = proto.username, - displayName = proto.name, - imageUrl = avatarUrl, - followerCount = proto.followerCount, - tipAddress = tipAddress, - verificationStatus = VerificationStatus.entries.getOrNull(proto.verifiedTypeValue) ?: VerificationStatus.unknown, - costOfFriendship = Fiat(currency = CurrencyCode.USD, amount = 1.00), - isFriend = false, - chatId = emptyList() - ) -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/model/protomapping/Verb.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/model/protomapping/Verb.kt deleted file mode 100644 index be40723fa..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/model/protomapping/Verb.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.getcode.model.protomapping - -import com.codeinc.gen.chat.v1.CodeChatService -import com.getcode.model.chat.Verb -import com.getcode.model.chat.Verb.Deposited -import com.getcode.model.chat.Verb.Gave -import com.getcode.model.chat.Verb.Paid -import com.getcode.model.chat.Verb.Purchased -import com.getcode.model.chat.Verb.Received -import com.getcode.model.chat.Verb.ReceivedTip -import com.getcode.model.chat.Verb.Returned -import com.getcode.model.chat.Verb.Sent -import com.getcode.model.chat.Verb.SentTip -import com.getcode.model.chat.Verb.Spent -import com.getcode.model.chat.Verb.Unknown -import com.getcode.model.chat.Verb.Withdrew - -fun Verb.Companion.invoke(proto: CodeChatService.ExchangeDataContent.Verb): Verb { - return when (proto) { - com.codeinc.gen.chat.v1.CodeChatService.ExchangeDataContent.Verb.UNKNOWN -> Unknown - com.codeinc.gen.chat.v1.CodeChatService.ExchangeDataContent.Verb.GAVE -> Gave - com.codeinc.gen.chat.v1.CodeChatService.ExchangeDataContent.Verb.RECEIVED -> Received - com.codeinc.gen.chat.v1.CodeChatService.ExchangeDataContent.Verb.WITHDREW -> Withdrew - com.codeinc.gen.chat.v1.CodeChatService.ExchangeDataContent.Verb.DEPOSITED -> Deposited - com.codeinc.gen.chat.v1.CodeChatService.ExchangeDataContent.Verb.SENT -> Sent - com.codeinc.gen.chat.v1.CodeChatService.ExchangeDataContent.Verb.RETURNED -> Returned - com.codeinc.gen.chat.v1.CodeChatService.ExchangeDataContent.Verb.SPENT -> Spent - com.codeinc.gen.chat.v1.CodeChatService.ExchangeDataContent.Verb.PAID -> Paid - com.codeinc.gen.chat.v1.CodeChatService.ExchangeDataContent.Verb.PURCHASED -> Purchased - com.codeinc.gen.chat.v1.CodeChatService.ExchangeDataContent.Verb.UNRECOGNIZED -> Unknown - com.codeinc.gen.chat.v1.CodeChatService.ExchangeDataContent.Verb.RECEIVED_TIP -> ReceivedTip - com.codeinc.gen.chat.v1.CodeChatService.ExchangeDataContent.Verb.SENT_TIP -> SentTip - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/BalanceController.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/BalanceController.kt deleted file mode 100644 index ea9de4844..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/BalanceController.kt +++ /dev/null @@ -1,253 +0,0 @@ -package com.getcode.network - -import androidx.compose.runtime.staticCompositionLocalOf -import com.getcode.model.Currency -import com.getcode.model.CurrencyCode -import com.getcode.model.Rate -import com.getcode.network.client.TransactionReceiver -import com.getcode.network.exchange.Exchange -import com.getcode.network.repository.AccountRepository -import com.getcode.network.repository.BalanceRepository -import com.getcode.network.repository.TransactionRepository -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Tray -import com.getcode.utils.FormatUtils -import com.getcode.utils.trace -import io.reactivex.rxjava3.core.Completable -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import timber.log.Timber -import xyz.flipchat.services.PaymentController -import xyz.flipchat.services.user.AuthState -import xyz.flipchat.services.user.UserManager -import java.util.Locale -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import kotlin.coroutines.resume - -data class BalanceDisplay( - val marketValue: Double = 0.0, - val formattedValue: String = "", - val currency: Currency? = null, -) - -open class BalanceController @Inject constructor( - exchange: Exchange, - networkObserver: com.getcode.utils.network.NetworkConnectivityListener, - private val balanceRepository: BalanceRepository, - private val transactionRepository: TransactionRepository, - private val accountRepository: AccountRepository, - private val transactionReceiver: TransactionReceiver, - private val getCurrencyFromCode: (CurrencyCode?) -> Currency?, - private val userManager: UserManager, - val suffix: (Currency?) -> String, -) { - private val scope = CoroutineScope(Dispatchers.IO) - fun observeRawBalance(): Flow = balanceRepository.balanceFlow - - val rawBalance: Double - get() = balanceRepository.balanceFlow.value - - private val _balanceDisplay = MutableStateFlow(null) - - val formattedBalance: StateFlow - get() = _balanceDisplay - .stateIn(scope, SharingStarted.Eagerly, BalanceDisplay()) - - fun reset() { - balanceRepository.clearBalance() - _balanceDisplay.value = null - } - - init { - userManager.state - .map { it.authState } - .filterIsInstance() - .flatMapLatest { networkObserver.state } - .map { it.connected } - .onEach { connected -> - if (connected) { - com.getcode.utils.network.retryable { this.getBalance() } - } - } - .flatMapLatest { - combine( - exchange.observeLocalRate() - .flowOn(Dispatchers.IO) - .onEach { - val display = _balanceDisplay.value ?: BalanceDisplay() - _balanceDisplay.value = - display.copy(currency = getCurrencyFromCode(it.currency)) - } - .onEach { exchange.fetchRatesIfNeeded() }, - balanceRepository.balanceFlow, - ) { rate, balance -> - rate to balance.coerceAtLeast(0.0) - }.map { (rate, balance) -> - refreshBalance(balance, rate) - } - }.distinctUntilChanged().onEach { (marketValue, amountText) -> - val display = _balanceDisplay.value ?: BalanceDisplay() - _balanceDisplay.value = - display.copy(marketValue = marketValue, formattedValue = amountText) - }.launchIn(scope) - } - - fun setTray(organizer: Organizer, tray: Tray) { - organizer.set(tray) - balanceRepository.setBalance(organizer.availableBalance.toKinTruncatingLong().toDouble()) - } - - fun getBalance(): Completable { - trace("fetchBalance") - val owner = userManager.keyPair - ?: return Completable.error(IllegalStateException("Missing Owner")) - - fun getTokenAccountInfos(): Completable { - return accountRepository.getTokenAccountInfos(owner) - .flatMapCompletable { infos -> - val organizer = userManager.organizer ?: return@flatMapCompletable Completable.error( - IllegalStateException("Missing Organizer") - ) - scope.launch { - organizer.setAccountInfo(infos) - userManager.set(organizer = organizer) - } - - balanceRepository.setBalance(organizer.availableBalance.toKinValueDouble()) - transactionReceiver.receiveFromIncomingCompletable(organizer) - } - .timeout(15, TimeUnit.SECONDS) - } - - return getTokenAccountInfos() - .doOnSubscribe { - Timber.i("Fetching Balance account info") - } - .onErrorResumeNext { - Timber.i("Error: ${it.javaClass.simpleName} ${it.cause}") - val organizer = userManager.organizer ?: return@onErrorResumeNext Completable.error( - IllegalStateException("Missing Organizer") - ) - - when (it) { - is AccountRepository.FetchAccountInfosException.NotFoundException -> { - transactionRepository.createAccounts( - organizer = organizer - ).ignoreElement().concatWith(getTokenAccountInfos()) - } - - else -> { - Completable.error(it) - } - } - } - } - - - suspend fun fetchBalance(): Result { - Timber.d("fetching balance") - val owner = userManager.keyPair - ?: return Result.failure(IllegalStateException("Missing Owner")) - - try { - val accountInfoResult = accountRepository.getTokenAccountInfosSuspend(owner) - accountInfoResult.exceptionOrNull()?.let { - throw it - } - - val accountInfo = accountInfoResult.getOrNull().orEmpty() - val organizer = userManager.organizer - ?: return Result.failure(IllegalStateException("Missing Organizer")) - - - organizer.setAccountInfo(accountInfo) - userManager.set(organizer = organizer) - if (organizer.isUnuseable) { - userManager.didDetectUnlockedAccount() - } - - balanceRepository.setBalance(organizer.availableBalance.toKinValueDouble()) - transactionReceiver.receiveFromIncoming(organizer) - scope.launch { - transactionRepository.swapIfNeeded(organizer) - } - - return Result.success(Unit) - } catch (ex: Exception) { - Timber.i("Error: ${ex.javaClass.simpleName} ${ex.message}") - val organizer = userManager.organizer - ?: return Result.failure(IllegalStateException("Missing Organizer")) - - return suspendCancellableCoroutine { cont -> - when (ex) { - is AccountRepository.FetchAccountInfosException.NotFoundException -> { - transactionRepository.createAccounts( - organizer = organizer - ).doOnError { cont.resume(Result.failure(it)) } - .doAfterSuccess { cont.resume(Result.success(Unit)) } - .subscribe() - } - else -> { - cont.resume(Result.failure(ex)) - } - } - } - } - } - - private fun refreshBalance(balance: Double, rate: Rate): Pair { - val preferredCurrency = getCurrencyFromCode(rate.currency) - val fiatValue = FormatUtils.getFiatValue(balance, rate.fx) - - val prefix = - formatPrefix(preferredCurrency).takeIf { it != preferredCurrency?.code }.orEmpty() - - val amountText = StringBuilder().apply { - append(prefix) - append(formatAmount(fiatValue, preferredCurrency)) - val suffix = suffix(preferredCurrency) - if (suffix.isNotEmpty()) { - append(" ") - append(suffix) - } - }.toString() - - Timber.d("formatted balance is now $prefix $amountText in ${preferredCurrency?.code}") - - return fiatValue to amountText - } - - private fun formatPrefix(selectedCurrency: Currency?): String { - if (selectedCurrency == null) return "" - return if (!isKin(selectedCurrency)) selectedCurrency.symbol else "" - } - - private fun isKin(selectedCurrency: Currency): Boolean = - selectedCurrency.code == com.getcode.model.CurrencyCode.KIN.name - - private fun formatAmount(amount: Double, currency: Currency?): String { - return if (amount % 1 == 0.0 || currency?.code == com.getcode.model.CurrencyCode.KIN.name) { - String.format(Locale.getDefault(), "%,.0f", amount) - } else { - String.format(Locale.getDefault(), "%,.2f", amount) - } - } -} - -val LocalBalanceController = staticCompositionLocalOf { null } diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/api/AccountApi.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/api/AccountApi.kt deleted file mode 100644 index 466be921c..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/api/AccountApi.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.getcode.network.api - -import com.codeinc.gen.account.v1.AccountGrpc -import com.codeinc.gen.account.v1.CodeAccountService as AccountService -import com.codeinc.gen.account.v1.CodeAccountService.LinkAdditionalAccountsRequest -import com.codeinc.gen.account.v1.CodeAccountService.LinkAdditionalAccountsResponse -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.network.repository.toSolanaAccount -import com.getcode.services.network.core.GrpcApi -import com.getcode.utils.sign -import io.grpc.ManagedChannel -import io.reactivex.rxjava3.core.Scheduler -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import xyz.flipchat.services.internal.annotations.PaymentsManagedChannel -import javax.inject.Inject - - -class AccountApi @Inject constructor( - @PaymentsManagedChannel - managedChannel: ManagedChannel, - private val scheduler: Scheduler = Schedulers.io(), -) : GrpcApi(managedChannel) { - private val api = AccountGrpc.newStub(managedChannel).withWaitForReady() - - fun isCodeAccount(owner: KeyPair): Flow { - val request = AccountService.IsCodeAccountRequest.newBuilder() - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .apply { setSignature(sign(owner)) } - .build() - - return api::isCodeAccount - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - fun getTokenAccountInfos(request: AccountService.GetTokenAccountInfosRequest): Single { - return api::getTokenAccountInfos - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun getTokenAccountInfosFlow(request: AccountService.GetTokenAccountInfosRequest): Flow { - return api::getTokenAccountInfos - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - fun linkAdditionalAccounts(owner: KeyPair, linkedAccount: KeyPair): Flow { - val request = LinkAdditionalAccountsRequest.newBuilder() - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .setSwapAuthority(linkedAccount.publicKeyBytes.toSolanaAccount()) - .let { it.addAllSignatures(listOf(it.sign(owner), it.sign(linkedAccount))) } - .build() - - return api::linkAdditionalAccounts - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/api/CurrencyApi.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/api/CurrencyApi.kt deleted file mode 100644 index 142b3b3dc..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/api/CurrencyApi.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.getcode.network.api - -import com.codeinc.gen.currency.v1.CurrencyGrpc -import com.codeinc.gen.currency.v1.CodeCurrencyService as CurrencyService -import com.getcode.services.network.core.GrpcApi -import io.grpc.ManagedChannel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import xyz.flipchat.services.internal.annotations.PaymentsManagedChannel -import javax.inject.Inject - -class CurrencyApi @Inject constructor( - @PaymentsManagedChannel - managedChannel: ManagedChannel, -) : GrpcApi(managedChannel) { - private val api = CurrencyGrpc.newStub(managedChannel).withWaitForReady() - - fun getRates(request: CurrencyService.GetAllRatesRequest = CurrencyService.GetAllRatesRequest.getDefaultInstance()): Flow = - api::getAllRates - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) -} diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/api/DeviceApi.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/api/DeviceApi.kt deleted file mode 100644 index 34b49bbc8..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/api/DeviceApi.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.getcode.network.api - -import com.codeinc.gen.common.v1.CodeModel as Model -import com.codeinc.gen.device.v1.DeviceGrpc -import com.codeinc.gen.device.v1.CodeDeviceService as DeviceService -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.network.repository.sign -import com.getcode.network.repository.toSolanaAccount -import com.getcode.services.network.core.GrpcApi -import io.grpc.ManagedChannel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import xyz.flipchat.services.internal.annotations.PaymentsManagedChannel -import javax.inject.Inject - -class DeviceApi @Inject constructor( - @PaymentsManagedChannel - managedChannel: ManagedChannel, -): GrpcApi(managedChannel) { - - private val api = DeviceGrpc.newStub(managedChannel).withWaitForReady() - - fun registerInstallation(owner: KeyPair, installationId: String) : Flow { - val request = DeviceService.RegisterLoggedInAccountsRequest.newBuilder() - .setAppInstall(Model.AppInstallId.newBuilder().setValue(installationId)) - .addOwners(owner.publicKeyBytes.toSolanaAccount()) - .apply { - addAllSignatures(listOf(sign(owner))) - } - .build() - - return api::registerLoggedInAccounts - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - - fun fetchInstallationAccounts(installationId: String): Flow { - val request = DeviceService.GetLoggedInAccountsRequest.newBuilder() - .setAppInstall(Model.AppInstallId.newBuilder().setValue(installationId)) - .build() - - return api::getLoggedInAccounts - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/api/IdentityApi.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/api/IdentityApi.kt deleted file mode 100644 index b6b54ba4f..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/api/IdentityApi.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.getcode.network.api - -import com.codeinc.gen.user.v1.IdentityGrpc -import com.codeinc.gen.user.v1.CodeIdentityService as IdentityService -import com.codeinc.gen.user.v1.CodeIdentityService.GetTwitterUserRequest -import com.codeinc.gen.user.v1.CodeIdentityService.LoginToThirdPartyAppRequest -import com.codeinc.gen.user.v1.CodeIdentityService.UpdatePreferencesRequest -import com.getcode.services.network.core.GrpcApi -import io.grpc.ManagedChannel -import io.reactivex.rxjava3.annotations.NonNull -import io.reactivex.rxjava3.core.Scheduler -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.flowOn -import xyz.flipchat.services.internal.annotations.PaymentsManagedChannel -import javax.inject.Inject - -class IdentityApi @Inject constructor( - @PaymentsManagedChannel - managedChannel: ManagedChannel, - private val scheduler: Scheduler = Schedulers.io(), -) : GrpcApi(managedChannel) { - private val api = IdentityGrpc.newStub(managedChannel).withWaitForReady() - - fun linkAccount(request: IdentityService.LinkAccountRequest): @NonNull Single { - return api::linkAccount - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun unlinkAccount(request: IdentityService.UnlinkAccountRequest): @NonNull Single { - return api::unlinkAccount - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun getUser(request: IdentityService.GetUserRequest): @NonNull Single { - return api::getUser - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun loginToThirdParty(request: LoginToThirdPartyAppRequest) = api::loginToThirdPartyApp - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - - fun updatePreferences(request: UpdatePreferencesRequest) = api::updatePreferences - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - - fun fetchTwitterUser(request: GetTwitterUserRequest) = api::getTwitterUser - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) -} diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/api/MessagingApi.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/api/MessagingApi.kt deleted file mode 100644 index 64b5c6a99..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/api/MessagingApi.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.getcode.network.api - -import com.codeinc.gen.messaging.v1.MessagingGrpc -import com.codeinc.gen.messaging.v1.CodeMessagingService.AckMessagesRequest -import com.codeinc.gen.messaging.v1.CodeMessagingService.AckMesssagesResponse -import com.codeinc.gen.messaging.v1.CodeMessagingService.OpenMessageStreamRequest -import com.codeinc.gen.messaging.v1.CodeMessagingService.OpenMessageStreamResponse -import com.codeinc.gen.messaging.v1.CodeMessagingService.PollMessagesRequest -import com.codeinc.gen.messaging.v1.CodeMessagingService.SendMessageRequest -import com.codeinc.gen.messaging.v1.CodeMessagingService.SendMessageResponse -import com.getcode.services.network.core.GrpcApi -import io.grpc.ManagedChannel -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Scheduler -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import xyz.flipchat.services.internal.annotations.PaymentsManagedChannel -import javax.inject.Inject - -class MessagingApi @Inject constructor( - @PaymentsManagedChannel - managedChannel: ManagedChannel, - private val scheduler: Scheduler = Schedulers.io() -) : GrpcApi(managedChannel) { - private val api = MessagingGrpc.newStub(managedChannel).withWaitForReady() - - fun openMessageStream(request: OpenMessageStreamRequest): Flowable = - api::openMessageStream - .callAsCancellableFlowable(request) - .subscribeOn(scheduler) - - fun ackMessages(request: AckMessagesRequest): Single = - api::ackMessages - .callAsSingle(request) - .subscribeOn(scheduler) - - fun sendMessage(request: SendMessageRequest): Single = - api::sendMessage - .callAsSingle(request) - .subscribeOn(scheduler) - - fun pollMessages(request: PollMessagesRequest) = api::pollMessages - .callAsSingle(request) - .subscribeOn(scheduler) -} diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/api/TransactionApiV2.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/api/TransactionApiV2.kt deleted file mode 100644 index b7b6b8c1f..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/api/TransactionApiV2.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.getcode.network.api - -import com.codeinc.gen.transaction.v2.CodeTransactionService -import com.codeinc.gen.transaction.v2.TransactionGrpc -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import com.getcode.services.network.core.GrpcApi -import io.grpc.ManagedChannel -import io.grpc.stub.StreamObserver -import io.reactivex.rxjava3.core.Scheduler -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import xyz.flipchat.services.internal.annotations.PaymentsManagedChannel -import javax.inject.Inject - -class TransactionApiV2 @Inject constructor( - @PaymentsManagedChannel - managedChannel: ManagedChannel, - private val scheduler: Scheduler = Schedulers.io(), -) : GrpcApi(managedChannel) { - private val api = TransactionGrpc.newStub(managedChannel).withWaitForReady() - - fun submitIntent(request: StreamObserver): StreamObserver { - return api.submitIntent(request) - } - - fun airdrop(request: TransactionService.AirdropRequest): Single { - return api::airdrop - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun getPrivacyUpgradeStatus(request: TransactionService.GetPrivacyUpgradeStatusRequest): Single { - return api::getPrivacyUpgradeStatus - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun getLimits(request: TransactionService.GetLimitsRequest): Single { - return api::getLimits - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun getIntentMetadata(request: TransactionService.GetIntentMetadataRequest): Single { - return api::getIntentMetadata - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun canWithdrawToAccount(request: TransactionService.CanWithdrawToAccountRequest): Single { - return api::canWithdrawToAccount - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun getPrioritizedIntentsForPrivacyUpgrade(request: TransactionService.GetPrioritizedIntentsForPrivacyUpgradeRequest): Single { - return api::getPrioritizedIntentsForPrivacyUpgrade - .callAsSingle(request) - .subscribeOn(scheduler) - } - - fun swap(observer: StreamObserver): StreamObserver { - return api.swap(observer) - } - - fun declareFiatPurchase(request: TransactionService.DeclareFiatOnrampPurchaseAttemptRequest): Flow { - return api::declareFiatOnrampPurchaseAttempt - .callAsCancellableFlow(request) - .flowOn(Dispatchers.IO) - } - -} diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/client/Client.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/client/Client.kt deleted file mode 100644 index d2b35dfb0..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/client/Client.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.getcode.network.client - -import com.getcode.network.BalanceController -import com.getcode.network.exchange.Exchange -import com.getcode.network.repository.AccountRepository -import com.getcode.network.repository.MessagingRepository -import com.getcode.network.repository.TransactionRepository -import com.getcode.utils.ErrorUtils -import com.getcode.utils.network.NetworkConnectivityListener -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import timber.log.Timber -import xyz.flipchat.services.user.AuthState -import xyz.flipchat.services.user.UserManager -import java.util.Timer -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.concurrent.fixedRateTimer -import kotlin.time.Duration.Companion.seconds - -internal const val TAG = "Client" - -@Singleton -class Client @Inject constructor( - internal val userManager: UserManager, - internal val transactionRepository: TransactionRepository, - internal val messagingRepository: MessagingRepository, - internal val balanceController: BalanceController, - internal val accountRepository: AccountRepository, - internal val exchange: Exchange, - internal val transactionReceiver: TransactionReceiver, - internal val networkObserver: NetworkConnectivityListener, -) { - private val scope = CoroutineScope(Dispatchers.IO) - - private var pollTimer: Timer? = null - private var lastPoll: Long = 0L - - private fun startPollTimerWhenAuthenticated() { - Timber.tag(TAG).i("Creating poll timer") - scope.launch { - while(true) { - delay(1.seconds) - if (userManager.authState is AuthState.LoggedIn) { - startPollTimer() - break - } - } - } - } - - private fun startPollTimer() { - pollTimer?.cancel() - pollTimer = fixedRateTimer("pollTimer", false, 0, 1000 * 10) { - scope.launch { - val time = System.currentTimeMillis() - val isPastThrottle = time - lastPoll > 1000 * 10 || lastPoll == 0L - - if (userManager.authState is AuthState.LoggedIn && isPastThrottle) { - Timber.tag(TAG).i("Timer Polling") - poll() - lastPoll = time - } - } - } - } - - private suspend fun poll() { - if (networkObserver.isConnected) { - try { - balanceController.getBalance() - exchange.fetchRatesIfNeeded() - } catch (e: Exception) { - ErrorUtils.handleError(e) - } - fetchLimits().andThen(fetchPrivacyUpgrades()).blockingSubscribe() - } - } - - fun startTimer() { - startPollTimerWhenAuthenticated() - } - - fun stopTimer() { - Timber.tag(TAG).i("Cancelling Poller") - pollTimer?.cancel() - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/client/Client_Transaction.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/client/Client_Transaction.kt deleted file mode 100644 index f644c1a84..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/client/Client_Transaction.kt +++ /dev/null @@ -1,593 +0,0 @@ -package com.getcode.network.client - -import android.annotation.SuppressLint -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.AccountInfo -import com.getcode.model.Domain -import com.getcode.model.Fee -import com.getcode.model.ID -import com.getcode.model.IntentMetadata -import com.getcode.model.Kin -import com.getcode.model.KinAmount -import com.getcode.model.Limits -import com.getcode.model.Rate -import com.getcode.model.generate -import com.getcode.model.intents.IntentDeposit -import com.getcode.model.intents.IntentEstablishRelationship -import com.getcode.model.intents.IntentPrivateTransfer -import com.getcode.model.intents.IntentPublicTransfer -import com.getcode.model.intents.IntentRemoteSend -import com.getcode.model.intents.SwapIntent -import com.getcode.network.repository.TransactionRepository -import com.getcode.network.repository.WithdrawException -import com.getcode.network.repository.initiateSwap -import com.getcode.services.model.ExtendedMetadata -import com.getcode.services.utils.flowInterval -import com.getcode.services.utils.mapResult -import com.getcode.services.utils.toKotlinResult -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.GiftCardAccount -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Relationship -import com.getcode.utils.TraceType -import com.getcode.utils.trace -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.takeWhile -import timber.log.Timber -import java.util.Calendar -import java.util.GregorianCalendar -import java.util.UUID -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger -import kotlin.math.min - - -fun Client.transfer( - amount: KinAmount, - fee: Kin, - additionalFees: List, - organizer: Organizer, - rendezvousKey: PublicKey, - destination: PublicKey, - isWithdrawal: Boolean, - metadata: ExtendedMetadata? = null, -): Completable { - return transferWithResultSingle( - amount, - fee, - additionalFees, - organizer, - rendezvousKey, - destination, - isWithdrawal - ).flatMapCompletable { - if (it.isSuccess) { - Timber.d("transfer successful") - Completable.complete() - } else { - Completable.error(it.exceptionOrNull() ?: Throwable("Failed to complete transfer")) - } - } -} - -suspend fun Client.publicPayment( - amount: KinAmount, - organizer: Organizer, - destination: PublicKey, - extendedMetadata: ExtendedMetadata? = null, -): Result = getTransferPreflightAction(amount.kin).toKotlinResult() - .mapResult { transactionRepository.publicPayment(amount, organizer, destination, extendedMetadata) } - .map { it.id.bytes } - -fun Client.transferWithResultSingle( - amount: KinAmount, - fee: Kin, - additionalFees: List, - organizer: Organizer, - rendezvousKey: PublicKey, - destination: PublicKey, - isWithdrawal: Boolean, - metadata: ExtendedMetadata? = null, -): Single> { - return getTransferPreflightAction(amount.kin) - .andThen(Single.defer { - transactionRepository.transfer( - amount, - fee, - additionalFees, - organizer, - rendezvousKey, - destination, - isWithdrawal, - metadata - ) - }) - .map { - if (it is IntentPrivateTransfer) { - balanceController.setTray(organizer, it.resultTray) - } - it - }.map { Result.success(it.id.bytes) } - .onErrorReturn { Result.failure(it) } -} - -fun Client.transferWithResult( - amount: KinAmount, - fee: Kin, - additionalFees: List, - organizer: Organizer, - rendezvousKey: PublicKey, - destination: PublicKey, - isWithdrawal: Boolean, - metadata: ExtendedMetadata? = null, -): Result { - return transferWithResultSingle( - amount = amount, - fee = fee, - additionalFees = additionalFees, - organizer = organizer, - rendezvousKey = rendezvousKey, - destination = destination, - isWithdrawal = isWithdrawal, - metadata = metadata, - ).blockingGet() -} - - -fun Client.sendRemotely( - amount: KinAmount, - rendezvousKey: PublicKey, - giftCard: GiftCardAccount -): Completable { - return Completable.defer { - val organizer = userManager.organizer ?: return@defer Completable.complete() - val truncatedAmount = amount.truncating() - getTransferPreflightAction(truncatedAmount.kin) - .andThen( - sendRemotely( - amount = truncatedAmount, - organizer = organizer, - rendezvousKey = rendezvousKey, - giftCard = giftCard - ) - ) - } -} - -fun Client.receiveRemote(giftCard: GiftCardAccount): Single { - // Before we can receive from the gift card account - // we have to determine the balance of the account - return accountRepository.getTokenAccountInfos(giftCard.cluster.authority.keyPair) - .flatMap { infos -> - val info: AccountInfo = infos.values.firstOrNull() - ?: return@flatMap Single.error(RemoteSendException.FailedToFetchGiftCardInfoException()) - val kinAmount = info.originalKinAmount - ?: return@flatMap Single.error(RemoteSendException.GiftCardBalanceNotFoundException()) - - if (info.claimState == AccountInfo.ClaimState.Claimed) { - return@flatMap Single.error(RemoteSendException.GiftCardClaimedException()) - } - - if (info.claimState == AccountInfo.ClaimState.Claimed || info.claimState == AccountInfo.ClaimState.Unknown) { - return@flatMap Single.error(RemoteSendException.GiftCardExpiredException()) - } - - val organizer = userManager.organizer ?: return@flatMap Single.error(Throwable("No organizer")) - - transactionReceiver.receiveRemotely( - giftCard = giftCard, - amount = info.balance, - organizer = organizer, - isVoiding = false - ) - .toSingleDefault(kinAmount) - } -} - -@SuppressLint("CheckResult") -suspend fun Client.cancelRemoteSend( - giftCard: GiftCardAccount, - amount: Kin, - organizer: Organizer -): Result = runCatching { - transactionReceiver.receiveRemotely( - amount = amount, - organizer = organizer, - giftCard = giftCard, - isVoiding = true - ).blockingAwait() - - balanceController.getBalance() - - balanceController.rawBalance -} - - -sealed class RemoteSendException : Exception() { - class FailedToFetchGiftCardInfoException : RemoteSendException() - class GiftCardBalanceNotFoundException : RemoteSendException() - class GiftCardClaimedException : RemoteSendException() - class GiftCardExpiredException : RemoteSendException() -} - -fun Client.withdrawExternally( - amount: KinAmount, - organizer: Organizer, - destination: PublicKey, -): Completable { - if (amount.kin.fractionalQuarks().quarks != 0L) { - throw WithdrawException.InvalidFractionalKinAmountException() - } - - if (amount.kin > organizer.availableBalance) { - throw WithdrawException.InsufficientFundsException() - } - - val intent = PublicKey.generate() - - val steps = mutableListOf() - steps.add("Attempting withdrawal...") - val primaryBalance = organizer.availableDepositBalance.toKinTruncating() - - // If the primary account has less Kin than the amount - // requested for withdrawal, we'll need to execute a - // private transfer to the primary account before we - // can make a public transfer to destination - return if (primaryBalance < amount.kin) { - var missingBalance = amount.kin - primaryBalance - steps.add("Amount exceeds primary balance.") - steps.add("Missing balance: $missingBalance") - - // 1. If we're missing funds, we'll pull funds - // from relationship accounts first. - if (missingBalance > 0) { - val receivedFromRelationships = - transactionReceiver.receiveFromRelationship(organizer, limit = missingBalance) - missingBalance -= receivedFromRelationships - - steps.add("Pulled from relationships: $receivedFromRelationships") - steps.add("Missing balance: $missingBalance") - } - - // 2. If we still need funds to fulfill the withdrawal - // it's likely that they are stuck in incoming and bucket - // accounts. We'll need to pull those out into primary. - if (missingBalance > 0) { - - // 3. It's possible that there's funds still left in - // an incoming account. If we're still missing funds - // for withdrawal, we'll pull from incoming. - if (transactionReceiver.availableIncomingAmount(organizer) > 0) { - val receivedFromIncoming = transactionReceiver.receiveFromIncoming( - organizer = organizer - ) - missingBalance -= receivedFromIncoming - - steps.add("Pulled from incoming: $receivedFromIncoming") - steps.add("Missing balance: $missingBalance") - } - } - - - // 4. In the event that it's a full withdrawal or if - // more funds are required, we'll need to do a private - // transfer from bucket accounts. - if (missingBalance > 0) { - // Move funds into primary from buckets - transfer( - amount = KinAmount.newInstance(kin = missingBalance, rate = Rate.oneToOne), - fee = Kin.fromKin(0), - additionalFees = emptyList(), - organizer = organizer, - rendezvousKey = intent, - destination = organizer.primaryVault, - isWithdrawal = true - ).doOnComplete { - steps.add("Pulled from buckets: $missingBalance") - }.concatWith(fetchLimits()).concatWith(balanceController.getBalance()) - } else { - // 5. Update balances and limits after the withdrawal since - // it's likely that this withdrawal affected both but at the - // very least, we need updated balances for all accounts. - balanceController.getBalance() - } - } else { - Completable.complete() - }.doOnComplete { - Timber.d(steps.joinToString("\n")) - }.concatWith( - // 6. Execute withdrawal - withdraw( - amount = amount, - organizer = organizer, - destination = destination, - ) - ).doOnComplete { - trace( - tag = "Trx", - message = "Withdraw completed", - type = TraceType.Process - ) - } -} - -private fun Client.withdraw( - amount: KinAmount, - organizer: Organizer, - destination: PublicKey -): Completable { - return Completable.defer { - transactionRepository.withdraw( - amount, organizer, destination - ) - .map { - if (it is IntentPublicTransfer) { - balanceController.setTray(organizer, it.resultTray) - } - } - .ignoreElement() - } -} - -fun Client.sendRemotely( - amount: KinAmount, - organizer: Organizer, - rendezvousKey: PublicKey, - giftCard: GiftCardAccount -): Completable { - return Completable.defer { - transactionRepository.sendRemotely( - amount, organizer, rendezvousKey, giftCard - ) - .map { - if (it is IntentRemoteSend) { - balanceController.setTray(organizer, it.resultTray) - } - } - .ignoreElement() - } -} - - -suspend fun Client.requestFirstKinAirdrop( - owner: KeyPair, -): Result { - Timber.d("requesting airdrop") - return transactionRepository.requestFirstKinAirdrop(owner) -} - -fun Client.pollIntentMetadata( - owner: KeyPair, - intentId: PublicKey, - maxAttempts: Int = 50, - debugLogs: Boolean = false, -): Flow { - val stopped = AtomicBoolean() - val attemptCount = AtomicInteger() - - if (debugLogs) { - Timber.tag("codescan").i("pollIntentMetadata: start polling") - } - - return flowInterval({ 50L * (attemptCount.get() / 10) }) - .takeWhile { !stopped.get() && attemptCount.get() < maxAttempts } - .map { attemptCount.incrementAndGet() } - .onEach { - if (debugLogs) { - Timber.tag("codescan").i("pollIntentMetadata: [${it}] fetch data") - } - } - .map { transactionRepository.fetchIntentMetadata(owner, intentId) } - .filter { !stopped.get() } - .mapNotNull { it.getOrNull() } - .map { - if (debugLogs) { - Timber.tag("codescan") - .i("pollMatchingRendezvous: stop polling :: took ${attemptCount.get()} attempts") - } - stopped.set(true) - it - } -} - -fun Client.fetchTransactionLimits( - owner: KeyPair, - isForce: Boolean = false -): Limits? { - val time = System.currentTimeMillis() - - val isStale = transactionRepository.areLimitsState - - if (!isStale && !isForce) { - return transactionRepository.limits - } - - Timber.i("fetchTransactionLimits") - lastLimitsFetch = time - - val date: Calendar = GregorianCalendar() - date.set(Calendar.HOUR_OF_DAY, 0) - date.set(Calendar.MINUTE, 0) - date.set(Calendar.SECOND, 0) - date.set(Calendar.MILLISECOND, 0) - - val seconds = date.timeInMillis / 1000 - return transactionRepository.fetchLimits(owner, seconds) - .subscribeOn(Schedulers.io()) - .blockingFirst() -} - -fun Client.fetchDestinationMetadata(destination: PublicKey): Single { - return transactionRepository.fetchDestinationMetadata(destination) -} - -// ----- -private var lastLimitsFetch: Long = 0L - -fun Client.fetchLimits(isForce: Boolean = false): Completable { - val owner = userManager.keyPair ?: return Completable.complete() - fetchTransactionLimits(owner, isForce) - return Completable.complete() -} - -fun Client.receiveIfNeeded(): Completable { - val organizer = userManager.organizer ?: return Completable.complete() - - if (organizer.slotsBalance < transactionRepository.maxDeposit) { - receiveFromRelationships(organizer, upTo = transactionRepository.maxDeposit) - } - - return Completable.concatArray( - receiveFromPrimaryIfWithinLimits(organizer), - transactionReceiver.receiveFromIncomingCompletable(organizer) - ) -} - -fun Client.receiveFromPrimaryIfWithinLimits(organizer: Organizer): Completable { - Timber.d("receive within limits") - val depositBalance = organizer.availableDepositBalance.toKinTruncating() - - // Nothing to deposit - if (!depositBalance.hasWholeKin()) { - Timber.d("nothing to deposit ($depositBalance)") - return Completable.complete() - } - - // We want to deposit the smaller of the two: balance in the - // primary account or the max allowed amount provided by server - return Single.just(transactionRepository.maxDeposit.toKinTruncatingLong()) - .map { maxDeposit -> - Pair( - Kin.fromKin(min(depositBalance.toKinValueDouble(), maxDeposit.toDouble())), - Kin.fromKin(maxDeposit) - ) - } - .filter { pair -> - val (depositAmount, _) = pair - depositAmount.hasWholeKin().also { Timber.d("hasWholeKin=$it") } - } - .flatMapSingle { pair -> - val (depositAmount, maxDeposit) = pair - Timber.i( - "Receiving from primary: ${depositAmount.toKin()}, Max allowed deposit: ${maxDeposit.toKin()}" - ) - transactionRepository.receiveFromPrimary(depositAmount, organizer) - } - .map { intent -> - if (intent is IntentDeposit) { - balanceController.setTray(organizer, intent.resultTray) - } - } - .doOnSuccess { - trace( - tag = "Trx", - message = "Received from primary", - type = TraceType.Process - ) - fetchLimits(isForce = true) - }.ignoreElement() -} - -fun Client.fetchPrivacyUpgrades(): Completable { - val owner = userManager.keyPair ?: return Completable.complete() - val organizer = userManager.organizer ?: return Completable.complete() - - return transactionRepository.fetchUpgradeableIntents(owner) - .flatMapCompletable { intents -> - Timber.w("Fetch Privacy size: ${intents.size}") - val completableList = mutableListOf() - - intents.forEachIndexed { index, intent -> - val completable = - transactionRepository.upgradePrivacy( - organizer.mnemonic, - intent - ).ignoreElement() - - completableList.add(completable) - } - - Completable.mergeArray(*completableList.toTypedArray()) - } -} - -fun Client.getTransferPreflightAction(amount: Kin): Completable { - val organizer = userManager.organizer ?: return Completable.complete() - val neededKin = - if (amount > organizer.slotsBalance) amount - organizer.slotsBalance else Kin.fromKin(0) - - // If the there's insufficient funds in the slots - // we'll need to top them up from incoming, relationship - // and primary accounts, in that order. - return if (neededKin > 0) { - // 1. Receive funds from incoming accounts as those - // will rotate more frequently than other types of accounts - val receivedKin = transactionReceiver.receiveFromIncoming(organizer) - Timber.d("received ${receivedKin.quarks} from incoming") - // 2. Pull funds from relationships if there's still funds - // missing in buckets after the receiving from primary - if (receivedKin < neededKin) { - Timber.d("attempt to pull funds from relationship to get to ${neededKin.quarks}") - val result = transactionReceiver.receiveFromRelationship( - organizer, - limit = neededKin - receivedKin - ) - Timber.d("received ${result.quarks} from relationships") - } - - // 3. If the amount is still larger than what's available - // in the slots, we'll need to move funds from primary - // deposits into slots after receiving - if (amount > organizer.slotsBalance) { - Timber.d("receive from primary") - receiveFromPrimaryIfWithinLimits(organizer) - } else { - Completable.complete() - } - } else { - Completable.complete() - } -} - -fun Client.receiveFromRelationships(organizer: Organizer, upTo: Kin? = null): Kin { - return transactionReceiver.receiveFromRelationship(organizer, upTo) -} - -@SuppressLint("CheckResult") -@Throws -fun Client.establishRelationshipSingle( - organizer: Organizer, - domain: Domain -): Single { - return transactionRepository.establishRelationshipSingle(organizer, domain) -} - -@Suppress("RedundantSuspendModifier") -@SuppressLint("CheckResult") -@Throws -suspend fun Client.awaitEstablishRelationship( - organizer: Organizer, - domain: Domain -): Result { - return transactionRepository.establishRelationship(organizer, domain) - .map { it.relationship } -} - -suspend fun Client.initiateSwap(organizer: Organizer): Result { - return transactionRepository.initiateSwap(organizer) -} - -suspend fun Client.declareFiatPurchase( - owner: KeyPair, - amount: KinAmount, - nonce: UUID -): Result { - return transactionRepository.declareFiatPurchase(owner, amount, nonce) -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/client/TransactionReceiver.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/client/TransactionReceiver.kt deleted file mode 100644 index 85274be36..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/client/TransactionReceiver.kt +++ /dev/null @@ -1,159 +0,0 @@ -package com.getcode.network.client - -import com.getcode.model.Kin -import com.getcode.model.intents.IntentPublicTransfer -import com.getcode.model.intents.IntentReceive -import com.getcode.model.intents.IntentRemoteReceive -import com.getcode.network.repository.BalanceRepository -import com.getcode.network.repository.TransactionRepository -import com.getcode.solana.organizer.GiftCardAccount -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Tray -import com.getcode.utils.ErrorUtils -import com.getcode.utils.TraceType -import com.getcode.utils.trace -import io.reactivex.rxjava3.core.Completable -import timber.log.Timber -import javax.inject.Inject - -class TransactionReceiver @Inject constructor( - private val balanceRepository: BalanceRepository, - private val transactionRepository: TransactionRepository -) { - fun receiveRemotely( - amount: Kin, - organizer: Organizer, - giftCard: GiftCardAccount, - isVoiding: Boolean - ): Completable { - return Completable.defer { - transactionRepository.receiveRemotely( - amount, organizer, giftCard, isVoiding - ) - .map { - if (it is IntentRemoteReceive) { - setTray(organizer, it.resultTray) - } - } - .ignoreElement() - } - } - - suspend fun receiveRemotelySuspend( - giftCard: GiftCardAccount, - amount: Kin, - organizer: Organizer, - isVoiding: Boolean - ) { - val intent = transactionRepository.receiveRemotely( - amount = amount, - organizer = organizer, - giftCard = giftCard, - isVoiding = isVoiding - ).blockingGet() - - if (intent is IntentRemoteReceive) { - setTray(organizer, intent.resultTray) - } - } - - fun receiveFromRelationship(organizer: Organizer, limit: Kin? = null): Kin { - var receivedTotal = Kin.fromKin(0) - - runCatching loop@{ - organizer.relationshipsLargestFirst().onEach { relationship -> - Timber.d("Receiving from relationships: domain ${relationship.domain.urlString} balance ${relationship.partialBalance}") - - // Ignore empty relationship accounts - if (relationship.partialBalance > 0) { - val intent = transactionRepository.receiveFromRelationship( - relationship = relationship, - organizer = organizer - ).blockingGet() - - trace( - tag = "Trx", - message = "Received from relationship", - type = TraceType.Process, - metadata = { - "domain" to relationship.domain.relationshipHost - "kin" to relationship.partialBalance - } - ) - - receivedTotal += relationship.partialBalance - - trace( - tag = "Trx", - message = "Received from incoming", - type = TraceType.Process - ) - - if (intent is IntentPublicTransfer) { - setTray(organizer, intent.resultTray) - } - - // Bail early if a limit is set - if (limit != null && receivedTotal >= limit) { - return@loop // break loop - } - } - } - }.onFailure { - ErrorUtils.handleError(it) - it.printStackTrace() - } - - return receivedTotal - } - - fun receiveFromIncoming(organizer: Organizer): Kin { - val incomingBalance = availableIncomingAmount(organizer) - return if (incomingBalance <= 0) { - Kin.fromKin(0) - } else { - receiveFromIncoming( - amount = incomingBalance, - organizer = organizer - ).blockingAwait() - incomingBalance - } - } - - fun receiveFromIncomingCompletable(organizer: Organizer): Completable { - val incomingBalance = availableIncomingAmount(organizer) - return if (incomingBalance <= 0) { - Completable.complete() - } else { - receiveFromIncoming( - amount = incomingBalance, - organizer = organizer - ) - } - } - - fun receiveFromIncoming(amount: Kin, organizer: Organizer): Completable { - trace( - "receiveFromIncoming $amount", - type = TraceType.Silent - ) - return transactionRepository.receiveFromIncoming(amount, organizer).map { - if (it is IntentReceive) { - setTray(organizer, it.resultTray) - } - }.ignoreElement() - } - - suspend fun swapIfNeeded(organizer: Organizer) { - transactionRepository.swapIfNeeded(organizer) - } - - private fun setTray(organizer: Organizer, tray: Tray) { - organizer.set(tray) - balanceRepository.setBalance(organizer.availableBalance.toKinTruncatingLong().toDouble()) - } - - fun availableIncomingAmount(organizer: Organizer): Kin { - return organizer.availableIncomingBalance.toKinTruncating() - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/exchange/Exchange.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/exchange/Exchange.kt deleted file mode 100644 index 664c8a6c0..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/exchange/Exchange.kt +++ /dev/null @@ -1,226 +0,0 @@ -package com.getcode.network.exchange - -import com.getcode.model.Rate -import com.getcode.network.service.CurrencyService -import com.getcode.util.format -import com.getcode.utils.TraceType -import com.getcode.utils.network.retryable -import com.getcode.utils.trace -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch -import kotlinx.datetime.Instant -import java.util.Date -import javax.inject.Inject -import kotlin.time.Duration.Companion.minutes - - -class CodeExchange @Inject constructor( - private val currencyService: CurrencyService, - private val preferredCurrency: suspend () -> com.getcode.model.Currency?, - private val defaultCurrency: suspend () -> com.getcode.model.Currency?, -) : Exchange, CoroutineScope by CoroutineScope(Dispatchers.IO) { - - private var _entryRate = MutableStateFlow(Rate.oneToOne) - override val entryRate: Rate - get() = _entryRate.value - - override fun observeEntryRate(): Flow = _entryRate - - private val _localRate = MutableStateFlow(Rate.oneToOne) - override val localRate - get() = _localRate.value - - override fun observeLocalRate(): Flow = _localRate - - private var rateDate: Long = System.currentTimeMillis() - - private var localCurrency: com.getcode.model.CurrencyCode? = null - private var entryCurrency: com.getcode.model.CurrencyCode? = null - - private val _rates = MutableStateFlow(emptyMap()) - private var rates = RatesBox(0, emptyMap()) - set(value) { - field = value - _rates.value = value.rates - } - - override fun rates() = rates.rates - override fun observeRates(): Flow> = _rates - - private val isStale: Boolean - get() { - if (rates.rates.isEmpty()) return true - // Remember, the exchange rates date is the server-provided - // date-of-rate and not the time the rate was fetched. It - // might be reasonable for the server to return a date that - // is dated 11 minutes or older. - val threshold = 20.minutes.inWholeMilliseconds - return System.currentTimeMillis() - rates.dateMillis > threshold - } - - init { - launch { - localCurrency = com.getcode.model.CurrencyCode.tryValueOf(preferredCurrency()?.code.orEmpty()) - entryCurrency = com.getcode.model.CurrencyCode.tryValueOf(defaultCurrency()?.code.orEmpty()) - -// prefs.observeOrDefault(PrefsString.KEY_ENTRY_CURRENCY, "") -// .map { it.takeIf { it.isNotEmpty() } } -// .map { com.getcode.model.CurrencyCode.tryValueOf(it.orEmpty()) } -// .mapNotNull { preferred -> -// preferred ?: com.getcode.model.CurrencyCode.tryValueOf(defaultCurrency()?.code.orEmpty()) -// }.onEach { setEntryCurrency(it) } -// .launchIn(this@CodeExchange) - } - - launch { -// db?.exchangeDao()?.query()?.let { exchangeData -> -// val rates = exchangeData.map { Rate(it.fx, it.currency) } -// val dateMillis = exchangeData.minOf { it.synced } -// set(RatesBox(dateMillis = dateMillis, rates = rates)) -// } - - fetchRatesIfNeeded() - } - -// prefs.observeOrDefault(PrefsString.KEY_LOCAL_CURRENCY, "") -// .map { it.takeIf { it.isNotEmpty() } } -// .map { com.getcode.model.CurrencyCode.tryValueOf(it.orEmpty()) } -// .mapNotNull { preferred -> -// preferred ?: com.getcode.model.CurrencyCode.tryValueOf(defaultCurrency()?.code.orEmpty()) -// }.onEach { setLocalCurrency(it) } -// .launchIn(this) - } - - override suspend fun fetchRatesIfNeeded() { - if (isStale) { - retryable( - call = { - currencyService.getRates() - .onSuccess { (updatedRates, date) -> -// db?.exchangeDao()?.insert(rates = updatedRates, syncedAt = date) - set(RatesBox(date, updatedRates)) - } - } - ) - } - - updateRates() - } - - private fun setEntryCurrency(currency: com.getcode.model.CurrencyCode) { - entryCurrency = currency - updateRates() - } - - private fun setLocalCurrency(currency: com.getcode.model.CurrencyCode) { - localCurrency = currency - updateRates() - } - - private suspend fun set(ratesBox: RatesBox) { - rates = ratesBox - rateDate = ratesBox.dateMillis - - setLocalEntryCurrencyIfNeeded() - updateRates() - } - - private suspend fun setLocalEntryCurrencyIfNeeded() { - if (entryCurrency != null) { - return - } - - val localRegionCurrency = defaultCurrency() ?: return - val currency = com.getcode.model.CurrencyCode.tryValueOf(localRegionCurrency.code) - entryCurrency = currency - } - - override fun rateFor(currencyCode: com.getcode.model.CurrencyCode): Rate? = rates.rateFor(currencyCode) - - override fun rateForUsd(): Rate? = rates.rateForUsd() - - private fun updateRates() { - if (rates.isEmpty) { - return - } - - val localRate = localCurrency?.let { rates.rateFor(it) } - val localChanged = _localRate.value != localRate - if (localChanged) { - _localRate.value = if (localRate != null) { - trace( - tag = "Background", - message = "Updated the local currency: $localCurrency, " + - "Staleness ${System.currentTimeMillis() - rates.dateMillis} ms, " + - "Date: ${Date(rates.dateMillis)}", - type = TraceType.Process - ) - localRate - } else { - trace( - tag = "Background", - message = "local:: Rate for $localCurrency not found. Defaulting to USD.", - type = TraceType.Process - ) - rates.rateForUsd()!! - } - } - - - val entryRate = entryCurrency?.let { rates.rateFor(it) } - val entryChanged = _entryRate.value != entryRate - if (entryChanged) { - _entryRate.value = if (entryRate != null) { - trace( - tag = "Background", - message = "Updated the entry currency: $entryCurrency, " + - "Staleness ${System.currentTimeMillis() - rates.dateMillis} ms, " + - "Date: ${Date(rates.dateMillis)}", - type = TraceType.Process - ) - entryRate - } else { - trace( - tag = "Background", - message = "entry:: Rate for $entryCurrency not found. Defaulting to USD.", - type = TraceType.Process - ) - rates.rateForUsd()!! - } - } - - if (localChanged || entryChanged) { - trace(tag = "Background", - message = "Updated rates", - type = TraceType.Process, - metadata = { - "date" to Instant.fromEpochMilliseconds(rates.dateMillis) - .format("yyyy-MM-dd HH:mm:ss") - } - ) - } - } -} - -private data class RatesBox(val dateMillis: Long, val rates: Map) { - constructor(dateMillis: Long, rates: List) : this( - dateMillis, - rates.associateBy { it.currency }) - - val isEmpty: Boolean - get() = rates.isEmpty() - - fun rateFor(currencyCode: com.getcode.model.CurrencyCode): Rate? = rates[currencyCode] - - fun rateFor(currency: com.getcode.model.Currency): Rate? { - val currencyCode = com.getcode.model.CurrencyCode.tryValueOf(currency.code) - return currencyCode?.let { rates[it] } - } - - fun rateForUsd(): Rate? { - return rates[com.getcode.model.CurrencyCode.USD] - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/AccountRepository.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/AccountRepository.kt deleted file mode 100644 index dc85a9bd7..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/AccountRepository.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.getcode.network.repository - -import com.codeinc.gen.account.v1.CodeAccountService as AccountService -import com.getcode.ed25519.Ed25519 -import com.getcode.model.* -import com.getcode.network.api.AccountApi -import com.getcode.services.network.core.NetworkOracle -import com.getcode.solana.keys.PublicKey -import com.getcode.utils.getPublicKeyBase58 -import io.reactivex.rxjava3.core.Single -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import timber.log.Timber -import javax.inject.Inject - -private const val TAG = "AccountRepository" - -@Deprecated("Replaced with Account Service") -class AccountRepository @Inject constructor( - private val accountApi: AccountApi, - private val networkOracle: NetworkOracle, -) { - suspend fun getTokenAccountInfosSuspend( - owner: Ed25519.KeyPair, - ): Result> { - val request = AccountService.GetTokenAccountInfosRequest.newBuilder() - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .apply { setSignature(sign(owner)) } - .build() - - Timber.d("token info fetch") - return try { - networkOracle.managedRequest(accountApi.getTokenAccountInfosFlow(request)) - .map { response -> - when (response.result) { - AccountService.GetTokenAccountInfosResponse.Result.OK -> { - Timber.d("token account infos fetched") - val container = mutableMapOf() - - for ((base58, info) in response.tokenAccountInfosMap) { - val account = PublicKey.fromBase58(base58) - val accountInfo = AccountInfo.newInstance(info) - if (accountInfo == null) { - Timber.i("Failed to parse account info: $info") - continue - } - - container[account] = accountInfo - } - Timber.d("token account infos handled") - Result.success(container) - } - - AccountService.GetTokenAccountInfosResponse.Result.NOT_FOUND -> { - Timber.i("Account not found for owner: ${owner.getPublicKeyBase58()}") - Result.failure(FetchAccountInfosException.NotFoundException()) - } - - else -> { - Timber.i("Unknown exception") - Result.failure(FetchAccountInfosException.UnknownException()) - } - } - } - .first() - } catch (e: Exception) { - Result.failure(e) - } - } - - fun getTokenAccountInfos( - owner: Ed25519.KeyPair, - ): Single> { - val request = AccountService.GetTokenAccountInfosRequest.newBuilder() - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .apply { setSignature(sign(owner)) } - .build() - - Timber.d("token info fetch") - return accountApi.getTokenAccountInfos(request) - .flatMap { response -> - when (response.result) { - AccountService.GetTokenAccountInfosResponse.Result.OK -> { - Timber.d("token account infos fetched") - val container = mutableMapOf() - - for ((base58, info) in response.tokenAccountInfosMap) { - val account = PublicKey.fromBase58(base58) - val accountInfo = AccountInfo.newInstance(info) - if (accountInfo == null) { - Timber.i("Failed to parse account info: $info") - continue - } - - container[account] = accountInfo - } - Timber.d("token account infos handled") - Single.just(container) - } - AccountService.GetTokenAccountInfosResponse.Result.NOT_FOUND -> { - Timber.i("Account not found for owner: ${owner.getPublicKeyBase58()}") - Single.error(FetchAccountInfosException.NotFoundException()) - } - else -> { - Timber.i("Unknown exception") - Single.error(FetchAccountInfosException.UnknownException()) - } - } - } - } - - sealed class FetchAccountInfosException : Exception() { - class NotFoundException : FetchAccountInfosException() - class UnknownException : FetchAccountInfosException() - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/BalanceRepository.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/BalanceRepository.kt deleted file mode 100644 index 53600bec6..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/BalanceRepository.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.getcode.network.repository - - -import kotlinx.coroutines.flow.MutableStateFlow -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class BalanceRepository @Inject constructor() { - - val balanceFlow = MutableStateFlow(-1.0) - - fun setBalance(balance: Double) { - balanceFlow.value = balance - } - - fun clearBalance() { - balanceFlow.value = 0.0 - } - -} diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/Extensions.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/Extensions.kt deleted file mode 100644 index 6fc94f94f..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/Extensions.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.getcode.network.repository - -import com.codeinc.gen.common.v1.CodeModel -import com.getcode.ed25519.Ed25519 -import com.getcode.utils.toByteString -import com.google.protobuf.MessageLite -import java.io.ByteArrayOutputStream - -fun isMock() = false - -fun ByteArray.toUserId(): CodeModel.UserId { - return CodeModel.UserId.newBuilder().setValue(this.toByteString()).build() -} - -fun String.toPhoneNumber(): CodeModel.PhoneNumber { - return CodeModel.PhoneNumber.newBuilder().setValue(this).build() -} - -fun List.toSolanaAccount(): CodeModel.SolanaAccountId { - return CodeModel.SolanaAccountId.newBuilder().setValue(this.toByteArray().toByteString()) - .build() -} - -fun ByteArray.toSolanaAccount(): CodeModel.SolanaAccountId { - return CodeModel.SolanaAccountId.newBuilder().setValue(this.toByteString()) - .build() -} - -fun ByteArray.toSignature(): CodeModel.Signature { - return CodeModel.Signature.newBuilder().setValue(this.toByteString()) - .build() -} - -fun com.getcode.solana.keys.PublicKey.toIntentId(): CodeModel.IntentId { - return CodeModel.IntentId.newBuilder().setValue(this.byteArray.toByteString()).build() -} - -fun MessageLite.Builder.sign(owner: Ed25519.KeyPair): CodeModel.Signature { - val bos = ByteArrayOutputStream() - this.buildPartial().writeTo(bos) - return Ed25519.sign(bos.toByteArray(), owner).toSignature() -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/MessagingRepository.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/MessagingRepository.kt deleted file mode 100644 index d56039df4..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/MessagingRepository.kt +++ /dev/null @@ -1,315 +0,0 @@ -package com.getcode.network.repository - -import com.codeinc.gen.common.v1.CodeModel as Model -import com.codeinc.gen.messaging.v1.CodeMessagingService as MessagingService -import com.codeinc.gen.messaging.v1.CodeMessagingService.ClientRejectedLogin -import com.codeinc.gen.messaging.v1.CodeMessagingService.CodeScanned -import com.codeinc.gen.messaging.v1.CodeMessagingService.PollMessagesRequest -import com.codeinc.gen.messaging.v1.CodeMessagingService.RendezvousKey -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import com.getcode.ed25519.Ed25519 -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.Domain -import com.getcode.model.Fiat -import com.getcode.model.PaymentRequest -import com.getcode.model.StreamMessage -import com.getcode.model.toPublicKey -import com.getcode.network.api.MessagingApi -import com.getcode.services.network.core.INFINITE_STREAM_TIMEOUT -import com.getcode.services.network.core.NetworkOracle -import com.getcode.utils.ErrorUtils -import com.getcode.utils.getPublicKeyBase58 -import com.getcode.utils.hexEncodedString -import com.google.protobuf.ByteString -import com.google.protobuf.Timestamp -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.reactive.asFlow -import timber.log.Timber -import java.io.ByteArrayOutputStream -import javax.inject.Inject - -private const val TAG = "MessagingRepository" - -class MessagingRepository @Inject constructor( - private val messagingApi: MessagingApi, - private val networkOracle: NetworkOracle, -) { - - fun openMessageStream( - rendezvousKeyPair: KeyPair, - ): Flowable { - Timber.i("openMessageStream") - - val request = MessagingService.OpenMessageStreamRequest.newBuilder() - .setRendezvousKey( - RendezvousKey.newBuilder().setValue( - ByteString.copyFrom(rendezvousKeyPair.publicKeyBytes) - ) - ) - .build() - - return messagingApi.openMessageStream(request) - .let { networkOracle.managedRequest(it, INFINITE_STREAM_TIMEOUT) } - .map { - Timber.d("message stream response received") - it.messagesList - .filter { message -> - message.kindCase == MessagingService.Message.KindCase.REQUEST_TO_GRAB_BILL - } - } - .doOnNext { messagesList -> - if (messagesList.isEmpty()) { - return@doOnNext - } - ackMessages(rendezvousKeyPair, messagesList.map { it.id }) - .subscribe({ Timber.d("acked") }, ErrorUtils::handleError) - } - .filter { it.isNotEmpty() } - .map { messagesList -> - messagesList.map { message -> - val account = - message.requestToGrabBill.requestorAccount.value.toByteArray().toPublicKey() - val signature = - com.getcode.solana.keys.Signature( - message.sendMessageRequestSignature.value.toByteArray().toList() - ) - PaymentRequest(account, signature) - }.first() - } - .retry(10L) { - it.printStackTrace() - true - } - .subscribeOn(Schedulers.computation()) - } - - private fun ackMessages( - rendezvousKeyPair: KeyPair, - messageIds: List - ): Completable { - val request = MessagingService.AckMessagesRequest.newBuilder() - .addAllMessageIds(messageIds) - .setRendezvousKey( - RendezvousKey.newBuilder().setValue( - ByteString.copyFrom(rendezvousKeyPair.publicKeyBytes) - ) - ) - .build() - - return networkOracle.managedRequest(messagingApi.ackMessages(request)) - .flatMapCompletable { - if (it.result == MessagingService.AckMesssagesResponse.Result.OK) { - Timber.i("ackMessages: Result.OK: $messageIds") - Completable.complete() - } else { - Completable.error(RuntimeException("Failed to ack message with ids: $messageIds")) - } - } - } - - fun verifyRequestToGrabBill( - destination: com.getcode.solana.keys.PublicKey, - rendezvousKey: KeyPair, - signature: com.getcode.solana.keys.Signature - ): Boolean { - val messageData = sendRequestToGrabBill(destination = destination).build().toByteArray() - return rendezvousKey.verify(signature.byteArray, messageData) - } - - suspend fun sendRequestToLogin( - domain: Domain, - verifier: KeyPair, - rendezvous: KeyPair, - ): Result { - val message = requestToLogin(domain, verifier, rendezvous) - return sendRendezvousMessage(message, rendezvous) - } - - fun sendRequestToGrabBill( - destination: ByteArray, - rendezvousKeyPair: KeyPair, - ): Flowable { - val requestor = destination.toSolanaAccount() - val paymentRequest = MessagingService.RequestToGrabBill.newBuilder() - .setRequestorAccount(requestor) - val message = MessagingService.Message.newBuilder() - .setRequestToGrabBill(paymentRequest) - - return sendRendezvousMessageFlowable(message, rendezvousKeyPair) - .doOnEach { - Timber.i("sendRequestForPayment: result: ${it.value?.result}") - } - } - - suspend fun sendRequestToReceiveBill( - destination: com.getcode.solana.keys.PublicKey, - fiat: Fiat, - rendezvous: KeyPair - ): Result { - val message = MessagingService.Message.newBuilder() - .setRequestToReceiveBill( - MessagingService.RequestToReceiveBill.newBuilder() - .setRequestorAccount(destination.byteArray.toSolanaAccount()) - .setPartial( - TransactionService.ExchangeDataWithoutRate.newBuilder() - .setCurrency(fiat.currency.name) - .setNativeAmount(fiat.amount) - ) - .build() - ) - - return sendRendezvousMessage(message, rendezvous) - } - - fun fetchMessages(rendezvous: KeyPair): Result> { - val request = PollMessagesRequest.newBuilder() - .setRendezvousKey( - RendezvousKey.newBuilder() - .setValue(ByteString.copyFrom(rendezvous.publicKeyBytes)) - ).apply { setSignature(sign(rendezvous)) } - .build() - - return networkOracle.managedRequest(messagingApi.pollMessages(request)) - .observeOn(Schedulers.io()) - .map { response -> - Timber.d("response=${response.messagesList}") - response.messagesList.mapNotNull { m -> StreamMessage.getInstance(m) } - }.firstOrError().blockingGet().runCatching { this } - } - - suspend fun codeScanned(rendezvous: KeyPair): Result { - val message = MessagingService.Message.newBuilder() - .setCodeScanned( - CodeScanned.newBuilder() - .setTimestamp( - Timestamp.newBuilder().setSeconds(System.currentTimeMillis() / 1_000) - ) - ) - - return sendRendezvousMessage(message, rendezvous) - } - - suspend fun rejectPayment(rendezvous: KeyPair): Result { - val rejection = MessagingService.ClientRejectedPayment.newBuilder() - .setIntentId(com.getcode.solana.keys.PublicKey.fromBase58(rendezvous.getPublicKeyBase58()).toIntentId()) - .build() - - val message = MessagingService.Message.newBuilder() - .setClientRejectedPayment(rejection) - - return sendRendezvousMessage(message, rendezvous) - } - - suspend fun rejectLogin(rendezvous: KeyPair): Result { - val message = MessagingService.Message - .newBuilder() - .setClientRejectedLogin( - ClientRejectedLogin.newBuilder() - .setTimestamp( - Timestamp.newBuilder().setSeconds(System.currentTimeMillis() / 1_000) - ) - ) - - return sendRendezvousMessage(message, rendezvous) - } - - private fun sendRequestToGrabBill(destination: com.getcode.solana.keys.PublicKey): MessagingService.Message.Builder { - return MessagingService.Message - .newBuilder() - .setRequestToGrabBill( - MessagingService.RequestToGrabBill - .newBuilder() - .setRequestorAccount(destination.bytes.toSolanaAccount()) - ) - } - - private fun requestToLogin( - domain: Domain, - verifier: KeyPair, - rendezvous: KeyPair - ): MessagingService.Message.Builder { - return MessagingService.Message - .newBuilder() - .setRequestToLogin( - MessagingService.RequestToLogin - .newBuilder() - .setDomain( - Model.Domain.newBuilder() - .setValue(domain.relationshipHost) - ) - .setRendezvousKey( - RendezvousKey.newBuilder() - .setValue(ByteString.copyFrom(rendezvous.publicKeyBytes)) - ).setVerifier(verifier.publicKeyBytes.toSolanaAccount()) - .let { - val bos = ByteArrayOutputStream() - it.buildPartial().writeTo(bos) - it.setSignature(Ed25519.sign(bos.toByteArray(), rendezvous).toSignature()) - } - ) - } - - private suspend fun sendRendezvousMessage( - message: MessagingService.Message.Builder, - rendezvous: KeyPair - ): Result { - val signature = ByteArrayOutputStream().let { - message.buildPartial().writeTo(it) - val signed = Ed25519.sign(it.toByteArray(), rendezvous) - Model.Signature.newBuilder().setValue(ByteString.copyFrom(signed)) - } - - val request = MessagingService.SendMessageRequest.newBuilder() - .setMessage(message) - .setRendezvousKey( - RendezvousKey.newBuilder().setValue( - ByteString.copyFrom(rendezvous.publicKeyBytes) - ) - ) - .setSignature(signature) - .build() - - return runCatching { - messagingApi.sendMessage(request) - .let { networkOracle.managedRequest(it) } - .asFlow() - .firstOrNull() ?: throw IllegalArgumentException() - }.onSuccess { - Timber.i( - "message sent: ${ - it.messageId.value.toList().hexEncodedString() - }: result: ${it.result}" - ) - }.onFailure { - ErrorUtils.handleError(it) - Timber.e(t = it, message = "Failed to send rendezvous message.") - } - } - - private fun sendRendezvousMessageFlowable( - message: MessagingService.Message.Builder, - rendezvous: KeyPair - ): Flowable { - val signature = ByteArrayOutputStream().let { - message.buildPartial().writeTo(it) - val signed = Ed25519.sign(it.toByteArray(), rendezvous) - Model.Signature.newBuilder().setValue(ByteString.copyFrom(signed)) - } - - val request = MessagingService.SendMessageRequest.newBuilder() - .setMessage(message) - .setRendezvousKey( - RendezvousKey.newBuilder().setValue( - ByteString.copyFrom(rendezvous.publicKeyBytes) - ) - ) - .setSignature(signature) - .build() - - return messagingApi.sendMessage(request) - .let { networkOracle.managedRequest(it) } - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/PaymentRepository.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/PaymentRepository.kt deleted file mode 100644 index 9212d7f7d..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/PaymentRepository.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.getcode.network.repository - -import com.getcode.model.ID -import com.getcode.model.KinAmount -import com.getcode.network.BalanceController -import com.getcode.network.client.Client -import com.getcode.network.client.fetchLimits -import com.getcode.network.client.publicPayment -import com.getcode.services.model.ExtendedMetadata -import com.getcode.solana.keys.PublicKey -import com.getcode.utils.ErrorUtils -import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject - - -class PaymentRepository @Inject constructor( - private val userManager: UserManager, - private val client: Client, - private val balanceController: BalanceController, -) : CoroutineScope by CoroutineScope(Dispatchers.IO) { - - suspend fun payPublicly( - amount: KinAmount, - destination: PublicKey, - extendedMetadata: ExtendedMetadata - ): ID { - val organizer = userManager.organizer ?: throw PaymentError.OrganizerNotFound() - val currentBalance = balanceController.rawBalance - if (amount.kin.toKinValueDouble() > currentBalance) throw PaymentError.InsufficientBalance() - return client.publicPayment( - amount = amount, - organizer = organizer, - destination = destination, - extendedMetadata = extendedMetadata - ).onSuccess { - balanceController.fetchBalance() - client.fetchLimits(isForce = true).observeOn(Schedulers.io()).subscribe() - }.onFailure { - ErrorUtils.handleError(it) - }.getOrThrow() - } -} - -sealed interface PaymentError { - val message: String? - - data class OrganizerNotFound(override val message: String? = "Organizer not found") : - PaymentError, Throwable(message) - - data class InsufficientBalance(override val message: String? = "Insufficient balance for payment") : - PaymentError, Throwable(message) -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/ReceiveTransactionRepository.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/ReceiveTransactionRepository.kt deleted file mode 100644 index 56056bd8b..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/ReceiveTransactionRepository.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.getcode.network.repository - -import com.codeinc.gen.messaging.v1.CodeMessagingService -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.IntentMetadata -import com.getcode.model.toPublicKey -import com.getcode.network.client.Client -import com.getcode.network.client.pollIntentMetadata -import com.getcode.solana.organizer.Organizer -import com.getcode.utils.ErrorUtils -import io.reactivex.rxjava3.core.Flowable -import kotlinx.coroutines.rx3.asFlowable -import javax.inject.Inject - -class ReceiveTransactionRepository @Inject constructor( - private val messagingRepository: MessagingRepository, - private val client: Client -) { - fun start(organizer: Organizer, rendezvous: KeyPair, debug: Boolean = false): Flowable { - return messagingRepository.sendRequestToGrabBill( - destination = organizer.incomingVault.byteArray, - rendezvousKeyPair = rendezvous - ) - .flatMap { paymentRequestResponse -> - if (paymentRequestResponse.result != CodeMessagingService.SendMessageResponse.Result.OK) { - Flowable.error(Exception("Error: ${paymentRequestResponse.result.name}")) - } else { - client.pollIntentMetadata( - owner = organizer.ownerKeyPair, - intentId = rendezvous.publicKeyBytes.toPublicKey(), - debugLogs = debug, - ).asFlowable() - } - } - .doOnError { - ErrorUtils.handleError(it) - } - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/SendTransactionRepository.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/SendTransactionRepository.kt deleted file mode 100644 index 34597e5c9..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/SendTransactionRepository.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.getcode.network.repository - -import com.getcode.ed25519.Ed25519 -import com.getcode.services.model.CodePayload -import com.getcode.model.IntentMetadata -import com.getcode.model.Kin -import com.getcode.model.KinAmount -import com.getcode.services.model.Kind -import com.getcode.model.toPublicKey -import com.getcode.network.client.Client -import com.getcode.network.client.pollIntentMetadata -import com.getcode.network.client.transfer -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.organizer.Organizer -import com.getcode.services.utils.nonce -import io.reactivex.rxjava3.core.Flowable -import kotlinx.coroutines.rx3.asFlowable -import xyz.flipchat.services.analytics.FlipchatAnalyticsService -import javax.inject.Inject - - -class SendTransactionRepository @Inject constructor( - private val messagingRepository: MessagingRepository, - private val analytics: FlipchatAnalyticsService, - private val client: Client, -) { - private lateinit var amount: KinAmount - private lateinit var organizer: Organizer - private lateinit var owner: Ed25519.KeyPair - private lateinit var payload: CodePayload - private lateinit var payloadData: List - - private lateinit var rendezvousKey: Ed25519.KeyPair - private var receivingAccount: PublicKey? = null - - fun init(amount: KinAmount, organizer: Organizer, owner: Ed25519.KeyPair): List { - this.amount = amount - this.organizer = organizer - this.owner = owner - - this.payload = CodePayload( - kind = Kind.Cash, - value = amount.kin, - nonce = nonce, - ) - - this.payloadData = payload.codeData.toList() - this.rendezvousKey = payload.rendezvous - this.receivingAccount = null - - return payloadData - } - - fun startTransaction(): Flowable { - return messagingRepository.openMessageStream(rendezvousKey) - .firstOrError() - .flatMapPublisher { paymentRequest -> - // 1. Validate that destination hasn't been tampered with by - // verifying the signature matches one that has been signed - // with the rendezvous key. - - val isValid = messagingRepository.verifyRequestToGrabBill( - destination = paymentRequest.account, - rendezvousKey = rendezvousKey, - signature = paymentRequest.signature - ) - - if (!isValid) { - // TODO: analytics -// analyticsManager.transfer( -// amount = amount, -// successful = false -// ) - - Flowable.error(SendTransactionException.DestinationSignatureInvalidException()) - } else { - // TODO: analytics -// analyticsManager.transfer( -// amount = amount, -// successful = true -// ) - - // 2. Send the funds to destination - sendFundsAndPoll(organizer, paymentRequest.account) - } - } - .doOnError { - // TODO: analytics -// analyticsManager.transfer( -// amount = amount, -// successful = false -// ) - } - } - - private fun sendFundsAndPoll( - organizer: Organizer, - destination: PublicKey - ): Flowable { - if (receivingAccount == destination) { - // Ensure that we're processing one, and only one - // transaction for each instance of SendTransaction. - // Completion will be called by the first invocation - // of this function. - return Flowable.error(SendTransactionException.DuplicateTransferException()) - } - - receivingAccount = destination - - // It's possible that we have funds sitting in the incoming - // account counting towards the active balance. If we don't - // deposit the funds and the transaction size exceeds what's - // in the buckets, the send will fail. - return client.transfer( - amount = amount.copy(kin = amount.kin.toKinTruncating()), - fee = Kin.fromKin(0), - additionalFees = emptyList(), - organizer = organizer, - rendezvousKey = rendezvousKey.publicKeyBytes.toPublicKey(), - destination = destination, - isWithdrawal = false - ) - .andThen( - client.pollIntentMetadata( - owner = organizer.ownerKeyPair, - intentId = rendezvousKey.publicKeyBytes.toPublicKey() - ).asFlowable() - ) - } - - fun getAmount() = amount - fun getRendezvous() = rendezvousKey - - sealed class SendTransactionException : Exception() { - class DuplicateTransferException : SendTransactionException() - class DestinationSignatureInvalidException : SendTransactionException() - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/StatusRepository.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/StatusRepository.kt deleted file mode 100644 index 0fbfc40e3..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/StatusRepository.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.getcode.network.repository - -import io.reactivex.rxjava3.core.Single -import okhttp3.Call -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import org.json.JSONObject - -class StatusRepository { - fun getIsUpgradeRequired(currentVersionCode: Int): Single { - val request: Request = Request.Builder() - .url("https://app.getcode.com/status") - .build() - val call: Call = OkHttpClient().newCall(request) - - return Single.create { - val response: Response = call.execute() - if (!response.isSuccessful) { - it.onError(Exception()) - return@create - } - val json = JSONObject(response.body?.string().orEmpty()) - val minimumVersion = json.getInt("minimumClientVersion") - val isUpgradeRequired = currentVersionCode < minimumVersion - it.onSuccess(isUpgradeRequired) - } - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/TransactionRepository.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/TransactionRepository.kt deleted file mode 100644 index 283e61a0e..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/TransactionRepository.kt +++ /dev/null @@ -1,946 +0,0 @@ -package com.getcode.network.repository - -import android.annotation.SuppressLint -import com.codeinc.gen.transaction.v2.CodeTransactionService.DeclareFiatOnrampPurchaseAttemptResponse -import com.codeinc.gen.transaction.v2.CodeTransactionService.ExchangeDataWithoutRate -import com.codeinc.gen.transaction.v2.CodeTransactionService.SubmitIntentRequest -import com.codeinc.gen.transaction.v2.CodeTransactionService.SubmitIntentResponse -import com.codeinc.gen.transaction.v2.CodeTransactionService.SubmitIntentResponse.ResponseCase.ERROR -import com.codeinc.gen.transaction.v2.CodeTransactionService.SubmitIntentResponse.ResponseCase.SERVER_PARAMETERS -import com.codeinc.gen.transaction.v2.CodeTransactionService.SubmitIntentResponse.ResponseCase.SUCCESS -import com.getcode.crypt.MnemonicPhrase -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.BuyLimit -import com.getcode.model.CurrencyCode -import com.getcode.model.Domain -import com.getcode.model.Fee -import com.getcode.model.IntentMetadata -import com.getcode.model.Kin -import com.getcode.model.KinAmount -import com.getcode.model.Limits -import com.getcode.model.Rate -import com.getcode.model.SendLimit -import com.getcode.model.UpgradeableIntent -import com.getcode.model.UpgradeablePrivateAction -import com.getcode.model.extensions.newInstance -import com.getcode.model.fromProtoExchangeData -import com.getcode.model.intents.ActionGroup -import com.getcode.model.intents.IntentCreateAccounts -import com.getcode.model.intents.IntentDeposit -import com.getcode.model.intents.IntentEstablishRelationship -import com.getcode.model.intents.IntentPrivateTransfer -import com.getcode.model.intents.IntentPublicPayment -import com.getcode.model.intents.IntentPublicTransfer -import com.getcode.model.intents.IntentReceive -import com.getcode.model.intents.IntentRemoteReceive -import com.getcode.model.intents.IntentRemoteSend -import com.getcode.model.intents.IntentType -import com.getcode.model.intents.IntentUpgradePrivacy -import com.getcode.model.intents.ServerParameter -import com.getcode.network.api.TransactionApiV2 -import com.getcode.services.model.ExtendedMetadata -import com.getcode.services.observers.BidirectionalStreamReference -import com.getcode.solana.keys.AssociatedTokenAccount -import com.getcode.solana.keys.Mint -import com.getcode.solana.keys.PublicKey -import com.getcode.solana.keys.base58 -import com.getcode.solana.organizer.AccountType -import com.getcode.solana.organizer.GiftCardAccount -import com.getcode.solana.organizer.Organizer -import com.getcode.solana.organizer.Relationship -import com.getcode.utils.ErrorUtils -import com.getcode.utils.TraceType -import com.getcode.utils.base58 -import com.getcode.utils.bytes -import com.getcode.utils.toByteString -import com.getcode.utils.trace -import com.google.protobuf.Timestamp -import io.grpc.stub.StreamObserver -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.subjects.SingleSubject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.reactive.asFlow -import kotlinx.coroutines.suspendCancellableCoroutine -import timber.log.Timber -import xyz.flipchat.services.payments.BuildConfig -import java.util.UUID -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.coroutines.resume -import kotlin.time.Duration.Companion.seconds -import com.codeinc.gen.common.v1.CodeModel as Model -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService - -private const val TAG = "TransactionRepositoryV2" - -typealias BidirectionalIntentStream = BidirectionalStreamReference - -@Singleton -class TransactionRepository @Inject constructor( - val transactionApi: TransactionApiV2, -) : CoroutineScope by CoroutineScope(Dispatchers.IO) { - - var limits: Limits? = null - private set - - val areLimitsState: Boolean - get() = limits == null || limits?.isStale == true - - private var lastSwap: Long = 0L - - var maxDeposit: Kin = Kin.fromKin(0) - - fun setMaximumDeposit(deposit: Kin) { - maxDeposit = deposit - } - - fun buyLimitFor(currencyCode: CurrencyCode): BuyLimit? { - return limits?.buyLimitFor(currencyCode) - } - - fun sendLimitFor(currencyCode: CurrencyCode): SendLimit? { - return limits?.sendLimitFor(currencyCode) - } - - fun hasAvailableTransactionLimit(amount: KinAmount): Boolean { - return (sendLimitFor(amount.rate.currency)?.nextTransaction ?: 0.0) >= amount.fiat - } - - fun hasAvailableDailyLimit(): Boolean { - return (sendLimitFor(currencyCode = CurrencyCode.USD)?.nextTransaction ?: 0.0) > 0 - } - - private fun setLimits(limits: Limits) { - trace("updating limits") - this.limits = limits - } - - fun clear() { - Timber.d("clearing transactions") - maxDeposit = Kin.fromKin(0) - } - - fun createAccounts(organizer: Organizer): Single { - if (isMock()) return Single.just( - IntentCreateAccounts( - id = PublicKey(bytes = listOf()), - actionGroup = ActionGroup(), - organizer = organizer - ) as IntentType - ).delay(1, TimeUnit.SECONDS) - - val createAccounts = IntentCreateAccounts.newInstance(organizer) - - return submit(createAccounts, organizer.tray.owner.getCluster().authority.keyPair, null) - } - - suspend fun publicPayment( - amount: KinAmount, - organizer: Organizer, - destination: PublicKey, - extendedMetadata: ExtendedMetadata? = null, - ): Result { - val intent = IntentPublicPayment.newInstance( - organizer = organizer, - destination = destination, - amount = amount.copy(kin = amount.kin.toKinTruncating()), - source = AccountType.Primary, - extendedMetadata = extendedMetadata - ) - - return submitForResult( - intent = intent, - owner = organizer.tray.owner.getCluster().authority.keyPair - ) - } - - fun transfer( - amount: KinAmount, - fee: Kin, - additionalFees: List, - organizer: Organizer, - rendezvousKey: PublicKey, - destination: PublicKey, - isWithdrawal: Boolean, - metadata: ExtendedMetadata? = null, - ): Single { - if (isMock()) return Single.just( - IntentPrivateTransfer( - id = PublicKey(bytes = listOf()), - actionGroup = ActionGroup(), - organizer = organizer, - destination = destination, - grossAmount = amount, - netAmount = amount, - fee = fee, - additionalFees = emptyList(), - resultTray = organizer.tray, - isWithdrawal = isWithdrawal, - metadata = metadata - ) as IntentType - ) - .delay(1, TimeUnit.SECONDS) - - val intent = IntentPrivateTransfer.newInstance( - rendezvousKey = rendezvousKey, - organizer = organizer, - destination = destination, - amount = amount.copy(kin = amount.kin.toKinTruncating()), - fee = fee, - additionalFees = additionalFees, - isWithdrawal = isWithdrawal, - metadata = metadata - ) - - return submit(intent = intent, owner = organizer.tray.owner.getCluster().authority.keyPair) - } - - fun receiveFromIncoming( - amount: Kin, - organizer: Organizer - ): Single { - val intent = IntentReceive.newInstance( - organizer = organizer, - amount = amount.toKinTruncating() - ) - return submit(intent = intent, owner = organizer.tray.owner.getCluster().authority.keyPair) - } - - suspend fun swapIfNeeded(organizer: Organizer) { - // We need to check and see if the USDC account has a balance, - // if so, we'll initiate a swap to Kin. The nuance here is that - // the balance of the USDC account is reported as `Kin`, where the - // quarks represent the lamport balance of the account. - val info = organizer.info(AccountType.Swap) ?: return - if (info.balance.quarks <= 0) return - - val timeout = 45.seconds - - // Ensure that it's been at least `timeout` seconds since we try - // another swap if one is already in-flight. - if (System.currentTimeMillis() - lastSwap < timeout.inWholeMilliseconds) return - - lastSwap = System.currentTimeMillis() - - initiateSwap(organizer) - .onFailure { ErrorUtils.handleError(it) } - } - - fun receiveFromPrimary(amount: Kin, organizer: Organizer): Single { - val intent = IntentDeposit.newInstance( - source = AccountType.Primary, - organizer = organizer, - amount = amount.toKinTruncating() - ) - return submit(intent = intent, owner = organizer.tray.owner.getCluster().authority.keyPair) - } - - fun receiveFromRelationship( - relationship: Relationship, - organizer: Organizer - ): Single { - val intent = IntentPublicTransfer.newInstance( - source = AccountType.Relationship(relationship.domain), - organizer = organizer, - amount = KinAmount.newInstance(relationship.partialBalance, Rate.oneToOne), - destination = IntentPublicTransfer.Destination.Local(AccountType.Primary) - ) - return submit(intent, owner = organizer.tray.owner.getCluster().authority.keyPair) - } - - fun withdraw( - amount: KinAmount, - organizer: Organizer, - destination: PublicKey - ): Single { - val intent = IntentPublicTransfer.newInstance( - organizer = organizer, - amount = amount, - destination = IntentPublicTransfer.Destination.External(destination), - source = AccountType.Primary, - ) - - return submit(intent = intent, owner = organizer.tray.owner.getCluster().authority.keyPair) - } - - fun upgradePrivacy( - mnemonic: MnemonicPhrase, - upgradeableIntent: UpgradeableIntent - ): Single { - val intent = IntentUpgradePrivacy.newInstance( - mnemonic = mnemonic, - upgradeableIntent = upgradeableIntent - ) - return submit(intent, owner = mnemonic.getSolanaKeyPair()) - } - - fun sendRemotely( - amount: KinAmount, - organizer: Organizer, - rendezvousKey: PublicKey, - giftCard: GiftCardAccount - ): Single { - val intent = IntentRemoteSend.newInstance( - rendezvousKey = rendezvousKey, - organizer = organizer, - giftCard = giftCard, - amount = amount, - ) - return submit(intent, owner = organizer.tray.owner.getCluster().authority.keyPair) - } - - fun receiveRemotely( - amount: Kin, - organizer: Organizer, - giftCard: GiftCardAccount, - isVoiding: Boolean - ): Single { - val intent = IntentRemoteReceive.newInstance( - organizer = organizer, - giftCard = giftCard, - amount = amount, - isVoidingGiftCard = isVoiding - ) - return submit(intent, owner = organizer.tray.owner.getCluster().authority.keyPair) - } - - private suspend fun submitForResult( - intent: IntentType, - owner: KeyPair, - deviceToken: String? = null - ): Result = suspendCancellableCoroutine { cont -> - val reference = BidirectionalIntentStream(this) - - // Intentionally creates a retain-cycle using closures to ensure that we have - // a strong reference to the stream at all times. Doing so ensures that the - // callers don't have to manage the pointer to this stream and keep it alive - reference.retain() - - reference.stream = - transactionApi.submitIntent(object : StreamObserver { - override fun onNext(value: SubmitIntentResponse?) { - when (value?.responseCase) { - // 2. Upon successful submission of intent action the server will - // respond with parameters that we'll need to apply to the intent - // before crafting and signing the transactions. - SERVER_PARAMETERS -> { - try { - intent.apply( - value.serverParameters.serverParametersList - .map { p -> ServerParameter.newInstance(p) } - ) - - Timber.i( - "Received ${value.serverParameters.serverParametersList.size} parameters. Submitting signatures..." - ) - - val submitSignatures = intent.requestToSubmitSignatures() - reference.stream?.onNext(submitSignatures) - } catch (e: Exception) { - if (BuildConfig.DEBUG) { - e.printStackTrace() - } - Timber.i( - "Received ${value.serverParameters.serverParametersList.size} parameters but failed to apply them: ${e.javaClass.simpleName} ${e.message})" - ) - reference.stream?.onError( - ErrorSubmitIntentException( - ErrorSubmitIntent.Unknown - ) - ) - } - } - // 3. If submitted transaction signatures are valid and match - // the server, we'll receive a success for the submitted intent. - SUCCESS -> { - Timber.i("Success.") - reference.stream?.onCompleted() - cont.resume(Result.success(intent)) - } - // 3. If the submitted transaction signatures don't match, the - // intent is considered failed. Something must have gone wrong - // on the transaction creation or signing on our side. - ERROR -> { - val errors = mutableListOf() - - value.error.errorDetailsList.forEach { error -> - when (error.typeCase) { - TransactionService.ErrorDetails.TypeCase.REASON_STRING -> { - errors.add("Reason: ${error.reasonString}") - } - - TransactionService.ErrorDetails.TypeCase.INVALID_SIGNATURE -> { - val expectedVixn = - error.invalidSignature.expectedVixnHash.value.toByteArray() - val produced = intent.vixnHash() - errors.add("Signature mismatch: :: VIXN:: expected=${expectedVixn.base58}, produced=${produced.base58()}") - } - - TransactionService.ErrorDetails.TypeCase.DENIED -> { - errors.add("Denied: ${error.denied.reason}") - } - - else -> Unit - } - } - - trace( - "Error: ${errors.joinToString("\n")}", - type = TraceType.Error - ) - - reference.stream?.onCompleted() - cont.resume( - Result.failure( - ErrorSubmitIntentException( - ErrorSubmitIntent.invoke( - value.error - ), - ) - ) - ) - } - - else -> { - Timber.i("Else case. ${value?.responseCase}") - reference.stream?.onCompleted() - cont.resume(Result.failure(ErrorSubmitIntentException(ErrorSubmitIntent.Unknown))) - } - } - } - - override fun onError(t: Throwable?) { - Timber.i("onError: " + t?.message.orEmpty()) - t?.let { - ErrorUtils.handleError(it) - } - cont.resume( - Result.failure( - ErrorSubmitSwapIntentException(ErrorSubmitSwapIntent.Unknown, t) - ) - ) - } - - override fun onCompleted() { - Timber.i("onCompleted") - } - }) - - - // 1. Send `submitActions` request with - // actions generated by the intent - reference.stream?.onNext(intent.requestToSubmitActions(owner, deviceToken)) - } - - private fun submit( - intent: IntentType, - owner: KeyPair, - deviceToken: String? = null - ): Single { - Timber.i("Submit ${intent.javaClass.simpleName}") - val subject = SingleSubject.create() - - var serverMessageStream: StreamObserver? = null - val serverResponse = object : StreamObserver { - override fun onNext(value: SubmitIntentResponse?) { - Timber.i("Received: " + value?.responseCase?.name.orEmpty()) - - when (value?.responseCase) { - // 2. Upon successful submission of intent action the server will - // respond with parameters that we'll need to apply to the intent - // before crafting and signing the transactions. - SERVER_PARAMETERS -> { - try { - intent.apply( - value.serverParameters.serverParametersList - .map { p -> ServerParameter.newInstance(p) } - ) - - Timber.i( - "Received ${value.serverParameters.serverParametersList.size} parameters. Submitting signatures..." - ) - - val submitSignatures = intent.requestToSubmitSignatures() - serverMessageStream?.onNext(submitSignatures) - } catch (e: Exception) { - if (BuildConfig.DEBUG) { - e.printStackTrace() - } - Timber.i( - "Received ${value.serverParameters.serverParametersList.size} parameters but failed to apply them: ${e.javaClass.simpleName} ${e.message})" - ) - subject.onError(ErrorSubmitIntentException(ErrorSubmitIntent.Unknown)) - } - } - // 3. If submitted transaction signatures are valid and match - // the server, we'll receive a success for the submitted intent. - SUCCESS -> { - Timber.i("Success.") - serverMessageStream?.onCompleted() - subject.onSuccess(intent) - } - // 3. If the submitted transaction signatures don't match, the - // intent is considered failed. Something must have gone wrong - // on the transaction creation or signing on our side. - ERROR -> { - val errors = mutableListOf() - - value.error.errorDetailsList.forEach { error -> - when (error.typeCase) { - TransactionService.ErrorDetails.TypeCase.REASON_STRING -> { - errors.add("Reason: ${error.reasonString}") - } - - TransactionService.ErrorDetails.TypeCase.INVALID_SIGNATURE -> { - val expectedVixn = - error.invalidSignature.expectedVixnHash.value.toByteArray() - val produced = intent.vixnHash() - errors.add("Signature mismatch: :: VIXN:: expected=${expectedVixn.base58}, produced=${produced.base58()}") - } - - TransactionService.ErrorDetails.TypeCase.DENIED -> { - errors.add("Denied: ${error.denied.reason}") - } - - else -> Unit - } - } - - trace( - "Error: ${errors.joinToString("\n")}", - type = TraceType.Error - ) - - serverMessageStream?.onCompleted() - subject.onError( - ErrorSubmitIntentException( - ErrorSubmitIntent.invoke(value.error), - ) - ) - } - - else -> { - Timber.i("Else case. ${value?.responseCase}") - serverMessageStream?.onCompleted() - subject.onError(ErrorSubmitIntentException(ErrorSubmitIntent.Unknown)) - } - } - } - - override fun onError(t: Throwable?) { - Timber.i("onError: " + t?.message.orEmpty()) - subject.onError(ErrorSubmitIntentException(ErrorSubmitIntent.Unknown, t)) - } - - override fun onCompleted() { - Timber.i("onCompleted") - } - } - serverMessageStream = transactionApi.submitIntent(serverResponse) - - // 1. Send `submitActions` request with - // actions generated by the intent - serverMessageStream.onNext(intent.requestToSubmitActions(owner, deviceToken)) - - return subject - } - - fun establishRelationshipSingle( - organizer: Organizer, - domain: Domain, - ): Single { - val intent = IntentEstablishRelationship.newInstance(organizer, domain) - - return submit(intent = intent, organizer.tray.owner.getCluster().authority.keyPair) - .map { intent } - .doOnSuccess { Timber.d("established relationship") } - .doOnError { Timber.e(t = it, message = "failed to establish relationship") } - } - - @SuppressLint("CheckResult") - suspend fun establishRelationship( - organizer: Organizer, - domain: Domain - ): Result { - val intent = IntentEstablishRelationship.newInstance(organizer, domain) - - return runCatching { - submit(intent = intent, organizer.tray.owner.getCluster().authority.keyPair) - .map { intent } - .toFlowable() - .asFlow() - .firstOrNull() ?: throw IllegalArgumentException() - }.onSuccess { - Timber.d("established relationship") - }.onFailure { - ErrorUtils.handleError(it) - Timber.e(t = it, message = "failed to establish relationship") - } - } - - suspend fun declareFiatPurchase(owner: KeyPair, amount: KinAmount, nonce: UUID): Result { - val request = TransactionService.DeclareFiatOnrampPurchaseAttemptRequest.newBuilder() - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .setPurchaseAmount( - ExchangeDataWithoutRate.newBuilder() - .setCurrency(amount.rate.currency.name.lowercase()) - .setNativeAmount(amount.fiat) - ).setNonce( - Model.UUID.newBuilder().setValue(nonce.bytes.toByteString()) - ).apply { setSignature(sign(owner)) }.build() - - return runCatching { - transactionApi.declareFiatPurchase(request) - .firstOrNull() ?: throw IllegalArgumentException() - }.map { response -> - when (response.result) { - DeclareFiatOnrampPurchaseAttemptResponse.Result.OK -> Result.success(Unit) - DeclareFiatOnrampPurchaseAttemptResponse.Result.INVALID_OWNER -> { - val error = Throwable("Error: INVALID_OWNER") - ErrorUtils.handleError(error) - Result.failure(error) - } - - DeclareFiatOnrampPurchaseAttemptResponse.Result.UNSUPPORTED_CURRENCY -> { - val error = Throwable("Error: UNSUPPORTED_CURRENCY") - ErrorUtils.handleError(error) - Result.failure(error) - } - - DeclareFiatOnrampPurchaseAttemptResponse.Result.AMOUNT_EXCEEDS_MAXIMUM -> { - val error = Throwable("Error: AMOUNT_EXCEEDS_MAXIMUM") - ErrorUtils.handleError(error) - Result.failure(error) - } - - DeclareFiatOnrampPurchaseAttemptResponse.Result.UNRECOGNIZED -> { - val error = Throwable("Error: UNRECOGNIZED") - ErrorUtils.handleError(error) - Result.failure(error) - } - - else -> { - val error = Throwable("Error: Unknown Error") - ErrorUtils.handleError(error) - Result.failure(error) - } - } - } - } - - // TODO: potentially make this more generic in the event we introduce more airdrop types - // that can be requested for - suspend fun requestFirstKinAirdrop(owner: KeyPair): Result { - val request = TransactionService.AirdropRequest.newBuilder() - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .setAirdropType(TransactionService.AirdropType.GET_FIRST_KIN) - .apply { setSignature(sign(owner)) } - .build() - - return runCatching { - transactionApi.airdrop(request) - .toFlowable() - .asFlow() - .flowOn(Dispatchers.IO) - .map { - when (it.result) { - TransactionService.AirdropResponse.Result.OK -> { - KinAmount.fromProtoExchangeData(it.exchangeData) - } - - TransactionService.AirdropResponse.Result.ALREADY_CLAIMED -> { - throw AirdropException.AlreadyClaimedException() - } - - TransactionService.AirdropResponse.Result.UNAVAILABLE -> { - throw AirdropException.UnavailableException() - } - - else -> { - throw AirdropException.UnknownException() - } - } - }.first() - } - } - - suspend fun fetchIntentMetadata( - owner: KeyPair, - intentId: PublicKey - ): Result { - val request = TransactionService.GetIntentMetadataRequest.newBuilder() - .setIntentId(intentId.toIntentId()) - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .apply { setSignature(sign(owner)) } - .build() - - return runCatching { - transactionApi.getIntentMetadata(request) - .toFlowable() - .asFlow() - .flowOn(Dispatchers.IO) - .map { - if (it.result != TransactionService.GetIntentMetadataResponse.Result.OK) { - throw IllegalStateException() - } else { - IntentMetadata.newInstance(it.metadata) ?: throw IllegalStateException() - } - } - .firstOrNull() ?: throw IllegalStateException() - } - } - - @SuppressLint("CheckResult") - fun fetchLimits( - owner: KeyPair, - timestamp: Long - ): Flowable { - val request = TransactionService.GetLimitsRequest.newBuilder() - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .setConsumedSince(Timestamp.newBuilder().setSeconds(timestamp)) - .apply { setSignature(sign(owner)) } - .build() - - return transactionApi.getLimits(request) - .flatMap { - if (it.result != TransactionService.GetLimitsResponse.Result.OK) { - Single.error(IllegalStateException()) - } else { - Limits.newInstance( - sinceDate = timestamp, - fetchDate = System.currentTimeMillis(), - sendLimits = it.sendLimitsByCurrencyMap, - buyLimits = it.buyModuleLimitsByCurrencyMap, - deposits = it.depositLimit - ).let { limits -> - Single.just(limits) - } - } - } - .doOnSuccess { - setLimits(it) - setMaximumDeposit(it.maxDeposit) - trace( - tag = "Trx", - message = "Fetched limits", - type = TraceType.Process, - metadata = { - val sendLimit = it.sendLimitFor(CurrencyCode.USD) - if (sendLimit != null) { - "limitNextTx" to sendLimit - } - } - ) - } - .doOnError(ErrorUtils::handleError) - .toFlowable() - } - - fun fetchDestinationMetadata(destination: PublicKey): Single { - val request = TransactionService.CanWithdrawToAccountRequest.newBuilder() - .setAccount(destination.bytes.toSolanaAccount()) - .build() - - return transactionApi.canWithdrawToAccount(request).map { - DestinationMetadata.newInstance( - destination = destination, - isValid = it.isValidPaymentDestination, - kind = DestinationMetadata.Kind.tryValueOf(it.accountType.name) - ?: DestinationMetadata.Kind.Unknown - ) - } - .onErrorReturn { - DestinationMetadata.newInstance( - destination = destination, - isValid = false, - kind = DestinationMetadata.Kind.Unknown - ) - } - } - - fun fetchUpgradeableIntents(owner: KeyPair): Single> { - val request = TransactionService.GetPrioritizedIntentsForPrivacyUpgradeRequest.newBuilder() - .setOwner(owner.publicKeyBytes.toSolanaAccount()) - .setLimit(100) //TODO: implement paging - .apply { setSignature(sign(owner)) } - .build() - - return transactionApi.getPrioritizedIntentsForPrivacyUpgrade(request).flatMap { - when (it.result) { - TransactionService.GetPrioritizedIntentsForPrivacyUpgradeResponse.Result.OK -> { - try { - val items = it.itemsList.map { item -> UpgradeableIntent.newInstance(item) } - Single.just(items) - } catch (e: UpgradeablePrivateAction.UpgradeablePrivateActionException.DeserializationFailedException) { - Single.error(FetchUpgradeableIntentsException.DeserializationException()) - } catch (e: Exception) { - Single.error(e) - } - } - - TransactionService.GetPrioritizedIntentsForPrivacyUpgradeResponse.Result.NOT_FOUND -> { - Single.just(listOf()) - } - - else -> { - Single.error(FetchUpgradeableIntentsException.UnknownException()) - } - } - } - } - - data class DestinationMetadata( - val destination: PublicKey, - val isValid: Boolean, - val kind: Kind, - - val hasResolvedDestination: Boolean, - val resolvedDestination: PublicKey - ) { - enum class Kind { - Unknown, - TokenAccount, - OwnerAccount; - - companion object { - fun tryValueOf(value: String): Kind? { - return try { - valueOf(value) - } catch (e: Exception) { - null - } - } - } - } - - companion object { - fun newInstance( - destination: PublicKey, - isValid: Boolean, - kind: Kind - ): DestinationMetadata { - val hasResolvedDestination: Boolean - val resolvedDestination: PublicKey - - when (kind) { - Kind.Unknown, Kind.TokenAccount -> { - hasResolvedDestination = false - resolvedDestination = destination - } - - Kind.OwnerAccount -> { - hasResolvedDestination = true - resolvedDestination = - AssociatedTokenAccount.newInstance( - owner = destination, - mint = Mint.kin - ).ata.publicKey - } - } - - return DestinationMetadata( - destination = destination, - isValid = isValid, - kind = kind, - hasResolvedDestination = hasResolvedDestination, - resolvedDestination = resolvedDestination - ) - } - } - } -} - -class ErrorSubmitIntentException( - val errorSubmitIntent: ErrorSubmitIntent, - cause: Throwable? = null, - val messageString: String = "" -) : Exception(cause) { - override val message: String - get() = "${errorSubmitIntent.javaClass.simpleName} $messageString" -} - -enum class DeniedReason { - Unspecified, - TooManyFreeAccountsForPhoneNumber, - TooManyFreeAccountsForDevice, - UnsupportedCountry, - UnsupportedDevice; - - companion object { - fun fromValue(value: Int): DeniedReason { - return entries.firstOrNull { it.ordinal == value } ?: Unspecified - } - } -} - -sealed class ErrorSubmitIntent(val value: Int) { - data class Denied(val reasons: List = emptyList()) : ErrorSubmitIntent(0) - data class InvalidIntent(val reasons: List) : ErrorSubmitIntent(1) - data object SignatureError : ErrorSubmitIntent(2) - data class StaleState(val reasons: List) : ErrorSubmitIntent(3) - data object Unknown : ErrorSubmitIntent(-1) - data object DeviceTokenUnavailable : ErrorSubmitIntent(-2) - - override fun toString(): String { - return when (this) { - is Denied -> "denied(${reasons.joinToString()})" - DeviceTokenUnavailable -> "deviceTokenUnavailable" - is InvalidIntent -> "invalidIntent(${reasons.joinToString()})" - SignatureError -> "signatureError" - is StaleState -> "staleState(${reasons.joinToString()})" - Unknown -> "unknown" - } - } - - companion object { - operator fun invoke(proto: SubmitIntentResponse.Error): ErrorSubmitIntent { - val reasonStrings = proto.errorDetailsList.mapNotNull { - when (it.typeCase) { - TransactionService.ErrorDetails.TypeCase.REASON_STRING -> - it.reasonString.reason.takeIf { reason -> reason.isNotEmpty() } - - else -> null - } - } - return when (proto.code) { - SubmitIntentResponse.Error.Code.DENIED -> { - val reasons = proto.errorDetailsList.mapNotNull { - if (!it.hasDenied()) return@mapNotNull null - DeniedReason.fromValue(it.denied.codeValue) - } - - Denied(reasons) - } - - SubmitIntentResponse.Error.Code.INVALID_INTENT -> InvalidIntent(reasonStrings) - SubmitIntentResponse.Error.Code.SIGNATURE_ERROR -> SignatureError - SubmitIntentResponse.Error.Code.STALE_STATE -> StaleState(reasonStrings) - SubmitIntentResponse.Error.Code.UNRECOGNIZED -> Unknown - else -> return Unknown - } - } - } -} - -sealed class WithdrawException : Exception() { - class InvalidFractionalKinAmountException : WithdrawException() - class InsufficientFundsException : WithdrawException() -} - -sealed class FetchUpgradeableIntentsException : Exception() { - class DeserializationException : FetchUpgradeableIntentsException() - class UnknownException : FetchUpgradeableIntentsException() -} - -sealed class AirdropException : Exception() { - class AlreadyClaimedException : AirdropException() - class UnavailableException : AirdropException() - class UnknownException : AirdropException() -} diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/TransactionRepository_Swap.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/TransactionRepository_Swap.kt deleted file mode 100644 index d351330c0..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/repository/TransactionRepository_Swap.kt +++ /dev/null @@ -1,173 +0,0 @@ -package com.getcode.network.repository - -import com.codeinc.gen.transaction.v2.CodeTransactionService as TransactionService -import com.codeinc.gen.transaction.v2.CodeTransactionService.SwapRequest -import com.codeinc.gen.transaction.v2.CodeTransactionService.SwapRequest.Initiate -import com.codeinc.gen.transaction.v2.CodeTransactionService.SwapResponse -import com.getcode.model.intents.SwapConfigParameters -import com.getcode.model.intents.SwapIntent -import com.getcode.model.intents.requestToSubmitSignatures -import com.getcode.services.observers.BidirectionalStreamReference -import com.getcode.solana.SolanaTransaction -import com.getcode.solana.diff -import com.getcode.solana.keys.base58 -import com.getcode.solana.organizer.Organizer -import com.getcode.utils.ErrorUtils -import io.grpc.stub.StreamObserver -import kotlinx.coroutines.suspendCancellableCoroutine -import timber.log.Timber -import kotlin.coroutines.resume - -typealias BidirectionalSwapStream = BidirectionalStreamReference - -suspend fun TransactionRepository.initiateSwap(organizer: Organizer): Result { - val intent = SwapIntent.newInstance(organizer) - - Timber.d("Swap ID: ${intent.id.base58()}") - - return submit(intent) -} - -private suspend fun TransactionRepository.submit(intent: SwapIntent): Result = suspendCancellableCoroutine { cont -> - val reference = BidirectionalSwapStream(this) - - // Intentionally creates a retain-cycle using closures to ensure that we have - // a strong reference to the stream at all times. Doing so ensures that the - // callers don't have to manage the pointer to this stream and keep it alive - reference.retain() - - reference.stream = transactionApi.swap(object : StreamObserver { - override fun onNext(value: SwapResponse?) { - when (val response = value?.responseCase) { - // 2. Upon successful submission of intent action the server will - // respond with parameters that we'll need to apply to the intent - // before crafting and signing the transactions. - SwapResponse.ResponseCase.SERVER_PARAMETERS -> { - try { - val configParameters = SwapConfigParameters.invoke(value.serverParameters) ?: throw IllegalArgumentException() - intent.parameters = configParameters - - val submitSignatures = intent.requestToSubmitSignatures() - reference.stream?.onNext(submitSignatures) - Timber.d("Sent swap request, Intent: ${intent.id.base58()}") - } catch (e: Exception) { - Timber.e(t = e, message = "Received parameters but failed to apply them. Intent: ${intent.id.base58()}") - cont.resume(Result.failure(e)) - } - } - - // 3. If submitted transaction signatures are valid and match - // the server, we'll receive a success for the submitted intent. - SwapResponse.ResponseCase.SUCCESS -> { - Timber.d("Success: ${value.success.codeValue}, Intent: ${intent.id.base58()}") - reference.stream?.onCompleted() - } - - // 3. If the submitted transaction signatures don't match, the - // intent is considered failed. Something must have gone wrong - // on the transaction creation or signing on our side. - SwapResponse.ResponseCase.ERROR -> { - - val errors = mutableListOf() - - value.error.errorDetailsList.forEach { error -> - when (error.typeCase) { - TransactionService.ErrorDetails.TypeCase.REASON_STRING -> { - errors.add("Reason: ${error.reasonString}") - } - TransactionService.ErrorDetails.TypeCase.INVALID_SIGNATURE -> { - val expected = SolanaTransaction.fromList(error.invalidSignature.expectedTransaction.value.toByteArray().toList()) - val produced = intent.transaction(intent.parameters!!) - errors.addAll( - listOf( - "Action index: ${error.invalidSignature.actionId}", - "Invalid signature: ${ - com.getcode.solana.keys.Signature( - error.invalidSignature.providedSignature.value.toByteArray() - .toList() - ).base58()}", - "Transaction bytes: ${error.invalidSignature.expectedTransaction.value}", - "Transaction expected: $expected", - "Android produced: $produced" - ) - ) - - expected?.diff(produced) - } - TransactionService.ErrorDetails.TypeCase.DENIED -> { - errors.add("Denied: ${error.denied.reason}") - } - else -> Unit - } - - Timber.e( - "Error: ${errors.joinToString("\n")}" - ) - } - - reference.stream?.onCompleted() - cont.resume( - Result.failure( - ErrorSubmitSwapIntentException( - ErrorSubmitSwapIntent.valueOf(value.error.codeValue), - ) - ) - ) - } - else -> { - reference.stream?.onCompleted() - } - } - } - - override fun onError(t: Throwable?) { - Timber.i("onError: " + t?.message.orEmpty()) - t?.let { - ErrorUtils.handleError(it) - } - cont.resume( - Result.failure( - ErrorSubmitSwapIntentException(ErrorSubmitSwapIntent.Unknown, t) - ) - ) - } - - override fun onCompleted() { - Timber.i("onCompleted") - } - }) - - val initiateSwap = SwapRequest.newBuilder() - .setInitiate(Initiate.newBuilder() - .setOwner(intent.owner.publicKeyBytes.toSolanaAccount()) - .setSwapAuthority(intent.swapCluster.authorityPublicKey.byteArray.toSolanaAccount()) - .apply { setSignature(sign(intent.owner)) } - .build() - ).build() - - reference.stream?.onNext(initiateSwap) -} - -class ErrorSubmitSwapIntentException( - val errorSubmitSwapIntent: ErrorSubmitSwapIntent, - cause: Throwable? = null, - val messageString: String = "" -) : Exception(cause) { - override val message: String - get() = "${errorSubmitSwapIntent.name} $messageString" -} - -enum class ErrorSubmitSwapIntent(val value: Int) { - Denied(0), - InvalidIntent(1), - SignatureError(2), - StaleState(3), - Unknown(-1), - DeviceTokenUnavailable(-2); - - companion object { - fun valueOf(value: Int): ErrorSubmitSwapIntent { - return entries.firstOrNull { it.value == value } ?: Unknown - } - } -} diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/network/service/CurrencyService.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/network/service/CurrencyService.kt deleted file mode 100644 index 513184fd0..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/network/service/CurrencyService.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.getcode.network.service - -import com.getcode.model.Rate -import com.getcode.network.api.CurrencyApi -import com.getcode.services.network.core.NetworkOracle -import com.getcode.utils.ErrorUtils -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import timber.log.Timber -import javax.inject.Inject -import kotlin.time.Duration.Companion.convert -import kotlin.time.DurationUnit -import kotlin.time.ExperimentalTime - -data class ApiRateResult( - val rates: List, - val dateMillis: Long, -) - -class CurrencyService @Inject constructor( - private val api: CurrencyApi, - private val networkOracle: NetworkOracle, -) { - @OptIn(ExperimentalTime::class) - suspend fun getRates(): Result { - Timber.d("fetching rates") - return try { - networkOracle.managedRequest(api.getRates()) - .map { response -> - val rates = response.ratesMap.mapNotNull { (key, value) -> - val currency = com.getcode.model.CurrencyCode.tryValueOf(key) ?: return@mapNotNull null - Rate(fx = value, currency = currency) - }.toMutableList() - - if (rates.none { it.currency == com.getcode.model.CurrencyCode.KIN }) { - rates.add(Rate(fx = 1.0, currency = com.getcode.model.CurrencyCode.KIN)) - } - - Result.success(ApiRateResult( - rates = rates.toList(), - dateMillis = convert( - value = response.asOf.seconds.toDouble(), - sourceUnit = DurationUnit.SECONDS, - targetUnit = DurationUnit.MILLISECONDS - ).toLong() - )) - }.first() - } catch (e: Exception) { - ErrorUtils.handleError(e) - Result.failure(e) - } - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/com/getcode/utils/SignMessage.kt b/services/flipchat/payments/src/main/kotlin/com/getcode/utils/SignMessage.kt deleted file mode 100644 index 1a9f9392e..000000000 --- a/services/flipchat/payments/src/main/kotlin/com/getcode/utils/SignMessage.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.getcode.utils - -import com.codeinc.gen.common.v1.CodeModel -import com.getcode.ed25519.Ed25519 -import com.getcode.network.repository.toSignature -import com.google.protobuf.GeneratedMessageLite -import java.io.ByteArrayOutputStream - -fun , B : GeneratedMessageLite.Builder> GeneratedMessageLite.Builder.sign(owner: Ed25519.KeyPair): CodeModel.Signature { - // dump message up until this point into a ByteArray - val bos = ByteArrayOutputStream() - this.buildPartial().writeTo(bos) - - /** - * sign message up to this point with owner and return as [com.codeinc.gen.common.v1.Model.Signature] - */ - return Ed25519.sign(bos.toByteArray(), owner).toSignature() -} diff --git a/services/flipchat/payments/src/main/kotlin/xyz/flipchat/services/FcPaymentsConfig.kt b/services/flipchat/payments/src/main/kotlin/xyz/flipchat/services/FcPaymentsConfig.kt deleted file mode 100644 index 9b741c7b1..000000000 --- a/services/flipchat/payments/src/main/kotlin/xyz/flipchat/services/FcPaymentsConfig.kt +++ /dev/null @@ -1,9 +0,0 @@ -package xyz.flipchat.services - -import com.getcode.services.ChannelConfig -import xyz.flipchat.services.payments.BuildConfig - -internal data class FcPaymentsConfig( - override val baseUrl: String = "payments.api.flipchat-infra.xyz", - override val userAgent: String = "Flipchat/Payments/Android/${BuildConfig.VERSION_NAME}", -): ChannelConfig diff --git a/services/flipchat/payments/src/main/kotlin/xyz/flipchat/services/PaymentController.kt b/services/flipchat/payments/src/main/kotlin/xyz/flipchat/services/PaymentController.kt deleted file mode 100644 index bae81e7ac..000000000 --- a/services/flipchat/payments/src/main/kotlin/xyz/flipchat/services/PaymentController.kt +++ /dev/null @@ -1,229 +0,0 @@ -package xyz.flipchat.services - -import androidx.compose.runtime.staticCompositionLocalOf -import com.getcode.domain.BillController -import com.getcode.manager.TopBarManager -import com.getcode.model.ID -import com.getcode.model.KinAmount -import com.getcode.models.BillState -import com.getcode.models.ConfirmationState -import com.getcode.models.MessageTipPaymentConfirmation -import com.getcode.models.PublicPaymentConfirmation -import com.getcode.network.repository.PaymentError -import com.getcode.network.repository.PaymentRepository -import com.getcode.services.model.ExtendedMetadata -import com.getcode.solana.keys.PublicKey -import com.getcode.util.resources.ResourceHelper -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import xyz.flipchat.services.payments.R -import javax.inject.Inject -import javax.inject.Singleton - -data class PaymentState( - val billState: BillState = BillState.Default, -) - -sealed interface PaymentEvent { - data class OnPaymentSuccess( - val intentId: ID, - val destination: PublicKey, - val metadata: ExtendedMetadata?, - val amount: KinAmount, - val acknowledge: (Boolean, () -> Unit) -> Unit // Caller returns true if they want to proceed as success, false as error - ) : PaymentEvent - data object OnPaymentCancelled : PaymentEvent - data class OnPaymentError(val error: Throwable): PaymentEvent -} - -val LocalPaymentController = staticCompositionLocalOf { null } - -@Singleton -open class PaymentController @Inject constructor( - private val paymentRepository: PaymentRepository, - private val resources: ResourceHelper, - private val billController: BillController, -) { - private val scope = CoroutineScope(Dispatchers.IO) - - val state = billController.state.map { - PaymentState(it) - }.stateIn(scope, started = SharingStarted.Eagerly, initialValue = PaymentState()) - - private val _eventFlow: MutableSharedFlow = MutableSharedFlow() - val eventFlow: SharedFlow = _eventFlow.asSharedFlow() - - fun presentPublicPaymentConfirmation( - destination: PublicKey, - amount: KinAmount, - metadata: ExtendedMetadata, - ) { - billController.update { - it.copy( - publicPaymentConfirmation = PublicPaymentConfirmation( - state = ConfirmationState.AwaitingConfirmation, - amount = amount, - destination = destination, - metadata = metadata, - ) - ) - } - } - - fun completePublicPayment() = - scope.launch { - val confirmation = billController.state.value.publicPaymentConfirmation ?: return@launch - val destination = confirmation.destination - val amount = confirmation.amount - val metadata = confirmation.metadata - - billController.update { - it.copy( - publicPaymentConfirmation = it.publicPaymentConfirmation?.copy(state = ConfirmationState.Sending), - ) - } - - runCatching { - paymentRepository.payPublicly(amount, destination, metadata) - }.onSuccess { - _eventFlow.emit(PaymentEvent.OnPaymentSuccess( - intentId = it, - destination = destination, - metadata = metadata, - amount = amount, - acknowledge = { isSuccess, after -> - if (isSuccess) { - scope.launch { - billController.update { billState -> - val publicPaymentConfirmation = - billState.publicPaymentConfirmation ?: return@update billState - billState.copy( - publicPaymentConfirmation = publicPaymentConfirmation.copy(state = ConfirmationState.Sent), - ) - } - cancelPayment(fromUser = false) - after() - } - } else { - billController.reset() - after() - - } - } - )) - }.onFailure { - when { - it is PaymentError -> { - when (it) { - is PaymentError.InsufficientBalance -> presentInsufficientFundsError() - is PaymentError.OrganizerNotFound -> presentPaymentFailedError() - } - } - else -> presentPaymentFailedError() - } - - _eventFlow.emit(PaymentEvent.OnPaymentError(it)) - - billController.reset() - } - } - - fun cancelPayment(fromUser: Boolean = true) { - scope.launch { - billController.reset() - if (fromUser) { - _eventFlow.emit(PaymentEvent.OnPaymentCancelled) - } - } - } - - fun presentMessageTipConfirmation(metadata: ExtendedMetadata, destination: PublicKey) { - billController.update { - it.copy( - messageTipPaymentConfirmation = MessageTipPaymentConfirmation( - state = ConfirmationState.AwaitingConfirmation, - metadata = metadata, - destination = destination, - ) - ) - } - } - - fun completeMessageTip(amount: KinAmount) = - scope.launch { - val confirmation = billController.state.value.messageTipPaymentConfirmation ?: return@launch - val destination = confirmation.destination - val metadata = confirmation.metadata - - billController.update { - it.copy( - messageTipPaymentConfirmation = it.messageTipPaymentConfirmation?.copy(state = ConfirmationState.Sending), - ) - } - - runCatching { - paymentRepository.payPublicly(amount, destination, metadata) - }.onSuccess { - _eventFlow.emit(PaymentEvent.OnPaymentSuccess( - intentId = it, - destination = destination, - metadata = metadata, - amount = amount, - acknowledge = { isSuccess, after -> - if (isSuccess) { - scope.launch { - billController.update { billState -> - val messageTipPaymentConfirmation = - billState.messageTipPaymentConfirmation ?: return@update billState - billState.copy( - messageTipPaymentConfirmation = messageTipPaymentConfirmation.copy(state = ConfirmationState.Sent), - ) - } - cancelPayment(fromUser = false) - after() - } - } else { - billController.reset() - after() - - } - } - )) - }.onFailure { - when { - it is PaymentError -> { - when (it) { - is PaymentError.InsufficientBalance -> presentInsufficientFundsError() - is PaymentError.OrganizerNotFound -> presentPaymentFailedError() - } - } - else -> presentPaymentFailedError() - } - - _eventFlow.emit(PaymentEvent.OnPaymentError(it)) - - billController.reset() - } - } - - private fun presentInsufficientFundsError() { - TopBarManager.showMessage( - resources.getString(R.string.error_title_paymentFailedDueToInsufficientFunds), - resources.getString(R.string.error_description_paymentFailedDueToInsufficientFunds), - ) - } - - private fun presentPaymentFailedError() { - TopBarManager.showMessage( - resources.getString(R.string.error_title_paymentFailed), - resources.getString(R.string.error_description_paymentFailed), - ) - } -} \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/xyz/flipchat/services/internal/annotations/PaymentsManagedChannel.kt b/services/flipchat/payments/src/main/kotlin/xyz/flipchat/services/internal/annotations/PaymentsManagedChannel.kt deleted file mode 100644 index a4eabbf9f..000000000 --- a/services/flipchat/payments/src/main/kotlin/xyz/flipchat/services/internal/annotations/PaymentsManagedChannel.kt +++ /dev/null @@ -1,13 +0,0 @@ -package xyz.flipchat.services.internal.annotations - -import javax.inject.Qualifier - -@Qualifier -@Target( - AnnotationTarget.FUNCTION, - AnnotationTarget.PROPERTY_GETTER, - AnnotationTarget.PROPERTY_SETTER, - AnnotationTarget.VALUE_PARAMETER, - AnnotationTarget.FIELD -) -annotation class PaymentsManagedChannel \ No newline at end of file diff --git a/services/flipchat/payments/src/main/kotlin/xyz/flipchat/services/internal/inject/FcPaymentsModule.kt b/services/flipchat/payments/src/main/kotlin/xyz/flipchat/services/internal/inject/FcPaymentsModule.kt deleted file mode 100644 index f9efae676..000000000 --- a/services/flipchat/payments/src/main/kotlin/xyz/flipchat/services/internal/inject/FcPaymentsModule.kt +++ /dev/null @@ -1,48 +0,0 @@ -package xyz.flipchat.services.internal.inject - -import android.content.Context -import com.getcode.services.utils.logging.LoggingClientInterceptor -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import io.grpc.ManagedChannel -import io.grpc.android.AndroidChannelBuilder -import org.kin.sdk.base.network.api.agora.OkHttpChannelBuilderForcedTls12 -import xyz.flipchat.services.FcPaymentsConfig -import xyz.flipchat.services.internal.annotations.PaymentsManagedChannel -import xyz.flipchat.services.payments.BuildConfig -import java.util.concurrent.TimeUnit -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object FcPaymentsModule { - - @Singleton - @Provides - fun providesPaymentsServicesConfig(): FcPaymentsConfig { - return FcPaymentsConfig() - } - - @Singleton - @Provides - @PaymentsManagedChannel - fun provideManagedChannel( - @ApplicationContext context: Context, - config: FcPaymentsConfig, - ): ManagedChannel { - return AndroidChannelBuilder - .usingBuilder(OkHttpChannelBuilderForcedTls12.forAddress(config.baseUrl, config.port)) - .context(context) - .userAgent(config.userAgent) - .keepAliveTime(config.keepAlive.inWholeMilliseconds, TimeUnit.MILLISECONDS) - .apply { - if (BuildConfig.DEBUG) { - this.intercept(LoggingClientInterceptor()) - } - } - .build() - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/.gitignore b/services/flipchat/sdk/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/services/flipchat/sdk/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/services/flipchat/sdk/build.gradle.kts b/services/flipchat/sdk/build.gradle.kts deleted file mode 100644 index a18961304..000000000 --- a/services/flipchat/sdk/build.gradle.kts +++ /dev/null @@ -1,118 +0,0 @@ -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - id(Plugins.android_library) - id(Plugins.kotlin_android) - id(Plugins.kotlin_kapt) - id(Plugins.kotlin_serialization) -} - -android { - namespace = "${Gradle.flipchatNamespace}.services.sdk" - compileSdk = Android.compileSdkVersion - defaultConfig { - minSdk = Android.minSdkVersion - testInstrumentationRunner = Android.testInstrumentationRunner - - consumerProguardFiles("consumer-rules.pro") - - buildConfigField("String", "VERSION_NAME", "\"${Packaging.Flipchat.versionName}\"") - - buildConfigField("Boolean", "NOTIFY_ERRORS", "false") - buildConfigField( - "String", - "GOOGLE_CLOUD_PROJECT_NUMBER", - "\"${tryReadProperty(rootProject.rootDir, "GOOGLE_CLOUD_PROJECT_NUMBER", "-1L")}\"" - ) - - buildConfigField( - "String", - "FINGERPRINT_API_KEY", - "\"${tryReadProperty(rootProject.rootDir, "FINGERPRINT_API_KEY")}\"" - ) - - javaCompileOptions { - annotationProcessorOptions { - arguments += mapOf("room.schemaLocation" to "$projectDir/schemas") - } - } - } - - buildFeatures { - buildConfig = true - } -} - -kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(Versions.java)) - } - - compilerOptions { - jvmTarget.set(JvmTarget.fromTarget(Versions.java)) - optIn.addAll( - "kotlin.time.ExperimentalTime", - "kotlin.ExperimentalUnsignedTypes", - "kotlin.RequiresOptIn" - ) - } -} - -dependencies { - implementation(project(":definitions:flipchat:models")) - api(project(":services:flipchat:core")) - api(project(":services:flipchat:chat")) - api(project(":services:flipchat:payments")) - implementation(project(":libs:emojis")) - implementation(project(":libs:requests")) - implementation(project(":ui:resources")) - - implementation(platform(Libs.compose_bom)) - implementation(Libs.compose_ui) - - implementation(Libs.kotlinx_coroutines_core) - implementation(Libs.kotlinx_serialization_json) - implementation(Libs.inject) - - implementation(Libs.grpc_android) - implementation(Libs.grpc_okhttp) - implementation(Libs.grpc_kotlin) - implementation(Libs.androidx_lifecycle_runtime) - implementation(Libs.androidx_room_runtime) - implementation(Libs.androidx_room_ktx) - implementation(Libs.androidx_room_paging) - implementation(Libs.androidx_room_rxjava3) - implementation(Libs.okhttp) - implementation(Libs.mixpanel) - - implementation(platform(Libs.firebase_bom)) - implementation(Libs.firebase_crashlytics) - implementation(Libs.firebase_installations) - implementation(Libs.firebase_perf) - implementation(Libs.firebase_messaging) - - implementation(Libs.play_integrity) - - implementation(Libs.androidx_paging_runtime) - - kapt(Libs.androidx_room_compiler) - implementation(Libs.sqlcipher) - - implementation(Libs.fingerprint_pro) - - implementation(Libs.lib_phone_number_google) - - androidTestImplementation(Libs.androidx_junit) - androidTestImplementation(Libs.junit) - androidTestImplementation(Libs.androidx_test_runner) - - implementation(Libs.androidx_work) - - implementation(Libs.hilt) - implementation(Libs.hilt_worker) - kapt(Libs.hilt_android_compiler) - kapt(Libs.hilt_compiler) - - implementation(Libs.timber) - implementation(Libs.bugsnag) -} diff --git a/services/flipchat/sdk/consumer-rules.pro b/services/flipchat/sdk/consumer-rules.pro deleted file mode 100644 index 5f642a47c..000000000 --- a/services/flipchat/sdk/consumer-rules.pro +++ /dev/null @@ -1,6 +0,0 @@ -# Needed to keep generic signatures --keepattributes Signature - --keepclasseswithmembernames class * { - native ; -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/1.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/1.json deleted file mode 100644 index 2240fdcc9..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/1.json +++ /dev/null @@ -1,260 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 1, - "identityHash": "21df295ddc43f1ac6661169bf5cca785", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `title` TEXT, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `unreadCount` INTEGER NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `dateMillis` INTEGER NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "message_contents", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`messageIdBase58`, `content`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58", - "content" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '21df295ddc43f1ac6661169bf5cca785')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/10.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/10.json deleted file mode 100644 index 12d4e2b0e..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/10.json +++ /dev/null @@ -1,338 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 10, - "identityHash": "7217ed18e0e307ed362d1e7aaf05229f", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `ownerIdBase58` TEXT, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `unreadCount` INTEGER NOT NULL, `coverChargeQuarks` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ownerIdBase58", - "columnName": "ownerIdBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "coverChargeQuarks", - "columnName": "coverChargeQuarks", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isHost` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "message_contents", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`messageIdBase58`, `content`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58", - "content" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7217ed18e0e307ed362d1e7aaf05229f')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/11.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/11.json deleted file mode 100644 index c137208fe..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/11.json +++ /dev/null @@ -1,345 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 11, - "identityHash": "781e4bc06ff540e47ea520e5e2f39f35", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `ownerIdBase58` TEXT, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `unreadCount` INTEGER NOT NULL, `coverChargeQuarks` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ownerIdBase58", - "columnName": "ownerIdBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "coverChargeQuarks", - "columnName": "coverChargeQuarks", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isHost` INTEGER NOT NULL DEFAULT false, `isMuted` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "message_contents", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`messageIdBase58`, `content`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58", - "content" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '781e4bc06ff540e47ea520e5e2f39f35')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/12.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/12.json deleted file mode 100644 index 6662559d7..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/12.json +++ /dev/null @@ -1,402 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 12, - "identityHash": "5fb9b50af4f9ba1005b386c7b1cbdb7b", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `ownerIdBase58` TEXT, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `unreadCount` INTEGER NOT NULL, `coverChargeQuarks` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ownerIdBase58", - "columnName": "ownerIdBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "coverChargeQuarks", - "columnName": "coverChargeQuarks", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isHost` INTEGER NOT NULL DEFAULT false, `isMuted` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [ - { - "name": "index_members_memberIdBase58", - "unique": false, - "columnNames": [ - "memberIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_memberIdBase58` ON `${TABLE_NAME}` (`memberIdBase58`)" - }, - { - "name": "index_members_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_messages_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - }, - { - "name": "index_messages_senderIdBase58", - "unique": false, - "columnNames": [ - "senderIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_senderIdBase58` ON `${TABLE_NAME}` (`senderIdBase58`)" - }, - { - "name": "index_messages_dateMillis", - "unique": false, - "columnNames": [ - "dateMillis" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_dateMillis` ON `${TABLE_NAME}` (`dateMillis`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "message_contents", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`messageIdBase58`, `content`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58", - "content" - ] - }, - "indices": [ - { - "name": "index_message_contents_messageIdBase58", - "unique": false, - "columnNames": [ - "messageIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_message_contents_messageIdBase58` ON `${TABLE_NAME}` (`messageIdBase58`)" - } - ], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5fb9b50af4f9ba1005b386c7b1cbdb7b')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/13.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/13.json deleted file mode 100644 index 0d6b0118b..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/13.json +++ /dev/null @@ -1,409 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 13, - "identityHash": "aedd745db33530cad54445629f3dbc43", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `ownerIdBase58` TEXT, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `canMute` INTEGER NOT NULL DEFAULT true, `unreadCount` INTEGER NOT NULL, `coverChargeQuarks` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ownerIdBase58", - "columnName": "ownerIdBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "canMute", - "columnName": "canMute", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "coverChargeQuarks", - "columnName": "coverChargeQuarks", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isHost` INTEGER NOT NULL DEFAULT false, `isMuted` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [ - { - "name": "index_members_memberIdBase58", - "unique": false, - "columnNames": [ - "memberIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_memberIdBase58` ON `${TABLE_NAME}` (`memberIdBase58`)" - }, - { - "name": "index_members_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_messages_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - }, - { - "name": "index_messages_senderIdBase58", - "unique": false, - "columnNames": [ - "senderIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_senderIdBase58` ON `${TABLE_NAME}` (`senderIdBase58`)" - }, - { - "name": "index_messages_dateMillis", - "unique": false, - "columnNames": [ - "dateMillis" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_dateMillis` ON `${TABLE_NAME}` (`dateMillis`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "message_contents", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`messageIdBase58`, `content`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58", - "content" - ] - }, - "indices": [ - { - "name": "index_message_contents_messageIdBase58", - "unique": false, - "columnNames": [ - "messageIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_message_contents_messageIdBase58` ON `${TABLE_NAME}` (`messageIdBase58`)" - } - ], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'aedd745db33530cad54445629f3dbc43')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/14.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/14.json deleted file mode 100644 index 365127591..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/14.json +++ /dev/null @@ -1,416 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 14, - "identityHash": "51fe51d1440bd508116bb2339ac5de55", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `ownerIdBase58` TEXT, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `canMute` INTEGER NOT NULL DEFAULT true, `unreadCount` INTEGER NOT NULL, `coverChargeQuarks` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ownerIdBase58", - "columnName": "ownerIdBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "canMute", - "columnName": "canMute", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "coverChargeQuarks", - "columnName": "coverChargeQuarks", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isHost` INTEGER NOT NULL DEFAULT false, `isMuted` INTEGER NOT NULL DEFAULT false, `isFullMember` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isFullMember", - "columnName": "isFullMember", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [ - { - "name": "index_members_memberIdBase58", - "unique": false, - "columnNames": [ - "memberIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_memberIdBase58` ON `${TABLE_NAME}` (`memberIdBase58`)" - }, - { - "name": "index_members_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_messages_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - }, - { - "name": "index_messages_senderIdBase58", - "unique": false, - "columnNames": [ - "senderIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_senderIdBase58` ON `${TABLE_NAME}` (`senderIdBase58`)" - }, - { - "name": "index_messages_dateMillis", - "unique": false, - "columnNames": [ - "dateMillis" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_dateMillis` ON `${TABLE_NAME}` (`dateMillis`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "message_contents", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`messageIdBase58`, `content`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58", - "content" - ] - }, - "indices": [ - { - "name": "index_message_contents_messageIdBase58", - "unique": false, - "columnNames": [ - "messageIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_message_contents_messageIdBase58` ON `${TABLE_NAME}` (`messageIdBase58`)" - } - ], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '51fe51d1440bd508116bb2339ac5de55')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/15.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/15.json deleted file mode 100644 index ac97ca1ec..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/15.json +++ /dev/null @@ -1,416 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 15, - "identityHash": "51fe51d1440bd508116bb2339ac5de55", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `ownerIdBase58` TEXT, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `canMute` INTEGER NOT NULL DEFAULT true, `unreadCount` INTEGER NOT NULL, `coverChargeQuarks` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ownerIdBase58", - "columnName": "ownerIdBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "canMute", - "columnName": "canMute", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "coverChargeQuarks", - "columnName": "coverChargeQuarks", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isHost` INTEGER NOT NULL DEFAULT false, `isMuted` INTEGER NOT NULL DEFAULT false, `isFullMember` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isFullMember", - "columnName": "isFullMember", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [ - { - "name": "index_members_memberIdBase58", - "unique": false, - "columnNames": [ - "memberIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_memberIdBase58` ON `${TABLE_NAME}` (`memberIdBase58`)" - }, - { - "name": "index_members_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_messages_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - }, - { - "name": "index_messages_senderIdBase58", - "unique": false, - "columnNames": [ - "senderIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_senderIdBase58` ON `${TABLE_NAME}` (`senderIdBase58`)" - }, - { - "name": "index_messages_dateMillis", - "unique": false, - "columnNames": [ - "dateMillis" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_dateMillis` ON `${TABLE_NAME}` (`dateMillis`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "message_contents", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`messageIdBase58`, `content`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58", - "content" - ] - }, - "indices": [ - { - "name": "index_message_contents_messageIdBase58", - "unique": false, - "columnNames": [ - "messageIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_message_contents_messageIdBase58` ON `${TABLE_NAME}` (`messageIdBase58`)" - } - ], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '51fe51d1440bd508116bb2339ac5de55')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/16.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/16.json deleted file mode 100644 index e55bf1b89..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/16.json +++ /dev/null @@ -1,423 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 16, - "identityHash": "cb1501f407ce2507e171c285e685bc0c", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `ownerIdBase58` TEXT, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `canMute` INTEGER NOT NULL DEFAULT true, `unreadCount` INTEGER NOT NULL, `coverChargeQuarks` INTEGER, `hasMoreUnread` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ownerIdBase58", - "columnName": "ownerIdBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "canMute", - "columnName": "canMute", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "coverChargeQuarks", - "columnName": "coverChargeQuarks", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "hasMoreUnread", - "columnName": "hasMoreUnread", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isHost` INTEGER NOT NULL DEFAULT false, `isMuted` INTEGER NOT NULL DEFAULT false, `isFullMember` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isFullMember", - "columnName": "isFullMember", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [ - { - "name": "index_members_memberIdBase58", - "unique": false, - "columnNames": [ - "memberIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_memberIdBase58` ON `${TABLE_NAME}` (`memberIdBase58`)" - }, - { - "name": "index_members_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_messages_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - }, - { - "name": "index_messages_senderIdBase58", - "unique": false, - "columnNames": [ - "senderIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_senderIdBase58` ON `${TABLE_NAME}` (`senderIdBase58`)" - }, - { - "name": "index_messages_dateMillis", - "unique": false, - "columnNames": [ - "dateMillis" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_dateMillis` ON `${TABLE_NAME}` (`dateMillis`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "message_contents", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`messageIdBase58`, `content`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58", - "content" - ] - }, - "indices": [ - { - "name": "index_message_contents_messageIdBase58", - "unique": false, - "columnNames": [ - "messageIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_message_contents_messageIdBase58` ON `${TABLE_NAME}` (`messageIdBase58`)" - } - ], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cb1501f407ce2507e171c285e685bc0c')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/17.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/17.json deleted file mode 100644 index f5e873ef1..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/17.json +++ /dev/null @@ -1,400 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 17, - "identityHash": "7472edb154421ae341376a6cb19c5af1", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `ownerIdBase58` TEXT, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `canMute` INTEGER NOT NULL DEFAULT true, `unreadCount` INTEGER NOT NULL, `coverChargeQuarks` INTEGER, `hasMoreUnread` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ownerIdBase58", - "columnName": "ownerIdBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "canMute", - "columnName": "canMute", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "coverChargeQuarks", - "columnName": "coverChargeQuarks", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "hasMoreUnread", - "columnName": "hasMoreUnread", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isHost` INTEGER NOT NULL DEFAULT false, `isMuted` INTEGER NOT NULL DEFAULT false, `isFullMember` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isFullMember", - "columnName": "isFullMember", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [ - { - "name": "index_members_memberIdBase58", - "unique": false, - "columnNames": [ - "memberIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_memberIdBase58` ON `${TABLE_NAME}` (`memberIdBase58`)" - }, - { - "name": "index_members_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, `type` INTEGER NOT NULL DEFAULT 1, `content` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "1" - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_messages_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - }, - { - "name": "index_messages_senderIdBase58", - "unique": false, - "columnNames": [ - "senderIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_senderIdBase58` ON `${TABLE_NAME}` (`senderIdBase58`)" - }, - { - "name": "index_messages_dateMillis", - "unique": false, - "columnNames": [ - "dateMillis" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_dateMillis` ON `${TABLE_NAME}` (`dateMillis`)" - } - ], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7472edb154421ae341376a6cb19c5af1')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/18.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/18.json deleted file mode 100644 index e597f92c7..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/18.json +++ /dev/null @@ -1,407 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 18, - "identityHash": "ac813724bab87a6eb46acca31b65a3b5", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `ownerIdBase58` TEXT, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `canMute` INTEGER NOT NULL DEFAULT true, `unreadCount` INTEGER NOT NULL, `coverChargeQuarks` INTEGER, `hasMoreUnread` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ownerIdBase58", - "columnName": "ownerIdBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "canMute", - "columnName": "canMute", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "coverChargeQuarks", - "columnName": "coverChargeQuarks", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "hasMoreUnread", - "columnName": "hasMoreUnread", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isHost` INTEGER NOT NULL DEFAULT false, `isMuted` INTEGER NOT NULL DEFAULT false, `isFullMember` INTEGER NOT NULL DEFAULT false, `isBlocked` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isFullMember", - "columnName": "isFullMember", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isBlocked", - "columnName": "isBlocked", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [ - { - "name": "index_members_memberIdBase58", - "unique": false, - "columnNames": [ - "memberIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_memberIdBase58` ON `${TABLE_NAME}` (`memberIdBase58`)" - }, - { - "name": "index_members_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, `type` INTEGER NOT NULL DEFAULT 1, `content` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "1" - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_messages_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - }, - { - "name": "index_messages_senderIdBase58", - "unique": false, - "columnNames": [ - "senderIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_senderIdBase58` ON `${TABLE_NAME}` (`senderIdBase58`)" - }, - { - "name": "index_messages_dateMillis", - "unique": false, - "columnNames": [ - "dateMillis" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_dateMillis` ON `${TABLE_NAME}` (`dateMillis`)" - } - ], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ac813724bab87a6eb46acca31b65a3b5')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/19.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/19.json deleted file mode 100644 index a1850d893..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/19.json +++ /dev/null @@ -1,413 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 19, - "identityHash": "9c9f066316b87831894e6f65cbdfc60f", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `ownerIdBase58` TEXT, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `canMute` INTEGER NOT NULL DEFAULT true, `unreadCount` INTEGER NOT NULL, `coverChargeQuarks` INTEGER, `hasMoreUnread` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ownerIdBase58", - "columnName": "ownerIdBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "canMute", - "columnName": "canMute", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "coverChargeQuarks", - "columnName": "coverChargeQuarks", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "hasMoreUnread", - "columnName": "hasMoreUnread", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isHost` INTEGER NOT NULL DEFAULT false, `isMuted` INTEGER NOT NULL DEFAULT false, `isFullMember` INTEGER NOT NULL DEFAULT false, `isBlocked` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isFullMember", - "columnName": "isFullMember", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isBlocked", - "columnName": "isBlocked", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [ - { - "name": "index_members_memberIdBase58", - "unique": false, - "columnNames": [ - "memberIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_memberIdBase58` ON `${TABLE_NAME}` (`memberIdBase58`)" - }, - { - "name": "index_members_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, `deletedByBase58` TEXT, `type` INTEGER NOT NULL DEFAULT 1, `content` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "deletedByBase58", - "columnName": "deletedByBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "1" - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_messages_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - }, - { - "name": "index_messages_senderIdBase58", - "unique": false, - "columnNames": [ - "senderIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_senderIdBase58` ON `${TABLE_NAME}` (`senderIdBase58`)" - }, - { - "name": "index_messages_dateMillis", - "unique": false, - "columnNames": [ - "dateMillis" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_dateMillis` ON `${TABLE_NAME}` (`dateMillis`)" - } - ], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9c9f066316b87831894e6f65cbdfc60f')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/2.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/2.json deleted file mode 100644 index 01118ddb4..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/2.json +++ /dev/null @@ -1,299 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 2, - "identityHash": "73d09888385f047520f90e3ae9fc75f1", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `title` TEXT NOT NULL, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `unreadCount` INTEGER NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `dateMillis` INTEGER NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "message_contents", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`messageIdBase58`, `content`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58", - "content" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '73d09888385f047520f90e3ae9fc75f1')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/20.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/20.json deleted file mode 100644 index e302f5412..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/20.json +++ /dev/null @@ -1,420 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 20, - "identityHash": "0c22cc344f2b02a0048048bfe634dbe7", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `ownerIdBase58` TEXT, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `canMute` INTEGER NOT NULL DEFAULT true, `unreadCount` INTEGER NOT NULL, `coverChargeQuarks` INTEGER, `hasMoreUnread` INTEGER NOT NULL DEFAULT false, `isOpen` INTEGER NOT NULL DEFAULT true, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ownerIdBase58", - "columnName": "ownerIdBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "canMute", - "columnName": "canMute", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "coverChargeQuarks", - "columnName": "coverChargeQuarks", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "hasMoreUnread", - "columnName": "hasMoreUnread", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isOpen", - "columnName": "isOpen", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isHost` INTEGER NOT NULL DEFAULT false, `isMuted` INTEGER NOT NULL DEFAULT false, `isFullMember` INTEGER NOT NULL DEFAULT false, `isBlocked` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isFullMember", - "columnName": "isFullMember", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isBlocked", - "columnName": "isBlocked", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [ - { - "name": "index_members_memberIdBase58", - "unique": false, - "columnNames": [ - "memberIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_memberIdBase58` ON `${TABLE_NAME}` (`memberIdBase58`)" - }, - { - "name": "index_members_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, `deletedByBase58` TEXT, `type` INTEGER NOT NULL DEFAULT 1, `content` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "deletedByBase58", - "columnName": "deletedByBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "1" - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_messages_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - }, - { - "name": "index_messages_senderIdBase58", - "unique": false, - "columnNames": [ - "senderIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_senderIdBase58` ON `${TABLE_NAME}` (`senderIdBase58`)" - }, - { - "name": "index_messages_dateMillis", - "unique": false, - "columnNames": [ - "dateMillis" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_dateMillis` ON `${TABLE_NAME}` (`dateMillis`)" - } - ], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0c22cc344f2b02a0048048bfe634dbe7')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/21.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/21.json deleted file mode 100644 index e6c821ed5..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/21.json +++ /dev/null @@ -1,481 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 21, - "identityHash": "c34cc1c2cab1a9bb6e2f0e7d0beb9413", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `ownerIdBase58` TEXT, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `canMute` INTEGER NOT NULL DEFAULT true, `unreadCount` INTEGER NOT NULL, `coverChargeQuarks` INTEGER, `hasMoreUnread` INTEGER NOT NULL DEFAULT false, `isOpen` INTEGER NOT NULL DEFAULT true, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ownerIdBase58", - "columnName": "ownerIdBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "canMute", - "columnName": "canMute", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "coverChargeQuarks", - "columnName": "coverChargeQuarks", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "hasMoreUnread", - "columnName": "hasMoreUnread", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isOpen", - "columnName": "isOpen", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isHost` INTEGER NOT NULL DEFAULT false, `isMuted` INTEGER NOT NULL DEFAULT false, `isFullMember` INTEGER NOT NULL DEFAULT false, `isBlocked` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isFullMember", - "columnName": "isFullMember", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isBlocked", - "columnName": "isBlocked", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [ - { - "name": "index_members_memberIdBase58", - "unique": false, - "columnNames": [ - "memberIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_memberIdBase58` ON `${TABLE_NAME}` (`memberIdBase58`)" - }, - { - "name": "index_members_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, `deletedByBase58` TEXT, `inReplyToBase58` TEXT, `tipCount` INTEGER NOT NULL DEFAULT 0, `type` INTEGER NOT NULL DEFAULT 1, `content` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "deletedByBase58", - "columnName": "deletedByBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "inReplyToBase58", - "columnName": "inReplyToBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "tipCount", - "columnName": "tipCount", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "1" - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_messages_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - }, - { - "name": "index_messages_senderIdBase58", - "unique": false, - "columnNames": [ - "senderIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_senderIdBase58` ON `${TABLE_NAME}` (`senderIdBase58`)" - }, - { - "name": "index_messages_dateMillis", - "unique": false, - "columnNames": [ - "dateMillis" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_dateMillis` ON `${TABLE_NAME}` (`dateMillis`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "tips", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `messageIdBase58` TEXT NOT NULL, `amount` INTEGER NOT NULL, `tipperIdBase58` TEXT NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "tipperIdBase58", - "columnName": "tipperIdBase58", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_tips_messageIdBase58", - "unique": false, - "columnNames": [ - "messageIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_tips_messageIdBase58` ON `${TABLE_NAME}` (`messageIdBase58`)" - } - ], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c34cc1c2cab1a9bb6e2f0e7d0beb9413')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/22.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/22.json deleted file mode 100644 index 9e4361481..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/22.json +++ /dev/null @@ -1,494 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 22, - "identityHash": "ddf50bec418df53870ee520e05dcb3cc", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `ownerIdBase58` TEXT, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `canMute` INTEGER NOT NULL DEFAULT true, `unreadCount` INTEGER NOT NULL, `coverChargeQuarks` INTEGER, `hasMoreUnread` INTEGER NOT NULL DEFAULT false, `isOpen` INTEGER NOT NULL DEFAULT true, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ownerIdBase58", - "columnName": "ownerIdBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "canMute", - "columnName": "canMute", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "messagingFee", - "columnName": "coverChargeQuarks", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "hasMoreUnread", - "columnName": "hasMoreUnread", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isOpen", - "columnName": "isOpen", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isHost` INTEGER NOT NULL DEFAULT false, `isMuted` INTEGER NOT NULL DEFAULT false, `isFullMember` INTEGER NOT NULL DEFAULT false, `isBlocked` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isFullMember", - "columnName": "isFullMember", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isBlocked", - "columnName": "isBlocked", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [ - { - "name": "index_members_memberIdBase58", - "unique": false, - "columnNames": [ - "memberIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_memberIdBase58` ON `${TABLE_NAME}` (`memberIdBase58`)" - }, - { - "name": "index_members_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, `deletedByBase58` TEXT, `inReplyToBase58` TEXT, `sentOffStage` INTEGER NOT NULL DEFAULT false, `isApproved` INTEGER, `tipCount` INTEGER NOT NULL DEFAULT 0, `type` INTEGER NOT NULL DEFAULT 1, `content` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "deletedByBase58", - "columnName": "deletedByBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "inReplyToBase58", - "columnName": "inReplyToBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "sentOffStage", - "columnName": "sentOffStage", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isApproved", - "columnName": "isApproved", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "tipCount", - "columnName": "tipCount", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "1" - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_messages_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - }, - { - "name": "index_messages_senderIdBase58", - "unique": false, - "columnNames": [ - "senderIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_senderIdBase58` ON `${TABLE_NAME}` (`senderIdBase58`)" - }, - { - "name": "index_messages_dateMillis", - "unique": false, - "columnNames": [ - "dateMillis" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_dateMillis` ON `${TABLE_NAME}` (`dateMillis`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "tips", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `messageIdBase58` TEXT NOT NULL, `amount` INTEGER NOT NULL, `tipperIdBase58` TEXT NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "tipperIdBase58", - "columnName": "tipperIdBase58", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_tips_messageIdBase58", - "unique": false, - "columnNames": [ - "messageIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_tips_messageIdBase58` ON `${TABLE_NAME}` (`messageIdBase58`)" - } - ], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ddf50bec418df53870ee520e05dcb3cc')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/23.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/23.json deleted file mode 100644 index 992c7cf39..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/23.json +++ /dev/null @@ -1,513 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 23, - "identityHash": "2833cbe331cac7c0adf5fbea7479bbb0", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "users", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isBlocked` INTEGER, PRIMARY KEY(`userIdBase58`))", - "fields": [ - { - "fieldPath": "userIdBase58", - "columnName": "userIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isBlocked", - "columnName": "isBlocked", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "userIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `ownerIdBase58` TEXT, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `canMute` INTEGER NOT NULL DEFAULT true, `unreadCount` INTEGER NOT NULL, `coverChargeQuarks` INTEGER, `hasMoreUnread` INTEGER NOT NULL DEFAULT false, `isOpen` INTEGER NOT NULL DEFAULT true, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ownerIdBase58", - "columnName": "ownerIdBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "canMute", - "columnName": "canMute", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "messagingFee", - "columnName": "coverChargeQuarks", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "hasMoreUnread", - "columnName": "hasMoreUnread", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isOpen", - "columnName": "isOpen", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `isHost` INTEGER NOT NULL DEFAULT false, `isMuted` INTEGER NOT NULL DEFAULT false, `isFullMember` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isFullMember", - "columnName": "isFullMember", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [ - { - "name": "index_members_memberIdBase58", - "unique": false, - "columnNames": [ - "memberIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_memberIdBase58` ON `${TABLE_NAME}` (`memberIdBase58`)" - }, - { - "name": "index_members_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, `deletedByBase58` TEXT, `inReplyToBase58` TEXT, `sentOffStage` INTEGER NOT NULL DEFAULT false, `isApproved` INTEGER, `tipCount` INTEGER NOT NULL DEFAULT 0, `type` INTEGER NOT NULL DEFAULT 1, `content` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "deletedByBase58", - "columnName": "deletedByBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "inReplyToBase58", - "columnName": "inReplyToBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "sentOffStage", - "columnName": "sentOffStage", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isApproved", - "columnName": "isApproved", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "tipCount", - "columnName": "tipCount", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "1" - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_messages_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - }, - { - "name": "index_messages_senderIdBase58", - "unique": false, - "columnNames": [ - "senderIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_senderIdBase58` ON `${TABLE_NAME}` (`senderIdBase58`)" - }, - { - "name": "index_messages_dateMillis", - "unique": false, - "columnNames": [ - "dateMillis" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_dateMillis` ON `${TABLE_NAME}` (`dateMillis`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "tips", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `messageIdBase58` TEXT NOT NULL, `amount` INTEGER NOT NULL, `tipperIdBase58` TEXT NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "tipperIdBase58", - "columnName": "tipperIdBase58", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_tips_messageIdBase58", - "unique": false, - "columnNames": [ - "messageIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_tips_messageIdBase58` ON `${TABLE_NAME}` (`messageIdBase58`)" - } - ], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2833cbe331cac7c0adf5fbea7479bbb0')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/24.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/24.json deleted file mode 100644 index fe7c0f5f4..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/24.json +++ /dev/null @@ -1,569 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 24, - "identityHash": "fbc0b0628fbc9a73116e013b7a85b7f3", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "users", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isBlocked` INTEGER, PRIMARY KEY(`userIdBase58`))", - "fields": [ - { - "fieldPath": "userIdBase58", - "columnName": "userIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isBlocked", - "columnName": "isBlocked", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "userIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `ownerIdBase58` TEXT, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `canMute` INTEGER NOT NULL DEFAULT true, `unreadCount` INTEGER NOT NULL, `coverChargeQuarks` INTEGER, `hasMoreUnread` INTEGER NOT NULL DEFAULT false, `isOpen` INTEGER NOT NULL DEFAULT true, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ownerIdBase58", - "columnName": "ownerIdBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "canMute", - "columnName": "canMute", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "messagingFee", - "columnName": "coverChargeQuarks", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "hasMoreUnread", - "columnName": "hasMoreUnread", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isOpen", - "columnName": "isOpen", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `isHost` INTEGER NOT NULL DEFAULT false, `isMuted` INTEGER NOT NULL DEFAULT false, `isFullMember` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isFullMember", - "columnName": "isFullMember", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [ - { - "name": "index_members_memberIdBase58", - "unique": false, - "columnNames": [ - "memberIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_memberIdBase58` ON `${TABLE_NAME}` (`memberIdBase58`)" - }, - { - "name": "index_members_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, `deletedByBase58` TEXT, `inReplyToBase58` TEXT, `sentOffStage` INTEGER NOT NULL DEFAULT false, `isApproved` INTEGER, `tipCount` INTEGER NOT NULL DEFAULT 0, `type` INTEGER NOT NULL DEFAULT 1, `content` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "deletedByBase58", - "columnName": "deletedByBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "inReplyToBase58", - "columnName": "inReplyToBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "sentOffStage", - "columnName": "sentOffStage", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isApproved", - "columnName": "isApproved", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "tipCount", - "columnName": "tipCount", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "1" - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_messages_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - }, - { - "name": "index_messages_senderIdBase58", - "unique": false, - "columnNames": [ - "senderIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_senderIdBase58` ON `${TABLE_NAME}` (`senderIdBase58`)" - }, - { - "name": "index_messages_dateMillis", - "unique": false, - "columnNames": [ - "dateMillis" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_dateMillis` ON `${TABLE_NAME}` (`dateMillis`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "tips", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `messageIdBase58` TEXT NOT NULL, `amount` INTEGER NOT NULL, `tipperIdBase58` TEXT NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "tipperIdBase58", - "columnName": "tipperIdBase58", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_tips_messageIdBase58", - "unique": false, - "columnNames": [ - "messageIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_tips_messageIdBase58` ON `${TABLE_NAME}` (`messageIdBase58`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "social_profiles", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `memberIdBase58` TEXT NOT NULL, `platformType` TEXT NOT NULL, `username` TEXT NOT NULL, `profileImageUrl` TEXT, `verified` INTEGER NOT NULL, `extraData` TEXT, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "platformType", - "columnName": "platformType", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "username", - "columnName": "username", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "profileImageUrl", - "columnName": "profileImageUrl", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "verified", - "columnName": "verified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "extraData", - "columnName": "extraData", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fbc0b0628fbc9a73116e013b7a85b7f3')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/25.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/25.json deleted file mode 100644 index 0ecdb08bb..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/25.json +++ /dev/null @@ -1,632 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 25, - "identityHash": "4a523cd7f879a706edf4011b23e2486c", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "users", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isBlocked` INTEGER, PRIMARY KEY(`userIdBase58`))", - "fields": [ - { - "fieldPath": "userIdBase58", - "columnName": "userIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isBlocked", - "columnName": "isBlocked", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "userIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `ownerIdBase58` TEXT, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `canMute` INTEGER NOT NULL DEFAULT true, `unreadCount` INTEGER NOT NULL, `coverChargeQuarks` INTEGER, `hasMoreUnread` INTEGER NOT NULL DEFAULT false, `isOpen` INTEGER NOT NULL DEFAULT true, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ownerIdBase58", - "columnName": "ownerIdBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "canMute", - "columnName": "canMute", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "messagingFee", - "columnName": "coverChargeQuarks", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "hasMoreUnread", - "columnName": "hasMoreUnread", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isOpen", - "columnName": "isOpen", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `isHost` INTEGER NOT NULL DEFAULT false, `isMuted` INTEGER NOT NULL DEFAULT false, `isFullMember` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isFullMember", - "columnName": "isFullMember", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [ - { - "name": "index_members_memberIdBase58", - "unique": false, - "columnNames": [ - "memberIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_memberIdBase58` ON `${TABLE_NAME}` (`memberIdBase58`)" - }, - { - "name": "index_members_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, `deletedByBase58` TEXT, `inReplyToBase58` TEXT, `sentOffStage` INTEGER NOT NULL DEFAULT false, `isApproved` INTEGER, `tipCount` INTEGER NOT NULL DEFAULT 0, `reactionCount` INTEGER NOT NULL DEFAULT 0, `type` INTEGER NOT NULL DEFAULT 1, `content` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "deletedByBase58", - "columnName": "deletedByBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "inReplyToBase58", - "columnName": "inReplyToBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "sentOffStage", - "columnName": "sentOffStage", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isApproved", - "columnName": "isApproved", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "tipCount", - "columnName": "tipCount", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "reactionCount", - "columnName": "reactionCount", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "1" - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_messages_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - }, - { - "name": "index_messages_senderIdBase58", - "unique": false, - "columnNames": [ - "senderIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_senderIdBase58` ON `${TABLE_NAME}` (`senderIdBase58`)" - }, - { - "name": "index_messages_dateMillis", - "unique": false, - "columnNames": [ - "dateMillis" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_dateMillis` ON `${TABLE_NAME}` (`dateMillis`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "tips", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `messageIdBase58` TEXT NOT NULL, `amount` INTEGER NOT NULL, `tipperIdBase58` TEXT NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "tipperIdBase58", - "columnName": "tipperIdBase58", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_tips_messageIdBase58", - "unique": false, - "columnNames": [ - "messageIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_tips_messageIdBase58` ON `${TABLE_NAME}` (`messageIdBase58`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "reactions", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `messageIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL, `emoji` TEXT NOT NULL, `deleted` INTEGER NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "emoji", - "columnName": "emoji", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_reactions_messageIdBase58_senderIdBase58_emoji", - "unique": false, - "columnNames": [ - "messageIdBase58", - "senderIdBase58", - "emoji" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_messageIdBase58_senderIdBase58_emoji` ON `${TABLE_NAME}` (`messageIdBase58`, `senderIdBase58`, `emoji`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "social_profiles", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `memberIdBase58` TEXT NOT NULL, `platformType` TEXT NOT NULL, `username` TEXT NOT NULL, `profileImageUrl` TEXT, `verified` INTEGER NOT NULL, `extraData` TEXT, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "platformType", - "columnName": "platformType", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "username", - "columnName": "username", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "profileImageUrl", - "columnName": "profileImageUrl", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "verified", - "columnName": "verified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "extraData", - "columnName": "extraData", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4a523cd7f879a706edf4011b23e2486c')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/26.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/26.json deleted file mode 100644 index fcac2cb79..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/26.json +++ /dev/null @@ -1,639 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 26, - "identityHash": "cce4bcb46e79d7dd2427d95c4fca186c", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "users", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isBlocked` INTEGER, PRIMARY KEY(`userIdBase58`))", - "fields": [ - { - "fieldPath": "userIdBase58", - "columnName": "userIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isBlocked", - "columnName": "isBlocked", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "userIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `ownerIdBase58` TEXT, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `canMute` INTEGER NOT NULL DEFAULT true, `unreadCount` INTEGER NOT NULL, `coverChargeQuarks` INTEGER, `hasMoreUnread` INTEGER NOT NULL DEFAULT false, `isOpen` INTEGER NOT NULL DEFAULT true, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ownerIdBase58", - "columnName": "ownerIdBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "canMute", - "columnName": "canMute", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "messagingFee", - "columnName": "coverChargeQuarks", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "hasMoreUnread", - "columnName": "hasMoreUnread", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isOpen", - "columnName": "isOpen", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `isHost` INTEGER NOT NULL DEFAULT false, `isMuted` INTEGER NOT NULL DEFAULT false, `isFullMember` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isFullMember", - "columnName": "isFullMember", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [ - { - "name": "index_members_memberIdBase58", - "unique": false, - "columnNames": [ - "memberIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_memberIdBase58` ON `${TABLE_NAME}` (`memberIdBase58`)" - }, - { - "name": "index_members_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, `deletedByBase58` TEXT, `inReplyToBase58` TEXT, `sentOffStage` INTEGER NOT NULL DEFAULT false, `isApproved` INTEGER, `tipCount` INTEGER NOT NULL DEFAULT 0, `reactionCount` INTEGER NOT NULL DEFAULT 0, `type` INTEGER NOT NULL DEFAULT 1, `content` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "deletedByBase58", - "columnName": "deletedByBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "inReplyToBase58", - "columnName": "inReplyToBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "sentOffStage", - "columnName": "sentOffStage", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isApproved", - "columnName": "isApproved", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "tipCount", - "columnName": "tipCount", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "reactionCount", - "columnName": "reactionCount", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "1" - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_messages_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - }, - { - "name": "index_messages_senderIdBase58", - "unique": false, - "columnNames": [ - "senderIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_senderIdBase58` ON `${TABLE_NAME}` (`senderIdBase58`)" - }, - { - "name": "index_messages_dateMillis", - "unique": false, - "columnNames": [ - "dateMillis" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_dateMillis` ON `${TABLE_NAME}` (`dateMillis`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "tips", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `messageIdBase58` TEXT NOT NULL, `amount` INTEGER NOT NULL, `tipperIdBase58` TEXT NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "tipperIdBase58", - "columnName": "tipperIdBase58", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_tips_messageIdBase58", - "unique": false, - "columnNames": [ - "messageIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_tips_messageIdBase58` ON `${TABLE_NAME}` (`messageIdBase58`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "reactions", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `messageIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL, `emoji` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `sentAt` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "emoji", - "columnName": "emoji", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "sentAt", - "columnName": "sentAt", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_reactions_messageIdBase58_senderIdBase58_emoji", - "unique": false, - "columnNames": [ - "messageIdBase58", - "senderIdBase58", - "emoji" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_messageIdBase58_senderIdBase58_emoji` ON `${TABLE_NAME}` (`messageIdBase58`, `senderIdBase58`, `emoji`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "social_profiles", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `memberIdBase58` TEXT NOT NULL, `platformType` TEXT NOT NULL, `username` TEXT NOT NULL, `profileImageUrl` TEXT, `verified` INTEGER NOT NULL, `extraData` TEXT, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "platformType", - "columnName": "platformType", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "username", - "columnName": "username", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "profileImageUrl", - "columnName": "profileImageUrl", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "verified", - "columnName": "verified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "extraData", - "columnName": "extraData", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cce4bcb46e79d7dd2427d95c4fca186c')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/27.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/27.json deleted file mode 100644 index c6e212804..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/27.json +++ /dev/null @@ -1,645 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 27, - "identityHash": "c771b93df521b42cf6aa6cf3dd8fc4b0", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "users", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isBlocked` INTEGER, PRIMARY KEY(`userIdBase58`))", - "fields": [ - { - "fieldPath": "userIdBase58", - "columnName": "userIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isBlocked", - "columnName": "isBlocked", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "userIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `ownerIdBase58` TEXT, `title` TEXT NOT NULL, `description` TEXT, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `canMute` INTEGER NOT NULL DEFAULT true, `unreadCount` INTEGER NOT NULL, `coverChargeQuarks` INTEGER, `hasMoreUnread` INTEGER NOT NULL DEFAULT false, `isOpen` INTEGER NOT NULL DEFAULT true, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ownerIdBase58", - "columnName": "ownerIdBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "canMute", - "columnName": "canMute", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "messagingFee", - "columnName": "coverChargeQuarks", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "hasMoreUnread", - "columnName": "hasMoreUnread", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isOpen", - "columnName": "isOpen", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "true" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `isHost` INTEGER NOT NULL DEFAULT false, `isMuted` INTEGER NOT NULL DEFAULT false, `isFullMember` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isFullMember", - "columnName": "isFullMember", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [ - { - "name": "index_members_memberIdBase58", - "unique": false, - "columnNames": [ - "memberIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_memberIdBase58` ON `${TABLE_NAME}` (`memberIdBase58`)" - }, - { - "name": "index_members_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_members_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, `deletedByBase58` TEXT, `inReplyToBase58` TEXT, `sentOffStage` INTEGER NOT NULL DEFAULT false, `isApproved` INTEGER, `tipCount` INTEGER NOT NULL DEFAULT 0, `reactionCount` INTEGER NOT NULL DEFAULT 0, `type` INTEGER NOT NULL DEFAULT 1, `content` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "deletedByBase58", - "columnName": "deletedByBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "inReplyToBase58", - "columnName": "inReplyToBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "sentOffStage", - "columnName": "sentOffStage", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - }, - { - "fieldPath": "isApproved", - "columnName": "isApproved", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "tipCount", - "columnName": "tipCount", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "reactionCount", - "columnName": "reactionCount", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "1" - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_messages_conversationIdBase58", - "unique": false, - "columnNames": [ - "conversationIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_conversationIdBase58` ON `${TABLE_NAME}` (`conversationIdBase58`)" - }, - { - "name": "index_messages_senderIdBase58", - "unique": false, - "columnNames": [ - "senderIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_senderIdBase58` ON `${TABLE_NAME}` (`senderIdBase58`)" - }, - { - "name": "index_messages_dateMillis", - "unique": false, - "columnNames": [ - "dateMillis" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_dateMillis` ON `${TABLE_NAME}` (`dateMillis`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "tips", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `messageIdBase58` TEXT NOT NULL, `amount` INTEGER NOT NULL, `tipperIdBase58` TEXT NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "amount", - "columnName": "amount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "tipperIdBase58", - "columnName": "tipperIdBase58", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_tips_messageIdBase58", - "unique": false, - "columnNames": [ - "messageIdBase58" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_tips_messageIdBase58` ON `${TABLE_NAME}` (`messageIdBase58`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "reactions", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `messageIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL, `emoji` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `sentAt` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "emoji", - "columnName": "emoji", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "sentAt", - "columnName": "sentAt", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [ - { - "name": "index_reactions_messageIdBase58_senderIdBase58_emoji", - "unique": false, - "columnNames": [ - "messageIdBase58", - "senderIdBase58", - "emoji" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_reactions_messageIdBase58_senderIdBase58_emoji` ON `${TABLE_NAME}` (`messageIdBase58`, `senderIdBase58`, `emoji`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "social_profiles", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `memberIdBase58` TEXT NOT NULL, `platformType` TEXT NOT NULL, `username` TEXT NOT NULL, `profileImageUrl` TEXT, `verified` INTEGER NOT NULL, `extraData` TEXT, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "platformType", - "columnName": "platformType", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "username", - "columnName": "username", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "profileImageUrl", - "columnName": "profileImageUrl", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "verified", - "columnName": "verified", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "extraData", - "columnName": "extraData", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c771b93df521b42cf6aa6cf3dd8fc4b0')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/3.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/3.json deleted file mode 100644 index cba5037cc..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/3.json +++ /dev/null @@ -1,306 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 3, - "identityHash": "bb5c31bd3d5381054721acae94b59342", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `unreadCount` INTEGER NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `dateMillis` INTEGER NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "message_contents", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`messageIdBase58`, `content`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58", - "content" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bb5c31bd3d5381054721acae94b59342')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/4.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/4.json deleted file mode 100644 index 9d285201e..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/4.json +++ /dev/null @@ -1,312 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 4, - "identityHash": "cbb2ea84e3bb3240d7b9a075d7454677", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `unreadCount` INTEGER NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "message_contents", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`messageIdBase58`, `content`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58", - "content" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cbb2ea84e3bb3240d7b9a075d7454677')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/5.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/5.json deleted file mode 100644 index 34450bcb8..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/5.json +++ /dev/null @@ -1,319 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 5, - "identityHash": "9f3802b9cb1f5539419c93ddc19965f0", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `unreadCount` INTEGER NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "message_contents", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`messageIdBase58`, `content`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58", - "content" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9f3802b9cb1f5539419c93ddc19965f0')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/6.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/6.json deleted file mode 100644 index 29bcad1fe..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/6.json +++ /dev/null @@ -1,326 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 6, - "identityHash": "b674ac7360aca7a357892a8d70f74722", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `unreadCount` INTEGER NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isHost` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "message_contents", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`messageIdBase58`, `content`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58", - "content" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b674ac7360aca7a357892a8d70f74722')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/7.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/7.json deleted file mode 100644 index cfddcd4f4..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/7.json +++ /dev/null @@ -1,326 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 7, - "identityHash": "b674ac7360aca7a357892a8d70f74722", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `unreadCount` INTEGER NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isHost` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "message_contents", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`messageIdBase58`, `content`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58", - "content" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b674ac7360aca7a357892a8d70f74722')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/8.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/8.json deleted file mode 100644 index a3a1538bb..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/8.json +++ /dev/null @@ -1,332 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 8, - "identityHash": "b519189cb8dcbb32702186e5d00a61d5", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `ownerIdBase58` TEXT, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `unreadCount` INTEGER NOT NULL, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ownerIdBase58", - "columnName": "ownerIdBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isHost` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "message_contents", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`messageIdBase58`, `content`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58", - "content" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b519189cb8dcbb32702186e5d00a61d5')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/9.json b/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/9.json deleted file mode 100644 index a564ab292..000000000 --- a/services/flipchat/sdk/schemas/xyz.flipchat.internal.db.FcAppDatabase/9.json +++ /dev/null @@ -1,338 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 9, - "identityHash": "7217ed18e0e307ed362d1e7aaf05229f", - "entities": [ - { - "tableName": "PrefInt", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefString", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefBool", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "PrefDouble", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` REAL NOT NULL, PRIMARY KEY(`key`))", - "fields": [ - { - "fieldPath": "key", - "columnName": "key", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "REAL", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "key" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `ownerIdBase58` TEXT, `title` TEXT NOT NULL, `roomNumber` INTEGER NOT NULL DEFAULT 0, `imageUri` TEXT, `lastActivity` INTEGER, `isMuted` INTEGER NOT NULL, `unreadCount` INTEGER NOT NULL, `coverChargeQuarks` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "ownerIdBase58", - "columnName": "ownerIdBase58", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "roomNumber", - "columnName": "roomNumber", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lastActivity", - "columnName": "lastActivity", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "isMuted", - "columnName": "isMuted", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "unreadCount", - "columnName": "unreadCount", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "coverChargeQuarks", - "columnName": "coverChargeQuarks", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "members", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`memberIdBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `memberName` TEXT, `imageUri` TEXT, `isHost` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`memberIdBase58`, `conversationIdBase58`))", - "fields": [ - { - "fieldPath": "memberIdBase58", - "columnName": "memberIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "memberName", - "columnName": "memberName", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "imageUri", - "columnName": "imageUri", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "isHost", - "columnName": "isHost", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "false" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "memberIdBase58", - "conversationIdBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "conversation_pointers", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationIdBase58` TEXT NOT NULL, `messageIdString` TEXT NOT NULL, `status` TEXT NOT NULL, PRIMARY KEY(`conversationIdBase58`, `status`))", - "fields": [ - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "messageIdString", - "columnName": "messageIdString", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "status", - "columnName": "status", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "conversationIdBase58", - "status" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idBase58` TEXT NOT NULL, `conversationIdBase58` TEXT NOT NULL, `senderIdBase58` TEXT NOT NULL DEFAULT '', `dateMillis` INTEGER NOT NULL, `deleted` INTEGER, PRIMARY KEY(`idBase58`))", - "fields": [ - { - "fieldPath": "idBase58", - "columnName": "idBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "conversationIdBase58", - "columnName": "conversationIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "senderIdBase58", - "columnName": "senderIdBase58", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" - }, - { - "fieldPath": "dateMillis", - "columnName": "dateMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "deleted", - "columnName": "deleted", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "idBase58" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "message_contents", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageIdBase58` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`messageIdBase58`, `content`))", - "fields": [ - { - "fieldPath": "messageIdBase58", - "columnName": "messageIdBase58", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "content", - "columnName": "content", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "messageIdBase58", - "content" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7217ed18e0e307ed362d1e7aaf05229f')" - ] - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/FlipchatServices.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/FlipchatServices.kt deleted file mode 100644 index 345170d92..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/FlipchatServices.kt +++ /dev/null @@ -1,49 +0,0 @@ -package xyz.flipchat - -import android.content.Context -import androidx.work.BackoffPolicy -import androidx.work.Constraints -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.NetworkType -import androidx.work.PeriodicWorkRequestBuilder -import androidx.work.WorkManager -import com.getcode.services.db.Database -import kotlin.time.Clock -import xyz.flipchat.internal.db.FcAppDatabase -import xyz.flipchat.workers.ChatSyncWorker -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import kotlin.time.Duration.Companion.hours - -class FlipchatServices @Inject constructor() { - - companion object { - fun openDatabase(context: Context, entropy: String) { - if (!FcAppDatabase.isOpen()) { - FcAppDatabase.init(context, entropy) - Database.register(FcAppDatabase.requireInstance()) - } - } - - /** - * Schedules a worker to synchronize chats, members, and messages with the server. - */ - fun scheduleChatSync(context: Context) { - val request = PeriodicWorkRequestBuilder(3, TimeUnit.HOURS) - .setConstraints( - Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - ) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 15, TimeUnit.MINUTES) - .build() - - WorkManager.getInstance(context) - .enqueueUniquePeriodicWork( - uniqueWorkName = ChatSyncWorker.WORK_NAME, - existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.KEEP, - request = request - ) - } - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/chat/RoomController.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/chat/RoomController.kt deleted file mode 100644 index bb746b82e..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/chat/RoomController.kt +++ /dev/null @@ -1,413 +0,0 @@ -package xyz.flipchat.chat - -import androidx.core.app.NotificationManagerCompat -import androidx.paging.ExperimentalPagingApi -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.room.withTransaction -import com.getcode.model.ID -import com.getcode.model.KinAmount -import com.getcode.model.chat.MessageStatus -import com.getcode.model.uuid -import com.getcode.services.model.chat.OutgoingMessageContent -import com.getcode.utils.base58 -import com.getcode.utils.timestamp -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import xyz.flipchat.chat.paging.MessagingPagingSource -import xyz.flipchat.chat.paging.MessagingRemoteMediator -import xyz.flipchat.internal.db.FcAppDatabase -import xyz.flipchat.notifications.getRoomNotifications -import xyz.flipchat.services.data.ChatIdentifier -import xyz.flipchat.services.domain.mapper.ConversationMessageMapper -import xyz.flipchat.services.domain.model.chat.ConversationWithMembersAndLastPointers -import xyz.flipchat.services.domain.model.chat.InflatedConversationMessage -import xyz.flipchat.services.domain.model.chat.MessageReactionInfo -import xyz.flipchat.services.internal.data.mapper.ConversationMemberMapper -import xyz.flipchat.services.internal.data.mapper.UserMapper -import xyz.flipchat.services.internal.network.repository.chat.ChatRepository -import xyz.flipchat.services.internal.network.repository.messaging.MessagingRepository -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject - -class RoomController @Inject constructor( - private val chatRepository: ChatRepository, - private val messagingRepository: MessagingRepository, - private val conversationMemberMapper: ConversationMemberMapper, - private val userMapper: UserMapper, - private val conversationMessageMapper: ConversationMessageMapper, - private val notificationManager: NotificationManagerCompat, - private val userManager: UserManager, -) { - private val db: FcAppDatabase - get() = FcAppDatabase.requireInstance() - - fun observeConversation(id: ID): Flow { - return db.conversationDao().observeConversation(id) - } - - suspend fun getConversation(identifier: ID): ConversationWithMembersAndLastPointers? { - return db.conversationDao().findConversation(identifier) - } - - suspend fun getUnreadCount(identifier: ID): Int { - return db.conversationDao().getUnreadCount(identifier) ?: 0 - } - - suspend fun getEmojiReactionsForMessage(messageId: ID): List { - return db.conversationMessageDao().getReactionsForMessage(messageId) - } - - suspend fun getChatMembers(identifier: ID) { - chatRepository.getChatMembers(ChatIdentifier.Id(identifier)) - .onSuccess { - val dbMembers = it.map { m -> conversationMemberMapper.map(identifier to m) } - val memberIds = dbMembers.map { member -> member.id.base58 } - val users = it.map { m -> userMapper.map(m) } - db.withTransaction { - withContext(Dispatchers.IO) { - db.conversationMembersDao().purgeMembersNotIn(identifier, memberIds) - db.conversationMembersDao().upsertMembers(*dbMembers.toTypedArray()) - db.userDao().upsert(*users.toTypedArray()) - } - } - } - } - - fun openMessageStream(scope: CoroutineScope, identifier: ID) { - scope.launch { - val name = db.conversationDao().findConversation(identifier)?.conversation?.title - val notifications = notificationManager.getRoomNotifications(identifier, name.orEmpty()) - notifications.onEach { notificationId -> - notificationManager.cancel(notificationId) - } - } - - runCatching { - messagingRepository.openMessageStream( - coroutineScope = scope, - chatId = identifier, - onMessagesUpdated = { - scope.launch { - db.conversationMessageDao().upsertMessages(identifier, it, userManager.userId) - } - } - ) - } - } - - fun closeMessageStream() { - runCatching { messagingRepository.closeMessageStream() } - } - - suspend fun resetUnreadCount(conversationId: ID) { - db.conversationDao().resetUnreadCount(conversationId) - } - - suspend fun advancePointer( - conversationId: ID, - messageId: ID, - status: MessageStatus - ) { - when (status) { - MessageStatus.Sent -> { - messagingRepository.advancePointer(conversationId, messageId, status) - .onSuccess { - withContext(Dispatchers.IO) { - db.conversationPointersDao() - .insert(conversationId, messageId.uuid!!, status) - } - } - } - - MessageStatus.Delivered -> { - messagingRepository.advancePointer(conversationId, messageId, status) - .onSuccess { - withContext(Dispatchers.IO) { - db.conversationPointersDao() - .insert(conversationId, messageId.uuid!!, status) - } - } - } - - MessageStatus.Read -> { - messagingRepository.advancePointer(conversationId, messageId, status) - .onSuccess { - withContext(Dispatchers.IO) { - db.withTransaction { - db.conversationPointersDao() - .insert(conversationId, messageId.uuid!!, status) - val newest = - db.conversationMessageDao().getNewestMessage(conversationId) - if ((messageId.uuid?.timestamp - ?: -1) >= (newest?.id?.uuid?.timestamp ?: 0L) - ) { - db.conversationDao().resetUnreadCount(conversationId) - } - } - } - } - } - - MessageStatus.Unknown -> Unit - } - } - - suspend fun sendMessage( - conversationId: ID, - message: String, - paymentIntentId: ID? = null - ): Result { - val content = OutgoingMessageContent.Text(message, paymentIntentId) - return messagingRepository.sendMessage(conversationId, content) - .map { it.id } - } - - suspend fun sendReply( - conversationId: ID, - originalMessageId: ID, - message: String, - paymentIntentId: ID? = null - ): Result { - val content = OutgoingMessageContent.Reply(originalMessageId, message, paymentIntentId) - return messagingRepository.sendMessage(conversationId, content) - .map { it.id } - } - - suspend fun sendReaction( - conversationId: ID, - messageId: ID, - emoji: String, - paymentIntentId: ID? = null - ): Result { - val content = OutgoingMessageContent.Reaction(messageId, emoji, paymentIntentId) - return messagingRepository.sendMessage(conversationId, content) - .map { it.id } - } - - suspend fun sendTip( - conversationId: ID, - messageId: ID, - amount: KinAmount, - paymentIntentId: ID - ): Result { - val content = OutgoingMessageContent.Tip(messageId, amount, paymentIntentId) - return messagingRepository.sendMessage(conversationId, content) - .map { it.id } - } - - private val pagingConfig = - PagingConfig(pageSize = 25, initialLoadSize = 25, prefetchDistance = 10, enablePlaceholders = true) - - @OptIn(ExperimentalPagingApi::class) - fun messages(conversationId: ID): Pager = - Pager( - config = pagingConfig, - remoteMediator = MessagingRemoteMediator( - chatId = conversationId, - repository = messagingRepository, - conversationMessageMapper = conversationMessageMapper, - userId = { userManager.userId } - ) - ) { - MessagingPagingSource( - chatId = conversationId, - userId = { userManager.userId }, - db = db, - ) - } - - fun observeTyping(conversationId: ID) = messagingRepository.observeTyping(conversationId) - .distinctUntilChanged() - .flatMapLatest { userIds -> db.userDao().getUsersFromIds(userIds) } - - suspend fun onUserStartedTypingIn(conversationId: ID) { - messagingRepository.onStartedTyping(conversationId) - } - - suspend fun onUserStillTypingIn(conversationId: ID) { - messagingRepository.onStillTyping(conversationId) - } - - suspend fun onUserStoppedTypingIn(conversationId: ID) { - messagingRepository.onStoppedTyping(conversationId) - } - - suspend fun leaveRoom(conversationId: ID): Result { - return chatRepository.leaveChat(conversationId) - .onSuccess { - db.withTransaction { - withContext(Dispatchers.IO) { - db.conversationDao().deleteConversationById(conversationId) - db.conversationPointersDao().deletePointerForConversation(conversationId) - db.conversationMessageDao().removeForConversation(conversationId) - } - } - } - } - - suspend fun setDisplayName(conversationId: ID, displayName: String): Result { - return chatRepository.setDisplayName(conversationId, displayName) - .onSuccess { - val conversation = db.conversationDao() - .findConversation(conversationId)?.conversation?.copy(title = displayName) - if (conversation != null) { - db.conversationDao().setDisplayName(conversationId, displayName) - } - } - } - - suspend fun setDescription(conversationId: ID, description: String): Result { - return chatRepository.setDescription(conversationId, description) - .onSuccess { - val conversation = db.conversationDao() - .findConversation(conversationId)?.conversation?.copy(description = description) - if (conversation != null) { - db.conversationDao().setDescription(conversationId, description) - } - } - } - - suspend fun deleteMessage( - conversationId: ID, - messageId: ID, - ): Result { - return messagingRepository.deleteMessage(conversationId, messageId) - .onSuccess { - withContext(Dispatchers.IO) { - db.conversationMessageDao().markDeleted(messageId, userManager.userId!!) - } - } - } - - suspend fun removeReaction( - conversationId: ID, - reactionId: ID, - ): Result { - return messagingRepository.deleteMessage(conversationId, reactionId) - .onSuccess { - withContext(Dispatchers.IO) { - db.conversationMessageDao().removeReaction(reactionId) - } - } - } - - suspend fun removeUser( - conversationId: ID, - userId: ID - ): Result { - return chatRepository.removeUser(conversationId, userId) - .onSuccess { - withContext(Dispatchers.IO) { - db.conversationMembersDao().removeMemberFromConversation(userId, conversationId) - } - } - } - - suspend fun reportUserForMessage( - userId: ID, - messageId: ID, - ): Result { - return chatRepository.reportUserForMessage(userId, messageId) - } - - suspend fun muteUser( - chatId: ID, - userId: ID, - ): Result { - return chatRepository.muteUser(chatId, userId) - } - - suspend fun blockUser( - userId: ID, - ): Result { - withContext(Dispatchers.IO) { - db.userDao().blockUser(userId) - } - - return Result.success(Unit) - } - - suspend fun unblockUser( - userId: ID, - ): Result { - withContext(Dispatchers.IO) { - db.userDao().unblockUser(userId) - } - - return Result.success(Unit) - } - - @Deprecated("Replaced by setMessagingFee") - suspend fun setCoverCharge( - conversationId: ID, - amount: KinAmount - ): Result { - return chatRepository.setCoverCharge(conversationId, amount) - .onSuccess { - withContext(Dispatchers.IO) { - db.conversationDao().updateMessagingFee(conversationId, amount.kin) - } - } - } - - suspend fun promoteUser( - conversationId: ID, - userId: ID, - ): Result { - return chatRepository.promoteUser(conversationId, userId) - .onSuccess { - withContext(Dispatchers.IO) { - db.conversationMembersDao().promoteMember(conversationId, userId) - } - } - } - - suspend fun demoteUser( - conversationId: ID, - userId: ID, - ): Result { - return chatRepository.demoteUser(conversationId, userId) - .onSuccess { - withContext(Dispatchers.IO) { - db.conversationMembersDao().demoteMember(conversationId, userId) - } - } - } - - suspend fun setMessagingFee( - conversationId: ID, - amount: KinAmount - ): Result { - return chatRepository.setMessagingFee(conversationId, amount) - .onSuccess { - withContext(Dispatchers.IO) { - db.conversationDao().updateMessagingFee(conversationId, amount.kin) - } - } - } - - suspend fun enableChat(identifier: ID): Result { - return chatRepository.enableChat(identifier) - .onSuccess { - withContext(Dispatchers.IO) { - db.conversationDao().enableChatInRoom(identifier) - } - } - } - - suspend fun disableChat(identifier: ID): Result { - return chatRepository.disableChat(identifier) - .onSuccess { - withContext(Dispatchers.IO) { - db.conversationDao().disableChatInRoom(identifier) - } - } - } -} - diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/chat/paging/ChatsPagingSource.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/chat/paging/ChatsPagingSource.kt deleted file mode 100644 index 16a481652..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/chat/paging/ChatsPagingSource.kt +++ /dev/null @@ -1,51 +0,0 @@ -package xyz.flipchat.chat.paging - -import android.annotation.SuppressLint -import androidx.paging.PagingSource -import androidx.paging.PagingState -import androidx.room.paging.util.ThreadSafeInvalidationObserver -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import xyz.flipchat.internal.db.FcAppDatabase -import xyz.flipchat.services.domain.model.chat.ConversationWithMembersAndLastMessage - -internal class ChatsPagingSource( - private val db: FcAppDatabase -) : PagingSource() { - - @SuppressLint("RestrictedApi") - private val observer = - ThreadSafeInvalidationObserver(arrayOf("conversations", "messages", "members")) { - invalidate() - } - - override fun getRefreshKey(state: PagingState): Int? { - val anchorPos = state.anchorPosition ?: return null - val anchorItem = state.closestItemToPosition(anchorPos) ?: return null - // The anchor item *knows* which page it was loaded from: - return anchorItem.pageIndex - } - - @SuppressLint("RestrictedApi") - override suspend fun load(params: LoadParams): LoadResult { - observer.registerIfNecessary(db) - val currentPage = params.key ?: 0 - val pageSize = params.loadSize - val offset = currentPage * pageSize - - return withContext(Dispatchers.IO) { - try { - val conversations = db.conversationDao().getPagedConversations(pageSize, offset) - .map { it.apply { pageIndex = currentPage } } - - val prevKey = null - val nextKey = if (conversations.size < pageSize) null else currentPage + 1 - - - LoadResult.Page(conversations, prevKey, nextKey) - } catch (e: Exception) { - LoadResult.Error(e) - } - } - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/chat/paging/ChatsRemoteMediator.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/chat/paging/ChatsRemoteMediator.kt deleted file mode 100644 index 00fe19e00..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/chat/paging/ChatsRemoteMediator.kt +++ /dev/null @@ -1,94 +0,0 @@ -package xyz.flipchat.chat.paging - -import androidx.paging.ExperimentalPagingApi -import androidx.paging.LoadType -import androidx.paging.PagingState -import androidx.paging.RemoteMediator -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import xyz.flipchat.internal.db.FcAppDatabase -import xyz.flipchat.services.data.Room -import xyz.flipchat.services.domain.mapper.RoomConversationMapper -import xyz.flipchat.services.domain.model.chat.ConversationWithMembersAndLastMessage -import xyz.flipchat.services.domain.model.query.QueryOptions -import xyz.flipchat.services.internal.network.repository.chat.ChatRepository - -@OptIn(ExperimentalPagingApi::class) -internal class ChatsRemoteMediator( - private val repository: ChatRepository, - private val conversationMapper: RoomConversationMapper, -) : RemoteMediator() { - - private val db = FcAppDatabase.requireInstance() - - override suspend fun initialize(): InitializeAction { - return InitializeAction.SKIP_INITIAL_REFRESH - } - - private var lastResult = listOf() - - override suspend fun load( - loadType: LoadType, - state: PagingState - ): MediatorResult { - return try { - val loadKey = when (loadType) { - LoadType.REFRESH -> { - null - } - - LoadType.PREPEND -> { - return MediatorResult.Success(true) // Don't load newer messages - } - - LoadType.APPEND -> { - // Get the last item from our data - val lastItem = state.lastItemOrNull() - ?: return MediatorResult.Success( - // If we don't have any items, only signal end of pagination - // if we've had a refresh - endOfPaginationReached = state.pages.isNotEmpty() - ) - - lastItem.conversation.id - } - } - - val limit = state.config.pageSize - - val query = QueryOptions( - limit = limit, - token = loadKey, - descending = true - ) - - val response = repository.getChats(query) - val rooms = response.getOrNull().orEmpty() - - if (rooms.isEmpty() || lastResult.any { it.id == rooms.firstOrNull()?.id.orEmpty() }) { - lastResult = emptyList() - return MediatorResult.Success(true) - } - - lastResult = rooms - - // Map the rooms to your Room entities - val conversations = rooms.map { conversationMapper.map(it) } - - // Update the database with the new data (upsert) - withContext(Dispatchers.IO) { - if (loadType == LoadType.REFRESH) { - // Clear all conversations before loading the fresh data - db.conversationDao().clearConversations() - } - - // Insert or update the conversations - db.conversationDao().upsertConversations(*conversations.toTypedArray()) - } - - MediatorResult.Success(endOfPaginationReached = rooms.size < limit) - } catch (e: Exception) { - MediatorResult.Error(e) - } - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/chat/paging/MessagingPagingSource.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/chat/paging/MessagingPagingSource.kt deleted file mode 100644 index 96a24b5b0..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/chat/paging/MessagingPagingSource.kt +++ /dev/null @@ -1,59 +0,0 @@ -package xyz.flipchat.chat.paging - -import android.annotation.SuppressLint -import androidx.paging.PagingSource -import androidx.paging.PagingState -import androidx.room.paging.util.ThreadSafeInvalidationObserver -import com.getcode.model.ID -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import xyz.flipchat.internal.db.FcAppDatabase -import xyz.flipchat.services.domain.model.chat.InflatedConversationMessage - -internal class MessagingPagingSource( - private val chatId: ID, - private val userId: () -> ID?, - private val db: FcAppDatabase, -) : PagingSource() { - - @SuppressLint("RestrictedApi") - private val observer = - ThreadSafeInvalidationObserver(arrayOf("conversations", "messages", "members")) { - invalidate() - } - - override fun getRefreshKey(state: PagingState): Int? { - val anchorPos = state.anchorPosition ?: return null - val anchorItem = state.closestItemToPosition(anchorPos) ?: return null - // The anchor item *knows* which page it was loaded from: - return anchorItem.pageIndex - } - - @SuppressLint("RestrictedApi") - override suspend fun load(params: LoadParams): LoadResult { - observer.registerIfNecessary(db) - val currentPage = params.key ?: 0 - val pageSize = params.loadSize - val offset = currentPage * pageSize - - return withContext(Dispatchers.Default) { - try { - val messages = db.conversationMessageDao() - .getPagedMessagesWithDetails(chatId, pageSize, offset, userId()) - .map { it.copy(pageIndex = currentPage) } - - val prevKey = if (currentPage > 0 && messages.isNotEmpty()) currentPage - 1 else null - val nextKey = if (messages.size < pageSize) null else currentPage + 1 - - LoadResult.Page( - data = messages, - prevKey = prevKey, - nextKey = nextKey, - ) - } catch (e: Exception) { - LoadResult.Error(e) - } - } - } -} diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/chat/paging/MessagingRemoteMediator.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/chat/paging/MessagingRemoteMediator.kt deleted file mode 100644 index f0a28f76a..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/chat/paging/MessagingRemoteMediator.kt +++ /dev/null @@ -1,85 +0,0 @@ -package xyz.flipchat.chat.paging - -import androidx.paging.ExperimentalPagingApi -import androidx.paging.LoadType -import androidx.paging.PagingState -import androidx.paging.RemoteMediator -import com.getcode.model.ID -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import xyz.flipchat.internal.db.FcAppDatabase -import xyz.flipchat.services.domain.mapper.ConversationMessageMapper -import xyz.flipchat.services.domain.model.chat.InflatedConversationMessage -import xyz.flipchat.services.domain.model.query.QueryOptions -import xyz.flipchat.services.internal.network.repository.messaging.MessagingRepository - -@OptIn(ExperimentalPagingApi::class) -internal class MessagingRemoteMediator( - private val chatId: ID, - private val repository: MessagingRepository, - private val conversationMessageMapper: ConversationMessageMapper, - private val userId: () -> ID? -) : RemoteMediator() { - - private val db = FcAppDatabase.requireInstance() - - override suspend fun initialize(): InitializeAction { - return InitializeAction.SKIP_INITIAL_REFRESH - } - - override suspend fun load( - loadType: LoadType, - state: PagingState - ): MediatorResult { - return try { - val loadKey = when (loadType) { - LoadType.REFRESH -> { - null - } - - LoadType.PREPEND -> { - return MediatorResult.Success(true) // Don't load newer messages - } - - LoadType.APPEND -> { - db.conversationMessageDao().getOldestMessage(chatId)?.id - } - } - - val limit = state.config.pageSize - - val query = QueryOptions( - limit = limit, - token = loadKey, - descending = true - ) - - val response = withContext(Dispatchers.IO) { repository.getMessages(chatId, query) } - val messages = response.getOrNull().orEmpty() - - val conversationMessages = - messages.map { conversationMessageMapper.map(chatId to it) } - - if (conversationMessages.isEmpty()) { - return MediatorResult.Success(true) - } - - withContext(Dispatchers.IO) { - if (loadType == LoadType.REFRESH) { - db.conversationMessageDao().clearMessagesForChat(chatId) - } - - db.conversationMessageDao() - .upsertMessages( - chatId = chatId, - messages = conversationMessages, - selfID = userId() - ) - } - - MediatorResult.Success(endOfPaginationReached = messages.size < limit) - } catch (e: Exception) { - MediatorResult.Error(e) - } - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/controllers/AuthController.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/controllers/AuthController.kt deleted file mode 100644 index 2646f23df..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/controllers/AuthController.kt +++ /dev/null @@ -1,21 +0,0 @@ -package xyz.flipchat.controllers - -import com.getcode.model.ID -import xyz.flipchat.services.internal.network.repository.accounts.AccountRepository -import javax.inject.Inject - -class AuthController @Inject constructor( - private val repository: AccountRepository, -) { - suspend fun createAccount(): Result { - return repository.createAccount() - } - - suspend fun register(displayName: String): Result { - return repository.register(displayName) - } - - suspend fun login(): Result { - return repository.login() - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/controllers/ChatsController.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/controllers/ChatsController.kt deleted file mode 100644 index 23cf49fbb..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/controllers/ChatsController.kt +++ /dev/null @@ -1,484 +0,0 @@ -package xyz.flipchat.controllers - -import androidx.paging.ExperimentalPagingApi -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.room.withTransaction -import com.getcode.model.ID -import com.getcode.util.resources.ResourceHelper -import com.getcode.utils.TraceType -import com.getcode.utils.base58 -import com.getcode.utils.trace -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import xyz.flipchat.chat.paging.ChatsPagingSource -import xyz.flipchat.chat.paging.ChatsRemoteMediator -import xyz.flipchat.internal.db.FcAppDatabase -import xyz.flipchat.services.data.ChatIdentifier -import xyz.flipchat.services.data.RoomWithMembers -import xyz.flipchat.services.data.StartChatRequestType -import xyz.flipchat.services.domain.mapper.ConversationMessageMapper -import xyz.flipchat.services.domain.mapper.RoomConversationMapper -import xyz.flipchat.services.domain.model.chat.ConversationMessage -import xyz.flipchat.services.domain.model.chat.ConversationWithMembersAndLastMessage -import xyz.flipchat.services.domain.model.chat.db.ConversationMemberUpdate -import xyz.flipchat.services.domain.model.chat.db.ConversationUpdate -import xyz.flipchat.services.domain.model.query.QueryOptions -import xyz.flipchat.services.internal.data.mapper.ConversationMemberMapper -import xyz.flipchat.services.internal.data.mapper.UserMapper -import xyz.flipchat.services.internal.network.repository.chat.ChatRepository -import xyz.flipchat.services.internal.network.repository.messaging.MessagingRepository -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ChatsController @Inject constructor( - private val conversationMapper: RoomConversationMapper, - private val conversationMemberMapper: ConversationMemberMapper, - private val userMapper: UserMapper, - private val conversationMessageMapper: ConversationMessageMapper, - private val chatRepository: ChatRepository, - private val messagingRepository: MessagingRepository, - private val userManager: UserManager, - private val resources: ResourceHelper, -) { - private val db: FcAppDatabase - get() = FcAppDatabase.requireInstance() - - private val pagingConfig = PagingConfig(pageSize = 20) - - @OptIn(ExperimentalPagingApi::class) - val chats: Pager by lazy { - Pager( - config = pagingConfig, - remoteMediator = ChatsRemoteMediator(chatRepository, conversationMapper) - ) { - ChatsPagingSource(db) - } - } - - suspend fun updateRooms() = coroutineScope { - chatRepository.getChats() - .onSuccess { rooms -> - // remove rooms no longer apart of - db.conversationDao().purgeConversationsNotIn(rooms.map { it.id }) - rooms.map { room -> - async { updateRoom(room.id) } - }.forEach { it.await() } - } - } - - suspend fun updateRoom(roomId: ID) { - coroutineScope { - launch { - chatRepository.getChat(ChatIdentifier.Id(roomId)) - .onSuccess { (room, members) -> - db.withTransaction { - withContext(Dispatchers.IO) { - db.conversationDao().upsertConversations(conversationMapper.map(room)) - members.map { conversationMemberMapper.map(room.id to it) }.let { - db.conversationMembersDao().upsertMembers(*it.toTypedArray()) - } - members.map { userMapper.map(it) }.let { - db.userDao().upsert(*it.toTypedArray()) - } - - members.map { - val socialProfiles = it.identity?.socialProfiles - if (socialProfiles != null) { - db.userSocialDao().upsert(it.id, socialProfiles) - } - } - } - } - } - } - - launch { - syncMessagesFromLast(conversationId = roomId) - } - } - } - - fun openEventStream(coroutineScope: CoroutineScope) { - runCatching { - chatRepository.openEventStream(coroutineScope) { event -> - coroutineScope.launch { - withContext(Dispatchers.IO) { - - db.withTransaction { - event.metadata.onEach { update -> - when (update) { - is ConversationUpdate.CoverCharge -> { - db.conversationDao().updateMessagingFee(update.roomId, update.amount) - } - is ConversationUpdate.DisplayName -> { - val conversation = db.conversationDao().findConversationRaw(update.roomId) - if (conversation != null) { - db.conversationDao() - .setDisplayName(update.roomId, update.name) - } - } - is ConversationUpdate.LastActivity -> { - val conversation = db.conversationDao().findConversationRaw(update.roomId) - if (conversation != null) { - db.conversationDao().upsertConversations( - conversation.copy(lastActivity = update.timestamp) - ) - } - } - is ConversationUpdate.Refresh -> db.conversationDao().upsertConversations(update.conversation) - is ConversationUpdate.UnreadCount -> { - val conversation = db.conversationDao().findConversationRaw(update.roomId) - if (conversation != null) { - db.conversationDao().upsertConversations( - conversation.copy(unreadCount = update.numUnread, hasMoreUnread = update.hasMoreUnread) - ) - } - } - - is ConversationUpdate.OpenStatus -> { - val conversation = db.conversationDao().findConversationRaw(update.roomId) - if (conversation != null) { - db.conversationDao().upsertConversations( - conversation.copy(isOpen = update.nowOpen) - ) - } - } - - is ConversationUpdate.Description -> { - val conversation = db.conversationDao().findConversationRaw(update.roomId) - if (conversation != null) { - db.conversationDao() - .setDescription(update.roomId, update.description) - } - } - } - } - } - - db.withTransaction { - event.members.onEach { update -> - when (update) { - is ConversationMemberUpdate.FullRefresh -> { - val members = update.members.map { - conversationMemberMapper.map( - Pair( - update.roomId, - it - ) - ) - } - val users = update.members.map { - userMapper.map(it) - } - - db.conversationMembersDao() - .upsertMembers(*members.toTypedArray()) - db.userDao() - .upsert(*users.toTypedArray()) - - update.members.onEach { - val member = conversationMemberMapper.map(Pair(update.roomId, it)) - val user = userMapper.map(it) - val socialProfiles = it.identity?.socialProfiles - - db.conversationMembersDao().upsertMembers(member) - db.userDao().upsert(user) - if (socialProfiles != null) { - db.userSocialDao().upsert(it.id, socialProfiles) - } - } - } - - is ConversationMemberUpdate.IndividualRefresh -> { - val member = conversationMemberMapper.map( - Pair(update.roomId, update.member) - ) - - val user = userMapper.map(update.member) - db.conversationMembersDao().upsertMembers(member) - db.userDao().upsert(user) - - val socialProfiles = update.member.identity?.socialProfiles - if (socialProfiles != null) { - db.userSocialDao().upsert( - member.id, - socialProfiles - ) - } - } - - is ConversationMemberUpdate.Joined -> { - val member = conversationMemberMapper.map( - Pair(update.roomId, update.member) - ) - - val user = userMapper.map(update.member) - db.conversationMembersDao().upsertMembers(member) - db.userDao().upsert(user) - - val socialProfiles = update.member.identity?.socialProfiles - if (socialProfiles != null) { - db.userSocialDao().upsert( - member.id, - socialProfiles - ) - } - } - - is ConversationMemberUpdate.Left -> { - db.conversationMembersDao().removeMemberFromConversation( - memberId = update.memberId, - conversationId = update.roomId - ) - } - - is ConversationMemberUpdate.Muted -> { - db.conversationMembersDao().muteMember( - conversationId = update.roomId, - memberId = update.memberId - ) - } - - is ConversationMemberUpdate.Removed -> { - db.conversationMembersDao().removeMemberFromConversation( - memberId = update.memberId, - conversationId = update.roomId - ) - } - - is ConversationMemberUpdate.Demoted -> { - db.conversationMembersDao().demoteMember( - memberId = update.memberId, - conversationId = update.roomId - ) - } - is ConversationMemberUpdate.Promoted -> { - db.conversationMembersDao().promoteMember( - memberId = update.memberId, - conversationId = update.roomId - ) - } - - is ConversationMemberUpdate.IdentityChanged -> { - db.userDao().updateIdentity( - update.memberId, - update.identity - ) - - db.userSocialDao().upsert( - update.memberId, - update.identity.socialProfiles - ) - } - } - } - } - } - - event.message?.let { newMessage -> - syncMessagesFromLast(newMessage.conversationId, newMessage) - } - } - } - } - } - - private suspend fun syncMessagesFromLast( - conversationId: ID, - newMessage: ConversationMessage? = null - ) { - var token: ID? - if (newMessage != null) { - // sync between last in DB and this message - val newestInDb = - db.conversationMessageDao().getNewestMessage(conversationId) - if (newestInDb?.id == newMessage.id) { - withContext(Dispatchers.IO) { - db.conversationMessageDao().upsertMessages( - conversationId, - listOf(newMessage), - userManager.userId - ) - } - return - } - - token = newestInDb?.id - } else { - val newestInDb = - db.conversationMessageDao().getNewestMessage(conversationId) - token = newestInDb?.id - } - - while (true) { - val query = QueryOptions(token = token, descending = false, limit = 1_000) - messagingRepository.getMessages(conversationId, query) - .onSuccess { syncedMessages -> - trace( - "synced ${syncedMessages.count()} missing messages for ${conversationId.base58}", - type = TraceType.Silent - ) - val messagesWithContent = syncedMessages.map { - conversationMessageMapper.map(conversationId to it) - } - - withContext(Dispatchers.IO) { - db.conversationMessageDao().upsertMessages( - conversationId, - messagesWithContent, - userManager.userId - ) - } - - val nextToken = - db.conversationMessageDao().getNewestMessage(conversationId)?.id - if (nextToken == token || messagesWithContent.isEmpty()) { - return - } - - token = nextToken - } - .onFailure { - if (newMessage != null) { - withContext(Dispatchers.IO) { - db.conversationMessageDao().upsertMessages( - conversationId, - listOf(newMessage), - userManager.userId - ) - } - } - return - } - } - } - - fun closeEventStream() { - runCatching { - chatRepository.closeEventStream() - } - } - - suspend fun lookupRoom(roomNumber: Long): Result { - return chatRepository.getChat(identifier = ChatIdentifier.RoomNumber(roomNumber)) - } - - suspend fun lookupRoom(id: ID): Result { - return chatRepository.getChat(identifier = ChatIdentifier.Id(id)) - } - - - suspend fun createDirectMessage(recipient: ID): Result { - return chatRepository.startChat(StartChatRequestType.TwoWay(recipient)) - .onSuccess { result -> - val members = - result.members.map { conversationMemberMapper.map(result.room.id to it) } - val users = result.members.map { userMapper.map(it) } - val socials = result.members.mapNotNull { - val profiles = it.identity?.socialProfiles ?: return@mapNotNull null - it.id to profiles - } - - db.withTransaction { - withContext(Dispatchers.IO) { - db.conversationDao() - .upsertConversations(conversationMapper.map(result.room)) - db.conversationMembersDao().upsertMembers(*members.toTypedArray()) - db.userDao().upsert(*users.toTypedArray()) - db.userSocialDao().upsert(*socials.toTypedArray()) - } - } - } - } - - suspend fun createGroup( - title: String? = null, - participants: List = emptyList(), - paymentId: ID, - ): Result { - return chatRepository.startChat(StartChatRequestType.Group(title, participants, paymentId)) - .onSuccess { result -> - val members = - result.members.map { conversationMemberMapper.map(result.room.id to it) } - val users = result.members.map { userMapper.map(it) } - val socials = result.members.mapNotNull { - val profiles = it.identity?.socialProfiles ?: return@mapNotNull null - it.id to profiles - } - db.withTransaction { - withContext(Dispatchers.IO) { - db.conversationDao() - .upsertConversations(conversationMapper.map(result.room)) - db.conversationMembersDao().upsertMembers(*members.toTypedArray()) - db.userDao().upsert(*users.toTypedArray()) - db.userSocialDao().upsert(*socials.toTypedArray()) - } - } - } - } - - suspend fun joinRoomAsSpectator(roomId: ID): Result { - return chatRepository.joinChat(ChatIdentifier.Id(roomId)) - .onSuccess { result -> - val members = - result.members.map { conversationMemberMapper.map(result.room.id to it) } - val users = result.members.map { userMapper.map(it) } - val socials = result.members.mapNotNull { - val profiles = it.identity?.socialProfiles ?: return@mapNotNull null - it.id to profiles - } - db.withTransaction { - withContext(Dispatchers.IO) { - db.conversationDao() - .upsertConversations(conversationMapper.map(result.room)) - db.conversationMembersDao().upsertMembers(*members.toTypedArray()) - db.userDao().upsert(*users.toTypedArray()) - db.userSocialDao().upsert(*socials.toTypedArray()) - } - } - } - } - - suspend fun joinRoomAsFullMember(roomId: ID, paymentId: ID?): Result { - return chatRepository.joinChat(ChatIdentifier.Id(roomId), paymentId) - .onSuccess { result -> - val members = - result.members.map { conversationMemberMapper.map(result.room.id to it) } - val users = result.members.map { userMapper.map(it) } - val socials = result.members.mapNotNull { - val profiles = it.identity?.socialProfiles ?: return@mapNotNull null - it.id to profiles - } - db.withTransaction { - withContext(Dispatchers.IO) { - db.conversationDao() - .upsertConversations(conversationMapper.map(result.room)) - db.conversationMembersDao().upsertMembers(*members.toTypedArray()) - db.userDao().upsert(*users.toTypedArray()) - db.userSocialDao().upsert(*socials.toTypedArray()) - } - } - } - } - - suspend fun checkDisplayNameForRoom(name: String): Result { - return chatRepository.checkDisplayName(name) - } - - suspend fun muteRoom(roomId: ID): Result { - return chatRepository.mute(roomId) - .onSuccess { db.conversationDao().muteChat(roomId) } - } - - suspend fun unmuteRoom(roomId: ID): Result { - return chatRepository.unmute(roomId) - .onSuccess { db.conversationDao().unmuteChat(roomId) } - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/controllers/CodeController.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/controllers/CodeController.kt deleted file mode 100644 index c42898d20..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/controllers/CodeController.kt +++ /dev/null @@ -1,37 +0,0 @@ -package xyz.flipchat.controllers - -import android.annotation.SuppressLint -import com.getcode.model.KinAmount -import com.getcode.network.BalanceController -import com.getcode.network.client.Client -import com.getcode.network.client.receiveFromPrimaryIfWithinLimits -import com.getcode.network.repository.TransactionRepository -import io.reactivex.rxjava3.core.Completable -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject - -class CodeController @Inject constructor( - private val userManager: UserManager, - private val balanceController: BalanceController, - private val transactionRepository: TransactionRepository, - private val client: Client, -) { - @SuppressLint("CheckResult") - suspend fun requestAirdrop(): Result { - val owner = userManager.keyPair ?: return Result.failure(Throwable("No owner")) - return transactionRepository.requestFirstKinAirdrop(owner) - .onSuccess { - balanceController.fetchBalance() - - val organizer = userManager.organizer - val receiveWithinLimits = organizer?.let { - client.receiveFromPrimaryIfWithinLimits(it) - } ?: Completable.complete() - receiveWithinLimits.subscribe({}, {}) - } - } - - suspend fun fetchBalance(): Result { - return balanceController.fetchBalance() - } -} diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/controllers/ProfileController.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/controllers/ProfileController.kt deleted file mode 100644 index 4a66389ed..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/controllers/ProfileController.kt +++ /dev/null @@ -1,66 +0,0 @@ -package xyz.flipchat.controllers - -import com.getcode.model.ID -import com.getcode.model.social.user.SocialProfile -import com.getcode.services.model.profile.LinkingToken -import com.getcode.services.model.profile.SocialAccountLinkRequest -import com.getcode.services.model.profile.SocialAccountUnlinkRequest -import com.getcode.solana.keys.PublicKey -import xyz.flipchat.services.data.PaymentTarget -import xyz.flipchat.services.user.UserFlags -import xyz.flipchat.services.domain.model.profile.UserProfile -import xyz.flipchat.services.internal.network.repository.accounts.AccountRepository -import xyz.flipchat.services.internal.network.repository.profile.ProfileRepository -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject - -class ProfileController @Inject constructor( - private val userManager: UserManager, - private val repository: ProfileRepository, - private val accountRepository: AccountRepository, -) { - - suspend fun getProfile(userId: ID): Result { - return repository.getProfile(userId) - .onSuccess { - if (userManager.isSelf(userId)) { - userManager.set(it.displayName) - userManager.setSocialProfiles(it.socialProfiles) - } - } - } - - suspend fun setDisplayName(name: String): Result { - return repository.setDisplayName(name) - } - - - suspend fun getPaymentDestinationForUser(userId: ID): Result { - return accountRepository.getPaymentDestination(PaymentTarget.User(userId)) - } - - suspend fun getUserFlags(): Result { - return accountRepository.getUserFlags() - .onSuccess { - userManager.set(userFlags = it) - } - } - - suspend fun linkXAccount(token: LinkingToken): Result { - return repository.linkSocialAccount( - request = SocialAccountLinkRequest.X(token) - ).onSuccess { - val profiles = userManager.socialProfiles - userManager.setSocialProfiles(profiles + it) - } - } - - suspend fun unlinkXAccount(profile: SocialProfile.X): Result { - return repository.unlinkSocialAccount( - request = SocialAccountUnlinkRequest.X(profile.id) - ).onSuccess { - val profiles = userManager.socialProfiles - userManager.setSocialProfiles(profiles - profile) - } - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/controllers/PurchaseController.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/controllers/PurchaseController.kt deleted file mode 100644 index bcd09d8e6..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/controllers/PurchaseController.kt +++ /dev/null @@ -1,12 +0,0 @@ -package xyz.flipchat.controllers - -import xyz.flipchat.services.internal.network.repository.iap.InAppPurchaseRepository -import javax.inject.Inject - -class PurchaseController @Inject constructor( - private val repository: InAppPurchaseRepository -) { - suspend fun onPurchaseCompleted(receipt: String): Result { - return repository.onPurchaseCompleted(receipt) - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/controllers/PushController.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/controllers/PushController.kt deleted file mode 100644 index 9cae877bc..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/controllers/PushController.kt +++ /dev/null @@ -1,34 +0,0 @@ -package xyz.flipchat.controllers - -import com.getcode.services.utils.installationId -import com.getcode.utils.ErrorUtils -import com.google.firebase.Firebase -import com.google.firebase.installations.installations -import xyz.flipchat.services.internal.network.repository.push.PushRepository -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject - -class PushController @Inject constructor( - private val userManager: UserManager, - private val repository: PushRepository, -) { - suspend fun addToken(token: String): Result { - val owner = userManager.keyPair ?: return Result.failure(Throwable("No owner")) - val installationId = Firebase.installations.installationId() - return repository.addToken(owner, token, installationId) - .onFailure { ErrorUtils.handleError(it) } - } - - suspend fun deleteToken(token: String): Result { - val owner = userManager.keyPair ?: return Result.failure(Throwable("No owner")) - return repository.deleteToken(owner, token) - .onFailure { ErrorUtils.handleError(it) } - } - - suspend fun deleteTokens(): Result { - val owner = userManager.keyPair ?: return Result.failure(Throwable("No owner")) - val installationId = Firebase.installations.installationId() - return repository.deleteTokens(owner, installationId) - .onFailure { ErrorUtils.handleError(it) } - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/EmojiQueryProvider.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/EmojiQueryProvider.kt deleted file mode 100644 index a5634bbc9..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/EmojiQueryProvider.kt +++ /dev/null @@ -1,18 +0,0 @@ -package xyz.flipchat.internal - -import com.getcode.libs.emojis.EmojiQueryProvider -import xyz.flipchat.internal.db.FcAppDatabase -import xyz.flipchat.services.user.UserManager -import javax.inject.Inject - -class EmojiQueryProviderImpl @Inject constructor( - private val userManager: UserManager, -) : EmojiQueryProvider { - - private val db: FcAppDatabase - get() = FcAppDatabase.requireInstance() - - override suspend fun getFrequentEmojis(): List { - return db.conversationMessageDao().getFrequentEmojis(userManager.userId.orEmpty()) - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/db/Converters.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/db/Converters.kt deleted file mode 100644 index 11b3558b9..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/db/Converters.kt +++ /dev/null @@ -1,14 +0,0 @@ -package xyz.flipchat.internal.db - -import androidx.room.TypeConverter -import xyz.flipchat.services.data.Member -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json - -internal object Converters { - @TypeConverter - fun membersToString(members: List) = Json.encodeToString(members) - - @TypeConverter - fun stringToMembers(value: String) = Json.decodeFromString>(value) -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/db/FcAppDatabase.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/db/FcAppDatabase.kt deleted file mode 100644 index fc0fe4435..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/db/FcAppDatabase.kt +++ /dev/null @@ -1,219 +0,0 @@ -package xyz.flipchat.internal.db - -import android.content.Context -import androidx.room.AutoMigration -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import androidx.room.migration.AutoMigrationSpec -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase -import com.getcode.services.db.ClosableDatabase -import com.getcode.services.db.SharedConverters -import com.getcode.services.model.PrefBool -import com.getcode.services.model.PrefDouble -import com.getcode.services.model.PrefInt -import com.getcode.services.model.PrefString -import com.getcode.utils.TraceType -import com.getcode.utils.trace -import com.getcode.vendor.Base58 -import org.kin.sdk.base.tools.subByteArray -import timber.log.Timber -import xyz.flipchat.services.domain.model.chat.Conversation -import xyz.flipchat.services.domain.model.chat.ConversationMember -import xyz.flipchat.services.domain.model.chat.ConversationMessage -import xyz.flipchat.services.domain.model.chat.ConversationMessageReaction -import xyz.flipchat.services.domain.model.chat.ConversationMessageTip -import xyz.flipchat.services.domain.model.chat.ConversationPointerCrossRef -import xyz.flipchat.services.domain.model.profile.MemberSocialProfile -import xyz.flipchat.services.domain.model.people.FlipchatUser -import xyz.flipchat.services.internal.db.ConversationDao -import xyz.flipchat.services.internal.db.ConversationMemberDao -import xyz.flipchat.services.internal.db.ConversationMessageDao -import xyz.flipchat.services.internal.db.ConversationPointerDao -import xyz.flipchat.services.internal.db.MemberSocialProfileDao -import xyz.flipchat.services.internal.db.UserDao -import java.io.File - -@Database( - entities = [ - PrefInt::class, - PrefString::class, - PrefBool::class, - PrefDouble::class, - FlipchatUser::class, - Conversation::class, - ConversationMember::class, - ConversationPointerCrossRef::class, - ConversationMessage::class, - ConversationMessageTip::class, - ConversationMessageReaction::class, - MemberSocialProfile::class, - ], - autoMigrations = [ - AutoMigration(from = 1, to = 2), - AutoMigration(from = 2, to = 3), - AutoMigration(from = 3, to = 4), - AutoMigration(from = 4, to = 5), - AutoMigration(from = 5, to = 6), - AutoMigration(from = 6, to = 7, spec = FcAppDatabase.Migration6To7::class), - AutoMigration(from = 7, to = 8, spec = FcAppDatabase.Migration7To8::class), - AutoMigration(from = 8, to = 9), - AutoMigration(from = 9, to = 10, spec = FcAppDatabase.Migration9To10::class), - AutoMigration(from = 10, to = 11), - AutoMigration(from = 11, to = 12, spec = FcAppDatabase.Migration11To12::class), - AutoMigration(from = 12, to = 13), - AutoMigration(from = 13, to = 14, spec = FcAppDatabase.Migration13To14::class), - AutoMigration(from = 14, to = 15, spec = FcAppDatabase.Migration14To15::class), - AutoMigration(from = 15, to = 16), - // explicit no migration to fallback to reset (from = 16, to = 17) - AutoMigration(from = 17, to = 18), - AutoMigration(from = 18, to = 19), - AutoMigration(from = 19, to = 20), - AutoMigration(from = 20, to = 21, spec = FcAppDatabase.Migration20To21::class), - AutoMigration(from = 21, to = 22, spec = FcAppDatabase.Migration21To22::class), - // explicit no migration to fallback to reset (from = 22, to = 23) - AutoMigration(from = 23, to = 24), - AutoMigration(from = 24, to = 25), - AutoMigration(from = 25, to = 26, spec = FcAppDatabase.Migration25To26::class), - AutoMigration(from = 26, to = 27), - ], - version = 27, -) -@TypeConverters(SharedConverters::class, Converters::class) -abstract class FcAppDatabase : RoomDatabase(), ClosableDatabase { - abstract fun prefIntDao(): PrefIntDao - abstract fun prefStringDao(): PrefStringDao - abstract fun prefBoolDao(): PrefBoolDao - abstract fun prefDoubleDao(): PrefDoubleDao - - abstract fun conversationDao(): ConversationDao - abstract fun conversationPointersDao(): ConversationPointerDao - abstract fun conversationMessageDao(): ConversationMessageDao - abstract fun conversationMembersDao(): ConversationMemberDao - abstract fun userSocialDao(): MemberSocialProfileDao - abstract fun userDao(): UserDao - - class Migration6To7 : Migration(6, 7), AutoMigrationSpec { - override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL("DELETE FROM members") - } - } - - class Migration7To8 : Migration(7, 8), AutoMigrationSpec { - override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL("DELETE FROM conversations") - } - } - - class Migration9To10 : Migration(9, 10), AutoMigrationSpec { - override fun migrate(db: SupportSQLiteDatabase) { - // drop messages to allow proper mapping for announcements - db.execSQL("DELETE FROM messages") - } - } - - class Migration11To12 : Migration(11, 12), AutoMigrationSpec { - override fun migrate(db: SupportSQLiteDatabase) { - // Add indexes for messages - db.execSQL("CREATE INDEX IF NOT EXISTS index_messages_conversationIdBase58 ON messages(conversationIdBase58)") - db.execSQL("CREATE INDEX IF NOT EXISTS index_messages_senderIdBase58 ON messages(senderIdBase58)") - db.execSQL("CREATE INDEX IF NOT EXISTS index_messages_dateMillis ON messages(dateMillis)") - - // Add index for message_contents - db.execSQL("CREATE INDEX IF NOT EXISTS index_message_contents_messageIdBase58 ON message_contents(messageIdBase58)") - - // Add indexes for members - db.execSQL("CREATE INDEX IF NOT EXISTS index_members_memberIdBase58 ON members(memberIdBase58)") - db.execSQL("CREATE INDEX IF NOT EXISTS index_members_conversationIdBase58 ON members(conversationIdBase58)") - } - } - - class Migration13To14 : Migration(13, 14), AutoMigrationSpec { - override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL("ALTER TABLE members ADD COLUMN isFullMember INTEGER NOT NULL DEFAULT 0") - db.execSQL("UPDATE members SET isFullMember = 1") - } - } - - class Migration14To15 : Migration(14, 15), AutoMigrationSpec { - override fun migrate(db: SupportSQLiteDatabase) { - // drop messages to allow proper use of message ID as the timestamp - db.execSQL("DELETE FROM messages") - } - } - - class Migration20To21 : Migration(20, 21), AutoMigrationSpec { - override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL("DELETE FROM messages") - } - } - - class Migration21To22 : Migration(21, 22), AutoMigrationSpec { - override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL("DELETE FROM messages") - db.execSQL("DELETE FROM members") - } - } - - class Migration25To26 : Migration(25, 26), AutoMigrationSpec { - override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL("DELETE FROM messages") - db.execSQL("DELETE FROM reactions") - } - } - - @Synchronized - override fun closeDb() { - if (instance != null) { - Timber.d("close") - instance?.close() - instance = null - } - } - - @Synchronized - override fun deleteDb(context: Context) { - Timber.d("delete") - closeDb() - if (dbName.isEmpty()) return - - val databases = File(context.applicationInfo.dataDir + "/databases") - val db = File(databases, dbName) - db.delete() - - val journal = File(databases, "$dbName-journal") - val shm = File(databases, "$dbName-shm") - val wal = File(databases, "$dbName-wal") - - if (journal.exists()) journal.delete() - if (shm.exists()) shm.delete() - if (wal.exists()) shm.delete() - } - - companion object { - private var instance: FcAppDatabase? = null - fun requireInstance() = requireNotNull(instance) - fun getInstance(): FcAppDatabase? = instance - private var dbName: String = "" - - private const val dbNamePrefix = "FcAppDatabase" - - fun isOpen() = instance?.isOpen == true - - fun init(context: Context, entropyB64: String) { - val dbUniqueName = Base58.encode(entropyB64.toByteArray().subByteArray(0, 6)) - trace("database init start $dbUniqueName", type = TraceType.Process) - instance?.close() - dbName = "$dbNamePrefix-$dbUniqueName.db" - - instance = - Room.databaseBuilder(context, FcAppDatabase::class.java, dbName) - .fallbackToDestructiveMigration() - .build() - - trace("database init end", type = TraceType.Process) - } - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/db/PrefBoolDao.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/db/PrefBoolDao.kt deleted file mode 100644 index 411e13ed5..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/db/PrefBoolDao.kt +++ /dev/null @@ -1,22 +0,0 @@ -package xyz.flipchat.internal.db - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.getcode.services.model.PrefBool -import kotlinx.coroutines.flow.Flow - -@Dao -interface PrefBoolDao { - @Query("SELECT * FROM PrefBool WHERE key = :key") - suspend fun get(key: String): PrefBool? - @Query("SELECT * FROM PrefBool WHERE key = :key") - fun observe(key: String): Flow - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(item: PrefBool) - - @Query("DELETE FROM PrefBool") - suspend fun clear() -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/db/PrefDoubleDao.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/db/PrefDoubleDao.kt deleted file mode 100644 index 1de0ba0bc..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/db/PrefDoubleDao.kt +++ /dev/null @@ -1,24 +0,0 @@ -package xyz.flipchat.internal.db - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.getcode.services.model.PrefDouble -import kotlinx.coroutines.flow.Flow - -@Dao -interface PrefDoubleDao { - @Query("SELECT * FROM PrefDouble WHERE `key` = :key") - suspend fun get(key: String): PrefDouble? - - @Query("SELECT * FROM PrefDouble WHERE key = :key") - fun observe(key: String): Flow - - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(item: PrefDouble) - - @Query("DELETE FROM PrefDouble") - suspend fun clear() -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/db/PrefIntDao.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/db/PrefIntDao.kt deleted file mode 100644 index 0b8d4e82d..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/db/PrefIntDao.kt +++ /dev/null @@ -1,23 +0,0 @@ -package xyz.flipchat.internal.db - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.getcode.services.model.PrefInt -import kotlinx.coroutines.flow.Flow - -@Dao -interface PrefIntDao { - @Query("SELECT * FROM PrefInt WHERE key = :key") - suspend fun get(key: String): PrefInt? - - @Query("SELECT * FROM PrefInt WHERE key = :key") - fun observe(key: String): Flow - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(item: PrefInt) - - @Query("DELETE FROM PrefInt") - suspend fun clear() -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/db/PrefStringDao.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/db/PrefStringDao.kt deleted file mode 100644 index 9dcd8fbe1..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/db/PrefStringDao.kt +++ /dev/null @@ -1,23 +0,0 @@ -package xyz.flipchat.internal.db - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.getcode.services.model.PrefString -import kotlinx.coroutines.flow.Flow - -@Dao -interface PrefStringDao { - @Query("SELECT * FROM PrefString WHERE key = :key") - suspend fun get(key: String): PrefString? - - @Query("SELECT * FROM PrefString WHERE key = :key") - fun observe(key: String): Flow - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(item: PrefString) - - @Query("DELETE FROM PrefString") - suspend fun clear() -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/inject/FlipchatServicesModule.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/inject/FlipchatServicesModule.kt deleted file mode 100644 index 39634b132..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/inject/FlipchatServicesModule.kt +++ /dev/null @@ -1,98 +0,0 @@ -package xyz.flipchat.internal.inject - -import com.getcode.network.BalanceController -import com.getcode.network.api.TransactionApiV2 -import com.getcode.network.client.Client -import com.getcode.network.client.TransactionReceiver -import com.getcode.network.exchange.Exchange -import com.getcode.network.repository.AccountRepository -import com.getcode.network.repository.BalanceRepository -import com.getcode.network.repository.MessagingRepository -import com.getcode.network.repository.TransactionRepository -import com.getcode.services.db.CurrencyProvider -import com.getcode.services.network.core.NetworkOracle -import com.getcode.services.network.core.NetworkOracleImpl -import com.getcode.utils.CurrencyUtils -import com.getcode.utils.network.NetworkConnectivityListener -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import xyz.flipchat.services.user.UserManager -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object FlipchatServicesModule { - - @Provides - fun provideNetworkOracle(): NetworkOracle { - return NetworkOracleImpl() - } - - @Provides - fun providesOrganizerLookup(userManager: UserManager): () -> com.getcode.solana.organizer.Organizer? { - return { userManager.organizer } - } - - @Singleton - @Provides - fun provideClient( - userManager: UserManager, - transactionRepository: TransactionRepository, - messagingRepository: MessagingRepository, - accountRepository: AccountRepository, - balanceController: BalanceController, - transactionReceiver: TransactionReceiver, - exchange: Exchange, - networkObserver: NetworkConnectivityListener, - ): Client { - return Client( - userManager = userManager, - transactionRepository, - messagingRepository, - balanceController, - accountRepository, - exchange, - transactionReceiver, - networkObserver, - ) - } - - @Singleton - @Provides - fun provideBalanceController( - userManager: UserManager, - exchange: Exchange, - balanceRepository: BalanceRepository, - transactionRepository: TransactionRepository, - accountRepository: AccountRepository, - transactionReceiver: TransactionReceiver, - networkObserver: NetworkConnectivityListener, - currencyUtils: CurrencyUtils, - currencyProvider: CurrencyProvider, - ): BalanceController { - return BalanceController( - userManager = userManager, - exchange = exchange, - balanceRepository = balanceRepository, - transactionRepository = transactionRepository, - accountRepository = accountRepository, - transactionReceiver = transactionReceiver, - networkObserver = networkObserver, - getCurrencyFromCode = { - it?.name?.let(currencyUtils::getCurrency) - }, - suffix = { currency -> currencyProvider.suffix(currency) } - ) - } - - @Singleton - @Provides - fun provideTransactionRepository( - transactionApi: TransactionApiV2, - ): TransactionRepository { - return TransactionRepository(transactionApi) - } - -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/preferences/PreferenceStore.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/preferences/PreferenceStore.kt deleted file mode 100644 index 5c4419f5c..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/preferences/PreferenceStore.kt +++ /dev/null @@ -1,29 +0,0 @@ -package xyz.flipchat.internal.preferences - -import com.getcode.services.model.InternalRouting -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import xyz.flipchat.internal.db.FcAppDatabase -import javax.inject.Inject - -internal class PreferenceStore @Inject constructor() { - - private val db: FcAppDatabase - get() = FcAppDatabase.requireInstance() - - fun observe(pref: FcPref, default: Boolean): Flow { - return db.prefBoolDao().observe(pref.key) - .map { it?.value ?: default } - } - - fun observe(pref: FcPref): Flow { - return db.prefBoolDao().observe(pref.key) - .map { it?.value } - } -} - -sealed class FcPref(val key: String) { - data object EligibleForAirdrop: FcPref("is_eligible_get_first_kin_airdrop"), - InternalRouting -} - diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/utils/String.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/utils/String.kt deleted file mode 100644 index ad73c6a4c..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/internal/utils/String.kt +++ /dev/null @@ -1,31 +0,0 @@ -package xyz.flipchat.internal.utils - -fun String.addLeadingZero(upTo: Int): String { - if (upTo < length) return this - val padding = "0".repeat(length - upTo) - return "$padding$this" -} - -fun String.base64EncodedData(): ByteArray { - val data = toByteArray() - val r = data.size % 4 - if (r > 0) { - val requiredPadding = data.size + 4 - r - val padding = "=".repeat(requiredPadding) - return data + padding.toByteArray() - } - return data -} - -fun String.padded(minCount: Int): String { - return if (this.length < minCount) { - val toInsert = minCount - this.length - val padding = " ".repeat(toInsert) - this + padding - } else { - this - } -} - -typealias Base64String = String -typealias Base58String = String \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/notifications/FcNotification.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/notifications/FcNotification.kt deleted file mode 100644 index 1cb729cd6..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/notifications/FcNotification.kt +++ /dev/null @@ -1,61 +0,0 @@ -package xyz.flipchat.notifications - -import com.getcode.model.ID -import com.getcode.model.chat.MessageContent -import com.getcode.utils.base58 -import com.getcode.utils.decodeBase64 - -data class FcNotification( - val type: FcNotificationType, - val title: String, - val body: MessageContent, -) - -private enum class TypeValue { - Unknown, ChatMessage -} - -sealed interface FcNotificationType { - val ordinal: Int - val name: String - - - sealed interface Notifiable - class Unknown: FcNotificationType, Notifiable { - override val ordinal: Int = 99 - override val name: String = "Misc" - } - - data class ChatMessage(val id: ID?, val roomNumber: Long?, val sender: String?): FcNotificationType, Notifiable { - override val ordinal: Int = 1 - override val name: String = "Chat Messages" - - constructor(): this(null, null, null) - } - - fun isNotifiable() = this is Notifiable - - companion object { - private const val TYPE = "type" - private const val CHAT_ID = "chat_id" - private const val SENDER = "sender" - - fun resolve(value: MutableMap): FcNotificationType { - val type = value[TYPE] - var notificationType = runCatching { TypeValue.valueOf(type.orEmpty()) }.getOrNull() - if (notificationType == null) { - // fallback to chat - notificationType = TypeValue.ChatMessage - } - - return when (notificationType) { - TypeValue.ChatMessage -> { - val chatId = value[CHAT_ID]?.decodeBase64()?.toList() - val sender = value[SENDER] - ChatMessage(chatId, null, sender) - } - else -> Unknown() - } - } - } -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/notifications/NotificationManager.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/notifications/NotificationManager.kt deleted file mode 100644 index 5b1f6b046..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/notifications/NotificationManager.kt +++ /dev/null @@ -1,23 +0,0 @@ -package xyz.flipchat.notifications - -import androidx.core.app.NotificationManagerCompat -import com.getcode.model.ID -import com.getcode.utils.base58 - -fun NotificationManagerCompat.getRoomNotifications(roomId: ID, roomName: String): List { - val barNotifications = activeNotifications - val roomNotifications = barNotifications.mapNotNull { notification -> - val roomIdHash = roomId.base58.hashCode() - val roomNameHash = roomName.hashCode() - - val isMatch = notification.id == roomIdHash || notification.id == roomNameHash - - if (isMatch) { - notification.id - } else { - null - } - } - - return roomNotifications -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/notifications/NotificationParser.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/notifications/NotificationParser.kt deleted file mode 100644 index 655978053..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/notifications/NotificationParser.kt +++ /dev/null @@ -1,25 +0,0 @@ -package xyz.flipchat.notifications - -import com.getcode.model.chat.MessageContent -import com.getcode.utils.ErrorUtils -import com.google.firebase.messaging.RemoteMessage -import timber.log.Timber - -fun RemoteMessage.parse(): FcNotification? { - Timber.d("data=$data") - val type = FcNotificationType.resolve(data) - if (type is FcNotificationType.Unknown) { - ErrorUtils.handleError(Throwable("Unknown notification type")) - return null - } - - if (!type.isNotifiable()) return FcNotification(type, "", MessageContent.Localized("", false)) - - val title = data["title"] ?: notification?.title.orEmpty() - val body = data["body"] ?: notification?.body.orEmpty() - return FcNotification( - type, - title, - MessageContent.RawText(body, false) - ) -} \ No newline at end of file diff --git a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/workers/ChatSyncWorker.kt b/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/workers/ChatSyncWorker.kt deleted file mode 100644 index 8ef8b172a..000000000 --- a/services/flipchat/sdk/src/main/kotlin/xyz/flipchat/workers/ChatSyncWorker.kt +++ /dev/null @@ -1,32 +0,0 @@ -package xyz.flipchat.workers - -import android.content.Context -import androidx.hilt.work.HiltWorker -import androidx.work.CoroutineWorker -import androidx.work.WorkerParameters -import com.getcode.utils.trace -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import xyz.flipchat.controllers.ChatsController - -@HiltWorker -class ChatSyncWorker @AssistedInject constructor( - @Assisted appContext: Context, - @Assisted workerParams: WorkerParameters, - private val chatsController: ChatsController, -) : CoroutineWorker(appContext, workerParams) { - override suspend fun doWork(): Result { - val result = chatsController.updateRooms() - - if (result.isFailure) { - trace(tag = "Chat-Sync", message = "Failed", error = result.exceptionOrNull()) - return Result.failure() - } - - return Result.success() - } - - companion object { - const val WORK_NAME = "chat-sync" - } -} \ No newline at end of file diff --git a/services/legacy-shared/.gitignore b/services/legacy-shared/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/services/legacy-shared/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/services/legacy-shared/build.gradle.kts b/services/legacy-shared/build.gradle.kts deleted file mode 100644 index 5c5756fde..000000000 --- a/services/legacy-shared/build.gradle.kts +++ /dev/null @@ -1,74 +0,0 @@ -plugins { - alias(libs.plugins.flipcash.android.library) - id("com.google.devtools.ksp") -} - -android { - namespace = "${Gradle.codeNamespace}.services.common" - - defaultConfig { - consumerProguardFiles("consumer-rules.pro") - } - - buildFeatures { - buildConfig = true - } -} - -dependencies { - api(project(":libs:datetime")) - api(project(":libs:encryption:base58")) - api(project(":libs:encryption:ed25519")) - api(project(":libs:encryption:hmac")) - api(project(":libs:encryption:keys")) - api(project(":libs:encryption:mnemonic")) - api(project(":libs:encryption:sha256")) - api(project(":libs:encryption:sha512")) - api(project(":libs:encryption:utils")) - api(project(":libs:currency")) - api(project(":libs:crypto:kin")) - api(project(":libs:crypto:solana")) - api(project(":libs:models")) - api(project(":libs:logging")) - api(project(":libs:network:exchange")) - api(project(":libs:network:connectivity:public")) - implementation(project(":ui:resources")) - implementation(project(":vendor:kik:scanner")) - - implementation(libs.rxjava) - implementation(libs.kotlinx.serialization.json) - implementation(libs.javax.inject) - - implementation(libs.grpc.okhttp) - implementation(libs.grpc.kotlin) - implementation(libs.androidx.lifecycle.runtime) - implementation(libs.androidx.room.runtime) - implementation(libs.androidx.room.ktx) - implementation(libs.androidx.room.rxjava3) - implementation(libs.androidx.room.paging) - implementation(libs.okhttp) - implementation(libs.mixpanel) - - implementation(platform(libs.firebase.bom)) - implementation(libs.firebase.installations) - implementation(libs.firebase.perf) - implementation(libs.firebase.messaging) - - implementation(libs.play.integrity) - - implementation(libs.androidx.paging.runtime) - - ksp(libs.androidx.room.compiler) - implementation(libs.sqlcipher) - - implementation(libs.fingerprint.pro) - - implementation(libs.lib.phone.number.google) - - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.junit) - androidTestImplementation(libs.androidx.test.runner) - implementation(libs.hilt.android) - - implementation(libs.bugsnag) -} diff --git a/services/legacy-shared/consumer-rules.pro b/services/legacy-shared/consumer-rules.pro deleted file mode 100644 index 5f642a47c..000000000 --- a/services/legacy-shared/consumer-rules.pro +++ /dev/null @@ -1,6 +0,0 @@ -# Needed to keep generic signatures --keepattributes Signature - --keepclasseswithmembernames class * { - native ; -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/ChannelConfig.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/ChannelConfig.kt deleted file mode 100644 index 4ee75d7b7..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/ChannelConfig.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.getcode.services - -import kotlin.time.Duration -import kotlin.time.Duration.Companion.minutes - -interface ChannelConfig { - val baseUrl: String - val port: Int - get() = 443 - val userAgent: String - val keepAlive: Duration - get() = 4.minutes - val keepAliveTimeout: Duration - get() = 1.minutes -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/db/CurrencyProvider.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/db/CurrencyProvider.kt deleted file mode 100644 index dd730d166..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/db/CurrencyProvider.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.getcode.services.db - -import com.getcode.model.Currency - -interface CurrencyProvider { - suspend fun preferredCurrency(): Currency? - suspend fun defaultCurrency(): Currency? - fun suffix(currency: Currency?): String -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/db/Database.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/db/Database.kt deleted file mode 100644 index 9d2461c18..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/db/Database.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.getcode.services.db - -import android.content.Context -import timber.log.Timber - -interface ClosableDatabase { - fun closeDb() - fun deleteDb(context: Context) -} - -object Database { - - private val instances = mutableListOf() - - fun register(database: ClosableDatabase) { - instances += database - } - - fun close() { - instances.onEach { - it.closeDb() - } - } - - fun delete(context: Context) { - instances.onEach { - it.deleteDb(context) - } - instances.clear() - } -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/db/SharedConverters.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/db/SharedConverters.kt deleted file mode 100644 index da891086a..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/db/SharedConverters.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.getcode.services.db - -import androidx.room.TypeConverter -import com.getcode.model.CurrencyCode -import com.getcode.model.KinAmount -import com.getcode.model.Rate -import com.getcode.model.chat.MessageContent -import com.getcode.model.chat.Pointer -import com.getcode.utils.decodeBase64 -import com.getcode.utils.encodeBase64 -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json - -class SharedConverters { - @TypeConverter - fun stringToByteList(value: String): List = value.decodeBase64().toList() - - @TypeConverter - fun byteListToString(list: List): String = list.toByteArray().encodeBase64() - - @TypeConverter - fun intListToString(list: List) = list.joinToString(",") - - @TypeConverter - fun stringToIntList(value: String) = value.split(",") - - @TypeConverter - fun currencyCodeToString(value: CurrencyCode) = value.name - @TypeConverter - fun stringToCurrencyCode(value: String) = CurrencyCode.tryValueOf(value) - - @TypeConverter - fun ratesToString(value: List) = Json.encodeToString(value) - @TypeConverter - fun stringToRates(value: String) = Json.decodeFromString>(value) - - @TypeConverter - fun messageContentToString(value: MessageContent) = Json.encodeToString(value) - @TypeConverter - fun stringToMessageContent(value: String) = Json.decodeFromString(value) - - @TypeConverter - fun kinAmountToString(value: KinAmount) = Json.encodeToString(KinAmount.serializer(), value) - - @TypeConverter - fun stringToKinAmount(value: String) = Json.decodeFromString(KinAmount.serializer(), value) - - @TypeConverter - fun pointerToString(pointer: Pointer) = Json.encodeToString(pointer) - - @TypeConverter - fun stringToPointer(value: String) = Json.decodeFromString(value) - - @TypeConverter - fun pointersToString(pointer: List) = Json.encodeToString(pointer) - - @TypeConverter - fun stringToPointers(value: String) = Json.decodeFromString>(value) -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/generator/Generator.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/generator/Generator.kt deleted file mode 100644 index 871e365e6..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/generator/Generator.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.getcode.services.generator - -interface Generator { - fun generate(predicate: D): R -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/generator/MnemonicGenerator.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/generator/MnemonicGenerator.kt deleted file mode 100644 index 27bd7bc80..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/generator/MnemonicGenerator.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.getcode.services.generator - -import com.getcode.crypt.MnemonicPhrase -import com.getcode.services.utils.Base58String -import com.getcode.services.utils.Base64String -import javax.inject.Inject - -class MnemonicGenerator @Inject constructor(): Generator { - - override fun generate(predicate: Base64String): MnemonicPhrase { - return MnemonicPhrase.fromEntropyB64(predicate) - } - - fun generateFromBase58(predicate: Base58String): MnemonicPhrase { - return MnemonicPhrase.fromEntropyB58(predicate) - } -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/manager/MnemonicManager.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/manager/MnemonicManager.kt deleted file mode 100644 index 7f2a54632..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/manager/MnemonicManager.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.getcode.services.manager - -import com.getcode.crypt.MnemonicCache -import com.getcode.crypt.MnemonicCode -import com.getcode.crypt.MnemonicPhrase -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.services.generator.MnemonicGenerator -import com.getcode.services.utils.Base58String -import com.getcode.services.utils.Base64String -import javax.inject.Inject - -class MnemonicManager @Inject constructor( - private val generator: MnemonicGenerator, -) { - fun fromEntropyBase58(cashLink: Base58String): MnemonicPhrase { - return generator.generateFromBase58(cashLink) - } - - fun fromEntropyBase64(entropy: Base64String): MnemonicPhrase { - return generator.generate(entropy) - } - - fun getKeyPair(entropy: String): KeyPair { - return fromEntropyBase64(entropy).getSolanaKeyPair() - } - - fun getKeyPair(mnemonicPhrase: MnemonicPhrase): KeyPair { - return mnemonicPhrase.getSolanaKeyPair() - } - - fun getEncodedBase64(mnemonicPhrase: MnemonicPhrase): String { - return mnemonicPhrase.getBase64EncodedEntropy() - } - - fun getEncodedBase58(mnemonicPhrase: MnemonicPhrase): String { - return mnemonicPhrase.getBase58EncodedEntropy() - } - - val mnemonicCode: MnemonicCode - get() = MnemonicCache.cachedCode -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/manager/ModalManager.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/manager/ModalManager.kt deleted file mode 100644 index 8ff461a5c..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/manager/ModalManager.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.getcode.services.manager - -import androidx.annotation.DrawableRes -import com.getcode.services.manager.ModalManager.ActionType -import com.getcode.services.manager.ModalManager.Message -import com.getcode.services.manager.ModalManager.MessageType -import com.getcode.services.manager.ModalManager._messages -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import java.util.UUID - -object ModalManager { - data class Message( - @DrawableRes - val icon: Int? = null, - val title: String, - val subtitle: String = "", - val positiveText: String, - val negativeText: String? = null, - val tertiaryText: String? = null, - val onPositive: () -> Unit, - val onNegative: () -> Unit = {}, - val onTertiary: () -> Unit = {}, - val onClose: (actionType: ActionType?) -> Unit = {}, - val type: MessageType = MessageType.DEFAULT, -// val isDismissibleByTouchOutside: Boolean = true, - val isDismissibleByBackButton: Boolean = true, - val timeoutSeconds: Int? = null, - val id: Long = UUID.randomUUID().mostSignificantBits, - ) - - private val _messages: MutableStateFlow> = MutableStateFlow( - listOf() - ) - val messages: StateFlow> get() = _messages.asStateFlow() - - fun showMessage(message: Message) { - _messages.update { currentMessages -> - currentMessages + message - } - } - - fun setMessageShown(messageId: Long) { - _messages.update { currentMessages -> - currentMessages.filterNot { it.id == messageId } - } - } - - fun clear() = _messages.update { listOf() } - - fun clearByType(type: MessageType) = _messages.update { it.filterNot { m -> m.type == type } } - - enum class MessageType { DEFAULT } - - enum class ActionType { - Positive, - Negative, - Tertiary - } - -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/mapper/Mapper.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/mapper/Mapper.kt deleted file mode 100644 index a3f9d0af3..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/mapper/Mapper.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.getcode.services.mapper - -interface Mapper { - fun map(from: F): T -} - -interface SuspendMapper { - suspend fun map(from: F): T -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/model/CodePayload.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/model/CodePayload.kt deleted file mode 100644 index 5927ab760..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/model/CodePayload.kt +++ /dev/null @@ -1,276 +0,0 @@ -package com.getcode.services.model - -import com.kik.scan.Scanner -import com.getcode.crypt.Sha256Hash -import com.getcode.ed25519.Ed25519.KeyPair -import com.getcode.model.CurrencyCode -import com.getcode.model.Fiat -import com.getcode.model.Kin -import com.getcode.model.Value -import com.getcode.services.model.payload.Username -import com.getcode.utils.DataSlice.byteToUnsignedInt -import com.getcode.utils.DataSlice.suffix -import com.getcode.utils.DataSlice.toLong -import com.getcode.services.utils.deriveRendezvousKey -import com.getcode.utils.encodeBase64 -import org.kin.sdk.base.tools.byteArrayToLong -import org.kin.sdk.base.tools.longToByteArray -import java.nio.ByteBuffer - -data class CodePayload( - val kind: Kind, - val value: Value, - val nonce: List = emptyList(), -) { - val rendezvous: KeyPair - - init { - rendezvous = when (value) { - is Fiat -> deriveRendezvousKey(encode(kind = kind, fiat = value, nonce = nonce).toByteArray()) - is Kin -> deriveRendezvousKey(encode(kind = kind, kin = value, nonce = nonce).toByteArray()) - is Username -> deriveRendezvousKey(encode(kind = kind, username = value).toByteArray()) - else -> throw IllegalArgumentException() - } - } - - val kin: Kin? - get() { - return value as? Kin ?: return null - } - - val fiat: Fiat? - get() { - return value as? Fiat ?: return null - } - - val username: String? - get() { - return (value as? Username)?.value ?: return null - } - - val codeData: ByteArray - get() = Scanner.encode(encode().toByteArray()) ?: byteArrayOf() - - fun encode(): List { - return when (value) { - is Kin -> encode(kind, value, nonce) - is Fiat -> encode(kind, value, nonce) - is Username -> encode(kind, value) - else -> throw IllegalArgumentException() - } - } - - private fun encode(kind: Kind, fiat: Fiat, nonce: List): List { - val data = MutableList(LENGTH) { 0 } - - val amount = (fiat.amount * 100).toLong() - - data[0] = kind.value.toByte() - - data[1] = fiat.currency.ordinal.toByte() - - amount.longToByteArray().forEachIndexed { index, byte -> - data[index + 2] = byte - } - - nonce.toByteArray().forEachIndexed { index, byte -> - data[index + OFFSET_NONCE] = byte - } - - return data - } - - private fun encode(kind: Kind, kin: Kin, nonce: List): List { - val data = MutableList(LENGTH) { 0 } - data[0] = kind.value.toByte() - - kin.quarks.longToByteArray().forEachIndexed { index, byte -> - data[index + OFFSET_QUARKS] = byte - } - - nonce.toByteArray().forEachIndexed { index, byte -> - data[index + OFFSET_NONCE] = byte - } - - return data - } - - private fun encode(kind: Kind, username: Username): List { - val data = MutableList(LENGTH) { 0 } - data[0] = kind.value.toByte() - - val usernameString = username.value.take(USERNAME_LENGTH) - - // The username that uniquely represents a user's tip code. Cannot be longer than 15 - // bytes. Any additional space is represented by the base64 encoded SHA256 hash of the username - // delimited by a period. - val paddedUsername = usernameString.let { - var padding = "" - val paddingRequired = (USERNAME_LENGTH - it.length) - if (paddingRequired > 0) { - padding = "." - } - - if (paddingRequired > 1) { - val hash = Sha256Hash.hash(usernameString.toByteArray()).encodeBase64() - padding += hash.take(paddingRequired - 1) - } - - "$it$padding" - } - - paddedUsername.toByteArray().forEachIndexed { index, byte -> - data[index + OFFSET_USERNAME] = byte - } - - return data - } - - companion object { - const val LENGTH = 20 - - const val USERNAME_LENGTH = 15 - - const val OFFSET_QUARKS = 1 - const val OFFSET_USERNAME = 5 - const val OFFSET_NONCE = 9 - - fun fromList(list: List): CodePayload { - val kind = Kind.entries.find { it.value == list[0].toInt() } ?: Kind.Cash - - val (value, nonce) = when (kind) { - Kind.Cash, - Kind.GiftCard -> { - val quarks = list.subList(1, OFFSET_NONCE).toByteArray().byteArrayToLong() - val nonce = list.suffix(OFFSET_NONCE) - - Kin(quarks) to nonce - } - - Kind.RequestPayment, - Kind.RequestPaymentV2, - Kind.Login -> { - // grab currency - val currencyIndex = list[1].byteToUnsignedInt() - val currency = CurrencyCode.entries.toList()[currencyIndex] - - // grab the fiat value - val amountData = ByteArray(7) - val buffer = ByteBuffer.wrap(list.toByteArray()) - buffer.position(2) - buffer.get(amountData, 0, 7) - val amountCents = amountData.toLong() - - val fiat = Fiat(currency = currency, amount = amountCents / 100.0) - - // grab nonce - val nonce = list.suffix(OFFSET_NONCE) - fiat to nonce - } - Kind.Tip -> { - val usernameBytes = list.suffix(OFFSET_USERNAME) - val usernameWithHash = String(usernameBytes.toByteArray()) - val hash = usernameWithHash.substringAfterLast(".") - val username = usernameWithHash.substringBeforeLast(".") - Username(username) to emptyList() - } - } - - return CodePayload(kind, value, nonce) - } - } -} - - - -enum class Kind(val value: Int) { - Cash(0), - GiftCard(1), - RequestPayment(2), - Login(3), - RequestPaymentV2(4), - Tip(5), -} - -/* - - Layout 0: Cash - - 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ - | T | Amount | Nonce | - +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ - - (T) Type (1 byte) - - The first byte of the data in all Code scan codes is reserved for the scan - code type. This field indicates which type of scan code data is contained - in the scan code. The expected format for each type is outlined below. - - Kin Amount in Quarks (8 bytes) - - This field indicates the number of quarks the payment is for. It should be - represented as a 64-bit unsigned integer. - - Nonce (11 bytes) - - This field is an 11-byte randomly-generated nonce. It should be regenerated - each time a new payment is initiated. - - Layout 1: Gift Card - - Same as layout 0. - - Layout 2: Payment Request - - 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ - | T | C | Fiat | Nonce | - +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ - - (T) Type (1 byte) - - The first byte of the data in all Code scan codes is reserved for the scan - code type. This field indicates which type of scan code data is contained - in the scan code. The expected format for each type is outlined below. - - (C) Currency Code (1 bytes) - - This field indicates the currency code for the fiat amount. The value is an - encoded index less than 255 that maps to a currency code in CurrencyCode.swift - - Fiat Amount (7 bytes) - - This field indicates the fiat amount, denominated in `Currency` above. The amount - is an integer value calculated as follows: $5.00 x 100 = 500. The decimals are - offset by multiplying by 100 and encoding the integer result. When decoding, the - amount should be divided by 100 again to return the original value. - - Nonce (11 bytes) - - This field is an 11-byte randomly-generated nonce. It should be regenerated - each time a new payment is initiated. - - Layout 5: Tip - - 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 - +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ - | T | Flags | username | ... remainder (0) | - +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ - - (T) Type (1 byte) - - The first byte of the data in all Code scan codes is reserved for the scan - code type. This field indicates which type of scan code data is contained - in the scan code. - - (F) Flags (4 bytes) - - Optional flags may provide additional context on the type of username embedded in - the scan code. - - Username (15 bytes) - - The username that uniquely represents a user's tip code. Cannot be longer than 15 - bytes. Any additional space is represented by an empty string in (remainder). -*/ \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/model/ExtendedMetadata.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/model/ExtendedMetadata.kt deleted file mode 100644 index 43e4c963c..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/model/ExtendedMetadata.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.getcode.services.model - -import com.getcode.model.SocialUser - -sealed interface ExtendedMetadata { - data class Tip(val socialUser: SocialUser): ExtendedMetadata - data class Any(val data: ByteArray, val typeUrl: String): ExtendedMetadata { - override fun equals(other: kotlin.Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Any - - if (!data.contentEquals(other.data)) return false - if (typeUrl != other.typeUrl) return false - - return true - } - - override fun hashCode(): Int { - var result = data.contentHashCode() - result = 31 * result + typeUrl.hashCode() - return result - } - - } -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/model/PrefBool.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/model/PrefBool.kt deleted file mode 100644 index 4388fd439..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/model/PrefBool.kt +++ /dev/null @@ -1,25 +0,0 @@ -@file:Suppress("ClassName") - -package com.getcode.services.model - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity -data class PrefBool( - @PrimaryKey val key: String, - val value: Boolean -) - -// Used internally to control logic and UI -interface InternalRouting -// Beta flag exposed in Settings -> Beta Flags to enable bleeding edge features -interface BetaFlag -// Dev settings -interface DevSetting -// This removes it from the UI in Settings -> Beta Flags -interface Immutable -// Once a feature behind a beta flag is made public, it becomes immutable -interface Launched: Immutable -// A feature flag can also be deemed deprecated and is also then immutable -interface Deprecated : Immutable diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/model/PrefDouble.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/model/PrefDouble.kt deleted file mode 100644 index fa36cf8ac..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/model/PrefDouble.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.getcode.services.model - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity -data class PrefDouble( - @PrimaryKey val key: String, - val value: Double -) \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/model/PrefInt.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/model/PrefInt.kt deleted file mode 100644 index 993665bb0..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/model/PrefInt.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.getcode.services.model - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity -data class PrefInt( - @PrimaryKey val key: String, - val value: Long -) - -sealed class PrefsInt(val value: String) { - data object AccountCreated: PrefsInt("account_created") -} diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/model/PrefString.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/model/PrefString.kt deleted file mode 100644 index 912bcbf41..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/model/PrefString.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.getcode.services.model - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity -data class PrefString( - @PrimaryKey val key: String, - val value: String -) - -enum class PrefsString(val value: String) { - KEY_USER_ID("user_id"), - KEY_DATA_CONTAINER_ID("data_container_id"), - KEY_ENTRY_CURRENCY("currency_selected"),//keep - KEY_CURRENCIES_RECENT("currencies_recent"),//keep - KEY_TIP_ACCOUNT("tip_account"), - KEY_LOCAL_CURRENCY("balance_currency_selected") -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/model/chat/OutgoingMessageContent.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/model/chat/OutgoingMessageContent.kt deleted file mode 100644 index 52a67931c..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/model/chat/OutgoingMessageContent.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.getcode.services.model.chat - -import com.getcode.model.ID -import com.getcode.model.KinAmount - -sealed interface OutgoingMessageContent { - data class Text(val text: String, val intentId: ID? = null): OutgoingMessageContent - data class Reply(val messageId: ID, val text: String, val intentId: ID? = null): OutgoingMessageContent - data class Reaction(val messageId: ID, val emoji: String, val intentId: ID? = null): OutgoingMessageContent - data class Tip(val messageId: ID, val amount: KinAmount, val intentId: ID): OutgoingMessageContent - data class DeleteRequest(val messageId: ID): OutgoingMessageContent -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/model/payload/Username.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/model/payload/Username.kt deleted file mode 100644 index 48b6562d2..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/model/payload/Username.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.getcode.services.model.payload - -import com.getcode.model.Value - -data class Username(val value: String): Value \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/model/profile/SocialAccountLinkRequest.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/model/profile/SocialAccountLinkRequest.kt deleted file mode 100644 index ce0c758bb..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/model/profile/SocialAccountLinkRequest.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.getcode.services.model.profile - -typealias LinkingToken = String - -sealed interface SocialAccountLinkRequest { - data class X(val token: LinkingToken): SocialAccountLinkRequest -} - -sealed interface SocialAccountUnlinkRequest { - data class X(val userId: String): SocialAccountUnlinkRequest -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/network/core/GrpcApi.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/network/core/GrpcApi.kt deleted file mode 100644 index 789a6d3fe..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/network/core/GrpcApi.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.getcode.services.network.core - -import io.grpc.ManagedChannel -import io.grpc.stub.ClientCallStreamObserver -import io.grpc.stub.ClientResponseObserver -import io.grpc.stub.StreamObserver -import io.reactivex.rxjava3.core.BackpressureStrategy -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Single -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.reactive.asFlow -import kotlin.reflect.KFunction2 - -abstract class GrpcApi(protected val managedChannel: ManagedChannel) { - fun KFunction2, Unit>.callAsSingle( - request: Request - ): Single = internalCallAsSingle(request) - - fun KFunction2, Unit>.callAsFlowable( - request: Request, - backpressureStrategy: BackpressureStrategy = BackpressureStrategy.BUFFER - ): Flowable = internalCallAsFlowable(request, backpressureStrategy) - - fun KFunction2, Unit>.callAsCancellableFlowable( - request: Request, - backpressureStrategy: BackpressureStrategy = BackpressureStrategy.BUFFER - ): Flowable = internalCallAsCancellableFlowable(request, backpressureStrategy) - - fun KFunction2, Unit>.callAsFlow( - request: Request, - backpressureStrategy: BackpressureStrategy = BackpressureStrategy.BUFFER - ): Flow = internalCallAsFlow(request, backpressureStrategy) - - fun KFunction2, Unit>.callAsCancellableFlow( - request: Request, - backpressureStrategy: BackpressureStrategy = BackpressureStrategy.BUFFER - ): Flow = internalCallAsCancellableFlow(request, backpressureStrategy) -} - -internal fun KFunction2, Unit>.internalCallAsSingle( - request: Request -): Single { - return Single.create { emitter -> - val observer = object : ClientResponseObserver { - override fun onNext(value: Response) = emitter.onSuccess(value) - override fun onError(error: Throwable) = emitter.onError(error) - override fun onCompleted() {} - - override fun beforeStart(requestStream: ClientCallStreamObserver?) { } - } - - try { - this(request, observer) - } catch (error: Throwable) { - emitter.onError(error) - } - } -} - -internal fun KFunction2, Unit>.internalCallAsFlowable( - request: Request, - backpressureStrategy: BackpressureStrategy = BackpressureStrategy.BUFFER -): Flowable { - return Flowable.create({ emitter -> - val observer = object : ClientResponseObserver { - override fun onNext(value: Response) = emitter.onNext(value) - override fun onError(error: Throwable) = emitter.onError(error) - override fun onCompleted() = emitter.onComplete() - - override fun beforeStart(requestStream: ClientCallStreamObserver?) { } - } - - try { - this(request, observer) - } catch (error: Throwable) { - emitter.onError(error) - } - }, backpressureStrategy) -} - -internal fun KFunction2, Unit>.internalCallAsFlow( - request: Request, - backpressureStrategy: BackpressureStrategy = BackpressureStrategy.BUFFER -): Flow { - return internalCallAsFlowable(request, backpressureStrategy).asFlow() -} - -internal fun KFunction2, Unit>.internalCallAsCancellableFlowable( - request: Request, - backpressureStrategy: BackpressureStrategy = BackpressureStrategy.BUFFER -): Flowable { - var streamObserver: ClientCallStreamObserver? = null - return Flowable.create({ emitter -> - val observer = object : ClientResponseObserver { - override fun onNext(value: Response) { - if (!emitter.isCancelled) { - emitter.onNext(value) - } - } - - override fun onError(error: Throwable) { - if (!emitter.isCancelled) { - emitter.onError(error) - } - } - - override fun onCompleted() { - if (!emitter.isCancelled) { - emitter.onComplete() - } - } - - override fun beforeStart(requestStream: ClientCallStreamObserver?) { - streamObserver = requestStream - } - } - - try { - this(request, observer) - } catch (error: Throwable) { - if (!emitter.isCancelled) { - emitter.onError(error) - } - } - }, backpressureStrategy) - .doOnCancel { - streamObserver?.cancel("subscription disposed, cancelling stream", null) - } -} - -internal fun KFunction2, Unit>.internalCallAsCancellableFlow( - request: Request, - backpressureStrategy: BackpressureStrategy = BackpressureStrategy.BUFFER -): Flow { - return internalCallAsCancellableFlowable(request, backpressureStrategy).asFlow() -} - diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/network/core/NetworkOracle.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/network/core/NetworkOracle.kt deleted file mode 100644 index 6d6aa90e0..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/network/core/NetworkOracle.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.getcode.services.network.core - -import io.reactivex.rxjava3.core.Flowable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.timeout -import kotlinx.coroutines.rx3.asCoroutineDispatcher -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit -import kotlin.time.Duration.Companion.seconds - -const val INFINITE_STREAM_TIMEOUT = -1L -const val DEFAULT_STREAM_TIMEOUT = 15L - -interface NetworkOracle { - fun managedRequest( - request: Single - ): Flowable = managedRequest(request.toFlowable()) - - fun managedRequest( - request: Flowable, - timeout: Long = DEFAULT_STREAM_TIMEOUT - ): Flowable - - fun managedRequest( - request: Flow, - timeout: Long = DEFAULT_STREAM_TIMEOUT - ): Flow -} - -class NetworkOracleImpl : NetworkOracle { - private val scheduler = Schedulers.from(Executors.newSingleThreadExecutor()) - - override fun managedRequest( - request: Flowable, - timeout: Long - ): Flowable { - return request - .let { - if (timeout != INFINITE_STREAM_TIMEOUT) { - it.timeout(timeout, TimeUnit.SECONDS) - } else { - it - } - } - .subscribeOn(scheduler) - } - - @OptIn(FlowPreview::class) - override fun managedRequest( - request: Flow, - timeout: Long - ): Flow { - return request - .let { - if (timeout != INFINITE_STREAM_TIMEOUT) { - it.timeout(timeout.seconds) - } else { - it - } - } - .flowOn(scheduler.asCoroutineDispatcher()) - } -} diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/observers/StreamObservers.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/observers/StreamObservers.kt deleted file mode 100644 index 57ed60c8a..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/observers/StreamObservers.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.getcode.services.observers - -import com.getcode.utils.TraceType -import com.getcode.utils.trace -import io.grpc.stub.StreamObserver -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import timber.log.Timber - - -class BidirectionalStreamReference(private val scope: CoroutineScope) : AutoCloseable { - - private val supervisorJob = SupervisorJob() - val coroutineScope = CoroutineScope(supervisorJob + scope.coroutineContext) - - var stream: StreamObserver? = null - set(value) { - field = value - if (value != null) { - postponeTimeout() - } else { - cancelTimeout() - } - } - - var timeoutHandler: (() -> Unit)? = null - - var onConnect: (() -> Unit)? = null - - private var lastPing: Long? = null - - private var pingTimeout = 15_000L - - private var closure: (() -> BidirectionalStreamReference)? = null - - private var timeoutTask: Job? = null - - override fun close() { - Timber.d("Deallocating bidirectional stream reference:") - } - - fun receivedPing(updatedTimeout: Long? = null) { - if (lastPing == null) { - onConnect?.invoke() - } - - lastPing = System.currentTimeMillis() - - // if the server provides a timeout, we'll update our local timeout accordingly. - updatedTimeout?.let { - // double provided timeout - val newTimeout = (it * 2) - if (pingTimeout != newTimeout) { - trace( - type = TraceType.StateChange, - message = "Updating timeout from $pingTimeout to $newTimeout" - ) - pingTimeout = newTimeout - } - } - - postponeTimeout() - } - - fun cancelTimeout() { - timeoutTask?.cancel() - timeoutTask = null - } - - fun postponeTimeout() { - cancelTimeout() - timeoutTask = coroutineScope.launch { - if (!isActive) return@launch - - delay(pingTimeout) - if (isActive) { - timeoutHandler?.invoke() - } - } - } - - fun destroy() { - lastPing = null - timeoutHandler = null - onConnect = null - cancelTimeout() - cancel() - release() - } - - fun cancel() { - stream?.onCompleted() - } - - fun retain() { - closure = { this } - } - - fun release() { - closure = null - } -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/CloudMessaging.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/CloudMessaging.kt deleted file mode 100644 index b6c7fd0aa..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/CloudMessaging.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.getcode.services.utils - -import com.getcode.utils.ErrorUtils -import com.google.firebase.messaging.FirebaseMessaging -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlin.coroutines.resume - -suspend fun FirebaseMessaging.token(): String? = suspendCancellableCoroutine { cont -> - - this.token.addOnCanceledListener { cont.resume(null) } - .addOnFailureListener { - ErrorUtils.handleError(it) - cont.resume(null) - }.addOnSuccessListener { cont.resume(it) } -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Completable.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Completable.kt deleted file mode 100644 index a3908bbab..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Completable.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.getcode.services.utils - -import io.reactivex.rxjava3.core.Completable - -fun Completable.toKotlinResult(): Result { - return try { - this.blockingAwait() - Result.success(Unit) - } catch (e: Throwable) { - Result.failure(e) - } -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Double.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Double.kt deleted file mode 100644 index b61af1a09..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Double.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.getcode.services.utils - - -fun Double.toByteArray(): ByteArray { - val longBits = java.lang.Double.doubleToLongBits(this) - val byteArray = ByteArray(8) - - for (i in 0 until 8) { - byteArray[i] = (longBits shr (8 * (7 - i))).toByte() - } - - return byteArray -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Flow.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Flow.kt deleted file mode 100644 index 6cbe9c612..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Flow.kt +++ /dev/null @@ -1,226 +0,0 @@ -@file:Suppress("UNCHECKED_CAST") - -package com.getcode.services.utils - -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.buffer -import kotlinx.coroutines.flow.cancellable -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.map -import timber.log.Timber -import kotlin.time.Duration - -fun tickerFlow(period: Duration): Flow = flow { - while (true) { - emit(Unit) - delay(period) - } -} - -fun flowInterval( - delayMillis: () -> Long, - initialDelayMillis: Long = 0L -) = flow { - require(delayMillis() >= 0) { "delayMillis must be positive" } - require(initialDelayMillis >=0) { "initialDelayMillis cannot be negative" } - if (initialDelayMillis > 0) { - delay(initialDelayMillis) - } - emit(System.currentTimeMillis()) - while (true) { - delay(delayMillis()) - emit(System.currentTimeMillis()) - } -}.cancellable().buffer() - -fun Flow.catchSafely( - action: suspend (T) -> Unit, -): Flow = catchSafely(action, onFailure = { Timber.e(it, it.message) }) - -fun Flow.catchSafely( - action: suspend (T) -> Unit, - onFailure: (Throwable) -> Unit = { Timber.e(it, it.message) }, -): Flow = map { - try { - action(it) - } catch (e: Exception) { - onFailure(e) - } -} - -fun combine( - flow: Flow, - flow2: Flow, - flow3: Flow, - flow4: Flow, - flow5: Flow, - flow6: Flow, - transform: suspend (T1, T2, T3, T4, T5, T6) -> R -): Flow = combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*> -> - transform( - args[0] as T1, - args[1] as T2, - args[2] as T3, - args[3] as T4, - args[4] as T5, - args[5] as T6, - ) -} - -fun combine( - flow: Flow, - flow2: Flow, - flow3: Flow, - flow4: Flow, - flow5: Flow, - flow6: Flow, - flow7: Flow, - transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R -): Flow = combine(flow, flow2, flow3, flow4, flow5, flow6, flow7) { args: Array<*> -> - transform( - args[0] as T1, - args[1] as T2, - args[2] as T3, - args[3] as T4, - args[4] as T5, - args[5] as T6, - args[6] as T7, - ) -} - -fun combine( - flow: Flow, - flow2: Flow, - flow3: Flow, - flow4: Flow, - flow5: Flow, - flow6: Flow, - flow7: Flow, - flow8: Flow, - transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R -): Flow = combine(flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8) { args: Array<*> -> - transform( - args[0] as T1, - args[1] as T2, - args[2] as T3, - args[3] as T4, - args[4] as T5, - args[5] as T6, - args[6] as T7, - args[7] as T8, - ) -} - -fun combine( - flow: Flow, - flow2: Flow, - flow3: Flow, - flow4: Flow, - flow5: Flow, - flow6: Flow, - flow7: Flow, - flow8: Flow, - flow9: Flow, - transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9) -> R -): Flow = combine(flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8, flow9) { args: Array<*> -> - transform( - args[0] as T1, - args[1] as T2, - args[2] as T3, - args[3] as T4, - args[4] as T5, - args[5] as T6, - args[6] as T7, - args[7] as T8, - args[8] as T9, - ) -} - -fun combine( - flow: Flow, - flow2: Flow, - flow3: Flow, - flow4: Flow, - flow5: Flow, - flow6: Flow, - flow7: Flow, - flow8: Flow, - flow9: Flow, - flow10: Flow, - transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) -> R -): Flow = combine(flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8, flow9, flow10) { args: Array<*> -> - transform( - args[0] as T1, - args[1] as T2, - args[2] as T3, - args[3] as T4, - args[4] as T5, - args[5] as T6, - args[6] as T7, - args[7] as T8, - args[8] as T9, - args[9] as T10, - ) -} - -fun combine( - flow: Flow, - flow2: Flow, - flow3: Flow, - flow4: Flow, - flow5: Flow, - flow6: Flow, - flow7: Flow, - flow8: Flow, - flow9: Flow, - flow10: Flow, - flow11: Flow, - transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11) -> R -): Flow = combine(flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8, flow9, flow10, flow11) { args: Array<*> -> - transform( - args[0] as T1, - args[1] as T2, - args[2] as T3, - args[3] as T4, - args[4] as T5, - args[5] as T6, - args[6] as T7, - args[7] as T8, - args[8] as T9, - args[9] as T10, - args[10] as T11, - ) -} - -fun combine( - flow: Flow, - flow2: Flow, - flow3: Flow, - flow4: Flow, - flow5: Flow, - flow6: Flow, - flow7: Flow, - flow8: Flow, - flow9: Flow, - flow10: Flow, - flow11: Flow, - flow12: Flow, - transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12) -> R -): Flow = combine(flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8, flow9, flow10, flow11, flow12) { args: Array<*> -> - transform( - args[0] as T1, - args[1] as T2, - args[2] as T3, - args[3] as T4, - args[4] as T5, - args[5] as T6, - args[6] as T7, - args[7] as T8, - args[8] as T9, - args[9] as T10, - args[10] as T11, - args[11] as T12, - ) -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Installations.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Installations.kt deleted file mode 100644 index 9d22f7980..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Installations.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.getcode.services.utils - -import com.getcode.utils.ErrorUtils -import com.google.firebase.installations.FirebaseInstallations -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlin.coroutines.resume - -suspend fun FirebaseInstallations.installationId(): String? = suspendCancellableCoroutine { cont -> - this.id.addOnCanceledListener { cont.resume(null) } - .addOnFailureListener { - ErrorUtils.handleError(it) - cont.resume(null) - }.addOnSuccessListener { cont.resume(it) } -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/KeyPair.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/KeyPair.kt deleted file mode 100644 index 35660a562..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/KeyPair.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.getcode.services.utils - -import android.util.Base64 -import com.getcode.crypt.Sha256Hash -import com.getcode.ed25519.Ed25519 -import com.getcode.ed25519.Ed25519.KeyPair - -fun deriveRendezvousKey(from: ByteArray): KeyPair { - return Ed25519.createKeyPair(Base64.encodeToString(Sha256Hash.hash(from), Base64.DEFAULT)) -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Long.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Long.kt deleted file mode 100644 index 3f6552a40..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Long.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.getcode.services.utils - -import kotlin.math.floor - -val Long.floored: Long - get() { - val seconds = this / 1000.0 - val flooredSeconds = floor(seconds) - return (flooredSeconds * 1_000).toLong() - } \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Maps.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Maps.kt deleted file mode 100644 index 06c84fa64..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Maps.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.getcode.services.utils - -inline fun MutableMap.getOrPutIfNonNull(key: K, defaultValue: () -> V?): V? { - val value = get(key) - return if (value == null) { - val answer = defaultValue() - if (answer != null) { - put(key, answer) - } - answer - } else { - value - } -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Nonce.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Nonce.kt deleted file mode 100644 index 061ac810e..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Nonce.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.getcode.services.utils - -import kotlin.random.Random - -val nonce - get() = Random.nextBytes(11).toList() diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Result.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Result.kt deleted file mode 100644 index 6e4b2e1d8..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/Result.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.getcode.services.utils - -import kotlinx.coroutines.delay -import kotlin.time.Duration - -suspend fun Result.mapResult(transform: suspend (T) -> Result): Result { - return try { - this.fold( - onSuccess = { value -> transform(value) }, - onFailure = { error -> Result.failure(error) } - ) - } catch (e: Throwable) { - Result.failure(e) - } -} - -suspend fun Result.onSuccessWithDelay(minimumDelay: Long, block: suspend (T) -> Unit): Result { - val startTime = System.currentTimeMillis() - return onSuccess { value -> - val elapsedTime = System.currentTimeMillis() - startTime - val remainingTime = minimumDelay - elapsedTime - if (remainingTime > 0) { - delay(remainingTime) - } - block(value) - } -} - -suspend fun Result.onSuccessWithDelay(minimumDelay: Duration, block: suspend (T) -> Unit): Result { - val startTime = System.currentTimeMillis() - return onSuccess { value -> - val elapsedTime = System.currentTimeMillis() - startTime - val remainingTime = minimumDelay.inWholeMilliseconds - elapsedTime - if (remainingTime > 0) { - delay(remainingTime) - } - block(value) - } -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/String.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/String.kt deleted file mode 100644 index 783cb071f..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/String.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.getcode.services.utils - -import com.getcode.utils.ErrorUtils -import com.google.i18n.phonenumbers.PhoneNumberUtil -import java.util.Locale - -fun String.makeE164(locale: Locale? = null): String { - return try { - val p = PhoneNumberUtil.getInstance().parse(this, (locale ?: Locale.getDefault()).country) - PhoneNumberUtil.getInstance().format(p, PhoneNumberUtil.PhoneNumberFormat.E164) - } catch(e: Exception) { - ErrorUtils.handleError(e) - "" - } -} - -fun String.addLeadingZero(upTo: Int): String { - if (upTo < length) return this - val padding = "0".repeat(length - upTo) - return "$padding$this" -} - -fun String.base64EncodedData(): ByteArray { - val data = toByteArray() - val r = data.size % 4 - if (r > 0) { - val requiredPadding = data.size + 4 - r - val padding = "=".repeat(requiredPadding) - return data + padding.toByteArray() - } - return data -} - -fun String.padded(minCount: Int): String { - return if (this.length < minCount) { - val toInsert = minCount - this.length - val padding = " ".repeat(toInsert) - this + padding - } else { - this - } -} - -typealias Base64String = String -typealias Base58String = String \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/logging/LoggingClientCall.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/logging/LoggingClientCall.kt deleted file mode 100644 index ae9153ab9..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/logging/LoggingClientCall.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.getcode.services.utils.logging - -import io.grpc.ClientCall -import io.grpc.ForwardingClientCall.SimpleForwardingClientCall -import java.util.Queue - -class LoggingClientCall( - delegate: ClientCall, - private val requestQueue: Queue, - private val responseQueue: Queue, -): SimpleForwardingClientCall(delegate) { - - override fun sendMessage(message: ReqT) { - requestQueue.offer(message) - super.sendMessage(message) - } - - override fun start(responseListener: Listener, headers: io.grpc.Metadata) { - super.start(LoggingClientCallListener(responseListener, requestQueue, responseQueue), headers) - } -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/logging/LoggingClientCallListener.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/logging/LoggingClientCallListener.kt deleted file mode 100644 index fbe38fba0..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/logging/LoggingClientCallListener.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.getcode.services.utils.logging - -import com.getcode.utils.TraceType -import com.getcode.utils.trace -import io.grpc.ClientCall -import io.grpc.ForwardingClientCallListener -import io.grpc.Status -import java.util.Queue - -class LoggingClientCallListener( - delegate: ClientCall.Listener, - private val requestQueue: Queue, - private val responseQueue: Queue -) : ForwardingClientCallListener.SimpleForwardingClientCallListener(delegate) { - - override fun onMessage(message: ResT) { - responseQueue.offer(message) - super.onMessage(message) - } - - override fun onClose(status: Status, trailers: io.grpc.Metadata?) { - val requestLog = requestQueue.joinToString( - separator = ", ", - prefix = "[", - postfix = "]" - ) { getObjectDetails(it) } - val responseLog = responseQueue.joinToString( - separator = ", ", - prefix = "[", - postfix = "]" - ) { getObjectDetails(it) } - - if (status.isOk) { - trace(tag = "RpcLogging", message = "Request: $requestLog", type = TraceType.Network) - trace(tag = "RpcLogging", message = "Response: $responseLog", type = TraceType.Network) - trace( - tag = "RpcLogging", - message = "The request was processed successfully", - type = TraceType.Network - ) - } else if (UNSUCCESSFUL_STATUS_CODES.contains(status.code)) { - trace(tag = "RpcLogging", message = "Request: $requestLog", type = TraceType.Network) - trace( - tag = "RpcLogging", - message = "An error occurred while processing the request", - type = TraceType.Error, - error = status.asRuntimeException() - ) - } - - super.onClose(status, trailers) - } - - private fun getObjectDetails(obj: Any?, maxDepth: Int = 2, currentDepth: Int = 0): String { - if (obj == null) return "null" - - // Prevents infinitely deep recursion by setting a maximum depth - if (currentDepth >= maxDepth) return "${obj::class.java.simpleName}(...)" - - return try { - val fields = obj::class.java.declaredFields - fields.joinToString(", ", "${obj::class.java.simpleName}(", ")") { field -> - field.isAccessible = true - val value = field.get(obj) - - // Check if the field itself is another complex object (not a primitive or string), then call recursively - val valueString = when { - value == null -> "null" - value::class.java.isPrimitive || value is String -> value.toString() - else -> getObjectDetails(value, maxDepth, currentDepth + 1) - } - - "${field.name}=$valueString" - } - } catch (e: Exception) { - "Unable to log details for ${obj::class.java.simpleName}" - } - } - - - companion object { - private val UNSUCCESSFUL_STATUS_CODES = listOf( - Status.INVALID_ARGUMENT.code, - Status.INTERNAL.code, - Status.NOT_FOUND.code - ) - } -} \ No newline at end of file diff --git a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/logging/LoggingClientInterceptor.kt b/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/logging/LoggingClientInterceptor.kt deleted file mode 100644 index 70e3f50d5..000000000 --- a/services/legacy-shared/src/main/kotlin/com/getcode/services/utils/logging/LoggingClientInterceptor.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.getcode.services.utils.logging - -import io.grpc.CallOptions -import io.grpc.Channel -import io.grpc.ClientCall -import io.grpc.ClientInterceptor -import io.grpc.MethodDescriptor -import java.util.concurrent.LinkedBlockingDeque - -class LoggingClientInterceptor: ClientInterceptor { - override fun interceptCall( - method: MethodDescriptor?, - callOptions: CallOptions?, - next: Channel - ): ClientCall { - return LoggingClientCall(next.newCall(method, callOptions), LinkedBlockingDeque(), LinkedBlockingDeque()) - } - -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 77ebd6ecf..0c668c0a6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -32,7 +32,7 @@ dependencyResolutionManagement { } } -rootProject.name = "Code" +rootProject.name = "Flipcash" include( // app containers @@ -153,7 +153,6 @@ include( ":libs:permissions:public", ":libs:quickresponse", - ":libs:requests", ":libs:search", ":libs:vibrator:bindings", @@ -161,14 +160,12 @@ include( ":libs:vibrator:public", // Services definition for app and lib access - ":services:legacy-shared", ":services:opencode", ":services:opencode-compose", ":services:flipcash", ":services:flipcash-compose", // common UI - ":ui:analytics", ":ui:biometrics", ":ui:core", ":ui:components", diff --git a/ui/analytics/.gitignore b/ui/analytics/.gitignore deleted file mode 100644 index 9f2a07880..000000000 --- a/ui/analytics/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -build/ -.gradle/ diff --git a/ui/analytics/build.gradle.kts b/ui/analytics/build.gradle.kts deleted file mode 100644 index 8b858a891..000000000 --- a/ui/analytics/build.gradle.kts +++ /dev/null @@ -1,23 +0,0 @@ -plugins { - alias(libs.plugins.flipcash.android.library.compose) -} - -android { - namespace = "${Gradle.codeNamespace}.ui.analytics" - - buildFeatures { - buildConfig = true - } -} - -dependencies { - api(project(":libs:analytics")) - implementation(project(":ui:components")) - implementation(project(":ui:navigation")) - - api(libs.androidx.annotation) - api(libs.kotlin.stdlib) - api(libs.kotlinx.coroutines.core) - implementation(libs.compose.animation) - implementation(libs.compose.material) -} diff --git a/ui/analytics/src/main/kotlin/com/getcode/ui/analytics/AnalyticsScreenWatcher.kt b/ui/analytics/src/main/kotlin/com/getcode/ui/analytics/AnalyticsScreenWatcher.kt deleted file mode 100644 index 7a2f286d0..000000000 --- a/ui/analytics/src/main/kotlin/com/getcode/ui/analytics/AnalyticsScreenWatcher.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.getcode.ui.analytics - -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.lifecycle.LifecycleOwner -import androidx.navigation3.runtime.NavKey -import com.getcode.libs.analytics.AppAction -import com.getcode.navigation.core.LocalCodeNavigator - -@Composable -fun AnalyticsScreenWatcher( - route: NavKey, - lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, - action: AppAction, -) { - val navigator = LocalCodeNavigator.current - val lastItem = navigator.lastItem - if (lastItem == route) { - AnalyticsWatcher( - lifecycleOwner = lifecycleOwner, - onEvent = { analytics, _ -> analytics.action(action) } - ) - } -} diff --git a/ui/analytics/src/main/kotlin/com/getcode/ui/analytics/AnalyticsWatcher.kt b/ui/analytics/src/main/kotlin/com/getcode/ui/analytics/AnalyticsWatcher.kt deleted file mode 100644 index d68d2fa12..000000000 --- a/ui/analytics/src/main/kotlin/com/getcode/ui/analytics/AnalyticsWatcher.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.getcode.ui.analytics - -import android.content.Context -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import com.getcode.libs.analytics.AnalyticsService -import com.getcode.libs.analytics.LocalAnalytics -import com.getcode.navigation.utils.lifecycle.RepeatOnLifecycle - -@Composable -fun AnalyticsWatcher( - lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, - onEvent: (AnalyticsService, Context) -> Unit, - onDispose: (AnalyticsService, Context) -> Unit = { _, _ -> }, -) { - val context = LocalContext.current - val analyticsService = LocalAnalytics.current - - val updatedOnEvent by rememberUpdatedState(newValue = onEvent) - val updatedOnDispose by rememberUpdatedState(newValue = onDispose) - - RepeatOnLifecycle( - lifecycleOwner = lifecycleOwner, - targetState = Lifecycle.State.RESUMED, - doOnDispose = { - updatedOnDispose(analyticsService, context) - }, - ) { - updatedOnEvent(analyticsService, context) - } -} \ No newline at end of file