diff --git a/api/src/main/java/com/getcode/analytics/AnalyticsManager.kt b/api/src/main/java/com/getcode/analytics/AnalyticsManager.kt index 833c1bf6c..1fccc04f1 100644 --- a/api/src/main/java/com/getcode/analytics/AnalyticsManager.kt +++ b/api/src/main/java/com/getcode/analytics/AnalyticsManager.kt @@ -323,7 +323,7 @@ class AnalyticsManager @Inject constructor( //Bill Bill("Bill"), Request("Request Card"), - TipCard("TIp Card"), + TipCard("Tip Card"), //Transfer Transfer("Transfer"), diff --git a/api/src/main/java/com/getcode/model/PrefBool.kt b/api/src/main/java/com/getcode/model/PrefBool.kt index 0b0f44bff..590c172e8 100644 --- a/api/src/main/java/com/getcode/model/PrefBool.kt +++ b/api/src/main/java/com/getcode/model/PrefBool.kt @@ -43,14 +43,13 @@ sealed class PrefsBool(val value: String) { 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 - - data object CHAT_UNSUB_ENABLED: PrefsBool("chat_unsub_enabled"), BetaFlag data object TIPS_ENABLED : PrefsBool("tips_enabled"), BetaFlag data object TIPS_CHAT_ENABLED: PrefsBool("tips_chat_enabled"), BetaFlag data object TIPS_CHAT_CASH_ENABLED: PrefsBool("tips_chat_cash_enabled"), BetaFlag data object BALANCE_CURRENCY_SELECTION_ENABLED: PrefsBool("balance_currency_enabled"), BetaFlag data object KADO_WEBVIEW_ENABLED : PrefsBool("kado_inapp_enabled"), BetaFlag + data object SHARE_TWEET_TO_TIP : PrefsBool("share_tweet_to_tip"), BetaFlag } val APP_SETTINGS: List = listOf(PrefsBool.CAMERA_START_BY_DEFAULT, PrefsBool.REQUIRE_BIOMETRICS) \ No newline at end of file diff --git a/api/src/main/java/com/getcode/network/repository/BetaFlagsRepository.kt b/api/src/main/java/com/getcode/network/repository/BetaFlagsRepository.kt index d42ded921..5d798f63b 100644 --- a/api/src/main/java/com/getcode/network/repository/BetaFlagsRepository.kt +++ b/api/src/main/java/com/getcode/network/repository/BetaFlagsRepository.kt @@ -19,6 +19,7 @@ data class BetaOptions( val tipsChatCashEnabled: Boolean, val balanceCurrencySelectionEnabled: Boolean, val kadoWebViewEnabled: Boolean, + val shareTweetToTip: Boolean, ) { companion object { // Default states for various beta flags in app. @@ -36,6 +37,7 @@ data class BetaOptions( tipsChatCashEnabled = false, balanceCurrencySelectionEnabled = true, kadoWebViewEnabled = false, + shareTweetToTip = false ) } } @@ -70,7 +72,8 @@ class BetaFlagsRepository @Inject constructor( observeBetaFlag(PrefsBool.TIPS_CHAT_CASH_ENABLED, default = defaults.tipsChatCashEnabled), 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.KADO_WEBVIEW_ENABLED, default = defaults.kadoWebViewEnabled), + observeBetaFlag(PrefsBool.SHARE_TWEET_TO_TIP, default = defaults.shareTweetToTip) ) { BetaOptions( showNetworkDropOff = it[0], @@ -86,6 +89,7 @@ class BetaFlagsRepository @Inject constructor( balanceCurrencySelectionEnabled = it[10], displayErrors = it[11], kadoWebViewEnabled = it[12], + shareTweetToTip = it[13] ) } } @@ -98,4 +102,8 @@ class BetaFlagsRepository @Inject constructor( b.takeIf { a } ?: default } } + + suspend fun isEnabled(flag: PrefsBool): Boolean { + return prefRepository.get(flag, false) + } } diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 572299105..6027ef141 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -223,5 +223,5 @@ dependencies { implementation(Libs.timber) implementation(Libs.bugsnag) - implementation("dev.chrisbanes.haze:haze:0.7.3") + implementation(Libs.haze) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 31e85cf5f..093d4174e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -152,6 +152,18 @@ + + + + + + + + diff --git a/app/src/main/java/com/getcode/util/Context.kt b/app/src/main/java/com/getcode/util/Context.kt index 1060a721a..867d5a51f 100644 --- a/app/src/main/java/com/getcode/util/Context.kt +++ b/app/src/main/java/com/getcode/util/Context.kt @@ -1,15 +1,8 @@ package com.getcode.util import android.content.Context -import androidx.biometric.BiometricManager -import androidx.biometric.BiometricPrompt -import androidx.biometric.BiometricPrompt.AuthenticationError import androidx.core.content.ContextCompat -import androidx.fragment.app.FragmentActivity import com.getcode.R -import com.getcode.network.repository.TransactionRepository.ErrorSubmitIntent -import timber.log.Timber -import java.util.concurrent.Executors fun Context.launchAppSettings() { val intent = IntentUtils.appSettings() diff --git a/app/src/main/java/com/getcode/util/DeeplinkHandler.kt b/app/src/main/java/com/getcode/util/DeeplinkHandler.kt index db994f286..cddc002c1 100644 --- a/app/src/main/java/com/getcode/util/DeeplinkHandler.kt +++ b/app/src/main/java/com/getcode/util/DeeplinkHandler.kt @@ -1,16 +1,27 @@ package com.getcode.util +import android.content.Context import android.content.Intent import android.net.Uri +import androidx.core.net.toUri import cafe.adriel.voyager.core.screen.Screen +import com.getcode.model.BetaFlag +import com.getcode.model.PrefsBool import com.getcode.models.DeepLinkRequest +import com.getcode.models.encode import com.getcode.navigation.screens.HomeScreen import com.getcode.navigation.screens.LoginScreen +import com.getcode.network.repository.BetaFlagsRepository +import com.getcode.network.repository.encodeBase64 import com.getcode.network.repository.urlDecode +import com.getcode.ui.utils.getActivity import com.getcode.utils.TraceType import com.getcode.utils.base64EncodedData import com.getcode.utils.trace +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -19,6 +30,7 @@ data class DeeplinkResult( val type: DeeplinkHandler.Type, val stack: List, ) + /** * This class is used to manage intent state across navigation. * @@ -33,7 +45,10 @@ data class DeeplinkResult( * in favour of the latest request in the navigation graph. */ @Singleton -class DeeplinkHandler @Inject constructor() { +class DeeplinkHandler @Inject constructor( + @ApplicationContext private val context: Context, + private val betaFlags: BetaFlagsRepository +) { var debounceIntent: Intent? = null set(value) { intent.value = value @@ -43,8 +58,18 @@ class DeeplinkHandler @Inject constructor() { val intent = MutableStateFlow(debounceIntent) - fun handle(intent: Intent? = debounceIntent): DeeplinkResult? { - val uri = intent?.data ?: return null + suspend fun handle(intent: Intent? = debounceIntent): DeeplinkResult? { + println(intent) + val 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() + } + + else -> null + } ?: return null + return when (val type = uri.deeplinkType) { is Type.Login -> { DeeplinkResult( @@ -63,7 +88,7 @@ class DeeplinkHandler @Inject constructor() { is Type.Sdk -> { Timber.d("sdk=${type.payload}") - val request = type.payload?.base64EncodedData()?.let { DeepLinkRequest.from(it) } + val request = type.payload?.base64EncodedData()?.let { DeepLinkRequest.from(it) } DeeplinkResult( type, listOf(HomeScreen(request = request)), @@ -74,7 +99,14 @@ class DeeplinkHandler @Inject constructor() { Timber.d("tipcard for ${type.username} on ${type.platform}") DeeplinkResult( type, - listOf(HomeScreen(request = DeepLinkRequest.fromTipCardUsername(type.platform, type.username))), + listOf( + HomeScreen( + request = DeepLinkRequest.fromTipCardUsername( + type.platform, + type.username + ) + ) + ), ) } @@ -82,11 +114,29 @@ class DeeplinkHandler @Inject constructor() { } } + /** + * 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 for tipcard URLs val components = pathSegments - if (components.count() == 2 && components[0] == "x" && components[1].isNotEmpty()) { + if (components.count() >= 2 && components[0] == "x" && components[1].isNotEmpty()) { return Type.Tip(components[0], components[1]) } @@ -124,12 +174,13 @@ class DeeplinkHandler @Inject constructor() { 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 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 Unknown(val path: String?) : Type } @@ -161,4 +212,5 @@ class DeeplinkHandler @Inject constructor() { } } -private operator fun Regex.contains(text: String?): Boolean = text?.let { this.matches(it) } ?: false +private operator fun Regex.contains(text: String?): Boolean = + text?.let { this.matches(it) } ?: false diff --git a/app/src/main/java/com/getcode/util/IntentUtils.kt b/app/src/main/java/com/getcode/util/IntentUtils.kt index 835b22f91..ada046265 100644 --- a/app/src/main/java/com/getcode/util/IntentUtils.kt +++ b/app/src/main/java/com/getcode/util/IntentUtils.kt @@ -5,15 +5,9 @@ import android.content.Intent import android.net.Uri import android.provider.Settings import com.getcode.BuildConfig -import com.getcode.R -import com.getcode.model.Currency -import com.getcode.model.KinAmount -import com.getcode.model.Username -import com.getcode.network.repository.replaceParam -import com.getcode.network.repository.urlEncode -import com.getcode.solana.organizer.GiftCardAccount import com.getcode.utils.makeE164 + object IntentUtils { fun appSettings() = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { @@ -30,7 +24,7 @@ object IntentUtils { } fun tweet(message: String) = Intent(Intent.ACTION_VIEW).apply { - val url = "https://www.twitter.com/intent/tweet?text=${message.urlEncode()}" + val url = Linkify.tweet(message) setData(Uri.parse(url)) flags = Intent.FLAG_ACTIVITY_NEW_TASK } @@ -47,8 +41,8 @@ object IntentUtils { return shareIntent } - fun tipCard(username: String): Intent { - val url = "https://tipcard.getcode.com/x/$username" + fun tipCard(username: String, platform: String): Intent { + val url = Linkify.tipCard(username, platform) val sendIntent: Intent = Intent().apply { action = Intent.ACTION_SEND @@ -67,7 +61,7 @@ object IntentUtils { entropy: String, formattedAmount: String, ): Intent { - val url = "https://cash.getcode.com/c/#/e=$entropy" + val url = Linkify.cashLink(entropy) val text = "$formattedAmount $url" val sendIntent: Intent = Intent().apply { diff --git a/app/src/main/java/com/getcode/util/Linkify.kt b/app/src/main/java/com/getcode/util/Linkify.kt new file mode 100644 index 000000000..3eb42501f --- /dev/null +++ b/app/src/main/java/com/getcode/util/Linkify.kt @@ -0,0 +1,9 @@ +package com.getcode.util + +import com.getcode.network.repository.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/app/src/main/java/com/getcode/util/Pacman.kt b/app/src/main/java/com/getcode/util/Pacman.kt new file mode 100644 index 000000000..db2500529 --- /dev/null +++ b/app/src/main/java/com/getcode/util/Pacman.kt @@ -0,0 +1,30 @@ +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/app/src/main/java/com/getcode/view/main/account/BetaFlagsScreen.kt b/app/src/main/java/com/getcode/view/main/account/BetaFlagsScreen.kt index 7cd07999b..30201acf8 100644 --- a/app/src/main/java/com/getcode/view/main/account/BetaFlagsScreen.kt +++ b/app/src/main/java/com/getcode/view/main/account/BetaFlagsScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import com.getcode.R import com.getcode.model.PrefsBool +import com.getcode.network.repository.BetaOptions import com.getcode.theme.CodeTheme import com.getcode.ui.components.ButtonState import com.getcode.ui.components.CodeButton @@ -44,7 +45,7 @@ fun BetaFlagsScreen( PrefsBool.VIBRATE_ON_SCAN, R.string.beta_vibrate_on_scan, stringResource(R.string.beta_vibrate_on_scan_description), - state.isVibrateOnScan + state.tickOnScan ), BetaFeature( PrefsBool.SHOW_CONNECTIVITY_STATUS, @@ -62,7 +63,7 @@ fun BetaFlagsScreen( PrefsBool.BALANCE_CURRENCY_SELECTION_ENABLED, R.string.beta_balance_currency, stringResource(R.string.beta_balance_currency_description), - state.currencySelectionBalanceEnabled + state.balanceCurrencySelectionEnabled ), BetaFeature( PrefsBool.GIVE_REQUESTS_ENABLED, @@ -74,7 +75,7 @@ fun BetaFlagsScreen( PrefsBool.BUY_MODULE_ENABLED, R.string.beta_buy_kin, stringResource(id = R.string.beta_buy_kin_description), - state.buyKinEnabled + state.buyModuleEnabled ), BetaFeature( PrefsBool.CHAT_UNSUB_ENABLED, @@ -106,6 +107,12 @@ fun BetaFlagsScreen( 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.DISPLAY_ERRORS, R.string.beta_display_errors, @@ -143,7 +150,7 @@ fun BetaFlagsScreen( } } -private fun BetaFlagsViewModel.State.canMutate(flag: PrefsBool): Boolean { +private fun BetaOptions.canMutate(flag: PrefsBool): Boolean { return when (flag) { PrefsBool.BUY_MODULE_ENABLED -> false PrefsBool.BALANCE_CURRENCY_SELECTION_ENABLED -> false diff --git a/app/src/main/java/com/getcode/view/main/account/BetaFlagsViewModel.kt b/app/src/main/java/com/getcode/view/main/account/BetaFlagsViewModel.kt index d212ecaab..a518f71a5 100644 --- a/app/src/main/java/com/getcode/view/main/account/BetaFlagsViewModel.kt +++ b/app/src/main/java/com/getcode/view/main/account/BetaFlagsViewModel.kt @@ -5,6 +5,7 @@ import com.getcode.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 @@ -18,26 +19,11 @@ import javax.inject.Inject class BetaFlagsViewModel @Inject constructor( betaFlags: BetaFlagsRepository, prefRepository: PrefRepository, -) : BaseViewModel2( - initialState = State(), + pacman: Pacman, +) : BaseViewModel2( + initialState = BetaOptions.Defaults, updateStateForEvent = updateStateForEvent ) { - data class State( - val showNetworkDropOff: Boolean = false, - val canViewBuckets: Boolean = false, - val isVibrateOnScan: Boolean = false, - val currencySelectionBalanceEnabled: Boolean = false, - val displayErrors: Boolean = false, - val giveRequestsEnabled: Boolean = false, - val buyKinEnabled: Boolean = false, - val establishCodeRelationship: Boolean = false, - val chatUnsubEnabled: Boolean = false, - val tipsEnabled: Boolean = false, - val tipsChatEnabled: Boolean = false, - val tipsChatCashEnabled: Boolean = false, - val kadoWebViewEnabled: Boolean = false, - ) - sealed interface Event { data class UpdateSettings(val settings: BetaOptions) : Event data class Toggle(val setting: PrefsBool, val state: Boolean): Event @@ -58,29 +44,21 @@ class BetaFlagsViewModel @Inject constructor( it.setting, it.state ) + + when (it.setting) { + PrefsBool.SHARE_TWEET_TO_TIP -> { + pacman.enableTweetShare(it.state) + } + else -> Unit + } }.launchIn(viewModelScope) } companion object { - val updateStateForEvent: (Event) -> ((State) -> State) = { event -> + val updateStateForEvent: (Event) -> ((BetaOptions) -> BetaOptions) = { event -> when (event) { - is Event.UpdateSettings -> { state -> - with(event.settings) { - state.copy( - showNetworkDropOff = showNetworkDropOff, - canViewBuckets = canViewBuckets, - isVibrateOnScan = tickOnScan, - currencySelectionBalanceEnabled = balanceCurrencySelectionEnabled, - displayErrors = displayErrors, - giveRequestsEnabled = giveRequestsEnabled, - buyKinEnabled = buyModuleEnabled, - chatUnsubEnabled = chatUnsubEnabled, - tipsEnabled = tipsEnabled, - tipsChatEnabled = tipsChatEnabled, - tipsChatCashEnabled = tipsChatCashEnabled, - kadoWebViewEnabled = kadoWebViewEnabled, - ) - } + is Event.UpdateSettings -> { _ -> + event.settings } is Event.Toggle -> { state -> state } diff --git a/app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt b/app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt index 0586737b0..b1f95b52b 100644 --- a/app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt +++ b/app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt @@ -1358,10 +1358,9 @@ class HomeViewModel @Inject constructor( } private fun shareTipCard() = viewModelScope.launch { - val username = tipController.connectedAccount.value?.username ?: return@launch - + val connectedAccount = tipController.connectedAccount.value ?: return@launch withContext(Dispatchers.Main) { - val shareIntent = IntentUtils.tipCard(username) + val shareIntent = IntentUtils.tipCard(connectedAccount.username, connectedAccount.platform) _eventFlow.emit(HomeEvent.SendIntent(shareIntent)) } diff --git a/app/src/main/res/values/strings-universal.xml b/app/src/main/res/values/strings-universal.xml index a04e8d6a5..55d4736d5 100644 --- a/app/src/main/res/values/strings-universal.xml +++ b/app/src/main/res/values/strings-universal.xml @@ -24,6 +24,7 @@ Tip Chats Cash Currency Selection in Balance Buy Kin Internally + Share Tweets to Tip 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. @@ -37,6 +38,7 @@ If enabled, you\'ll gain the ability to chat with tippers. If enabled, you\'ll gain the ability to send Kin in Tip Chats. 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. %1$s %2$s Reset Tooltips diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 15feed7ce..6820b088a 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -260,4 +260,7 @@ object Libs { const val sodium_bindings = "com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings-android:${Versions.sodium_bindings}" const val fingerprint_pro = "com.fingerprint.android:pro:2.4.0" + + const val haze = "dev.chrisbanes.haze:haze:0.7.3" + const val process_phoenix = "com.jakewharton:process-phoenix:3.0.0" }