Commit 927330f8 authored by clairehurst's avatar clairehurst 🌱
Browse files

Bug_44747: Tor x FtC crowdfunding campaign

We can drop this after the Fund the Commons campaign concludes on 2026-06-19
parent 259110bb
Loading
Loading
Loading
Loading
Loading
+12 −1
Original line number Diff line number Diff line
@@ -161,6 +161,7 @@ import org.mozilla.fenix.wallpapers.Wallpaper
import org.mozilla.fenix.GleanMetrics.TabStrip as TabStripMetrics

import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.tor.TorCampaignViewModel
import org.mozilla.fenix.tor.TorHomePage
import org.mozilla.fenix.tor.UrlQuickLoadViewModel

@@ -179,6 +180,7 @@ class HomeFragment : Fragment(), UserInteractionHandler {

    private val homeViewModel: HomeScreenViewModel by activityViewModels()
    private val urlQuickLoadViewModel: UrlQuickLoadViewModel by activityViewModels()
    private val torCampaignViewModel: TorCampaignViewModel by activityViewModels()

    private var _bottomToolbarContainerView: BottomToolbarContainerView? = null
    private val bottomToolbarContainerView: BottomToolbarContainerView
@@ -974,7 +976,16 @@ class HomeFragment : Fragment(), UserInteractionHandler {
            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                TorHomePage(
                    toolBarAtTop = settings().toolbarPosition == ToolbarPosition.TOP
                    shouldInitiallyShowPromo = torCampaignViewModel.shouldInitiallyShowPromo,
                    toolBarAtTop = settings().toolbarPosition == ToolbarPosition.TOP,
                    toolPair = torCampaignViewModel.getToolPair(),
                    onClicked = {
                        (requireActivity() as HomeActivity).openToBrowserAndLoad(
                            searchTermOrURL = "https://internetfreedom.torproject.org",
                            newTab = true,
                            from = BrowserDirection.FromHome,
                        )
                    }
                )
            }
        }
+1 −1
Original line number Diff line number Diff line
@@ -34,7 +34,7 @@ internal fun SearchBar(

@Composable
@PreviewLightDark
private fun SearchBarPreview() {
fun SearchBarPreview() {
    FirefoxTheme {
        Column(
            modifier = Modifier.background(color = FirefoxTheme.colors.layer1),
+310 −0
Original line number Diff line number Diff line
package org.mozilla.fenix.tor

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.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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
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.fromHtml
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.dp
import androidx.compose.ui.unit.sp
import mozilla.components.ui.colors.PhotonColors
import org.mozilla.fenix.R


private val alternateLayoutThreshHold = 500.dp

@Composable
@CampaignComposePreview
fun CampaignBox(
    shouldShowPromo: MutableState<Boolean> = mutableStateOf(true),
    onDonateButtonClicked: () -> Unit = {},
    toolPair: Pair<TorCampaignViewModel.Tool, TorCampaignViewModel.Tool> = Pair(TorCampaignViewModel.toolList[0], TorCampaignViewModel.toolList[1]),
    ) {
    BoxWithConstraints(
        contentAlignment = Alignment.Center,
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight(),
    ) {
        val alternateLayout = this.maxWidth >= alternateLayoutThreshHold

        CampaignLayout(
            alternateLayout,
            maxWidth = this.maxWidth,
            shouldShowPromo,
            onDonateButtonClicked = onDonateButtonClicked,
            toolPair = toolPair,
        )
    }
}

@Composable
private fun CampaignLayout(
    alternateLayout: Boolean,
    maxWidth: Dp,
    shouldShowPromo: MutableState<Boolean>,
    onDonateButtonClicked: () -> Unit,
    toolPair: Pair<TorCampaignViewModel.Tool, TorCampaignViewModel.Tool>,
) {
    Column(
        modifier = Modifier
            .padding(horizontal = 22.dp)
            .fillMaxWidth(getVariableWidth(maxWidth))
            .wrapContentHeight(),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        PurpleBox(
            alternateLayout,
            shouldShowPromo,
            onDonateButtonClicked = onDonateButtonClicked,
            toolPair = toolPair,
        )
        Spacer(Modifier.size(8.dp))
        Text(
            text = stringResource(R.string.no_donation_required_yec),
            style = TextStyle(
                fontSize = 12.5.sp,
                lineHeight = 18.75.sp,
                fontFamily = FontFamily.SansSerif,
                fontWeight = FontWeight(400),
                color = PhotonColors.LightGrey05,
                textAlign = TextAlign.Center,
            ),
        )
    }
}

private fun getVariableWidth(width: Dp): Float =
    (alternateLayoutThreshHold / width).coerceIn(0.80f, 1.0f)

@Composable
private fun PurpleBox(
    alternateLayout: Boolean,
    shouldShowPromo: MutableState<Boolean>,
    onDonateButtonClicked: () -> Unit,
    toolPair: Pair<TorCampaignViewModel.Tool, TorCampaignViewModel.Tool>,
) {
    Box(
        modifier = Modifier.background(
            colorResource(mozilla.components.ui.colors.R.color.photonViolet90),
            shape = RoundedCornerShape(8.dp),
        ),
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .wrapContentHeight(),
            horizontalArrangement = Arrangement.End,
        ) {
            ExitIcon(shouldShowPromo)
        }
        DynamicCampaignContent(
            alternateLayout, onDonateButtonClicked = onDonateButtonClicked,
            toolPair = toolPair
        )
    }
}

@Composable
private fun ExitIcon(shouldShowYec: MutableState<Boolean>) {
    IconButton(
        modifier = Modifier.padding(8.dp),
        onClick = {
            shouldShowYec.value = false
        },
    ) {
        Icon(
            painter = painterResource(id = R.drawable.ic_close),
            tint = PhotonColors.White,
            contentDescription = stringResource(R.string.close_yec_button_description),
        )
    }
}


@Composable
private fun DynamicCampaignContent(
    alternateLayout: Boolean,
    onDonateButtonClicked: () -> Unit,
    toolPair: Pair<TorCampaignViewModel.Tool, TorCampaignViewModel.Tool>,
) {
    @Composable
    fun Icon(shouldShow: Boolean) {
        if (shouldShow) {
            Image(
                painterResource(R.drawable.globe_chain_burst_yec),
                contentDescription = null,
                alignment = Alignment.Center,
            )
        }
    }
    Row(
        modifier = Modifier
            .padding(start = 16.dp, top = 32.dp, end = 16.dp, bottom = 24.dp)
            .fillMaxWidth(),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Column(
            modifier = Modifier.fillMaxWidth(if (alternateLayout) 0.75f else 1.0f),
            horizontalAlignment = if (alternateLayout) Alignment.Start else Alignment.CenterHorizontally,
        ) {
            Icon(shouldShow = !alternateLayout)
            Spacer(Modifier.size(24.dp))
            TitleText()
            Spacer(Modifier.size(16.dp))
            MainText( toolPair = toolPair )
            Spacer(Modifier.size(24.dp))
            Row(
                horizontalArrangement = if (alternateLayout) Arrangement.Start else Arrangement.Center,
                modifier = Modifier.fillMaxWidth(),
            ) {
                DonateButton(onDonateButtonClicked = onDonateButtonClicked, alternateLayout)
            }
        }
        Icon(shouldShow = alternateLayout)
    }
}


@Composable
private fun TitleText() {
    Text(
        text = stringResource(R.string.summer_2026_funding_heading),
        style = TextStyle(
            fontSize = 24.sp,
            lineHeight = 32.sp,
            fontWeight = FontWeight(400),
            color = PhotonColors.White,
            textAlign = TextAlign.Center,
            letterSpacing = 0.18.sp,
        ),
    )
}


@Composable
private fun MainText(toolPair: Pair<TorCampaignViewModel.Tool, TorCampaignViewModel.Tool>) {
    Column {
        Text(
            AnnotatedString.fromHtml(
                // Relevant documentation on HTML markup for android https://developer.android.com/guide/topics/resources/string-resource?utm_source=android-studio-app&utm_medium=app#StylingWithHTML
                stringResource(
                    R.string.summer_2026_funding_intro,
                    "<b>" + toolPair.first.name + "</b>",
                    stringResource(toolPair.first.description),
                    "<b>" + toolPair.second.name + "</b>",
                    stringResource(toolPair.second.description),
                ),
            ), style = TextStyle(
                fontSize = 14.sp,
                lineHeight = 20.sp,
                color = PhotonColors.White,
                fontWeight = FontWeight(400),
                letterSpacing = 0.25.sp,
            )
        )
        Spacer(Modifier.size(8.dp))
        Text(
            text = AnnotatedString.fromHtml(
                stringResource(
                    R.string.summer_2026_funding_outro,
                    "<b>" + stringResource(R.string.summer_2026_call_to_donate) + "</b>"
                )
            ),
            modifier = Modifier.fillMaxWidth(),
            style = TextStyle(
                fontSize = 14.sp,
                lineHeight = 20.sp,
                fontWeight = FontWeight(400),
                letterSpacing = 0.25.sp,
                color = PhotonColors.LightGrey05,
            )
        )
    }
}

@Composable
private fun DonateButton(onDonateButtonClicked: () -> Unit, alternateLayout: Boolean) {
    Button(
        onClick = onDonateButtonClicked,
        colors = ButtonDefaults.buttonColors(
            colorResource(mozilla.components.ui.colors.R.color.photonViolet60),
        ),
        shape = RoundedCornerShape(4.dp),
        contentPadding = PaddingValues(horizontal = 16.dp),
        modifier = if (alternateLayout) Modifier.wrapContentWidth() else Modifier.fillMaxWidth()
    ) {
        Image(
            painterResource(R.drawable.heart),
            contentDescription = null,
        )
        Spacer(
            Modifier.size(8.dp),
        )
        Text(
            text = stringResource(R.string.donate_now_yec),
            textAlign = TextAlign.Center,
            fontSize = 16.sp,
            fontWeight = FontWeight.SemiBold,
            color = PhotonColors.LightGrey05,
        )
    }
}



@Preview(
    name = "Small Window",
    widthDp = 400,
)
@Preview(
    name = "Medium Window",
    widthDp = 700,
)
@Preview(
    name = "Large Window",
    widthDp = 1000,
)
@Preview(
    name = "RTL Small Window",
    locale = "ar",
    widthDp = 400,
)
@Preview(
    name = "RTL Large Window",
    locale = "ar",
    widthDp = 1000
)
annotation class CampaignComposePreview
+67 −0
Original line number Diff line number Diff line
package org.mozilla.fenix.tor

import android.app.Application
import android.util.Log
import androidx.annotation.StringRes
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.application
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import java.text.SimpleDateFormat
import java.util.Date

class TorCampaignViewModel(application: Application) : AndroidViewModel(application) {

    val shouldInitiallyShowPromo: MutableState<Boolean> by lazy {
        mutableStateOf(shouldInitiallyShowPromo())
    }

    fun shouldInitiallyShowPromo(): Boolean {
        val dateFormat = SimpleDateFormat("yyyy-MM-dd-hh-zzz")
        // From https://gitlab.torproject.org/tpo/applications/tor-browser/-/work_items/44747
        val startDate = dateFormat.parse("2026-05-19-15-UTC")
        val endDate =   dateFormat.parse("2026-06-19-00-UTC")
        val currentDate = Date()

        if (currentDate.before(startDate) || currentDate.after(endDate)) {
            return false
        }
        Log.d(
            "TorCampaignViewModel",
            "org.mozilla.fenix.BuildConfig.BUILD_TYPE = ${org.mozilla.fenix.BuildConfig.BUILD_TYPE}"
        )
        return (org.mozilla.fenix.BuildConfig.BUILD_TYPE == "release") || (org.mozilla.fenix.BuildConfig.BUILD_TYPE == "debug")
    }

    companion object {
        val toolList: List<Tool> = listOf(
            Tool(name = "Onion Browser",             description = R.string.summer_2026_funding_tool_onion_browser_description),
            Tool(name = "Quiet",                     description = R.string.summer_2026_funding_tool_quiet_description),
            Tool(name = "Ricochet Refresh",          description = R.string.summer_2026_funding_tool_ricochet_refresh_description),
            Tool(name = "SecureDrop",                description = R.string.summer_2026_funding_tool_securedrop_description),
            Tool(name = "OnionShare",                description = R.string.summer_2026_funding_tool_onionshare_description),
            Tool(name = "Digital Security Helpdesk", description = R.string.summer_2026_funding_tool_digital_security_helpdesk_description),
            Tool(name = "Paskoocheh",                description = R.string.summer_2026_funding_tool_paskoocheh_description),
            Tool(name = "Unredacted",                description = R.string.summer_2026_funding_tool_unredacted_description),
            Tool(name = "Osservatorio Nessuno",      description = R.string.summer_2026_funding_tool_osservatorio_nessuno_description),
            Tool(name = "Save",                      description = R.string.summer_2026_funding_tool_save_description),
            Tool(name = "OONI",                      description = R.string.summer_2026_funding_tool_ooni_description),
        ).shuffled()
    }

    private var toolIndex = 0
    private fun incrementToolIndex() {
        toolIndex = (toolIndex + 2) % toolList.size
    }

    data class Tool(
        val name: String,
        @param:StringRes val description: Int,
    )

    fun getToolPair() : Pair<Tool, Tool> {
        return Pair(toolList[toolIndex], toolList[(toolIndex + 1) % toolList.size]).also { incrementToolIndex() }
    }
}
+94 −24
Original line number Diff line number Diff line
package org.mozilla.fenix.tor

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -12,6 +13,10 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.paint
@@ -26,16 +31,33 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import mozilla.components.compose.base.annotation.FlexibleWindowLightDarkPreview
import org.mozilla.fenix.R
import org.mozilla.fenix.home.ui.SearchBarPreview

@Composable
@FlexibleWindowLightDarkPreview
fun TorHomePage(
    toolBarAtTop: Boolean = true,
    shouldInitiallyShowPromo: MutableState<Boolean> = mutableStateOf(true),
    onClicked: () -> Unit = {},
    toolBarAtTop: Boolean = false,
    toolPair: Pair<TorCampaignViewModel.Tool, TorCampaignViewModel.Tool>,
) {
    // Will persist across a single session, but not multiple sessions.
    // Tapping the close button 'X' will hide the promo for the duration of the session
    val shouldShowPromo = rememberSaveable {
        shouldInitiallyShowPromo
    }

    // Will persist through screen rotations, but not navigations (e.g. Tap on settings -> come back will update)
    // Is expected to change with every new visit to about:tor
    val toolPair = remember {
        toolPair
    }

    Column(
        modifier = Modifier
            .fillMaxSize()
@@ -82,6 +104,14 @@ fun TorHomePage(
            )
        }
        Spacer(Modifier.weight(1f))
        if (shouldShowPromo.value) {
            CampaignBox(
                shouldShowPromo,
                onDonateButtonClicked = onClicked,
                toolPair = toolPair
            )
            Spacer(Modifier.weight(1f))
        } else {
            Text(
                // Moved from the commit 5bb3cc6b93346dabd8d46677fae7f86a8f8a4fc2
                // "[android] Modify UI/UX", and the file HomeFragment.
@@ -99,6 +129,8 @@ fun TorHomePage(
                ),
                modifier = Modifier.align(Alignment.CenterHorizontally),
            )
        }
        if (!shouldShowPromo.value) {
            Spacer(Modifier.weight(1f))
            Image(
                painter = painterResource(
@@ -107,5 +139,43 @@ fun TorHomePage(
                contentDescription = null, Modifier.fillMaxWidth(),
            )
        }
    }
    Spacer(modifier = Modifier.size(17.dp))
}

@Composable
@Preview
/**
 * Relevant documentation
 * https://developer.android.com/develop/ui/compose/tooling/previews#preview-viewmodel
 */
private fun TorHomePagePreview(
    @PreviewParameter(
        BooleanBooleanPreviewParameterProvider::class,
    ) booleanMatrix: Pair<Boolean, Boolean>,
) {
    val toolbarAtTop = booleanMatrix.second
    Box(
        contentAlignment = if (toolbarAtTop) Alignment.TopStart else Alignment.BottomStart,
        modifier = Modifier.fillMaxSize(),
    ) {
        SearchBarPreview() // unrestricted vertically so will follow contentAlignment
        TorHomePage(
            // restricted vertically so will not follow contentAlignment
            shouldInitiallyShowPromo = mutableStateOf(booleanMatrix.first),
            toolBarAtTop = toolbarAtTop,
            toolPair = Pair(TorCampaignViewModel.toolList[0], TorCampaignViewModel.toolList[1]),
        )
    }
}

private class BooleanBooleanPreviewParameterProvider :
    PreviewParameterProvider<Pair<Boolean, Boolean>> {
    override val values: Sequence<Pair<Boolean, Boolean>>
        get() = sequenceOf(
            Pair(true, true),
            Pair(true, false),
            Pair(false, true),
            Pair(false, false),
        )
}
Loading