Skip to content

Commit fcb492e

Browse files
authored
Merge pull request #503 from code-payments/feat/share-tweet-to-tip
chore: share tweet to tip behind beta flag
2 parents c56efbc + f386b19 commit fcb492e

15 files changed

Lines changed: 160 additions & 74 deletions

File tree

api/src/main/java/com/getcode/analytics/AnalyticsManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ class AnalyticsManager @Inject constructor(
323323
//Bill
324324
Bill("Bill"),
325325
Request("Request Card"),
326-
TipCard("TIp Card"),
326+
TipCard("Tip Card"),
327327

328328
//Transfer
329329
Transfer("Transfer"),

api/src/main/java/com/getcode/model/PrefBool.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,13 @@ sealed class PrefsBool(val value: String) {
4343
data object SHOW_CONNECTIVITY_STATUS: PrefsBool("debug_no_network"), BetaFlag
4444
data object GIVE_REQUESTS_ENABLED: PrefsBool("give_requests_enabled"), BetaFlag
4545
data object BUY_MODULE_ENABLED : PrefsBool("buy_kin_enabled"), BetaFlag
46-
47-
4846
data object CHAT_UNSUB_ENABLED: PrefsBool("chat_unsub_enabled"), BetaFlag
4947
data object TIPS_ENABLED : PrefsBool("tips_enabled"), BetaFlag
5048
data object TIPS_CHAT_ENABLED: PrefsBool("tips_chat_enabled"), BetaFlag
5149
data object TIPS_CHAT_CASH_ENABLED: PrefsBool("tips_chat_cash_enabled"), BetaFlag
5250
data object BALANCE_CURRENCY_SELECTION_ENABLED: PrefsBool("balance_currency_enabled"), BetaFlag
5351
data object KADO_WEBVIEW_ENABLED : PrefsBool("kado_inapp_enabled"), BetaFlag
52+
data object SHARE_TWEET_TO_TIP : PrefsBool("share_tweet_to_tip"), BetaFlag
5453
}
5554

5655
val APP_SETTINGS: List<AppSetting> = listOf(PrefsBool.CAMERA_START_BY_DEFAULT, PrefsBool.REQUIRE_BIOMETRICS)

api/src/main/java/com/getcode/network/repository/BetaFlagsRepository.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ data class BetaOptions(
1919
val tipsChatCashEnabled: Boolean,
2020
val balanceCurrencySelectionEnabled: Boolean,
2121
val kadoWebViewEnabled: Boolean,
22+
val shareTweetToTip: Boolean,
2223
) {
2324
companion object {
2425
// Default states for various beta flags in app.
@@ -36,6 +37,7 @@ data class BetaOptions(
3637
tipsChatCashEnabled = false,
3738
balanceCurrencySelectionEnabled = true,
3839
kadoWebViewEnabled = false,
40+
shareTweetToTip = false
3941
)
4042
}
4143
}
@@ -70,7 +72,8 @@ class BetaFlagsRepository @Inject constructor(
7072
observeBetaFlag(PrefsBool.TIPS_CHAT_CASH_ENABLED, default = defaults.tipsChatCashEnabled),
7173
observeBetaFlag(PrefsBool.BALANCE_CURRENCY_SELECTION_ENABLED, defaults.balanceCurrencySelectionEnabled),
7274
observeBetaFlag(PrefsBool.DISPLAY_ERRORS, default = defaults.displayErrors),
73-
observeBetaFlag(PrefsBool.KADO_WEBVIEW_ENABLED, default = defaults.kadoWebViewEnabled)
75+
observeBetaFlag(PrefsBool.KADO_WEBVIEW_ENABLED, default = defaults.kadoWebViewEnabled),
76+
observeBetaFlag(PrefsBool.SHARE_TWEET_TO_TIP, default = defaults.shareTweetToTip)
7477
) {
7578
BetaOptions(
7679
showNetworkDropOff = it[0],
@@ -86,6 +89,7 @@ class BetaFlagsRepository @Inject constructor(
8689
balanceCurrencySelectionEnabled = it[10],
8790
displayErrors = it[11],
8891
kadoWebViewEnabled = it[12],
92+
shareTweetToTip = it[13]
8993
)
9094
}
9195
}
@@ -98,4 +102,8 @@ class BetaFlagsRepository @Inject constructor(
98102
b.takeIf { a } ?: default
99103
}
100104
}
105+
106+
suspend fun isEnabled(flag: PrefsBool): Boolean {
107+
return prefRepository.get(flag, false)
108+
}
101109
}

app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,5 +223,5 @@ dependencies {
223223
implementation(Libs.timber)
224224
implementation(Libs.bugsnag)
225225

226-
implementation("dev.chrisbanes.haze:haze:0.7.3")
226+
implementation(Libs.haze)
227227
}

app/src/main/AndroidManifest.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,18 @@
152152

153153
</activity>
154154

155+
<activity-alias
156+
android:name="com.getcode.view.TweetShareHandler"
157+
android:exported="true"
158+
android:targetActivity="com.getcode.view.MainActivity"
159+
android:enabled="false">
160+
<intent-filter>
161+
<action android:name="android.intent.action.SEND"/>
162+
<category android:name="android.intent.category.DEFAULT"/>
163+
<data android:mimeType="text/plain"/>
164+
</intent-filter>
165+
</activity-alias>
166+
155167
<service
156168
android:name="com.getcode.util.AuthenticatorService"
157169
android:exported="false">

app/src/main/java/com/getcode/util/Context.kt

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
package com.getcode.util
22

33
import android.content.Context
4-
import androidx.biometric.BiometricManager
5-
import androidx.biometric.BiometricPrompt
6-
import androidx.biometric.BiometricPrompt.AuthenticationError
74
import androidx.core.content.ContextCompat
8-
import androidx.fragment.app.FragmentActivity
95
import com.getcode.R
10-
import com.getcode.network.repository.TransactionRepository.ErrorSubmitIntent
11-
import timber.log.Timber
12-
import java.util.concurrent.Executors
136

147
fun Context.launchAppSettings() {
158
val intent = IntentUtils.appSettings()

app/src/main/java/com/getcode/util/DeeplinkHandler.kt

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
11
package com.getcode.util
22

3+
import android.content.Context
34
import android.content.Intent
45
import android.net.Uri
6+
import androidx.core.net.toUri
57
import cafe.adriel.voyager.core.screen.Screen
8+
import com.getcode.model.BetaFlag
9+
import com.getcode.model.PrefsBool
610
import com.getcode.models.DeepLinkRequest
11+
import com.getcode.models.encode
712
import com.getcode.navigation.screens.HomeScreen
813
import com.getcode.navigation.screens.LoginScreen
14+
import com.getcode.network.repository.BetaFlagsRepository
15+
import com.getcode.network.repository.encodeBase64
916
import com.getcode.network.repository.urlDecode
17+
import com.getcode.ui.utils.getActivity
1018
import com.getcode.utils.TraceType
1119
import com.getcode.utils.base64EncodedData
1220
import com.getcode.utils.trace
21+
import dagger.hilt.android.qualifiers.ApplicationContext
1322
import kotlinx.coroutines.flow.MutableStateFlow
23+
import kotlinx.serialization.json.buildJsonObject
24+
import kotlinx.serialization.json.put
1425
import timber.log.Timber
1526
import javax.inject.Inject
1627
import javax.inject.Singleton
@@ -19,6 +30,7 @@ data class DeeplinkResult(
1930
val type: DeeplinkHandler.Type,
2031
val stack: List<Screen>,
2132
)
33+
2234
/**
2335
* This class is used to manage intent state across navigation.
2436
*
@@ -33,7 +45,10 @@ data class DeeplinkResult(
3345
* in favour of the latest request in the navigation graph.
3446
*/
3547
@Singleton
36-
class DeeplinkHandler @Inject constructor() {
48+
class DeeplinkHandler @Inject constructor(
49+
@ApplicationContext private val context: Context,
50+
private val betaFlags: BetaFlagsRepository
51+
) {
3752
var debounceIntent: Intent? = null
3853
set(value) {
3954
intent.value = value
@@ -43,8 +58,18 @@ class DeeplinkHandler @Inject constructor() {
4358

4459
val intent = MutableStateFlow(debounceIntent)
4560

46-
fun handle(intent: Intent? = debounceIntent): DeeplinkResult? {
47-
val uri = intent?.data ?: return null
61+
suspend fun handle(intent: Intent? = debounceIntent): DeeplinkResult? {
62+
println(intent)
63+
val uri = when {
64+
intent?.data != null -> intent.data
65+
intent?.getStringExtra(Intent.EXTRA_TEXT) != null -> {
66+
val sharedLink = intent.getStringExtra(Intent.EXTRA_TEXT)?.toUri() ?: return null
67+
sharedLink.resolveSharedEntity()
68+
}
69+
70+
else -> null
71+
} ?: return null
72+
4873
return when (val type = uri.deeplinkType) {
4974
is Type.Login -> {
5075
DeeplinkResult(
@@ -63,7 +88,7 @@ class DeeplinkHandler @Inject constructor() {
6388

6489
is Type.Sdk -> {
6590
Timber.d("sdk=${type.payload}")
66-
val request = type.payload?.base64EncodedData()?.let { DeepLinkRequest.from(it) }
91+
val request = type.payload?.base64EncodedData()?.let { DeepLinkRequest.from(it) }
6792
DeeplinkResult(
6893
type,
6994
listOf(HomeScreen(request = request)),
@@ -74,19 +99,44 @@ class DeeplinkHandler @Inject constructor() {
7499
Timber.d("tipcard for ${type.username} on ${type.platform}")
75100
DeeplinkResult(
76101
type,
77-
listOf(HomeScreen(request = DeepLinkRequest.fromTipCardUsername(type.platform, type.username))),
102+
listOf(
103+
HomeScreen(
104+
request = DeepLinkRequest.fromTipCardUsername(
105+
type.platform,
106+
type.username
107+
)
108+
)
109+
),
78110
)
79111
}
80112

81113
is Type.Unknown -> null
82114
}
83115
}
84116

117+
/**
118+
* Handles converting inbound shared content with possible deeplinks
119+
* e.g sharing a tweet to trigger a tipcard flow
120+
*/
121+
private suspend fun Uri.resolveSharedEntity(): Uri {
122+
when {
123+
this.host == "x.com" || this.host == "twitter.com" -> {
124+
// https://x.com/<username>/status/<tweetId>
125+
if (betaFlags.isEnabled(PrefsBool.SHARE_TWEET_TO_TIP)) {
126+
// convert shared tweets to owner's tip card
127+
val username = pathSegments.firstOrNull() ?: return this
128+
return Uri.parse(Linkify.tipCard(username, "x"))
129+
}
130+
}
131+
}
132+
return this
133+
}
134+
85135
private val Uri.deeplinkType: Type
86136
get() {
87137
// check for tipcard URLs
88138
val components = pathSegments
89-
if (components.count() == 2 && components[0] == "x" && components[1].isNotEmpty()) {
139+
if (components.count() >= 2 && components[0] == "x" && components[1].isNotEmpty()) {
90140
return Type.Tip(components[0], components[1])
91141
}
92142

@@ -124,12 +174,13 @@ class DeeplinkHandler @Inject constructor() {
124174
sealed interface Type {
125175
data class Login(val link: String?) : Type
126176
data class Cash(val link: String?) : Type
127-
data class Tip(val platform: String, val username: String): Type
177+
data class Tip(val platform: String, val username: String) : Type
128178
data class Sdk(val payload: String?) : Type {
129179
companion object {
130180
val regex = Regex("^(login|payment|tip)?-?request-(modal|page)-(mobile|desktop)\$")
131181
}
132182
}
183+
133184
data class Unknown(val path: String?) : Type
134185
}
135186

@@ -161,4 +212,5 @@ class DeeplinkHandler @Inject constructor() {
161212
}
162213
}
163214

164-
private operator fun Regex.contains(text: String?): Boolean = text?.let { this.matches(it) } ?: false
215+
private operator fun Regex.contains(text: String?): Boolean =
216+
text?.let { this.matches(it) } ?: false

app/src/main/java/com/getcode/util/IntentUtils.kt

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,9 @@ import android.content.Intent
55
import android.net.Uri
66
import android.provider.Settings
77
import com.getcode.BuildConfig
8-
import com.getcode.R
9-
import com.getcode.model.Currency
10-
import com.getcode.model.KinAmount
11-
import com.getcode.model.Username
12-
import com.getcode.network.repository.replaceParam
13-
import com.getcode.network.repository.urlEncode
14-
import com.getcode.solana.organizer.GiftCardAccount
158
import com.getcode.utils.makeE164
169

10+
1711
object IntentUtils {
1812

1913
fun appSettings() = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
@@ -30,7 +24,7 @@ object IntentUtils {
3024
}
3125

3226
fun tweet(message: String) = Intent(Intent.ACTION_VIEW).apply {
33-
val url = "https://www.twitter.com/intent/tweet?text=${message.urlEncode()}"
27+
val url = Linkify.tweet(message)
3428
setData(Uri.parse(url))
3529
flags = Intent.FLAG_ACTIVITY_NEW_TASK
3630
}
@@ -47,8 +41,8 @@ object IntentUtils {
4741
return shareIntent
4842
}
4943

50-
fun tipCard(username: String): Intent {
51-
val url = "https://tipcard.getcode.com/x/$username"
44+
fun tipCard(username: String, platform: String): Intent {
45+
val url = Linkify.tipCard(username, platform)
5246

5347
val sendIntent: Intent = Intent().apply {
5448
action = Intent.ACTION_SEND
@@ -67,7 +61,7 @@ object IntentUtils {
6761
entropy: String,
6862
formattedAmount: String,
6963
): Intent {
70-
val url = "https://cash.getcode.com/c/#/e=$entropy"
64+
val url = Linkify.cashLink(entropy)
7165
val text = "$formattedAmount $url"
7266

7367
val sendIntent: Intent = Intent().apply {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.getcode.util
2+
3+
import com.getcode.network.repository.urlEncode
4+
5+
object Linkify {
6+
fun cashLink(entropy: String): String = "https://cash.getcode.com/c/#/e=${entropy}"
7+
fun tipCard(username: String, platform: String): String = "https://tipcard.getcode.com/${platform}/${username}"
8+
fun tweet(message: String): String = "https://www.twitter.com/intent/tweet?text=${message.urlEncode()}"
9+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.getcode.util
2+
3+
import android.content.ComponentName
4+
import android.content.Context
5+
import android.content.pm.PackageManager
6+
import dagger.hilt.android.qualifiers.ApplicationContext
7+
import javax.inject.Inject
8+
9+
10+
class Pacman @Inject constructor(
11+
@ApplicationContext private val context: Context
12+
) {
13+
private val packageManager = context.packageManager
14+
15+
fun enableTweetShare(enable: Boolean) {
16+
val component = ComponentName(context.packageName, "com.getcode.view.TweetShareHandler")
17+
if (enable) {
18+
packageManager.setComponentEnabledSetting(
19+
component,
20+
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP
21+
)
22+
} else {
23+
packageManager.setComponentEnabledSetting(
24+
component,
25+
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP
26+
)
27+
}
28+
}
29+
30+
}

0 commit comments

Comments
 (0)