Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ fun appEntryProvider(

// Sheets (inner content — wrapped in Main.Sheet by navigateTo())
annotatedEntry<AppRoute.Sheets.Give> { key -> CashScreen(key.mint, key.fromTokenInfo) }
annotatedEntry<AppRoute.Sheets.Send> { }
annotatedEntry<AppRoute.Sheets.TokenSelection> { key -> TokenSelectScreen(key.purpose) }
annotatedEntry<AppRoute.Sheets.Wallet> { BalanceScreen() }
annotatedEntry<AppRoute.Sheets.ShareApp> { ShareAppScreen() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ sealed interface AppRoute : NavKey, Parcelable {
data class TokenSelection(val purpose: TokenPurpose) : Sheets
@Serializable
data class Give(val mint: Mint? = null, val fromTokenInfo: Boolean = false) : Sheets

@Serializable
data object Send: Sheets
@Serializable
data object Wallet : Sheets
@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ enum class NavBarButton {
Give,
Wallet,
Discover,
Send,
;

companion object {
val defaultOrder = listOf(Give, Wallet, Discover)
val defaultOrder = listOf(Discover, Give, Send, Wallet,)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,14 @@ fun NavigationBar(
badgeCount = 0,
onClick = { onButtonClick(NavBarButton.Discover) }
)

NavBarButton.Send -> BottomBarAction(
modifier = buttonModifier,
label = stringResource(R.string.action_send),
painter = painterResource(R.drawable.ic_send_outlined),
badgeCount = 0,
onClick = { onButtonClick(NavBarButton.Send) }
)
}
}
}
Expand Down
1 change: 1 addition & 0 deletions apps/flipcash/core/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@
<string name="subtitle_youReceive">You Receive</string>

<string name="action_give">Give</string>
<string name="action_send">Send</string>
<string name="title_transactionHistory">Transaction History</string>
<string name="action_viewTransactionHistory">Transaction History</string>
<string name="subtitle_marketcap">Market Cap</string>
Expand Down
2 changes: 2 additions & 0 deletions apps/flipcash/features/direct-send/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build/
.gradle/
24 changes: 24 additions & 0 deletions apps/flipcash/features/direct-send/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
plugins {
alias(libs.plugins.flipcash.android.feature)
}

android {
namespace = "${Gradle.flipcashNamespace}.features.directsend"
}

dependencies {
testImplementation(kotlin("test"))
testImplementation(libs.bundles.unit.testing)
testImplementation(libs.mockito.kotlin)
testImplementation(libs.robolectric)
testImplementation(project(":libs:test-utils"))

implementation(libs.kotlin.stdlib)
implementation(project(":apps:flipcash:shared:analytics"))
implementation(project(":apps:flipcash:shared:session"))
implementation(project(":apps:flipcash:shared:tokens"))
implementation(project(":libs:datetime"))
implementation(project(":libs:logging"))
implementation(project(":libs:messaging"))
implementation(project(":libs:permissions:bindings"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@ import com.flipcash.app.login.internal.AccessKeyScreen
import com.flipcash.features.login.R
import com.getcode.navigation.core.LocalCodeNavigator
import com.getcode.ui.components.AppBarWithTitle
import com.getcode.util.permissions.rememberNotificationPermission

@Composable
fun AccessKeyScreen() {
val viewModel = hiltViewModel<LoginAccessKeyViewModel>()
val navigator = LocalCodeNavigator.current
val notificationPermissions = rememberNotificationPermission()
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
Expand All @@ -33,11 +31,21 @@ fun AccessKeyScreen() {
if (requiresIap) {
navigator.push(AppRoute.Onboarding.Purchase())
} else {
if (notificationPermissions.isPermanentlyDenied) {
navigator.push(AppRoute.Onboarding.NotificationPermissionRationale(true))
val target = if (viewModel.isPhoneNumberSendEnabled) {
AppRoute.Onboarding.ContactPermission(postCreate = true)
} else {
navigator.push(AppRoute.Onboarding.NotificationPermission(true))
AppRoute.Onboarding.NotificationPermission(postCreate = true)
}

navigator.push(
AppRoute.Verification(
origin = AppRoute.Onboarding.AccessKey,
includePhone = true,
includeEmail = false,
target = target,
fullScreen = true,
)
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import com.flipcash.app.analytics.Button
import com.flipcash.app.analytics.FlipcashAnalyticsService
import com.flipcash.app.auth.AuthManager
import com.flipcash.app.core.storage.MediaSaver
import com.flipcash.app.featureflags.FeatureFlag
import com.flipcash.app.featureflags.FeatureFlagController
import com.flipcash.app.userflags.UserFlagsCoordinator
import com.flipcash.services.user.UserManager
import com.getcode.libs.qr.QRCodeGenerator
Expand All @@ -25,10 +27,15 @@ class LoginAccessKeyViewModel @Inject constructor(
mediaSaver: MediaSaver,
userManager: UserManager,
private val userFlags: UserFlagsCoordinator,
private val featureFlags: FeatureFlagController,
private val authManager: AuthManager,
private val analytics: FlipcashAnalyticsService,
): BaseAccessKeyViewModel(resources, mnemonicManager, mediaSaver, userManager, qrCodeGenerator) {

val isPhoneNumberSendEnabled: Boolean
get() = featureFlags.observe(FeatureFlag.PhoneNumberSend).value ||
userFlags.resolvedFlags.value.enablePhoneNumberSend.effectiveValue

suspend fun onWroteDownInstead(): Result<Boolean> {
trackButton(Button.WroteAccessKey)
uiFlow.update { it.copy(skipState = LoadingSuccessState(loading = true)) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ sealed class ScannerDecorItem(val screen: AppRoute) {
data object Menu : ScannerDecorItem(AppRoute.Sheets.Menu)
data object Logo: ScannerDecorItem(AppRoute.Sheets.ShareApp)
data object Discover: ScannerDecorItem(AppRoute.Token.Discovery)
data object Send: ScannerDecorItem(AppRoute.Sheets.Send)
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ internal fun DecorView(
state: SessionState,
billState: BillState,
isPaused: Boolean,
modifier: Modifier = Modifier,
isPinching: Boolean = false,
zoomRatio: Float = 1f,
modifier: Modifier = Modifier,
onAction: (ScannerDecorItem) -> Unit,
) {
val billPlayground = LocalBillPlaygroundController.current
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,14 @@ internal fun ScannerNavigationBar(
NavBarConfig.deserialize(navBarConfigString)
}

val effectiveConfig = remember(config, state.isPhoneNumberSendEnabled) {
if (state.isPhoneNumberSendEnabled) config
else config.copy(order = config.order.filter { it != NavBarButton.Send })
}

NavigationBar(
modifier = modifier,
config = config,
config = effectiveConfig,
state = NavigationBarState(
notificationUnreadCount = state.notificationUnreadCount,
showToast = billState.showToast && billState.toast != null,
Expand All @@ -45,6 +50,7 @@ internal fun ScannerNavigationBar(
NavBarButton.Give -> ScannerDecorItem.Give
NavBarButton.Wallet -> ScannerDecorItem.Wallet
NavBarButton.Discover -> ScannerDecorItem.Discover
NavBarButton.Send -> ScannerDecorItem.Send
}
onAction(item)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ internal class UserFlagsViewModel @Inject constructor(
R.string.label_flag_requiresPurchaseForAccount,
flags.data.requiresIapForRegistration.effectiveValue
),
ReadOnlyEntry(
R.string.label_flag_enablePhoneNumberSend,
flags.data.enablePhoneNumberSend.effectiveValue
),
)

else -> emptyList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.flipcash.services.controllers.ResolverController
import com.flipcash.services.models.CheckSyncError
import com.flipcash.services.models.ContactMethod
import com.flipcash.services.models.DeltaUploadError
import com.flipcash.services.models.GetContactsError
import com.getcode.opencode.model.accounts.AccountCluster
import com.getcode.opencode.providers.SessionListener
import com.getcode.solana.keys.Checksum
Expand Down Expand Up @@ -332,7 +333,13 @@ class ContactCoordinator @Inject constructor(
_state.update { it.copy(flipcashE164s = flipcashE164s) }
trace(tag = TAG, message = "Found ${flipcashE164s.size} contacts on Flipcash", type = TraceType.Process)
}?.onFailure { error ->
trace(tag = TAG, message = "GetFlipcashContacts failed: ${error.message}", type = TraceType.Error)
if (error is GetContactsError.NotFound) {
dao.clearFlipcashStatus()
_state.update { it.copy(flipcashE164s = emptySet()) }
trace(tag = TAG, message = "No contacts on Flipcash yet", type = TraceType.Process)
} else {
trace(tag = TAG, message = "GetFlipcashContacts failed: ${error.message}", type = TraceType.Error)
}
}
} catch (e: Exception) {
trace(tag = TAG, message = "GetFlipcashContacts exception: ${e.message}", error = e, type = TraceType.Error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,15 @@ sealed interface FeatureFlag<T: Any> {
override val persistLogOut: Boolean = true
}

@FeatureFlagMarker
data object PhoneNumberSend : FeatureFlag<Boolean> {
override val key: String = "phone_number_send_enabled"
override val default: Boolean = false
override val launched: Boolean = false
override val visible: Boolean = true
override val persistLogOut: Boolean = true
}

@FeatureFlagMarker
data object NavBar : FeatureFlag<NavBarConfig> {
override val key: String = "nav_bar_config"
Expand Down Expand Up @@ -219,6 +228,7 @@ val FeatureFlag<*>.title: String
FeatureFlag.DepositUsdc -> "Deposit USDC"
FeatureFlag.BackgroundReset -> "Background Reset"
FeatureFlag.ContactPickerMode -> "Contact Picker Mode"
FeatureFlag.PhoneNumberSend -> "Phone Number Send"
FeatureFlag.NavBar -> "Navigation Bar"
}

Expand All @@ -241,6 +251,7 @@ val FeatureFlag<*>.message: String
FeatureFlag.DepositUsdc -> "When enabled, you'll gain the ability to deposit USDC directly from any external wallet app instead of purchasing a currency first and sell"
FeatureFlag.BackgroundReset -> "Automatically returns the app to the camera screen after a period of inactivity with the app in the background"
FeatureFlag.ContactPickerMode -> "When enabled, contacts will be accessed via the system contact picker instead of requesting full READ_CONTACTS permission"
FeatureFlag.PhoneNumberSend -> "When enabled, you'll gain the ability to send cash directly to contacts via phone number"
FeatureFlag.NavBar -> "Customize the order and labels of navigation bar buttons"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ data class SessionState(
val isRemoteSendLoading: Boolean = false,
val notificationUnreadCount: Int = 0,
val tokens: List<Token> = emptyList(),
val isPhoneNumberSendEnabled: Boolean = false,
)

val LocalSessionController = staticCompositionLocalOf<SessionController?> { null }
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand Down Expand Up @@ -196,6 +197,13 @@ class RealSessionController @Inject constructor(
_state.update { it.copy(tokens = tokens) }
}.launchIn(scope)

combine(
featureFlagController.observe(FeatureFlag.PhoneNumberSend),
userManager.state.map { it.flags?.enablePhoneNumberSend == true }
) { beta, server -> beta || server }
.onEach { enabled -> _state.update { it.copy(isPhoneNumberSendEnabled = enabled) } }
.launchIn(scope)

// Retry updateUserFlags when network is restored
networkObserver.state
.map { it.connected }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,11 @@ class SessionControllerGiftCardErrorTest {
toastController = mockk(relaxed = true),
billingClient = mockk(relaxed = true),
tokenCoordinator = tokenCoordinator,
contactCoordinator = mockk(relaxed = true),
featureFlagController = mockk(relaxed = true),
analytics = analytics,
appSettingsCoordinator = mockk(relaxed = true),
usdcSweep = mockk(relaxed = true),
appSettingsCoordinator = mockk(relaxed = true),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ data class ResolvedUserFlags(
val newCurrencyFeeAmount: ResolvedFlag<Fiat>,
val withdrawalFeeAmount: ResolvedFlag<Fiat>,
val usdcOnRampLiquidityPool: ResolvedFlag<UsdcLiquidtyPool>,
val enablePhoneNumberSend: ResolvedFlag<Boolean>,
)

internal fun UserFlags.resolve(overrides: Overrides): ResolvedUserFlags = ResolvedUserFlags(
Expand All @@ -44,5 +45,6 @@ internal fun UserFlags.resolve(overrides: Overrides): ResolvedUserFlags = Resolv
newCurrencyPurchaseAmount = ResolvedFlag(newCurrencyPurchaseAmount, overrides.newCurrencyPurchaseAmount),
newCurrencyFeeAmount = ResolvedFlag(newCurrencyFeeAmount, overrides.newCurrencyPurchaseAmount),
withdrawalFeeAmount = ResolvedFlag(withdrawalFeeAmount, overrides.withdrawalFeeAmount),
usdcOnRampLiquidityPool = ResolvedFlag(preferredUsdcOnRampLiquidityPool, overrides.preferredUsdcOnRampLiquidityPool)
usdcOnRampLiquidityPool = ResolvedFlag(preferredUsdcOnRampLiquidityPool, overrides.preferredUsdcOnRampLiquidityPool),
enablePhoneNumberSend = ResolvedFlag(enablePhoneNumberSend, FieldOverride.None),
)
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
<string name="label_flag_withdrawalFeeAmount">Withdrawal Fee Amount</string>
<string name="hint_flag_withdrawalFeeAmount">Enter amount</string>
<string name="label_flag_preferredUsdcLiquidityPool">Preferred USDC On-Ramp Liquidity Pool</string>
<string name="label_flag_enablePhoneNumberSend">Phone Number Send Enabled</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,7 @@ message UserFlags {

// The preferred USDC liquidity pool for external wallet on ramp flows
UsdcLiquidityPool preferred_on_ramp_usdc_liquidity_pool = 11;

// Whether the send by phone number feature is enabled
bool enable_phone_number_send = 12;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import "validate/validate.proto";

option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/contact/v1;contactpb";
option java_package = "com.codeinc.flipcash.gen.contact.v1";
option objc_class_prefix = "FPBContactV1";

// ContactList manages a user's contact list and surfaces which contacts are
// Flipcash users.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import "validate/validate.proto";

option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/contact/v1;contactpb";
option java_package = "com.codeinc.flipcash.gen.contact.v1";
option objc_class_prefix = "FPBContactV1";

message FlipcashContact {
phone.v1.PhoneNumber phone = 1 [(validate.rules).message.required = true];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ internal class UserFlagsMapper @Inject constructor():
newCurrencyPurchaseAmount = Fiat(quarks = from.newCurrencyPurchaseAmount),
newCurrencyFeeAmount = Fiat(quarks = from.newCurrencyFeeAmount),
withdrawalFeeAmount = Fiat(quarks = from.withdrawalFeeAmount),
preferredUsdcOnRampLiquidityPool = from.preferredOnRampUsdcLiquidityPool.toDomain()
preferredUsdcOnRampLiquidityPool = from.preferredOnRampUsdcLiquidityPool.toDomain(),
enablePhoneNumberSend = from.enablePhoneNumberSend,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ data class UserFlags(
val newCurrencyFeeAmount: Fiat,
val withdrawalFeeAmount: Fiat,
val preferredUsdcOnRampLiquidityPool: UsdcLiquidtyPool,
val enablePhoneNumberSend: Boolean,
) {
companion object {
val Default = UserFlags(
Expand All @@ -31,6 +32,7 @@ data class UserFlags(
newCurrencyFeeAmount = Fiat.MAX_VALUE,
withdrawalFeeAmount = Fiat.Zero,
preferredUsdcOnRampLiquidityPool = UsdcLiquidtyPool.Unknown,
enablePhoneNumberSend = false,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,17 @@ class UserFlagsMapperTest {
assertEquals(5_000_000L, result.newCurrencyPurchaseAmount.quarks)
}

@Test
fun `maps enable phone number send`() {
val proto = userFlags {
enablePhoneNumberSend = true
}

val result = mapper.map(proto)

assertTrue(result.enablePhoneNumberSend)
}

@Test
fun `maps minimum version`() {
val proto = userFlags {
Expand Down
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ include(
":apps:flipcash:features:deposit",
":apps:flipcash:features:advanced",
":apps:flipcash:features:currency-creator",
":apps:flipcash:features:direct-send",
":apps:flipcash:features:device-logs",
":apps:flipcash:features:myaccount",
":apps:flipcash:features:backupkey",
Expand Down
Loading