Skip to content
Merged
Show file tree
Hide file tree
Changes from 69 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
53e264a
Add RevenueCatUI package
facumenzella Sep 10, 2025
f9683a5
revert code
facumenzella Sep 10, 2025
0a407d2
Add minimum
facumenzella Sep 10, 2025
151ef83
Moved files to RevenueCat/Android
facumenzella Sep 15, 2025
45ad7d0
Moved files from Runtime to Scripts
facumenzella Sep 15, 2025
c4ac9de
Use SimpleJSON
facumenzella Sep 15, 2025
e20e4ce
delete extra .meta
facumenzella Sep 15, 2025
cdd8ea2
Updated bool IsSupported(); doc
facumenzella Sep 15, 2025
be566dd
Added pure code options, removed GameObjecft
facumenzella Sep 15, 2025
c550a2e
Split callback
facumenzella Sep 15, 2025
d1d075e
added debug logs
facumenzella Sep 15, 2025
1a0e59c
Remove StubPaywallPresenter.cs
facumenzella Sep 15, 2025
8b7784b
Delete RevenueCatUICallbackHandler.cs
facumenzella Sep 15, 2025
69096d4
Add missing pieces
facumenzella Sep 15, 2025
c191052
Merge branch 'main' into feat/add-revenuecatui
facumenzella Sep 15, 2025
1ee5dcc
ready
facumenzella Sep 15, 2025
2cbfef6
remove reference to ui package
facumenzella Sep 15, 2025
160ed40
rename to com.revenuecat.unity.ui
facumenzella Sep 16, 2025
e03df34
Merge branch 'main' into feat/add-revenuecatui
facumenzella Sep 16, 2025
c3bd377
Update Subtester/Assets/Scenes/Main.unity
facumenzella Sep 17, 2025
0ad7ba1
Remove extra Stub.meta
facumenzella Sep 17, 2025
a8591ee
Remove README
facumenzella Sep 17, 2025
ffca566
Rename .asmdef files
facumenzella Sep 17, 2025
5417e16
Remove customer center code
facumenzella Sep 17, 2025
9ec1edf
Merge branch 'feat/add-revenuecatui' of github.com:RevenueCat/purchas…
facumenzella Sep 17, 2025
6a43d86
remove _isSupportedCache
facumenzella Sep 17, 2025
0a829d0
Update Subtester/Packages/manifest.json
facumenzella Sep 17, 2025
18b9ba1
renamed editor asmdef and deleted extra files
facumenzella Sep 17, 2025
21ac4df
Merge branch 'feat/add-revenuecatui' of github.com:RevenueCat/purchas…
facumenzella Sep 17, 2025
17aed4a
revert Subtester
facumenzella Sep 17, 2025
d3cca45
Update Subtester/Assets/Plugins/Android/mainTemplate.gradle
facumenzella Sep 17, 2025
f51be9e
Update Subtester/ProjectSettings/AndroidResolverDependencies.xml
facumenzella Sep 17, 2025
1e1ee01
Make properties internal
facumenzella Sep 17, 2025
0143afe
Merge branch 'feat/add-revenuecatui' of github.com:RevenueCat/purchas…
facumenzella Sep 17, 2025
2693a44
minimal paywalls presentation
vegaro Aug 28, 2025
97dec08
clean up
vegaro Sep 17, 2025
980a0e5
Merge branch 'main' into paywalls-android-poc
vegaro Sep 17, 2025
b9bb1e0
clean up
vegaro Sep 17, 2025
b048332
clean up
vegaro Sep 22, 2025
cfbc8d9
move into androidlib
vegaro Sep 22, 2025
1c4c865
add local dependency
vegaro Sep 22, 2025
f072bde
updated mainTemplate.gradle
vegaro Sep 22, 2025
2457709
update PurchasesListener
vegaro Sep 22, 2025
44bc909
gitignore
vegaro Sep 22, 2025
eb27acf
add log to AndroidPaywallPresenter
vegaro Sep 22, 2025
5da75f8
add displayCloseButton
vegaro Sep 22, 2025
56258da
fix compilation of Subtester when exporting package
vegaro Sep 22, 2025
be4ef13
use componentActivity
vegaro Sep 22, 2025
649cff4
presentPaywallIfNeeded
vegaro Sep 22, 2025
e55f375
add close button to log
vegaro Sep 22, 2025
26961f3
add PresentPaywallIfNeeded to PurchasesListener
vegaro Sep 22, 2025
07afd3d
update AndroidResolverDependencies
vegaro Sep 22, 2025
dde2af5
implements PaywallResultHandler
vegaro Sep 26, 2025
96d73f6
rename to PaywallTrampolineActivity and made transparent
vegaro Sep 26, 2025
cf1a839
remove unneeded comment
vegaro Sep 26, 2025
09651b4
TODOs on logs
vegaro Sep 26, 2025
8065057
remove UnityBridge
vegaro Sep 26, 2025
82643ac
Paywalls iOS (#675)
facumenzella Sep 26, 2025
52a8260
Merge branch 'main' into paywalls-android-poc
vegaro Sep 29, 2025
40f1404
small PR comments
vegaro Sep 29, 2025
830e978
Merge branch 'paywalls-android-poc' of github.com:RevenueCat/purchase…
vegaro Sep 29, 2025
588ea5d
Another TODO
vegaro Sep 29, 2025
79dfe20
static functions in trampoline activity
vegaro Sep 29, 2025
bb5af22
create PaywallUnityOptions
vegaro Sep 29, 2025
34b2f4a
add PaywallUnityOptions to signature
vegaro Sep 29, 2025
bba5ea9
remove gameObjectName from parameters
vegaro Sep 29, 2025
903aad4
fix versions
vegaro Sep 30, 2025
bed291d
Extract paywall result strings to constants in PaywallTrampolineActiv…
vegaro Oct 1, 2025
3ec3b42
fix indentation
vegaro Oct 1, 2025
cba2097
change default display close button
vegaro Oct 1, 2025
aabecf5
AndroidApplication.currentActivity
vegaro Oct 1, 2025
bcc83a6
better handling of exceptions on AndroidJavaClass
vegaro Oct 1, 2025
b36f845
remove MonoBehaviour
vegaro Oct 1, 2025
e032186
remove ?? "default"
vegaro Oct 1, 2025
ed13583
Merge branch 'main' into paywalls-android-poc
vegaro Oct 1, 2025
c4fa9e7
fix PurchasesListener
vegaro Oct 2, 2025
1601b1b
update version
vegaro Oct 2, 2025
08005f2
Merge branch 'main' into paywalls-android-poc
vegaro Oct 3, 2025
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ fastlane/.env
# CircleCI folders
vendor/
.bundle/
RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib/build
1 change: 0 additions & 1 deletion RevenueCat/Scripts/Purchases.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1499,7 +1499,6 @@ private void _handleLog(string logDetailsJson)
LogHandler(logLevel, messageInResponse);
}


// ReSharper disable once UnusedMember.Local
private void _restorePurchases(string customerInfoJson)
{
Expand Down
19 changes: 0 additions & 19 deletions RevenueCatUI/Editor/RevenueCatUI.Editor.asmdef

This file was deleted.

65 changes: 65 additions & 0 deletions RevenueCatUI/Plugins/Android/RevenueCatUI.androidlib.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application>
<activity
android:name=".PaywallTrampolineActivity"
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
tools:node="merge" />
</application>

<!-- No special permissions required -->

</manifest>


Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
apply plugin: 'com.android.library'

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.revenuecat.purchases:purchases-hybrid-common:17.8.0'
implementation 'com.revenuecat.purchases:purchases-hybrid-common-ui:17.8.0'
implementation 'androidx.activity:activity:1.8.2'
// TODO: Automatically update this to the latest version
}

android {
namespace 'com.revenuecat.purchasesunity.ui'

compileSdk getProperty("unity.compileSdkVersion") as int
buildToolsVersion = getProperty("unity.buildToolsVersion")

compileOptions {
sourceCompatibility JavaVersion.valueOf(getProperty("unity.javaCompatabilityVersion"))
targetCompatibility JavaVersion.valueOf(getProperty("unity.javaCompatabilityVersion"))
}

sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src/main/java']
res.srcDirs = ['src/main/res']
assets.srcDirs = ['src/main/assets']
jniLibs.srcDirs = ['src/main/jniLibs']
}
}

def unityLib = project(':unityLibrary').extensions.getByName('android')

defaultConfig {
consumerProguardFiles "consumer-proguard.pro"
minSdkVersion unityLib.defaultConfig.minSdkVersion.mApiLevel
targetSdkVersion unityLib.defaultConfig.targetSdkVersion.mApiLevel
}

lintOptions {
abortOnError false
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules. pro'
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-keep class com.unity3d.player.** { *; }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Come to think of it, why are we not moving this to PHC? At that point we could even remove the intermediate PaywallFragment, right? In any case, using this trampoline pattern in PHC would also avoid the need for FragmentActivity in purchases-flutter.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I thought about it but I don't know how we would do the UnityBridge.sendMessage(gameObject, method, resultName));, since we can't do onActivityResult directly from RevenueCatUI.java.

I think the only way would be to have a singleton in phc that we use as a message passing mechanism.

Or am I missing anything?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hah, I wrote this before I saw this comment. Great minds and all that 😄

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm so I'm not in love with holding references to objects like the listener statically... I think for most cases it would be ok, but could potentially lead to some memory leaks, and we should make sure the cleanup process works correctly... So in general I would try to avoid it, but considering that in Unity we kinda have to, I could see that we might want to move the logic to PHC in case it can be useful for other hybrids...

IMO, I do prefer the current production pattern we use in the other hybrids, where we don't hold anything statically as long as it works. I would only use this static holder approach when there is no alternative. Do we have other use cases for other hybrids where we need this? if so, I would be ok moving it to PHC. If not, I think better to keep it to Unity, and leave the other hybrids use the existing approach.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think the main advantage would be to move Flutter to this system, so developers don't need to implement FlutterFragmentActivity

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

But I kinda agree that I would prefer the FlutterFragmentActivity approach over the static PaywallResultListenerRegistry as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yea that's fair. I'm totally okay going with this for now!

Down the line we can look at using this pattern in PHC. Maybe we don't move PaywallTrampolineActivity to PHC, but we create a separate one. That way we can still avoid the PaywallFragment, and don't need to rely on keeping a reference to the listener. That way it would work like this:

╭-- Unity-------------------------╮
|           launchPaywall()       |
|                  ||             |
|                  \/             |
|  UnityPaywallTrampolineActivity |
╰---------------------------------╯
                   ||
                   \/ 
╭-- PHC---------------------------╮
|     PaywallTrampolineActivity   |
|                  ||             |
|                  \/             |
|          PaywallActivity        |
╰---------------------------------╯

But again, that's something we can worry about later. Let's go with what we have for now!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

From trampoline to trampoline!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Gymnasts! 💪

Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package com.revenuecat.purchasesunity.ui;

import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.activity.ComponentActivity;

import com.revenuecat.purchases.ui.revenuecatui.activity.PaywallActivityLauncher;
import com.revenuecat.purchases.ui.revenuecatui.activity.PaywallDisplayCallback;
import com.revenuecat.purchases.ui.revenuecatui.activity.PaywallResult;
import com.revenuecat.purchases.ui.revenuecatui.activity.PaywallResultHandler;
import com.revenuecat.purchases.PresentedOfferingContext;

public class PaywallTrampolineActivity extends ComponentActivity implements PaywallResultHandler {
public static final String EXTRA_PAYWALL_OPTIONS = "rc_paywall_options";

private static final String TAG = "PurchasesUnity";

private static final String RESULT_PURCHASED = "PURCHASED";
private static final String RESULT_RESTORED = "RESTORED";
private static final String RESULT_CANCELLED = "CANCELLED";
private static final String RESULT_ERROR = "ERROR";
private static final String RESULT_NOT_PRESENTED = "NOT_PRESENTED";

private PaywallActivityLauncher launcher;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

final Intent source = getIntent();
PaywallUnityOptions options = source.getParcelableExtra(EXTRA_PAYWALL_OPTIONS);

if (options == null) {
Log.e(TAG, "PaywallUnityOptions is null; cannot launch paywall");
RevenueCatUI.sendPaywallResult(RESULT_ERROR);
finish();
return;
}

launcher = new PaywallActivityLauncher(this, this);

if (options.getRequiredEntitlementIdentifier() != null) {
launchPaywallIfNeeded(options);
} else {
launchPaywall(options);
}
}

private void launchPaywallIfNeeded(PaywallUnityOptions options) {
String requiredEntitlementIdentifier = options.getRequiredEntitlementIdentifier();
String offeringId = options.getOfferingId();
boolean shouldDisplayDismissButton = options.getShouldDisplayDismissButton();

if (offeringId == null) {
launcher.launchIfNeeded(
requiredEntitlementIdentifier,
null,
null,
shouldDisplayDismissButton,
Build.VERSION.SDK_INT >= 35,
paywallDisplayResult -> {
if (!paywallDisplayResult) {
RevenueCatUI.sendPaywallResult(RESULT_NOT_PRESENTED);
finish();
}
}
);
} else {
launcher.launchIfNeededWithOfferingId(
requiredEntitlementIdentifier,
offeringId,
new PresentedOfferingContext(offeringId),
null,
shouldDisplayDismissButton,
Build.VERSION.SDK_INT >= 35,
paywallDisplayResult -> {
if (!paywallDisplayResult) {
RevenueCatUI.sendPaywallResult(RESULT_NOT_PRESENTED);
finish();
}
}
);
}
}

private void launchPaywall(PaywallUnityOptions options) {
String offeringId = options.getOfferingId();
boolean shouldDisplayDismissButton = options.getShouldDisplayDismissButton();

// TODO: add support for edge to edge, fonts, and offering context
if (offeringId != null) {
launcher.launchWithOfferingId(
offeringId,
new PresentedOfferingContext(offeringId), // TODO: support passing context
null,
shouldDisplayDismissButton
);
} else {
launcher.launch(
null,
null,
shouldDisplayDismissButton
);
}
}

@Override
public void onActivityResult(PaywallResult result) {
try {
if (result != null) {
sendPaywallResult(result);
}
} finally {
finish();
}
}


private void sendPaywallResult(PaywallResult result) {
final String resultName;
if (result instanceof PaywallResult.Purchased) {
resultName = RESULT_PURCHASED;
} else if (result instanceof PaywallResult.Restored) {
resultName = RESULT_RESTORED;
} else if (result instanceof PaywallResult.Cancelled) {
resultName = RESULT_CANCELLED;
} else if (result instanceof PaywallResult.Error) {
resultName = RESULT_ERROR;
} else {
resultName = RESULT_CANCELLED;
}

RevenueCatUI.sendPaywallResult(resultName);
}

public static void presentPaywall(Activity activity, @Nullable String offeringIdentifier, boolean displayCloseButton) {
if (activity == null) {
Log.e(TAG, "Activity is null; cannot launch paywall");
RevenueCatUI.sendPaywallResult(RESULT_ERROR);
return;
}

PaywallUnityOptions options = new PaywallUnityOptions(offeringIdentifier, displayCloseButton, null);

Intent intent = new Intent(activity, PaywallTrampolineActivity.class);
intent.putExtra(EXTRA_PAYWALL_OPTIONS, options);

try {
activity.startActivity(intent);
} catch (Throwable t) {
Log.e(TAG, "Error launching PaywallTrampolineActivity", t);
RevenueCatUI.sendPaywallResult(RESULT_ERROR);
}
}

public static void presentPaywallIfNeeded(Activity activity, String requiredEntitlementIdentifier, @Nullable String offeringIdentifier, boolean displayCloseButton) {
if (activity == null) {
Log.e(TAG, "Activity is null; cannot launch paywall");
RevenueCatUI.sendPaywallResult(RESULT_ERROR);
return;
}

if (requiredEntitlementIdentifier == null) {
Log.e(TAG, "Required entitlement identifier is null; cannot launch paywall if needed");
RevenueCatUI.sendPaywallResult(RESULT_ERROR);
return;
}

PaywallUnityOptions options = new PaywallUnityOptions(offeringIdentifier, displayCloseButton, requiredEntitlementIdentifier);

Intent intent = new Intent(activity, PaywallTrampolineActivity.class);
intent.putExtra(EXTRA_PAYWALL_OPTIONS, options);

try {
activity.startActivity(intent);
} catch (Throwable t) {
Log.e(TAG, "Error launching PaywallTrampolineActivity for presentPaywallIfNeeded", t);
RevenueCatUI.sendPaywallResult(RESULT_ERROR);
}
}
}
Loading