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: Add support to launch paywall as activity #1317

Merged
merged 6 commits into from
Oct 9, 2023

Conversation

tonidero
Copy link
Contributor

@tonidero tonidero commented Oct 6, 2023

Description

Adds a PaywallActivityLauncher class that devs can instantiate in their activity/fragment onCreate in order to launch the PaywallActivity which is a wrapper around the full screen PaywallView. Then, they receive the result in a PaywallResultHandler interface.

Once they want to launch the activity, they can call the launch method in that PaywallActivityLauncher.

@tonidero tonidero marked this pull request as ready for review October 6, 2023 10:52
@tonidero tonidero requested a review from a team October 6, 2023 10:52
@@ -53,6 +62,8 @@ dependencies {
implementation libs.androidx.lifecycle.viewmodel.compose
implementation libs.coil.compose
implementation libs.coil.svg
implementation libs.activity.compose
implementation libs.androidx.fragment.ktx
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've been thinking about splitting all this into a different module, so we don't clutter the ui:revenuecat dependency for people not using activities/fragments. The idea would be to have a different dependency for people using activities fragments with all these codes and dependencies.

The down side is to further split into more modules, and we might need to expose some things like the setOfferingId in the PaywallViewOptions... But I think it might be ok?

Copy link
Contributor

Choose a reason for hiding this comment

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

I would keep it in the same module, so we don't end up with many modules again. But it's true androidx.activity:activity-compose is 1.1 MB, which is not small compared to the 300 KB the billing client takes

Copy link
Contributor

Choose a reason for hiding this comment

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

Is 1MB much these days? 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, usually it's much less than that for devs minifying their apps. It's true that we're using a ton of dependencies in the revenuecatui module, so I wasn't sure about trying to optimize it a bit... But yeah, we can keep it in one module for now and avoid having a ton of modules 👍

@@ -24,6 +25,9 @@ android {

defaultConfig {
minSdkVersion 24 // MeasureFormat requires API 24
vectorDrawables {
useSupportLibrary true
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was added by the wizard when I created the activity. Need to test whether it's actually needed

resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same, added by the wizard. This shouldn't really hurt at all.

@codecov
Copy link

codecov bot commented Oct 6, 2023

Codecov Report

All modified lines are covered by tests ✅

Comparison is base (0cb8c8e) 85.06% compared to head (5968c44) 85.06%.

❗ Current head 5968c44 differs from pull request most recent head 6826ef7. Consider uploading reports for the commit 6826ef7 to get more accurate results

Additional details and impacted files
@@            Coverage Diff            @@
##           paywalls    #1317   +/-   ##
=========================================
  Coverage     85.06%   85.06%           
=========================================
  Files           193      193           
  Lines          6475     6475           
  Branches        942      942           
=========================================
  Hits           5508     5508           
  Misses          600      600           
  Partials        367      367           
Files Coverage Δ
...src/main/kotlin/com/revenuecat/purchases/errors.kt 100.00% <100.00%> (ø)

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@@ -53,6 +62,8 @@ dependencies {
implementation libs.androidx.lifecycle.viewmodel.compose
implementation libs.coil.compose
implementation libs.coil.svg
implementation libs.activity.compose
implementation libs.androidx.fragment.ktx
Copy link
Contributor

Choose a reason for hiding this comment

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

I would keep it in the same module, so we don't end up with many modules again. But it's true androidx.activity:activity-compose is 1.1 MB, which is not small compared to the 300 KB the billing client takes

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
paywallActivityLauncher = PaywallActivityLauncher(this, this)
Copy link
Contributor

Choose a reason for hiding this comment

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

What if we make PaywallResultHandler a lambda like this:

typealias PaywallResultHandler = (PaywallResult) -> Unit

Then in the constructor of the launcher, we can handle the ActivityResultCallback there directly:

    constructor(activity: ComponentActivity, resultHandler: PaywallResultHandler) {
        activityResultLauncher = activity.registerForActivityResult(PaywallContract()) { paywallResult ->
            resultHandler(it)
        }
    }

Then, the activity can be started like this:

paywallActivityLauncher = PaywallActivityLauncher(this) { result ->
    Log.e("PaywallsTester", "LAUNCH PAYWALL RESULT: $result")
}

This way, folks don't have to make their Activity conform to PaywallResultHandler (ActivityResultCallback) and override onActivityResult.

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 thought about that... My reasoning to go this way is in case we ever want to add another callback or anything, this would be more extensible even though slightly harder to use... Maybe it's ok though. We can discuss that

Copy link
Contributor

Choose a reason for hiding this comment

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

For adding another callback, we could have it as:

    constructor(activity: ComponentActivity, resultHandler: PaywallResultHandler, anotherCallback: NewCallback) {

And then we could do ourselves the complicated part, which is figuring out which callback to call using the activity result code. Otherwise devs would have to handle that

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, that's not as extensible IMO... Another point to consider is that this PaywallActivityLauncher needs to be created during the onCreate, it can't be created after (it will fail at runtime if you try). In that case, you would have a callback at the activity level, not inside a method, so you still need to reference some other class there. I think for a class variable, an interface is a more common pattern... What do you think?

Copy link
Contributor

@vegaro vegaro Oct 6, 2023

Choose a reason for hiding this comment

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

Makes sense. Using the interface is actually more flexible + it's using a "system" interface, that devs will probably be familiar with. Let's do the interface

@@ -5,25 +5,32 @@ import com.revenuecat.purchases.Offering
class PaywallViewOptions(builder: Builder) {

val offering: Offering?
internal val offeringId: String?
Copy link
Contributor

Choose a reason for hiding this comment

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

How come this is needed if we already have Offering?

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 can't pass an Offering object to the new activity since it's not serializable. As a fallback, in order to display a specific offering in the activity, we allow to pass an offering id and load the offering again from the paywall view itself.

I'm not exposing that API publicly right now, so devs still need to pass the full Offering object normally, this would only be used for activities

Copy link
Contributor

Choose a reason for hiding this comment

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

This is a weird definition of the options then.
What do you think about turning this into an enum:

sealed class Selection {
    data class Offering(val offering: Offering) : Selection()
    data class OfferingIdentifier(val id: String) : Selection()
    object None : Selection()
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm well, I'm currently not exposing the option to pass the identifier to the paywallView publicly. I guess we could hold that type internally to decide what to do though, since they are indeed exclusive. Will do that!

/**
* Result of the paywall activity.
*/
sealed class PaywallResult : Parcelable {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this usable from Java too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's usable yes, though you don't really have a nice switch as you do in Kotlin, you basically have to check the type and perform the downcast to the corresponding type. It's really a much nicer API in Kotlin, but still usable in Java.

Copy link
Contributor

Choose a reason for hiding this comment

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

👍🏻

@tonidero tonidero force-pushed the toniricodiez/pwl-276-support-fragments branch from 3a4bdaf to 5968c44 Compare October 9, 2023 07:04
@tonidero
Copy link
Contributor Author

tonidero commented Oct 9, 2023

Will merge this, if there are any further comments I can address separately

@tonidero tonidero enabled auto-merge (squash) October 9, 2023 09:57
@tonidero tonidero merged commit e44d4ce into paywalls Oct 9, 2023
5 checks passed
@tonidero tonidero deleted the toniricodiez/pwl-276-support-fragments branch October 9, 2023 10:11
tonidero added a commit that referenced this pull request Oct 31, 2023
### Description
Adds a `PaywallActivityLauncher` class that devs can instantiate in
their activity/fragment `onCreate` in order to launch the
`PaywallActivity` which is a wrapper around the full screen
`PaywallView`. Then, they receive the result in a `PaywallResultHandler`
interface.

Once they want to launch the activity, they can call the `launch` method
in that `PaywallActivityLauncher`.
NachoSoto added a commit that referenced this pull request Oct 31, 2023
### New Features
#### ✨📱 Introducing Android Paywalls 🐾🧱

RevenueCat's Paywalls allow you to remotely configure your entire
paywall view without any code changes or app updates.
Our paywall templates use native code to deliver smooth, intuitive
experiences to your customers when you’re ready to deliver them an
Offering; and you can use our Dashboard to pick the right template and
configuration to meet your needs.

To use RevenueCat Paywalls on Android, simply:

1. Create a Paywall on the Dashboard for the `Offering` you intend to
serve to your customers
2. Add the `RevenueCatUI` dependency to your project:
```groovy build.gradle
implementation 'com.revenuecat.purchases:purchases:7.1.0'
implementation 'com.revenuecat.purchases:purchases-ui:7.1.0'
```
3. Display a paywall:
```kotlin
@OptIn(ExperimentalPreviewRevenueCatUIPurchasesAPI::class)
@composable
private fun LockedScreen() {
    YourContent()

    PaywallDialog(
        PaywallDialogOptions.Builder()
            .setRequiredEntitlementIdentifier(Constants.ENTITLEMENT_ID)
            .build()
    )
}
```

> **Note**
> Android paywalls is currently behind an experimental flag:
`ExperimentalPreviewRevenueCatUIPurchasesAPI`.
> It is safe to release app updates with it. We guarantee that paywalls
will continue to work and any changes will always be backwards
compatible.
> They are stable, but migration steps may be required in the future.
We'll do our best to minimize any changes you have to make.

You can find more information in [our
documentation](https://rev.cat/paywalls).

<details>

<summary>List of changes</summary>

* `Paywalls`: Improve PaywallDialog look on tablets (#1419) via Toni
Rico (@tonidero)
* `Paywalls`: disallow Markdown links in PurchaseButton (#1421) via
NachoSoto (@NachoSoto)
* `Paywalls`: implemented template 5 (#1412) via NachoSoto (@NachoSoto)
* `Paywalls`: Fix wrong view model data after configuration changes
(#1417) via Toni Rico (@tonidero)
* `Paywalls`: update template 4 colors (#1410) via NachoSoto
(@NachoSoto)
* `Paywalls`: Add bundled font to paywall tester (#1415) via Toni Rico
(@tonidero)
* `Paywalls`: Fix purchase button text flashing when changing selected
package (#1416) via Toni Rico (@tonidero)
* `Paywalls`: support text3 and accent3 colors (#1409) via NachoSoto
(@NachoSoto)
* `Paywalls`: PaywallData errors shouldn't make Offerings fail to decode
(#1402) via NachoSoto (@NachoSoto)
* `Paywalls`: convert empty strings to null via NachoSoto (@NachoSoto)
* `Paywalls`: footer view should not render background image via
NachoSoto (@NachoSoto)
* `Paywalls`: improve template 4 A11y support via NachoSoto (@NachoSoto)
* `Paywalls`: Add close button option (#1390) via Toni Rico (@tonidero)
* `Paywalls`: fix MarkdownText stealing touches from package buttons
(#1411) via NachoSoto (@NachoSoto)
* Fix onDismiss getting called twice in PaywallDialogs (#1408) via Cesar
de la Vega (@vegaro)
* `Paywalls`: ignore URL deserialization errors (#1399) via NachoSoto
(@NachoSoto)
* Animate transition between loading and loaded paywall (#1404) via
Cesar de la Vega (@vegaro)
* Fix button padding in Loading paywall (#1405) via Cesar de la Vega
(@vegaro)
* Add test for `packageInfo.localizedDiscount` (#1407) via Cesar de la
Vega (@vegaro)
* `Paywalls`: RemoteImage always renders a placeholder on previews via
NachoSoto (@NachoSoto)
* `Paywalls`: decode empty images as null via NachoSoto (@NachoSoto)
* `Paywalls`: Allow trigger manual paywall tester release (#1406) via
Toni Rico (@tonidero)
* Fix navigation after closing paywall in Paywalls screen (#1403) via
Cesar de la Vega (@vegaro)
* `Paywalls`: Add experimental annotation to all public APIs in
RevenueCat UI (#1400) via Toni Rico (@tonidero)
* `Paywalls`: improve template 2 layout (#1396) via NachoSoto
(@NachoSoto)
* `Paywalls`: template 4 (#1349) via NachoSoto (@NachoSoto)
* `Paywalls`: fix template 3 offer details color (#1394) via NachoSoto
(@NachoSoto)
* `Paywalls`: PaywallDialogOptions no longer requires dismissRequest
(#1386) via NachoSoto (@NachoSoto)
* `Paywalls`: fixed accessibility across templates (#1392) via NachoSoto
(@NachoSoto)
* `Paywalls`: increase PaywallBackground blur radius to match iOS via
NachoSoto (@NachoSoto)
* `Paywalls`: use LocalUriHandler for opening links (#1388) via
NachoSoto (@NachoSoto)
* `Paywalls`: support for Markdown links (#1387) via NachoSoto
(@NachoSoto)
* `Paywalls`: display purchase/restore errors (#1384) via NachoSoto
(@NachoSoto)
* `Paywalls`: improve error handling (#1383) via NachoSoto (@NachoSoto)
* `Paywalls`: fixed incorrect background on footer (#1382) via NachoSoto
(@NachoSoto)
* `Paywalls`: fix backwards-compatible blurring of default paywall
background (#1380) via NachoSoto (@NachoSoto)
* `Paywalls`: polish template 2 spacing (#1381) via NachoSoto
(@NachoSoto)
* `Paywalls`: optimize backwards compatible blurring (#1379) via
NachoSoto (@NachoSoto)
* `Paywalls`: Predownload offering images if paywalls sdk exists (#1372)
via Toni Rico (@tonidero)
* `Paywalls`: PurchaseButton supports gradients (#1378) via NachoSoto
(@NachoSoto)
* `Paywalls`: optimize AdaptiveComposable (#1377) via NachoSoto
(@NachoSoto)
* `Paywalls`: improve FooterDialog corner radius (#1374) via NachoSoto
(@NachoSoto)
* `Paywalls`: optimize PurchaseButton (#1376) via NachoSoto (@NachoSoto)
* `Paywalls`: Enable footer modes in paywall tester paywalls tab (#1368)
via Toni Rico (@tonidero)
* `Paywalls`: Support footer in template 3 (#1367) via Toni Rico
(@tonidero)
* `Paywalls`: calculate discountRelativeToMostExpensivePerMonth (#1370)
via NachoSoto (@NachoSoto)
* `Paywalls`: Add offer details in template 2 when in condensed form
(#1371) via Toni Rico (@tonidero)
* `Paywalls`: improve handling of lifetime/custom packages (#1363) via
NachoSoto (@NachoSoto)
* `Paywalls`: Support footer in template 1 (#1366) via Toni Rico
(@tonidero)
* Add `StoreProduct.pricePerMonth` (#1369) via NachoSoto (@NachoSoto)
* `Paywalls`: Support condensed footer presentation in template 2
(#1365) via Toni Rico (@tonidero)
* `Paywalls`: finished localization support (#1362) via NachoSoto
(@NachoSoto)
* `Paywalls`: backwards compatible blurring (#1327) via Andy Boedo
(@AndyBoedo)
* `Paywalls`: PaywallViewModel tests (#1357) via NachoSoto (@NachoSoto)
* `Paywalls`: improve LoadingPaywall (#1364) via NachoSoto (@NachoSoto)
* `Paywalls`: Fix paywall compose previews (#1360) via Toni Rico
(@tonidero)
* `Paywalls`: Fix proguard rules kotlinx serialization (#1356) via Toni
Rico (@tonidero)
* `Paywalls`: Add custom font example to paywalls screen (#1358) via
Toni Rico (@tonidero)
* `Paywalls`: Support Google fonts and font families with multiple fonts
(#1338) via Toni Rico (@tonidero)
* `Paywalls`: Support custom fonts through FontProvider (#1328) via Toni
Rico (@tonidero)
* `Paywalls`: fixed Footer padding (#1354) via NachoSoto (@NachoSoto)
* `Paywalls`: Rename PaywallView to Paywall (#1351) via Toni Rico
(@tonidero)
* `Paywalls`: disable PurchaseButton during purchases (#1352) via
NachoSoto (@NachoSoto)
* `Paywalls`: enable library publishing (#1353) via NachoSoto
(@NachoSoto)
* `Paywalls`: handle "action in progress" state (#1346) via NachoSoto
(@NachoSoto)
* `Paywalls`: support {{ sub_duration_in_months }} (#1348) via NachoSoto
(@NachoSoto)
* `Paywalls`: disallow purchasing currently subscribed products (#1334)
via NachoSoto (@NachoSoto)
* `Paywalls`: new PaywallActivityLauncher.launchIfNeeded methods (#1335)
via NachoSoto (@NachoSoto)
* `Paywalls`: PaywallColor supports RGBA (#1332) via NachoSoto
(@NachoSoto)
* `Paywalls`: Make DialogScaffold private (#1329) via Toni Rico
(@tonidero)
* Add `revenuecatui` `gradle.properties` to specify name of dependency
(#1324) via Toni Rico (@tonidero)
* `Paywalls`: log error when failing to load images (#1321) via
NachoSoto (@NachoSoto)
* Log errors when displaying default paywall (#1318) via Cesar de la
Vega (@vegaro)
* Rename packages to packageIds in `PaywallData` (#1309) via Cesar de la
Vega (@vegaro)
* Add support for multiple intro offers in `IntroEligibilityStateView`
(#1319) via Cesar de la Vega (@vegaro)
* Fix material theme references to use Material3 versions (#1326) via
Toni Rico (@tonidero)
* `Paywalls`: Add support to launch paywall as activity (#1317) via Toni
Rico (@tonidero)
* Parse `{{ sub_offer_price_2 }}` and `{{ sub_offer_duration_2 }}`
variables (#1313) via Cesar de la Vega (@vegaro)
* `Paywalls`: changed PaywallsTester app icon (#1323) via NachoSoto
(@NachoSoto)
* `Paywalls`: fixed PaywallDialog.setRequiredEntitlementIdentifier
(#1322) via NachoSoto (@NachoSoto)
* `Paywalls`: Markdown support (#1312) via NachoSoto (@NachoSoto)
* `PaywallsTester`: added template name to offerings list (#1316) via
NachoSoto (@NachoSoto)
* `Paywalls`: Update paywall tester to be able to display paywall footer
(#1315) via Toni Rico (@tonidero)
* `Paywalls`: Add PaywallFooter composable to present a minified paywall
UI that allows for custom paywalls (#1314) via Toni Rico (@tonidero)
* `Paywalls`: use IntroEligibilityStateView (#1311) via NachoSoto
(@NachoSoto)
* `PaywallData` validation tests (#1310) via Cesar de la Vega (@vegaro)
* `Paywalls`: implemented LoadingPaywallView with placeholder (#1284)
via NachoSoto (@NachoSoto)
* `Paywalls`: created LoadingPaywallView (#1282) via NachoSoto
(@NachoSoto)
* Fix template test data (#1308) via Cesar de la Vega (@vegaro)
</details>

### Other Changes
Add `purchases.setOnesignalUserID` (#1304) via Raquel Diez
(@Raquel10-RevenueCat)

---------

Co-authored-by: revenuecat-ops <[email protected]>
Co-authored-by: NachoSoto <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants