diff --git a/apps/flipcash/app/build.gradle.kts b/apps/flipcash/app/build.gradle.kts index 3afcf1b8e..ff4ff6f12 100644 --- a/apps/flipcash/app/build.gradle.kts +++ b/apps/flipcash/app/build.gradle.kts @@ -161,6 +161,7 @@ dependencies { implementation(project(":apps:flipcash:shared:permissions")) implementation(project(":apps:flipcash:shared:phone")) implementation(project(":apps:flipcash:shared:shareable")) + implementation(project(":apps:flipcash:shared:invite")) implementation(project(":apps:flipcash:shared:tokens")) implementation(project(":apps:flipcash:shared:web")) implementation(project(":apps:flipcash:shared:workers")) @@ -186,6 +187,7 @@ dependencies { implementation(project(":apps:flipcash:features:bill-customization")) implementation(project(":apps:flipcash:features:currency-creator")) implementation(project(":apps:flipcash:features:direct-send")) + implementation(project(":apps:flipcash:features:invite")) implementation(project(":apps:flipcash:features:discovery")) implementation(project(":apps:flipcash:features:userflags")) diff --git a/apps/flipcash/app/src/main/AndroidManifest.xml b/apps/flipcash/app/src/main/AndroidManifest.xml index 525dd6a7c..0c8b07619 100644 --- a/apps/flipcash/app/src/main/AndroidManifest.xml +++ b/apps/flipcash/app/src/main/AndroidManifest.xml @@ -26,6 +26,23 @@ + + + + + + + + + + + + + + + + + { key -> AppRestrictedScreen(key.restrictionType) } annotatedEntry { ScannerScreen() } annotatedEntry { RegionSelectionScreen() } + annotatedEntry { key -> InviteContactScreen(key.phoneNumber) } // Sheets (inner content — wrapped in Main.Sheet by navigateTo()) annotatedEntry { key -> CashScreen(key.mint, key.fromTokenInfo) } - annotatedEntry { } + annotatedEntry { } annotatedEntry { key -> TokenSelectScreen(key.purpose) } annotatedEntry { BalanceScreen() } annotatedEntry { ShareAppScreen() } annotatedEntry { MenuScreen() } - // Tokens annotatedEntry { key -> TokenInfoScreen(key.mint, key.shortfall, key.fromDeeplink) @@ -97,6 +98,7 @@ fun appEntryProvider( annotatedEntry { key -> SwapFlowScreen(route = key, resultStateRegistry = resultStateRegistry) } + // TODO: fold this into above entry annotatedEntry { key -> TokenTxProcessingScreen(key.swapId, key.swapPurpose, key.amount, key.isFundingShortfall) } @@ -120,6 +122,7 @@ fun appEntryProvider( annotatedEntry { DeviceLogsScreen() } annotatedEntry { UserFlagsScreen() } + // Transfers annotatedEntry { key -> DepositFlowScreen(route = key, resultStateRegistry = resultStateRegistry) diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt index 98da0cc1f..b8fd13daf 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt @@ -101,6 +101,9 @@ sealed interface AppRoute : NavKey, Parcelable { @Serializable data object RegionSelection : Main + @Serializable + data class InviteContact(val phoneNumber: String) : com.getcode.navigation.Sheet, com.getcode.navigation.WrapContentSheet + @Serializable @Parcelize data class Sheet( diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/util/Linkify.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/util/Linkify.kt index ca0552371..0ba107fbf 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/util/Linkify.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/util/Linkify.kt @@ -8,6 +8,8 @@ import com.getcode.utils.urlEncode object Linkify { fun cashLink(entropy: String): String = "https://send.flipcash.com/c/#/e=${entropy}" fun download(shareRef: String): String = "https://flipcash.com/download?r=${shareRef}" + fun whatsApp(phoneNumber: String, message: String): String = + "https://wa.me/${phoneNumber.removePrefix("+")}?text=${message.urlEncode()}" fun tweet(message: String): String = "https://www.twitter.com/intent/tweet?text=${message.urlEncode()}" fun tokenInfo(token: Token): String = tokenInfo(token.address) fun tokenInfo(mint: Mint): String = "https://app.flipcash.com/token/${mint.base58()}" diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/util/MessagingPackages.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/util/MessagingPackages.kt new file mode 100644 index 000000000..4b33a8945 --- /dev/null +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/util/MessagingPackages.kt @@ -0,0 +1,14 @@ +package com.flipcash.app.core.util + +object MessagingPackages { + val whatsApp = listOf( + "com.whatsapp", + "com.whatsapp.w4b", + ) + + val all = setOf( + "org.thoughtcrime.securesms", // Signal + "org.telegram.messenger", // Telegram + "com.facebook.orca", // Messenger + ) + whatsApp +} diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml index b6ac480e9..fed1e7925 100644 --- a/apps/flipcash/core/src/main/res/values/strings.xml +++ b/apps/flipcash/core/src/main/res/values/strings.xml @@ -152,6 +152,8 @@ Get the Flipcash App Send %1$s + Invite to Flipcash + Download Flipcash so I can send you money\n\n%1$s Enter Solana address Where would you like to withdraw your %1$s to? @@ -693,4 +695,7 @@ Learn more Leaderboard Ranking People must have a minimum balance of %1$s to be counted + + Invite + More \ No newline at end of file diff --git a/apps/flipcash/features/invite/.gitignore b/apps/flipcash/features/invite/.gitignore new file mode 100644 index 000000000..9f2a07880 --- /dev/null +++ b/apps/flipcash/features/invite/.gitignore @@ -0,0 +1,2 @@ +build/ +.gradle/ diff --git a/apps/flipcash/features/invite/build.gradle.kts b/apps/flipcash/features/invite/build.gradle.kts new file mode 100644 index 000000000..806212c4c --- /dev/null +++ b/apps/flipcash/features/invite/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + alias(libs.plugins.flipcash.android.feature) +} + +android { + namespace = "${Gradle.flipcashNamespace}.features.invite" +} + +dependencies { + implementation(project(":apps:flipcash:shared:invite")) +} diff --git a/apps/flipcash/features/invite/src/main/kotlin/com/flipcash/app/invite/InviteContactScreen.kt b/apps/flipcash/features/invite/src/main/kotlin/com/flipcash/app/invite/InviteContactScreen.kt new file mode 100644 index 000000000..baf0c06d1 --- /dev/null +++ b/apps/flipcash/features/invite/src/main/kotlin/com/flipcash/app/invite/InviteContactScreen.kt @@ -0,0 +1,102 @@ +package com.flipcash.app.invite + +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.material.icons.Icons +import androidx.compose.material.icons.filled.MoreHoriz +import androidx.compose.material3.Icon +import androidx.compose.material3.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.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil3.compose.AsyncImage +import com.flipcash.core.R +import com.getcode.navigation.scenes.LocalBottomSheetDismissDispatcher +import com.getcode.theme.CodeTheme +import com.getcode.ui.core.rememberedClickable + +@Composable +fun InviteContactScreen(phoneNumber: String) { + val controller = LocalInviteController.current + val dismiss = LocalBottomSheetDismissDispatcher.current + val channels by controller.channels.collectAsStateWithLifecycle() + + Column( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = CodeTheme.dimens.inset, + vertical = CodeTheme.dimens.grid.x4, + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(R.string.title_inviteContact), + style = CodeTheme.typography.textLarge, + color = CodeTheme.colors.textMain, + modifier = Modifier.padding(bottom = CodeTheme.dimens.grid.x4), + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + channels.forEach { channel -> + ChannelItem( + channel = channel, + onClick = { + controller.launch(channel, phoneNumber) + dismiss() + }, + ) + } + } + } +} + +@Composable +private fun ChannelItem( + channel: InviteChannel, + onClick: () -> Unit, +) { + Column( + modifier = Modifier + .rememberedClickable(onClick = onClick) + .padding(CodeTheme.dimens.grid.x2), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x1), + ) { + if (channel.icon != null) { + AsyncImage( + model = channel.icon, + contentDescription = channel.label, + modifier = Modifier.size(CodeTheme.dimens.staticGrid.x10), + ) + } else { + Icon( + imageVector = Icons.Filled.MoreHoriz, + contentDescription = channel.label, + modifier = Modifier.size(CodeTheme.dimens.staticGrid.x10), + tint = CodeTheme.colors.textMain, + ) + } + Text( + text = channel.label, + style = CodeTheme.typography.textSmall, + color = CodeTheme.colors.textMain, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} diff --git a/apps/flipcash/shared/invite/.gitignore b/apps/flipcash/shared/invite/.gitignore new file mode 100644 index 000000000..9f2a07880 --- /dev/null +++ b/apps/flipcash/shared/invite/.gitignore @@ -0,0 +1,2 @@ +build/ +.gradle/ diff --git a/apps/flipcash/shared/invite/build.gradle.kts b/apps/flipcash/shared/invite/build.gradle.kts new file mode 100644 index 000000000..e5dd16d47 --- /dev/null +++ b/apps/flipcash/shared/invite/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + alias(libs.plugins.flipcash.android.feature) +} + +android { + namespace = "${Gradle.flipcashNamespace}.shared.invite" +} + +dependencies { + implementation(project(":apps:flipcash:shared:shareable")) +} diff --git a/apps/flipcash/shared/invite/src/main/kotlin/com/flipcash/app/invite/InviteController.kt b/apps/flipcash/shared/invite/src/main/kotlin/com/flipcash/app/invite/InviteController.kt new file mode 100644 index 000000000..24e515e8a --- /dev/null +++ b/apps/flipcash/shared/invite/src/main/kotlin/com/flipcash/app/invite/InviteController.kt @@ -0,0 +1,34 @@ +package com.flipcash.app.invite + +import android.graphics.drawable.Drawable +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.staticCompositionLocalOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +data class InviteChannel( + val label: String, + val icon: Drawable?, + val type: InviteChannelType, +) + +sealed interface InviteChannelType { + data object Sms : InviteChannelType + data class WhatsApp(val packageName: String) : InviteChannelType + data object More : InviteChannelType +} + +interface InviteController { + val channels: StateFlow> + fun refresh() + fun launch(channel: InviteChannel, phoneNumber: String) +} + +private object NoOpInviteController : InviteController { + override val channels: StateFlow> = MutableStateFlow(emptyList()) + override fun refresh() = Unit + override fun launch(channel: InviteChannel, phoneNumber: String) = Unit +} + +val LocalInviteController: ProvidableCompositionLocal = + staticCompositionLocalOf { NoOpInviteController } diff --git a/apps/flipcash/shared/invite/src/main/kotlin/com/flipcash/app/invite/inject/InviteModule.kt b/apps/flipcash/shared/invite/src/main/kotlin/com/flipcash/app/invite/inject/InviteModule.kt new file mode 100644 index 000000000..4ddb603da --- /dev/null +++ b/apps/flipcash/shared/invite/src/main/kotlin/com/flipcash/app/invite/inject/InviteModule.kt @@ -0,0 +1,30 @@ +package com.flipcash.app.invite.inject + +import android.content.Context +import com.flipcash.app.invite.InviteController +import com.flipcash.app.invite.internal.InternalInviteController +import com.flipcash.app.shareable.ShareSheetController +import com.getcode.util.resources.ResourceHelper +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 InviteModule { + + @Provides + @Singleton + fun provideInviteController( + @ApplicationContext context: Context, + resources: ResourceHelper, + shareController: ShareSheetController, + ): InviteController = InternalInviteController( + context = context, + resources = resources, + shareController = shareController, + ) +} diff --git a/apps/flipcash/shared/invite/src/main/kotlin/com/flipcash/app/invite/internal/InternalInviteController.kt b/apps/flipcash/shared/invite/src/main/kotlin/com/flipcash/app/invite/internal/InternalInviteController.kt new file mode 100644 index 000000000..620f4b9a4 --- /dev/null +++ b/apps/flipcash/shared/invite/src/main/kotlin/com/flipcash/app/invite/internal/InternalInviteController.kt @@ -0,0 +1,124 @@ +package com.flipcash.app.invite.internal + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import androidx.core.net.toUri +import com.flipcash.app.core.util.Linkify +import com.flipcash.app.core.util.MessagingPackages +import com.flipcash.app.invite.InviteChannel +import com.flipcash.app.invite.InviteChannelType +import com.flipcash.app.invite.InviteController +import com.flipcash.app.shareable.ShareSheetController +import com.flipcash.app.shareable.Shareable +import com.flipcash.shared.invite.R +import com.getcode.util.resources.ResourceHelper +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.runBlocking + +internal class InternalInviteController( + @param:ApplicationContext + private val context: Context, + private val resources: ResourceHelper, + private val shareController: ShareSheetController, +) : InviteController { + + private val _channels = MutableStateFlow(resolveChannels()) + override val channels: StateFlow> = _channels + + override fun refresh() { + _channels.value = resolveChannels() + } + + private fun resolveChannels(): List = buildList { + val pm = context.packageManager + + // SMS — always present + val smsIntent = Intent(Intent.ACTION_SENDTO, "smsto:".toUri()) + val smsApps = pm.queryIntentActivities(smsIntent, 0) + val smsInfo = smsApps.firstOrNull() + add( + InviteChannel( + label = smsInfo?.loadLabel(pm)?.toString() ?: "SMS", + icon = smsInfo?.loadIcon(pm), + type = InviteChannelType.Sms, + ) + ) + + // WhatsApp + for (pkg in MessagingPackages.whatsApp) { + try { + val appInfo = pm.getApplicationInfo(pkg, 0) + add( + InviteChannel( + label = pm.getApplicationLabel(appInfo).toString(), + icon = pm.getApplicationIcon(appInfo), + type = InviteChannelType.WhatsApp(pkg), + ) + ) + break // only show one WhatsApp variant + } catch (_: PackageManager.NameNotFoundException) { + // not installed + } + } + + // More (fallback) + add( + InviteChannel( + label = resources.getString(R.string.action_inviteMoreOptions), + icon = null, + type = InviteChannelType.More, + ) + ) + } + + private fun inviteMessage(): String { + val shareRef = resources.getString(R.string.app_download_link_share_ref) + val url = Linkify.download(shareRef) + return resources.getString(R.string.message_invite_contact, url) + } + + override fun launch(channel: InviteChannel, phoneNumber: String) { + when (channel.type) { + is InviteChannelType.Sms -> launchSms(phoneNumber) + is InviteChannelType.WhatsApp -> launchWhatsApp(channel.type.packageName, phoneNumber) + is InviteChannelType.More -> launchMore() + } + } + + private fun launchSms(phoneNumber: String) { + val intent = Intent( + Intent.ACTION_SENDTO, + "smsto:$phoneNumber".toUri() + ).apply { + putExtra("sms_body", inviteMessage()) + } + try { + context.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + } catch (_: ActivityNotFoundException) { + launchMore() + } + } + + private fun launchWhatsApp(packageName: String, phoneNumber: String) { + val intent = Intent( + Intent.ACTION_VIEW, + Linkify.whatsApp(phoneNumber, inviteMessage()).toUri() + ).apply { + setPackage(packageName) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + try { + context.startActivity(intent) + } catch (_: ActivityNotFoundException) { + launchMore() + } + } + + private fun launchMore() { + runBlocking { shareController.present(Shareable.Invite) } + } +} diff --git a/apps/flipcash/shared/shareable/src/main/kotlin/com/flipcash/app/shareable/ShareSheetController.kt b/apps/flipcash/shared/shareable/src/main/kotlin/com/flipcash/app/shareable/ShareSheetController.kt index 81532bae5..a373191e8 100644 --- a/apps/flipcash/shared/shareable/src/main/kotlin/com/flipcash/app/shareable/ShareSheetController.kt +++ b/apps/flipcash/shared/shareable/src/main/kotlin/com/flipcash/app/shareable/ShareSheetController.kt @@ -36,6 +36,10 @@ sealed interface Shareable { ): Shareable { override val pendingData: ShareablePendingData? = null } + + data object Invite : Shareable { + override val pendingData: ShareablePendingData? = null + } } sealed interface ShareablePendingData { diff --git a/apps/flipcash/shared/shareable/src/main/kotlin/com/flipcash/app/shareable/internal/InternalShareConfirmationController.kt b/apps/flipcash/shared/shareable/src/main/kotlin/com/flipcash/app/shareable/internal/InternalShareConfirmationController.kt index 9f50c14ea..01dbeefee 100644 --- a/apps/flipcash/shared/shareable/src/main/kotlin/com/flipcash/app/shareable/internal/InternalShareConfirmationController.kt +++ b/apps/flipcash/shared/shareable/src/main/kotlin/com/flipcash/app/shareable/internal/InternalShareConfirmationController.kt @@ -2,7 +2,6 @@ package com.flipcash.app.shareable.internal import com.flipcash.app.core.internal.bill.BillController import com.flipcash.app.shareable.ShareConfirmationResult -import com.flipcash.app.shareable.ShareConfirmationResult.* import com.flipcash.app.shareable.ShareResult import com.flipcash.app.shareable.Shareable import com.flipcash.app.shareable.ShareableConfirmationController @@ -26,8 +25,9 @@ internal class InternalShareConfirmationController( ): ShareConfirmationResult { return when (shareable) { is Shareable.CashLink -> confirmCashLink(shareResult, shareable.autoConfirmationAfter) - is Shareable.DownloadLink -> Confirmed(shareResult) - is Shareable.TokenInfo -> Confirmed(shareResult) + is Shareable.DownloadLink -> ShareConfirmationResult.Confirmed(shareResult) + is Shareable.TokenInfo -> ShareConfirmationResult.Confirmed(shareResult) + is Shareable.Invite -> ShareConfirmationResult.Confirmed(shareResult) } } diff --git a/apps/flipcash/shared/shareable/src/main/kotlin/com/flipcash/app/shareable/internal/InternalShareSheetController.kt b/apps/flipcash/shared/shareable/src/main/kotlin/com/flipcash/app/shareable/internal/InternalShareSheetController.kt index 000a897e6..fbd6048cf 100644 --- a/apps/flipcash/shared/shareable/src/main/kotlin/com/flipcash/app/shareable/internal/InternalShareSheetController.kt +++ b/apps/flipcash/shared/shareable/src/main/kotlin/com/flipcash/app/shareable/internal/InternalShareSheetController.kt @@ -3,12 +3,16 @@ package com.flipcash.app.shareable.internal import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.ClipboardManager +import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.os.Build +import androidx.core.net.toUri import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.flipcash.app.core.money.formatted import com.flipcash.app.core.util.Linkify +import com.flipcash.app.core.util.MessagingPackages import com.flipcash.app.shareable.ShareResult import com.flipcash.app.shareable.ShareSheetController import com.flipcash.app.shareable.ShareSheetController.Companion.ACTION_CASH_LINK_SHARED @@ -95,6 +99,7 @@ internal class InternalShareSheetController( Shareable.DownloadLink -> Unit is Shareable.TokenInfo -> Unit + is Shareable.Invite -> Unit } } } @@ -135,6 +140,10 @@ internal class InternalShareSheetController( is Shareable.TokenInfo -> { shareToken(shareable.token) } + + is Shareable.Invite -> { + shareInviteLink() + } } } @@ -207,6 +216,50 @@ internal class InternalShareSheetController( context.startActivity(share) } + private fun shareInviteLink() { + val shareRef = resources.getString(R.string.app_download_link_share_ref) + val url = Linkify.download(shareRef) + val message = resources.getString(R.string.message_invite_contact, url) + + val sendIntent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, message) + type = "text/plain" + } + + val chooser = Intent.createChooser(sendIntent, null).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val pm = context.packageManager + val messagingPackages = buildSet { + val messagingIntent = Intent(Intent.ACTION_MAIN).apply { + addCategory(Intent.CATEGORY_APP_MESSAGING) + } + pm.queryIntentActivities(messagingIntent, 0) + .forEach { add(it.activityInfo.packageName) } + + val smsIntent = Intent(Intent.ACTION_SENDTO, "smsto:".toUri()) + pm.queryIntentActivities(smsIntent, 0) + .forEach { add(it.activityInfo.packageName) } + + addAll(MessagingPackages.all) + } + + val excludedComponents = pm.queryIntentActivities(sendIntent, 0) + .filter { it.activityInfo.packageName !in messagingPackages } + .map { ComponentName(it.activityInfo.packageName, it.activityInfo.name) } + .toTypedArray() + + if (excludedComponents.isNotEmpty()) { + chooser.putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, excludedComponents) + } + } + + context.startActivity(chooser) + } + private fun shareToken(token: Token) { val url = Linkify.tokenInfo(token) val intent = Intent().apply { diff --git a/settings.gradle.kts b/settings.gradle.kts index 6212ce594..7f131424c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -71,6 +71,7 @@ include( ":apps:flipcash:shared:router", ":apps:flipcash:shared:session", ":apps:flipcash:shared:shareable", + ":apps:flipcash:shared:invite", ":apps:flipcash:shared:tokens", ":apps:flipcash:shared:tokens:core", ":apps:flipcash:shared:theme", @@ -91,6 +92,7 @@ include( ":apps:flipcash:features:advanced", ":apps:flipcash:features:currency-creator", ":apps:flipcash:features:direct-send", + ":apps:flipcash:features:invite", ":apps:flipcash:features:device-logs", ":apps:flipcash:features:myaccount", ":apps:flipcash:features:backupkey",