diff --git a/MykSuite/build.gradle.kts b/MykSuite/build.gradle.kts index 5070ae57..eacd22c2 100644 --- a/MykSuite/build.gradle.kts +++ b/MykSuite/build.gradle.kts @@ -48,6 +48,8 @@ android { dependencies { + implementation(project(":Core")) + implementation(core.androidx.core.ktx) implementation(core.material) implementation(core.navigation.fragment.ktx) diff --git a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/components/TextWithIcon.kt b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/components/TextWithIcon.kt new file mode 100644 index 00000000..8062e84e --- /dev/null +++ b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/components/TextWithIcon.kt @@ -0,0 +1,113 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2025 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.core.myksuite.ui.components + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.datasource.LoremIpsum +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.infomaniak.core.myksuite.R +import com.infomaniak.core.myksuite.ui.theme.LocalMyKSuiteColors +import com.infomaniak.core.myksuite.ui.theme.Margin +import com.infomaniak.core.myksuite.ui.theme.MyKSuiteTheme +import com.infomaniak.core.myksuite.ui.theme.Typography + +/** This component allows to put an icon on any line of the text, thanks to the [iconLine] parameter + * This code comes from [here](https://stackoverflow.com/questions/70708056/how-to-centrally-align-icon-to-first-line-of-a-text-component-in-compose/71312465#71312465) + */ +@Composable +internal fun TextWithIcon( + modifier: Modifier = Modifier, + text: String, + icon: ImageVector, + color: Color = Color.Unspecified, + style: TextStyle, + iconRightPadding: Dp = 0.dp, + iconLine: Int = 0, + iconTint: Color = Color.Black, +) { + val painter = rememberVectorPainter(image = icon) + var lineTop = 0f + var lineBottom = 0f + var lineLeft = 0f + with(LocalDensity.current) { + val imageSize = Size(icon.defaultWidth.toPx(), icon.defaultHeight.toPx()) + val rightPadding = iconRightPadding.toPx() + Text( + text = text, + color = color, + style = style, + onTextLayout = { layoutResult -> + val nbLines = layoutResult.lineCount + if (nbLines > iconLine) { + lineTop = layoutResult.getLineTop(iconLine) + lineBottom = layoutResult.getLineBottom(iconLine) + lineLeft = layoutResult.getLineLeft(iconLine) + } + }, + modifier = modifier + .padding(start = icon.defaultWidth + iconRightPadding) + .drawBehind { + with(painter) { + translate( + left = lineLeft - imageSize.width - rightPadding, + top = lineTop + (lineBottom - lineTop) / 2 - imageSize.height / 2, + ) { + draw(size = intrinsicSize, colorFilter = ColorFilter.tint(iconTint)) + } + } + } + ) + } +} + +@Preview +@Composable +private fun Preview() { + + fun getLoremText(words: Int) = LoremIpsum(words).values.joinToString(separator = " ") + + MyKSuiteTheme { + val localColors = LocalMyKSuiteColors.current + Surface { + TextWithIcon( + text = getLoremText(35), icon = ImageVector.vectorResource(R.drawable.ic_circle_i), + color = localColors.primaryTextColor, + style = Typography.bodyRegular, + iconRightPadding = Margin.Small, + iconLine = 0, + iconTint = localColors.iconColor, + ) + } + } +} diff --git a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/data/MyKSuiteData.kt b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/data/MyKSuiteData.kt index 9d50bb6e..0b7dd26f 100644 --- a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/data/MyKSuiteData.kt +++ b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/data/MyKSuiteData.kt @@ -24,6 +24,7 @@ import androidx.room.PrimaryKey import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient +import java.util.Date @Serializable @Entity @@ -62,4 +63,6 @@ data class MyKSuiteData( val isMyKSuitePlus get() = kSuitePack.type == KSuitePack.KSuitePackType.MY_KSUITE_PLUS || kSuitePack.type == KSuitePack.KSuitePackType.MY_KSUITE_PLUS_DRIVE_SOLO + + inline val trialExpiryDate get() = trialExpiryAt?.let { Date(it * 1_000) } } diff --git a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/network/ApiRoutes.kt b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/network/ApiRoutes.kt index 1d892f11..e311bf53 100644 --- a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/network/ApiRoutes.kt +++ b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/network/ApiRoutes.kt @@ -19,7 +19,10 @@ package com.infomaniak.core.myksuite.ui.network object ApiRoutes { - private const val BASE_URL = "https://api.staging-myksuite.dev.infomaniak.ch" + const val MANAGER_URL = "https://manager.infomaniak.com/v3/ng/home" + + private const val BASE_URL = "https://api.infomaniak.com" + + fun myKSuiteData() = "$BASE_URL/1/my_ksuite/current?with=drive,mail,pack,can_trial,has_auto_renew" - val myKSuiteData = "$BASE_URL/1/my_ksuite/current?with=*" } diff --git a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/MyKSuiteDashboardScreen.kt b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/MyKSuiteDashboardScreen.kt index 931fa57b..686d6d14 100644 --- a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/MyKSuiteDashboardScreen.kt +++ b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/MyKSuiteDashboardScreen.kt @@ -18,6 +18,7 @@ package com.infomaniak.core.myksuite.ui.screens import android.content.res.Configuration +import android.os.Parcelable import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -28,24 +29,26 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.infomaniak.core.FORMAT_DATE_SIMPLE +import com.infomaniak.core.extensions.openUrl +import com.infomaniak.core.format import com.infomaniak.core.myksuite.R import com.infomaniak.core.myksuite.ui.components.* +import com.infomaniak.core.myksuite.ui.network.ApiRoutes import com.infomaniak.core.myksuite.ui.screens.components.* import com.infomaniak.core.myksuite.ui.theme.* import com.infomaniak.core.myksuite.ui.theme.Typography +import kotlinx.parcelize.Parcelize +import java.util.Date @Composable -fun MyKSuiteDashboardScreen( - userName: String, - avatarUri: String = "", - dailySendingLimit: String, - onClose: () -> Unit = {}, -) { +fun MyKSuiteDashboardScreen(dashboardScreenData: () -> MyKSuiteDashboardScreenData, onClose: () -> Unit = {}) { MyKSuiteTheme { Scaffold( topBar = { TopAppBar(onClose) }, @@ -66,9 +69,14 @@ fun MyKSuiteDashboardScreen( ) Column(Modifier.padding(paddingValues), verticalArrangement = Arrangement.spacedBy(Margin.Large)) { val paddedModifier = Modifier.padding(horizontal = Margin.Medium) - SubscriptionInfoCard(paddedModifier, avatarUri, userName, dailySendingLimit) - // TODO: Add this line when we'll have In-app payments - // MyKSuitePlusPromotionCard(paddedModifier) {} + SubscriptionInfoCard(paddedModifier, dashboardScreenData) + + if (dashboardScreenData().myKSuiteTier == MyKSuiteTier.Free) { + // TODO: Add this line when we'll have In-app payments + // MyKSuitePlusPromotionCard(paddedModifier) {} + } else { + AdvantagesCard(paddedModifier) + } } } } @@ -106,10 +114,9 @@ private fun TopAppBar(onClose: () -> Unit) { @Composable private fun SubscriptionInfoCard( paddedModifier: Modifier, - avatarUri: String, - userName: String, - dailySendingLimit: String, + dashboardScreenData: () -> MyKSuiteDashboardScreenData, ) { + val context = LocalContext.current val localColors = LocalMyKSuiteColors.current Card( @@ -117,33 +124,60 @@ private fun SubscriptionInfoCard( shape = RoundedCornerShape(Dimens.largeCornerRadius), colors = CardDefaults.cardColors(containerColor = localColors.background), elevation = CardDefaults.elevatedCardElevation(defaultElevation = Dimens.cardElevation), - border = if (isSystemInDarkTheme()) BorderStroke(1.dp, localColors.cardBorderColor) else null, + border = cardBorder(), ) { Row( modifier = paddedModifier.padding(top = Margin.Medium), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(Margin.Mini), ) { - UserAvatar(avatarUri) + UserAvatar(dashboardScreenData().avatarUri) Text( modifier = Modifier.weight(1.0f), style = Typography.bodyRegular, color = localColors.primaryTextColor, - text = userName, + text = dashboardScreenData().email, maxLines = 1, overflow = TextOverflow.Ellipsis, ) - MyKSuiteChip(tier = MyKSuiteTier.Free) + MyKSuiteChip(tier = dashboardScreenData().myKSuiteTier) } PaddedDivider(paddedModifier) - AppStorageQuotas(paddedModifier) - PaddedDivider(paddedModifier) - ExpendableActionItem(iconRes = R.drawable.ic_envelope, textRes = R.string.myKSuiteDashboardFreeMailLabel) - ExpendableActionItem( - iconRes = R.drawable.ic_padlock, - textRes = R.string.myKSuiteDashboardLimitedFunctionalityLabel, - expendedView = { LimitedFunctionalities(paddedModifier, dailySendingLimit) }, + ProductsStorageQuotas( + modifier = paddedModifier, + myKSuiteTier = dashboardScreenData().myKSuiteTier, + kSuiteProductsWithQuotas = { dashboardScreenData().kSuiteProductsWithQuotas }, ) + PaddedDivider(paddedModifier) + + if (dashboardScreenData().myKSuiteTier == MyKSuiteTier.Free) { + ExpandableActionItem(iconRes = R.drawable.ic_envelope, textRes = R.string.myKSuiteDashboardFreeMailLabel) + ExpandableActionItem( + iconRes = R.drawable.ic_padlock, + textRes = R.string.myKSuiteDashboardLimitedFunctionalityLabel, + expandedView = { + LimitedFunctionalities( + modifier = paddedModifier, + dailySendingLimit = { dashboardScreenData().dailySendingLimit }, + ) + }, + ) + } else { + dashboardScreenData().trialExpiryDate?.let { expiryDate -> + MyKSuiteTextItem( + modifier = paddedModifier.heightIn(min = Dimens.textItemMinHeight), + title = stringResource(R.string.myKSuiteDashboardTrialPeriod), + value = stringResource(R.string.myKSuiteDashboardUntil, expiryDate.format(FORMAT_DATE_SIMPLE)), + ) + } + Spacer(Modifier.height(Margin.Large)) + InformationBlock( + modifier = paddedModifier, + text = stringResource(R.string.myKSuiteManageSubscriptionDescription), + buttonText = stringResource(R.string.myKSuiteManageSubscriptionButton), + onClick = { context.openUrl(ApiRoutes.MANAGER_URL) }, + ) + } Spacer(Modifier.height(Margin.Medium)) } } @@ -210,11 +244,70 @@ private fun MyKSuitePlusPromotionCard(modifier: Modifier = Modifier, onButtonCli } } +@Composable +private fun AdvantagesCard(modifier: Modifier) { + val localColors = LocalMyKSuiteColors.current + Card( + modifier = modifier, + shape = RoundedCornerShape(Dimens.largeCornerRadius), + elevation = CardDefaults.elevatedCardElevation(defaultElevation = Dimens.cardElevation), + colors = CardDefaults.elevatedCardColors(containerColor = localColors.background), + border = cardBorder(), + ) { + Column(modifier = Modifier.padding(Margin.Medium), verticalArrangement = Arrangement.spacedBy(Margin.Large)) { + Text( + text = stringResource(R.string.myKSuiteUpgradeBenefitsTitle), + color = localColors.secondaryTextColor, + style = Typography.bodySmallRegular, + ) + + val upgradeFeatureModifier = Modifier.fillMaxWidth() + val iconSize = Dimens.smallIconSize + UpgradeFeature(upgradeFeatureModifier, MyKSuiteUpgradeFeatures.DriveStorageFeature, iconSize) + UpgradeFeature(upgradeFeatureModifier, MyKSuiteUpgradeFeatures.MailUnlimitedFeature, iconSize) + UpgradeFeature(upgradeFeatureModifier, MyKSuiteUpgradeFeatures.MoreFeatures, iconSize) + } + } +} + +@Composable +private fun cardBorder() = if (isSystemInDarkTheme()) BorderStroke(1.dp, LocalMyKSuiteColors.current.cardBorderColor) else null + +@Parcelize +data class MyKSuiteDashboardScreenData( + val myKSuiteTier: MyKSuiteTier, + val email: String, + val dailySendingLimit: String, + val kSuiteProductsWithQuotas: List, + val trialExpiryDate: Date?, + val avatarUri: String = "", +) : Parcelable + @Preview(name = "(1) Light") @Preview(name = "(2) Dark", uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL) @Composable private fun Preview() { + val dashboardScreenData = MyKSuiteDashboardScreenData( + myKSuiteTier = MyKSuiteTier.Plus, + email = "Toto", + avatarUri = "", + dailySendingLimit = "500", + kSuiteProductsWithQuotas = listOf( + KSuiteProductsWithQuotas.Mail( + usedSize = "0.2 Go", + maxSize = "20 Go", + progress = 0.01f, + ), + KSuiteProductsWithQuotas.Drive( + usedSize = "6 Go", + maxSize = "15 Go", + progress = 0.4f, + ), + ), + trialExpiryDate = Date(), + ) + Surface(Modifier.fillMaxSize(), color = Color.White) { - MyKSuiteDashboardScreen(userName = "Toto", avatarUri = "", dailySendingLimit = "500") + MyKSuiteDashboardScreen(dashboardScreenData = { dashboardScreenData }) } } diff --git a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/MyKSuiteUpgradeBottomSheet.kt b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/MyKSuiteUpgradeBottomSheet.kt index afc42170..4db40b39 100644 --- a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/MyKSuiteUpgradeBottomSheet.kt +++ b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/MyKSuiteUpgradeBottomSheet.kt @@ -112,13 +112,25 @@ private fun UpgradeBottomSheetContent(app: KSuiteApp, onButtonClicked: () -> Uni } @Composable -private fun ColumnScope.UpgradeFeatures(app: KSuiteApp, modifier: Modifier) { - app.features.forEach { UpgradeFeature(it, modifier) } - UpgradeFeature(MyKSuiteUpgradeFeatures.MoreFeatures, modifier) +private fun UpgradeFeatures(app: KSuiteApp, modifier: Modifier) { + val upgradeFeatureModifier = Modifier.fillMaxWidth() + val textColor = LocalMyKSuiteColors.current.secondaryTextColor + + Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(Margin.Medium)) { + app.features.forEach { UpgradeFeature(modifier = upgradeFeatureModifier, customFeature = it, textColor = textColor) } + UpgradeFeature( + modifier = upgradeFeatureModifier, + customFeature = MyKSuiteUpgradeFeatures.MoreFeatures, + textColor = textColor, + ) + } } @Parcelize -enum class KSuiteApp(internal val features: List, internal val buttonStyle: MyKSuiteButtonType) : Parcelable { +enum class KSuiteApp( + internal val features: List, + internal val buttonStyle: MyKSuiteButtonType, +) : Parcelable { Mail( features = listOf(MyKSuiteUpgradeFeatures.MailUnlimitedFeature, MyKSuiteUpgradeFeatures.MailOtherFeature), buttonStyle = MyKSuiteButtonType.Mail, diff --git a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/AppStorageQuotas.kt b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/AppStorageQuotas.kt deleted file mode 100644 index e76eba79..00000000 --- a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/AppStorageQuotas.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Infomaniak Core - Android - * Copyright (C) 2025 Infomaniak Network SA - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.infomaniak.core.myksuite.ui.screens.components - -import android.content.res.Configuration -import androidx.compose.foundation.layout.* -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.infomaniak.core.myksuite.ui.components.WeightOneSpacer -import com.infomaniak.core.myksuite.ui.theme.LocalMyKSuiteColors -import com.infomaniak.core.myksuite.ui.theme.Margin -import com.infomaniak.core.myksuite.ui.theme.MyKSuiteTheme -import com.infomaniak.core.myksuite.ui.theme.Typography - -@Composable -internal fun AppStorageQuotas(modifier: Modifier) { - Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(Margin.Medium)) { - KSuiteAppWithQuotas.entries.forEach { AppStorageQuota(app = it) } - } -} - -@Composable -private fun AppStorageQuota(modifier: Modifier = Modifier, app: KSuiteAppWithQuotas) { - val localColors = LocalMyKSuiteColors.current - Column(modifier) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = app.displayName, - style = Typography.bodyRegular, - color = localColors.primaryTextColor, - ) - WeightOneSpacer(minWidth = Margin.Medium) - Text( - text = "0.2 Go / 20 Go", // TODO: Use real data - style = Typography.bodySmallRegular, - color = localColors.secondaryTextColor, - ) - } - Spacer(Modifier.height(Margin.Mini)) - val progressIndicatorHeight = 14.dp - LinearProgressIndicator( - modifier = Modifier - .height(progressIndicatorHeight) - .fillMaxWidth(), - color = app.color(), - trackColor = localColors.chipBackground, - strokeCap = StrokeCap.Round, - gapSize = -progressIndicatorHeight, - progress = { 0.5f }, // TODO: Use real values - drawStopIndicator = {}, - ) - } -} - -private enum class KSuiteAppWithQuotas(val displayName: String, val color: @Composable () -> Color) { - Mail("Mail", { LocalMyKSuiteColors.current.mail }), - Drive("kDrive", { LocalMyKSuiteColors.current.drive }), -} - -@Preview(name = "(1) Light") -@Preview(name = "(2) Dark", uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL) -@Composable -private fun Preview() { - MyKSuiteTheme { - Surface { - AppStorageQuotas(Modifier.padding(Margin.Medium)) - } - } -} diff --git a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/ExpandableItemView.kt b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/ExpandableItemView.kt index e775b2fe..b70e66ee 100644 --- a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/ExpandableItemView.kt +++ b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/ExpandableItemView.kt @@ -33,15 +33,14 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import com.infomaniak.core.myksuite.R import com.infomaniak.core.myksuite.ui.theme.* @Composable -internal fun ExpendableActionItem( +internal fun ExpandableActionItem( @DrawableRes iconRes: Int, @StringRes textRes: Int, - expendedView: (@Composable () -> Unit)? = null, + expandedView: (@Composable () -> Unit)? = null, ) { var isExpanded by remember { mutableStateOf(false) } @@ -51,10 +50,10 @@ internal fun ExpendableActionItem( Column { Row( modifier = Modifier - .heightIn(40.dp) + .heightIn(min = Dimens.textItemMinHeight) .fillMaxWidth() .then( // TODO add onClickLabel for accessibility - if (expendedView == null) Modifier else Modifier.clickable { isExpanded = !isExpanded } + if (expandedView == null) Modifier else Modifier.clickable { isExpanded = !isExpanded } ) .padding(horizontal = Margin.Medium), verticalAlignment = Alignment.CenterVertically, @@ -72,7 +71,7 @@ internal fun ExpendableActionItem( style = Typography.bodyRegular, color = localColors.primaryTextColor, ) - expendedView?.let { + expandedView?.let { val icon = ImageVector.vectorResource(if (isExpanded) R.drawable.ic_chevron_up else R.drawable.ic_chevron_down) Icon( imageVector = icon, @@ -81,7 +80,7 @@ internal fun ExpendableActionItem( ) } } - AnimatedVisibility(isExpanded) { expendedView?.invoke() } + AnimatedVisibility(isExpanded) { expandedView?.invoke() } } } @@ -92,11 +91,11 @@ private fun Preview() { MyKSuiteTheme { Surface { Column { - ExpendableActionItem(R.drawable.ic_padlock, R.string.myKSuiteDashboardLimitedFunctionalityLabel) - ExpendableActionItem( + ExpandableActionItem(R.drawable.ic_padlock, R.string.myKSuiteDashboardLimitedFunctionalityLabel) + ExpandableActionItem( iconRes = R.drawable.ic_padlock, textRes = R.string.myKSuiteDashboardLimitedFunctionalityLabel, - expendedView = { Text("") } + expandedView = { Text("") } ) } } diff --git a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/InformationBlock.kt b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/InformationBlock.kt new file mode 100644 index 00000000..eaeb088b --- /dev/null +++ b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/InformationBlock.kt @@ -0,0 +1,80 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2025 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.core.myksuite.ui.screens.components + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.datasource.LoremIpsum +import com.infomaniak.core.myksuite.R +import com.infomaniak.core.myksuite.ui.components.TextWithIcon +import com.infomaniak.core.myksuite.ui.theme.* + +@Composable +internal fun InformationBlock(modifier: Modifier = Modifier, text: String, buttonText: String, onClick: () -> Unit) { + val localColors = LocalMyKSuiteColors.current + + Column( + modifier + .background(color = localColors.informationBlockBackground, shape = RoundedCornerShape(Dimens.smallCornerRadius)) + .padding(start = Margin.Medium, top = Margin.Medium, end = Margin.Medium), + ) { + TextWithIcon( + text = text, + icon = ImageVector.vectorResource(R.drawable.ic_circle_i), + iconTint = localColors.iconColor, + modifier = Modifier.fillMaxWidth(), + style = Typography.bodyRegular, + iconRightPadding = Margin.Small, + ) + TextButton( + modifier = Modifier.padding(start = Margin.Medium), + onClick = onClick, + colors = ButtonDefaults.textButtonColors(contentColor = localColors.primaryButton), + ) { + Text(text = buttonText, style = Typography.bodyMedium) + } + } +} + + +@Preview(name = "(1) Light") +@Preview(name = "(2) Dark", uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL) +@Composable +private fun Preview() { + + fun getLoremText(words: Int) = LoremIpsum(words).values.joinToString(separator = " ") + + MyKSuiteTheme { + Surface { + InformationBlock(text = getLoremText(35), buttonText = getLoremText(1)) {} + } + } +} diff --git a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/LimitedFunctionnalities.kt b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/LimitedFunctionnalities.kt index 37b3adcd..4bdea849 100644 --- a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/LimitedFunctionnalities.kt +++ b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/LimitedFunctionnalities.kt @@ -34,9 +34,9 @@ import com.infomaniak.core.myksuite.ui.theme.MyKSuiteTheme import com.infomaniak.core.myksuite.ui.theme.Typography @Composable -internal fun LimitedFunctionalities(paddedModifier: Modifier, dailySendingLimit: String) { +internal fun LimitedFunctionalities(modifier: Modifier, dailySendingLimit: () -> String) { Column( - modifier = paddedModifier.padding(top = Margin.Mini), + modifier = modifier.padding(top = Margin.Mini), verticalArrangement = Arrangement.spacedBy(Margin.Mini), ) { LimitedFunctionalityLabel(textRes = R.string.myKSuiteDashboardFunctionalityMailAndDrive) @@ -44,7 +44,7 @@ internal fun LimitedFunctionalities(paddedModifier: Modifier, dailySendingLimit: LimitedFunctionalityLabel(modifier = Modifier.weight(1.0f), R.string.myKSuiteDashboardFunctionalityLimit) Text( modifier = Modifier.padding(start = Margin.Mini), - text = dailySendingLimit, + text = dailySendingLimit(), style = Typography.bodySmallMedium, ) } @@ -68,7 +68,7 @@ private fun LimitedFunctionalityLabel(modifier: Modifier = Modifier, @StringRes private fun Preview() { MyKSuiteTheme { Surface { - LimitedFunctionalities(Modifier.padding(horizontal = Margin.Medium), dailySendingLimit = "500") + LimitedFunctionalities(Modifier.padding(horizontal = Margin.Medium), dailySendingLimit = { "500" }) } } } diff --git a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/MyKSuiteTextItem.kt b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/MyKSuiteTextItem.kt new file mode 100644 index 00000000..19604724 --- /dev/null +++ b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/MyKSuiteTextItem.kt @@ -0,0 +1,71 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2025 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.core.myksuite.ui.screens.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import com.infomaniak.core.FORMAT_DATE_SIMPLE +import com.infomaniak.core.format +import com.infomaniak.core.myksuite.R +import com.infomaniak.core.myksuite.ui.theme.LocalMyKSuiteColors +import com.infomaniak.core.myksuite.ui.theme.Margin +import com.infomaniak.core.myksuite.ui.theme.MyKSuiteTheme +import com.infomaniak.core.myksuite.ui.theme.Typography +import java.util.Date + +@Composable +internal fun MyKSuiteTextItem( + modifier: Modifier = Modifier, + title: String, + value: String, + valueStyle: TextStyle = Typography.bodyRegular, +) { + val localColors = LocalMyKSuiteColors.current + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = title, color = localColors.primaryTextColor) + Text(text = value, color = localColors.secondaryTextColor, style = valueStyle) + } +} + +@Composable +@Preview +private fun Preview() { + MyKSuiteTheme { + Surface { + MyKSuiteTextItem( + modifier = Modifier.padding(horizontal = Margin.Medium), + title = stringResource(R.string.myKSuiteDashboardTrialPeriod), + value = stringResource(R.string.myKSuiteDashboardUntil, Date().format(FORMAT_DATE_SIMPLE)), + ) + } + } +} diff --git a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/ProductsStorageQuotas.kt b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/ProductsStorageQuotas.kt new file mode 100644 index 00000000..468f9827 --- /dev/null +++ b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/ProductsStorageQuotas.kt @@ -0,0 +1,125 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2025 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.core.myksuite.ui.screens.components + +import android.content.res.Configuration +import android.os.Parcelable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.infomaniak.core.myksuite.R +import com.infomaniak.core.myksuite.ui.components.MyKSuiteTier +import com.infomaniak.core.myksuite.ui.theme.LocalMyKSuiteColors +import com.infomaniak.core.myksuite.ui.theme.Margin +import com.infomaniak.core.myksuite.ui.theme.MyKSuiteTheme +import com.infomaniak.core.myksuite.ui.theme.Typography +import kotlinx.parcelize.Parcelize + +@Composable +internal fun ProductsStorageQuotas( + modifier: Modifier, + myKSuiteTier: MyKSuiteTier, + kSuiteProductsWithQuotas: () -> List, +) { + Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(Margin.Medium)) { + kSuiteProductsWithQuotas().forEach { ProductStorageQuota(myKSuiteTier = myKSuiteTier, product = it) } + } +} + +@Composable +private fun ProductStorageQuota(myKSuiteTier: MyKSuiteTier, product: KSuiteProductsWithQuotas) { + val localColors = LocalMyKSuiteColors.current + val isUnlimitedMail = myKSuiteTier == MyKSuiteTier.Plus && product is KSuiteProductsWithQuotas.Mail + + Column { + MyKSuiteTextItem( + title = product.displayName, + value = computeQuotasString(isUnlimitedMail, product), + valueStyle = Typography.bodySmallRegular, + ) + + if (!isUnlimitedMail) { + Spacer(Modifier.height(Margin.Mini)) + val progressIndicatorHeight = 14.dp + LinearProgressIndicator( + modifier = Modifier + .height(progressIndicatorHeight) + .fillMaxWidth(), + color = product.getColor(), + trackColor = localColors.chipBackground, + strokeCap = StrokeCap.Round, + gapSize = -progressIndicatorHeight, + progress = { product.progress }, + drawStopIndicator = {}, + ) + } + } +} + +@Composable +private fun computeQuotasString(isUnlimitedMail: Boolean, product: KSuiteProductsWithQuotas): String { + return if (isUnlimitedMail) { + stringResource(R.string.myKSuiteDashboardDataUnlimited) + } else { + "${product.usedSize} / ${product.maxSize}" + } +} + +@Parcelize +sealed class KSuiteProductsWithQuotas( + internal val displayName: String, + open val usedSize: String, + open val maxSize: String, + open val progress: Float, +) : Parcelable { + + class Mail(override val usedSize: String, override val maxSize: String, override val progress: Float) : + KSuiteProductsWithQuotas("Mail", usedSize, maxSize, progress) + + data class Drive(override val usedSize: String, override val maxSize: String, override val progress: Float) : + KSuiteProductsWithQuotas("kDrive", usedSize, maxSize, progress) + + @Composable + fun getColor() = if (this is Mail) LocalMyKSuiteColors.current.mail else LocalMyKSuiteColors.current.drive +} + +@Preview(name = "(1) Light") +@Preview(name = "(2) Dark", uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL) +@Composable +private fun Preview() { + MyKSuiteTheme { + Surface { + ProductsStorageQuotas( + modifier = Modifier.padding(Margin.Medium), + myKSuiteTier = MyKSuiteTier.Plus, + kSuiteProductsWithQuotas = { + listOf( + KSuiteProductsWithQuotas.Mail(usedSize = "0.2 Go", maxSize = "20 Go", progress = 0.01f), + KSuiteProductsWithQuotas.Drive(usedSize = "6 Go", maxSize = "15 Go", progress = 0.4f), + ) + }, + ) + } + } +} diff --git a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/UpgradeFeature.kt b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/UpgradeFeature.kt index 747a2aca..cbc158fd 100644 --- a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/UpgradeFeature.kt +++ b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/screens/components/UpgradeFeature.kt @@ -25,34 +25,41 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import com.infomaniak.core.myksuite.ui.screens.MyKSuiteUpgradeFeatures import com.infomaniak.core.myksuite.ui.screens.MyKSuiteUpgradeFeatures.* import com.infomaniak.core.myksuite.ui.theme.* @Composable -internal fun ColumnScope.UpgradeFeature(customFeature: MyKSuiteUpgradeFeatures, modifier: Modifier = Modifier) { +internal fun ColumnScope.UpgradeFeature( + modifier: Modifier = Modifier, + customFeature: MyKSuiteUpgradeFeatures, + iconSize: Dp = Dimens.iconSize, + textColor: Color = Color.Unspecified, +) { val localColors = LocalMyKSuiteColors.current Row( modifier = modifier - .padding(vertical = Margin.Mini) .align(Alignment.Start), verticalAlignment = Alignment.CenterVertically, ) { Icon( - modifier = Modifier.size(Dimens.iconSize), + modifier = Modifier.size(iconSize), imageVector = ImageVector.vectorResource(customFeature.icon), contentDescription = null, tint = localColors.iconColor, ) Spacer(Modifier.width(Margin.Mini)) Text( + modifier = modifier, text = stringResource(customFeature.title), style = Typography.bodyRegular, - color = localColors.secondaryTextColor, + color = textColor, ) } } @@ -63,10 +70,10 @@ internal fun ColumnScope.UpgradeFeature(customFeature: MyKSuiteUpgradeFeatures, private fun Preview() { MyKSuiteTheme { Surface { - Column { - UpgradeFeature(DriveStorageFeature) - UpgradeFeature(DriveDropboxFeature) - UpgradeFeature(MoreFeatures) + Column(verticalArrangement = Arrangement.spacedBy(Margin.Mini)) { + UpgradeFeature(customFeature = DriveStorageFeature) + UpgradeFeature(customFeature = DriveDropboxFeature) + UpgradeFeature(customFeature = MoreFeatures) } } } diff --git a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/theme/ColorsDark.kt b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/theme/ColorsDark.kt index f903df22..ca98fee8 100644 --- a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/theme/ColorsDark.kt +++ b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/theme/ColorsDark.kt @@ -48,6 +48,7 @@ internal val MyKSuiteDarkColors = MyKSuiteColors( background = Color(bat), secondaryBackground = Color(orca), topAppBarBackground = Color(orca), + informationBlockBackground = Color(orca), chipBackground = Color(orca), drive = Color(drive), mail = Color(mail), diff --git a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/theme/ColorsLight.kt b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/theme/ColorsLight.kt index 143dd09a..5b87d74a 100644 --- a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/theme/ColorsLight.kt +++ b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/theme/ColorsLight.kt @@ -55,6 +55,7 @@ internal val MyKSuiteLightColors = MyKSuiteColors( background = Color(white), secondaryBackground = Color(white), topAppBarBackground = Color(sky), + informationBlockBackground = Color(polar_bear), chipBackground = Color(rabbit), drive = Color(drive), mail = Color(mail), diff --git a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/theme/Dimens.kt b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/theme/Dimens.kt index 1b184898..65c7bcaf 100644 --- a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/theme/Dimens.kt +++ b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/theme/Dimens.kt @@ -27,8 +27,12 @@ object Dimens { val smallIconSize = 16.dp /** 24 dp */ val iconSize = 24.dp + /** 8 dp */ + val smallCornerRadius = 8.dp /** 16 dp */ val largeCornerRadius = 16.dp /** 5 dp */ val cardElevation = 5.dp + /** 40 dp */ + val textItemMinHeight = 40.dp } diff --git a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/theme/Theme.kt b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/theme/Theme.kt index fb1648b8..3e7ceb88 100644 --- a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/theme/Theme.kt +++ b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/theme/Theme.kt @@ -49,6 +49,7 @@ internal data class MyKSuiteColors( val background: Color = Color.Unspecified, val secondaryBackground: Color = Color.Unspecified, val topAppBarBackground: Color = Color.Unspecified, + val informationBlockBackground: Color = Color.Unspecified, val chipBackground: Color = Color.Unspecified, val drive: Color = Color.Unspecified, val mail: Color = Color.Unspecified, diff --git a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/views/MyKSuiteDashboardFragment.kt b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/views/MyKSuiteDashboardFragment.kt index 2ade4da3..ffcb9e59 100644 --- a/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/views/MyKSuiteDashboardFragment.kt +++ b/MykSuite/src/main/java/com/infomaniak/core/myksuite/ui/views/MyKSuiteDashboardFragment.kt @@ -27,25 +27,26 @@ import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.infomaniak.core.myksuite.ui.screens.MyKSuiteDashboardScreen +import com.infomaniak.core.myksuite.ui.screens.MyKSuiteDashboardScreenData open class MyKSuiteDashboardFragment : Fragment() { private val navigationArgs: MyKSuiteDashboardFragmentArgs by navArgs() + private var composeView: ComposeView? = null + + private val onClose: () -> Unit by lazy { { this@MyKSuiteDashboardFragment.findNavController().popBackStack() } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { return ComposeView(requireContext()).apply { + composeView = this setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { - val onClose: () -> Unit = { this@MyKSuiteDashboardFragment.findNavController().popBackStack() } - with(navigationArgs) { - MyKSuiteDashboardScreen( - userName = userName, - avatarUri = avatarUri, - dailySendingLimit = dailySendLimit, - onClose = onClose, - ) - } + MyKSuiteDashboardScreen({ navigationArgs.dashboardData }, onClose) } } } + + protected fun resetContent(dashboardData: MyKSuiteDashboardScreenData) { + composeView?.setContent { MyKSuiteDashboardScreen(dashboardScreenData = { dashboardData }, onClose = onClose) } + } } diff --git a/MykSuite/src/main/res/drawable/ic_circle_i.xml b/MykSuite/src/main/res/drawable/ic_circle_i.xml new file mode 100644 index 00000000..0001fa05 --- /dev/null +++ b/MykSuite/src/main/res/drawable/ic_circle_i.xml @@ -0,0 +1,47 @@ + + + + + + + diff --git a/MykSuite/src/main/res/navigation/my_ksuite_navigation.xml b/MykSuite/src/main/res/navigation/my_ksuite_navigation.xml index de744452..56e8da83 100644 --- a/MykSuite/src/main/res/navigation/my_ksuite_navigation.xml +++ b/MykSuite/src/main/res/navigation/my_ksuite_navigation.xml @@ -25,14 +25,8 @@ android:name="com.infomaniak.core.myksuite.ui.views.MyKSuiteDashboardFragment" android:label="MyKSuiteDashboardFragment"> - - + android:name="dashboardData" + app:argType="com.infomaniak.core.myksuite.ui.screens.MyKSuiteDashboardScreenData" /> - My kSuite - My kSuite + + my kSuite + my kSuite+ Manage my offer To manage or cancel your subscription, log on to the kSuite web interface. diff --git a/gradle/core.versions.toml b/gradle/core.versions.toml index 60455827..12bdd558 100644 --- a/gradle/core.versions.toml +++ b/gradle/core.versions.toml @@ -1,8 +1,8 @@ [versions] -androidxCore = "1.13.1" # Doesn't build when bumped to 1.15.0 (Waiting SDK 35) +androidxCore = "1.13.1" # Doesn't build when bumped to 1.15.0 (Waiting SDK 35) coil = "3.0.2" coilNetworkOkhttp = "3.0.4" -composeBom = "2024.12.01" +composeBom = "2025.01.01" integrity = "1.4.0" junit = "4.13.2" junitAndroidx = "1.2.1" diff --git a/src/main/kotlin/com/infomaniak/core/extensions/ContextExt.kt b/src/main/kotlin/com/infomaniak/core/extensions/ContextExt.kt index 71abfa9e..0164e636 100644 --- a/src/main/kotlin/com/infomaniak/core/extensions/ContextExt.kt +++ b/src/main/kotlin/com/infomaniak/core/extensions/ContextExt.kt @@ -20,7 +20,9 @@ package com.infomaniak.core.extensions import android.app.Activity import android.content.Context import android.content.ContextWrapper +import android.content.Intent import android.content.pm.PackageManager +import android.net.Uri import androidx.core.content.ContextCompat tailrec fun Context.findActivity(): Activity? = when (this) { @@ -34,3 +36,7 @@ fun Context.hasPermissions(permissions: Array): Boolean { ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED } } + +fun Context.openUrl(url: String) { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) +}