Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Paywalls: Support Google fonts and font families with multiple fonts #1338

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
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ compose-ui = { module = "androidx.compose.ui:ui" }
compose-ui-util = { module = "androidx.compose.ui:ui-util" }
compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
compose-ui-google-fonts = { module = "androidx.compose.ui:ui-text-google-fonts" }
compose-material = { module = "androidx.compose.material:material" }
compose-material3 = { module = "androidx.compose.material3:material3" }
compose-window-size = { module = "androidx.compose.material3:material3-window-size-class" }
Expand Down
1 change: 1 addition & 0 deletions ui/revenuecatui/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ dependencies {
implementation libs.commonmark.strikethrough
implementation libs.activity.compose
implementation libs.androidx.fragment.ktx
implementation libs.compose.ui.google.fonts
debugImplementation libs.compose.ui.tooling
debugImplementation libs.androidx.test.compose.manifest

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.core.content.res.ResourcesCompat
import androidx.compose.ui.text.googlefonts.Font
import androidx.compose.ui.text.googlefonts.GoogleFont
import com.revenuecat.purchases.CustomerInfo
import com.revenuecat.purchases.PurchasesError
import com.revenuecat.purchases.PurchasesErrorCode
Expand All @@ -21,6 +23,8 @@ import com.revenuecat.purchases.ui.revenuecatui.Paywall
import com.revenuecat.purchases.ui.revenuecatui.PaywallListener
import com.revenuecat.purchases.ui.revenuecatui.PaywallOptions
import com.revenuecat.purchases.ui.revenuecatui.fonts.FontProvider
import com.revenuecat.purchases.ui.revenuecatui.fonts.GoogleFontProvider
import com.revenuecat.purchases.ui.revenuecatui.fonts.PaywallFont
import com.revenuecat.purchases.ui.revenuecatui.fonts.TypographyType

/**
Expand All @@ -43,10 +47,23 @@ internal class PaywallActivity : ComponentActivity(), PaywallListener {
}

private fun getFontProvider(): FontProvider? {
val fontsMap = getArgs()?.fonts?.mapValues { entry ->
entry.value?.let { fontRes ->
ResourcesCompat.getFont(this, fontRes)?.let { FontFamily(it) }
val googleFontProviders = mutableMapOf<GoogleFontProvider, GoogleFont.Provider>()
val fontsMap = getArgs()?.fonts?.mapValues { (_, fontFamily) ->
val fonts = fontFamily?.fonts?.map { font ->
when (font) {
is PaywallFont.ResourceFont -> Font(font.resourceId, font.fontWeight, font.fontStyle)
is PaywallFont.GoogleFont -> {
val googleFontProvider = font.fontProvider
val provider = googleFontProviders.getOrElse(googleFontProvider) {
val googleProvider = googleFontProvider.toGoogleProvider()
googleFontProviders[googleFontProvider] = googleProvider
googleProvider
}
Font(GoogleFont(font.fontName), provider, font.fontWeight, font.fontStyle)
}
}
}
fonts?.let { FontFamily(it) }
} ?: return null
return object : FontProvider {
override fun getFont(type: TypographyType): FontFamily? {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
package com.revenuecat.purchases.ui.revenuecatui.activity

import android.os.Parcelable
import com.revenuecat.purchases.ui.revenuecatui.fonts.FontResourceProvider
import com.revenuecat.purchases.ui.revenuecatui.fonts.ParcelizableFontProvider
import com.revenuecat.purchases.ui.revenuecatui.fonts.PaywallFontFamily
import com.revenuecat.purchases.ui.revenuecatui.fonts.TypographyType
import kotlinx.parcelize.Parcelize

@Parcelize
internal data class PaywallActivityArgs(
val offeringId: String? = null,
val fonts: Map<TypographyType, Int?>? = null,
val fonts: Map<TypographyType, PaywallFontFamily?>? = null,
) : Parcelable {
constructor(offeringId: String? = null, fontProvider: FontResourceProvider?) : this(
constructor(offeringId: String? = null, fontProvider: ParcelizableFontProvider?) : this(
offeringId,
fontProvider?.let { TypographyType.values().associateBy({ it }, { fontProvider.getFontResourceId(it) }) },
fontProvider?.let { TypographyType.values().associateBy({ it }, { fontProvider.getFont(it) }) },
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.fragment.app.Fragment
import com.revenuecat.purchases.CustomerInfo
import com.revenuecat.purchases.Offering
import com.revenuecat.purchases.ui.revenuecatui.fonts.FontResourceProvider
import com.revenuecat.purchases.ui.revenuecatui.fonts.ParcelizableFontProvider
import com.revenuecat.purchases.ui.revenuecatui.helpers.shouldDisplayBlockForEntitlementIdentifier
import com.revenuecat.purchases.ui.revenuecatui.helpers.shouldDisplayPaywall

Expand Down Expand Up @@ -40,47 +40,61 @@ class PaywallActivityLauncher {
/**
* Launch the paywall activity.
* @param offering The offering to be shown in the paywall. If null, the current offering will be shown.
* @param fontProvider The [ParcelizableFontProvider] to be used in the paywall. If null, the default fonts
* will be used.
*/
@JvmOverloads
fun launch(offering: Offering? = null, fontResourceProvider: FontResourceProvider? = null) {
fun launch(offering: Offering? = null, fontProvider: ParcelizableFontProvider? = null) {
activityResultLauncher.launch(
PaywallActivityArgs(
offeringId = offering?.identifier,
fontProvider = fontResourceProvider,
fontProvider = fontProvider,
),
)
}

/**
* Launch the paywall activity if the current user does not have [requiredEntitlementIdentifier] active.
* @param offering The offering to be shown in the paywall. If null, the current offering will be shown.
* @param fontProvider The [ParcelizableFontProvider] to be used in the paywall. If null, the default fonts
* will be used.
* @param requiredEntitlementIdentifier the paywall will be displayed only if the current user does not
* have this entitlement active.
*/
@JvmOverloads
fun launchIfNeeded(
offering: Offering? = null,
fontProvider: ParcelizableFontProvider? = null,
requiredEntitlementIdentifier: String,
) {
launchIfNeeded(
offering = offering,
fontProvider = fontProvider,
shouldDisplayBlock = shouldDisplayBlockForEntitlementIdentifier(requiredEntitlementIdentifier),
)
}

/**
* Launch the paywall activity based on whether the result of [shouldDisplayBlock] is true.
* @param offering The offering to be shown in the paywall. If null, the current offering will be shown.
* @param fontProvider The [ParcelizableFontProvider] to be used in the paywall. If null, the default fonts
* will be used.
* @param shouldDisplayBlock the paywall will be displayed only if this returns true.
*/
@JvmOverloads
fun launchIfNeeded(
offering: Offering? = null,
fontProvider: ParcelizableFontProvider? = null,
shouldDisplayBlock: (CustomerInfo) -> Boolean,
) {
shouldDisplayPaywall(shouldDisplayBlock) { shouldDisplay ->
if (shouldDisplay) {
activityResultLauncher.launch(PaywallActivityArgs(offeringId = offering?.identifier))
activityResultLauncher.launch(
PaywallActivityArgs(
offeringId = offering?.identifier,
fontProvider = fontProvider,
),
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.revenuecat.purchases.ui.revenuecatui.fonts

import androidx.compose.ui.text.font.FontFamily

/**
* Class that allows to provide a font family for all text styles.
* @param fontFamily the [FontFamily] to be used for all text styles.
*/
class CustomFontProvider(private val fontFamily: FontFamily) : FontProvider {
override fun getFont(type: TypographyType) = fontFamily
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.revenuecat.purchases.ui.revenuecatui.fonts

/**
* Class that allows to provide a font family for all text styles.
* @param fontFamily the [PaywallFontFamily] to be used for all text styles.
*/
class CustomParcelizableFontProvider(
private val fontFamily: PaywallFontFamily,
) : ParcelizableFontProvider {
override fun getFont(type: TypographyType) = fontFamily
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.ui.text.font.FontFamily
/**
* Implement this interface to provide custom fonts to the [PaywallView]. If you don't, the current material3 theme
* typography will be used.
* If you only want to use a single FontFamily for all text styles use [CustomFontProvider].
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻

* This can't be used when launching the paywall as an activity since the fonts are not parcelable/serializable.
* Use [FontResourceProvider] instead.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.revenuecat.purchases.ui.revenuecatui.fonts

import android.os.Parcel
import androidx.compose.ui.text.font.FontStyle
import kotlinx.parcelize.Parceler

internal object FontStyleParceler : Parceler<FontStyle> {
override fun create(parcel: Parcel) = FontStyle(parcel.readInt())

override fun FontStyle.write(parcel: Parcel, flags: Int) {
parcel.writeInt(value)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.revenuecat.purchases.ui.revenuecatui.fonts

import android.os.Parcel
import androidx.compose.ui.text.font.FontWeight
import kotlinx.parcelize.Parceler

internal object FontWeightParceler : Parceler<FontWeight> {
override fun create(parcel: Parcel) = FontWeight(parcel.readInt())

override fun FontWeight.write(parcel: Parcel, flags: Int) {
parcel.writeInt(weight)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.revenuecat.purchases.ui.revenuecatui.fonts

import android.os.Parcelable
import androidx.annotation.ArrayRes
import androidx.compose.ui.text.googlefonts.GoogleFont
import kotlinx.parcelize.Parcelize

/**
* Represents a Google font provider.
*/
@Parcelize
data class GoogleFontProvider(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this data class? Can we use GoogleFont.Provider directly? It's pretty much the same but with defaults right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh and it's parcelable, is that the reason?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, GoogleFont.Provider is not parcelable... I thought about writing a custom parceler, but we don't have access to the underlying data.

/**
* The resource ID of the font provider's certificate(s).
*/
@ArrayRes val certificates: Int,
val providerAuthority: String = "com.google.android.gms.fonts",
val providerPackage: String = "com.google.android.gms",
) : Parcelable {

fun toGoogleProvider(): GoogleFont.Provider {
return GoogleFont.Provider(
providerAuthority,
providerPackage,
certificates,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
package com.revenuecat.purchases.ui.revenuecatui.fonts

import androidx.annotation.FontRes
import com.revenuecat.purchases.ui.revenuecatui.activity.PaywallActivityLauncher

/**
* Implement this interface to provide custom fonts to the [PaywallActivityLauncher].
* If you don't, the default material3 theme fonts will be used.
* If you only want to use a single [PaywallFontFamily] for all text styles use [CustomParcelizableFontProvider].
* Use [FontProvider] instead if you are using Compose with [PaywallView] or [PaywallDialog].
*/
interface FontResourceProvider {
interface ParcelizableFontProvider {
/**
* Returns the font resource id to be used for the given [TypographyType]. If null is returned,
* Returns the [PaywallFontFamily] to be used for the given [TypographyType]. If null is returned,
* the default font will be used.
* @param type the [TypographyType] for which the font is being requested.
* @return the font resource id to be used for the given [TypographyType].
* @return the [PaywallFontFamily] to be used for the given [TypographyType].
*/
@FontRes
fun getFontResourceId(type: TypographyType): Int?
fun getFont(type: TypographyType): PaywallFontFamily?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.revenuecat.purchases.ui.revenuecatui.fonts

import android.os.Parcelable
import androidx.annotation.FontRes
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.TypeParceler

/**
* Represents a font. You can create either a [GoogleFont] or a [ResourceFont].
*/
sealed class PaywallFont : Parcelable {
/**
* Represents a downloadable Google Font.
*/
@Parcelize
data class GoogleFont(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually wondered whether we wanted to offer a similar API for compose... But I think it's ok to leave the responsibility of building the FontFamily (from any source) to the developer

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would that look like?

Copy link
Contributor Author

@tonidero tonidero Oct 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically it would be to allow using a ParcelableFontProvider in PaywallViewOptions, so both using a GoogleFont or a ResourceFont are allowed... This would complicate the API though. And again, I think if they are using a custom font from those places, it's easier for them to just pass us the FontFamily directly in compose world, where there is no need for this data to be parcelable.

/**
* Name of the Google font you want to use.
*/
val fontName: String,
/**
* Provider of the Google font.
*/
val fontProvider: GoogleFontProvider,
/**
* The weight of the font. The system uses this to match a font to a font request.
*/
@TypeParceler<FontWeight, FontWeightParceler>()
val fontWeight: FontWeight = FontWeight.Normal,
/**
* The style of the font, normal or italic. The system uses this to match a font to a font request.
*/
@TypeParceler<FontStyle, FontStyleParceler>()
val fontStyle: FontStyle = FontStyle.Normal,
) : PaywallFont()

@Parcelize
data class ResourceFont(
/**
* The resource ID of the font file in font resources.
*/
@FontRes
val resourceId: Int,
/**
* The weight of the font. The system uses this to match a font to a font request.
*/
@TypeParceler<FontWeight, FontWeightParceler>()
val fontWeight: FontWeight = FontWeight.Normal,
/**
* The style of the font, normal or italic. The system uses this to match a font to a font request.
*/
@TypeParceler<FontStyle, FontStyleParceler>()
val fontStyle: FontStyle = FontStyle.Normal,
) : PaywallFont()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.revenuecat.purchases.ui.revenuecatui.fonts

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

/**
* Represents a font family. You can add one ore more [PaywallFont] with different weights and font styles.
*/
@Parcelize
data class PaywallFontFamily(val fonts: List<PaywallFont>) : Parcelable
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably not have this and just have a list of fonts... But seemed like a better idea to have a wrapper to mimick what it looks like for Google.