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

RevenueCatUI initial support #852

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from 6 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
4 changes: 3 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version '6.4.0-SNAPSHOT'

buildscript {
ext.kotlin_version = '1.7.21'
ext.common_version = '7.3.3'
ext.common_version = '7.4.0-beta.3'
repositories {
google()
mavenCentral()
Expand Down Expand Up @@ -65,4 +65,6 @@ android {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.revenuecat.purchases:purchases-hybrid-common:$common_version"
// TODO: extract version and automate updating it
api "com.revenuecat.purchases:purchases-ui:7.2.2"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.revenuecat.purchases_flutter;

import android.content.Intent;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.fragment.app.FragmentManager;

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

import io.flutter.embedding.android.FlutterFragment;
import io.flutter.embedding.android.FlutterFragmentActivity;
import io.flutter.plugin.common.MethodChannel;

@OptIn(markerClass = ExperimentalPreviewRevenueCatUIPurchasesAPI.class)
public class PurchasesFlutterActivity extends FlutterFragmentActivity implements PaywallResultHandler {
private PaywallActivityLauncher paywallActivityLauncher;

private MethodChannel.Result paywallResult;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
paywallActivityLauncher = new PaywallActivityLauncher(this, this);
}

void presentPaywall(final MethodChannel.Result result, final @Nullable String requiredEntitlementIdentifier) {
paywallResult = result;

if (requiredEntitlementIdentifier != null) {
paywallActivityLauncher.launchIfNeeded(requiredEntitlementIdentifier);
} else {
paywallActivityLauncher.launch();
}
}

@Override
public void onActivityResult(PaywallResult o) {
if (paywallResult == null) { return; }

if (o instanceof PaywallResult.Purchased) {
paywallResult.success(true);
} else if (o instanceof PaywallResult.Cancelled) {
paywallResult.success(false);
} else if (o instanceof PaywallResult.Error) {
// TODO: forward error
paywallResult.success(false);
}

paywallResult = null;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.revenuecat.purchases_flutter;

import static com.revenuecat.purchases.hybridcommon.PaywallHelpersKt.presentPaywallFromFragment;

import android.app.Activity;
import android.content.Context;
import android.os.Handler;
Expand All @@ -8,6 +10,7 @@

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;

import com.revenuecat.purchases.DangerousSettings;
import com.revenuecat.purchases.Purchases;
Expand Down Expand Up @@ -43,7 +46,8 @@
* PurchasesFlutterPlugin
*/
public class PurchasesFlutterPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware {
private String INVALID_ARGS_ERROR_CODE = "invalidArgs";
private static final String TAG = "PurchasesFlutter";
private static final String INVALID_ARGS_ERROR_CODE = "invalidArgs";

private static final String CUSTOMER_INFO_UPDATED = "Purchases-CustomerInfoUpdated";
protected static final String LOG_HANDLER_EVENT = "Purchases-LogHandlerEvent";
Expand Down Expand Up @@ -126,6 +130,17 @@ public Activity getActivity() {
return registrar != null ? registrar.activity() : activity;
}

private @Nullable PurchasesFlutterActivity getActivityFragment() {
final Activity activity = getActivity();

if (activity instanceof PurchasesFlutterActivity) {
return (PurchasesFlutterActivity) activity;
} else {
Log.e(TAG, "Paywalls require your activity to subclass FlutterFragmentActivity");
return null;
}
}

@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
switch (call.method) {
Expand Down Expand Up @@ -350,6 +365,12 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
syncObserverModeAmazonPurchase(productID, receiptID, amazonUserID, isoCurrencyCode,
price, result);
break;
case "presentPaywall":
presentPaywall(result, null);
break;
case "presentPaywallIfNeeded":
final String requiredEntitlementIdentifier = call.argument("requiredEntitlementIdentifier");
presentPaywall(result, requiredEntitlementIdentifier);
default:
result.notImplemented();
break;
Expand Down Expand Up @@ -708,14 +729,21 @@ private void showInAppMessages(final ArrayList<Integer> messageTypes, final Resu
if (messageType != null) {
messageTypesList.add(messageType);
} else {
Log.e("RNPurchases", "Unsupported in-app message type: " + messageTypeInt);
Log.e(TAG, "Unsupported in-app message type: " + messageTypeInt);
}
}
CommonKt.showInAppMessagesIfNeeded(activity, messageTypesList);
}
result.success(null);
}

private void presentPaywall(final Result result, final @Nullable String requiredEntitlementIdentifier) {
final PurchasesFlutterActivity fragment = getActivityFragment();
if (fragment != null) {
fragment.presentPaywall(result, requiredEntitlementIdentifier);
}
}

private void runOnUiThread(Runnable runnable) {
handler.post(runnable);
}
Expand Down
5 changes: 3 additions & 2 deletions api_tester/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ android {
}

defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.revenuecat.api_tester"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
minSdkVersion flutter.minSdkVersion
// TODO: change
// minSdkVersion flutter.minSdkVersion
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@tonidero we should be able to change this now, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm we would need to split the API tests since currently they also have the new package. Not sure if it's worth doing that though?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh okay makes sense 👍🏻 I'll remove this then.

minSdkVersion 24
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
Expand Down
5 changes: 5 additions & 0 deletions api_tester/lib/api_tests/purchases_flutter_api_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -512,4 +512,9 @@ class _PurchasesFlutterApiTest {
Future<void> future = Purchases.showInAppMessages(types: {InAppMessageType.billingIssue,
InAppMessageType.priceIncreaseConsent, InAppMessageType.generic});
}

void _checkPaywalls() async {
Future<bool> future1 = Purchases.presentPaywall();
Future<bool> future2 = Purchases.presentPaywallIfNeeded("test");
}
}
45 changes: 45 additions & 0 deletions ios/Classes/PurchasesFlutterPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ @interface PurchasesFlutterPlugin () <RCPurchasesDelegate>
@property (nonatomic, retain) NSObject <FlutterPluginRegistrar> *registrar;
@property (nonatomic, retain) NSMutableArray<RCStartPurchaseBlock> *startPurchaseBlocks;

@property (nonatomic, strong) id paywallProxy;

@end

NSString *PurchasesCustomerInfoUpdatedEvent = @"Purchases-CustomerInfoUpdated";
Expand All @@ -30,6 +32,17 @@ - (instancetype)initWithChannel:(FlutterMethodChannel *)channel
NSAssert(self, @"super init cannot be nil");
self.channel = channel;
self.registrar = registrar;

#if TARGET_OS_IPHONE
if (@available(iOS 15.0, *)) {
self.paywallProxy = [PaywallProxy new];
} else {
self.paywallProxy = nil;
}
#else
self.paywallProxy = nil;
#endif

return self;
}

Expand Down Expand Up @@ -217,6 +230,10 @@ - (void)handleMethodCall:(FlutterMethodCall *)call
} else if ([@"syncObserverModeAmazonPurchase" isEqualToString:call.method]) {
// NOOP
result(nil);
} else if ([@"presentPaywall" isEqualToString:call.method]) {
[self presentPaywallWithResult:result requiredEntitlementIdentifier:nil];
} else if ([@"presentPaywallIfNeeded" isEqualToString:call.method]) {
[self presentPaywallWithResult:result requiredEntitlementIdentifier:arguments[@"requiredEntitlementIdentifier"]];
} else {
result(FlutterMethodNotImplemented);
}
Expand Down Expand Up @@ -591,6 +608,34 @@ - (void)setLogHandlerWithResult:(FlutterResult)result {
result(nil);
}

#pragma mark -
#pragma mark Paywalls

#if TARGET_OS_IPHONE
- (PaywallProxy *)paywalls API_AVAILABLE(ios(15.0)){
return self.paywallProxy;
}
#endif

- (void)presentPaywallWithResult:(FlutterResult)result requiredEntitlementIdentifier:(NSString * _Nullable)requiredEntitlementIdentifier {
#if TARGET_OS_IPHONE
if (@available(iOS 15.0, *)) {
if (requiredEntitlementIdentifier) {
[self.paywalls presentPaywallIfNeededWithRequiredEntitlementIdentifier:requiredEntitlementIdentifier];
} else {
[self.paywalls presentPaywall];
}
} else {
NSLog(@"[Purchases] Warning: tried to display paywall, but it's only available on iOS 15.0 or greater.");
}
#else
NSLog(@"[Purchases] Warning: tried to display paywall, but it's only available on iOS 15.0 or greater.");
#endif

result(nil);
}


#pragma mark -
#pragma mark Delegate Methods

Expand Down
2 changes: 1 addition & 1 deletion ios/purchases_flutter.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Pod::Spec.new do |s|
s.source_files = 'Classes/**/*'
s.public_header_files = 'Classes/**/*.h'
s.dependency 'Flutter'
s.dependency 'PurchasesHybridCommon', '7.3.3'
s.dependency 'PurchasesHybridCommon', '7.4.0-beta.3'
s.ios.deployment_target = '11.0'
s.swift_version = '5.0'

Expand Down
25 changes: 17 additions & 8 deletions lib/purchases_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -259,9 +259,9 @@ class Purchases {
String typeString;
// ignore: deprecated_member_use_from_same_package
if (type != PurchaseType.subs) {
typeString = describeEnum(type);
typeString = type.name;
} else {
typeString = describeEnum(productCategory);
typeString = productCategory.name;
}

final List<dynamic> result = await _channel.invokeMethod('getProductInfo', {
Expand Down Expand Up @@ -304,7 +304,7 @@ class Purchases {
final prorationMode = upgradeInfo?.prorationMode;
final customerInfo = await _invokeReturningCustomerInfo('purchaseProduct', {
'productIdentifier': productIdentifier,
'type': describeEnum(type),
'type': type.name,
'googleOldProductIdentifier': upgradeInfo?.oldSKU,
'googleProrationMode': prorationMode?.index,
'googleIsPersonalizedPrice': null,
Expand Down Expand Up @@ -339,9 +339,7 @@ class Purchases {
final prorationMode = googleProductChangeInfo?.prorationMode?.value;
final customerInfo = await _invokeReturningCustomerInfo('purchaseProduct', {
'productIdentifier': storeProduct.identifier,
'type': storeProduct.productCategory != null
? describeEnum(storeProduct.productCategory!)
: null,
'type': storeProduct.productCategory?.name,
'googleOldProductIdentifier':
googleProductChangeInfo?.oldProductIdentifier,
'googleProrationMode': prorationMode,
Expand Down Expand Up @@ -540,7 +538,7 @@ class Purchases {
/// The default is {LOG_LEVEL.INFO} in release builds and {LOG_LEVEL.DEBUG} in debug builds.
static Future<void> setLogLevel(LogLevel level) => _channel.invokeMethod(
'setLogLevel',
{'level': describeEnum(level).toUpperCase()},
{'level': level.name.toUpperCase()},
);

///
Expand Down Expand Up @@ -929,13 +927,24 @@ class Purchases {
final args = Map<String, dynamic>.from(call.arguments);
final logLevelName = args['logLevel'];
final logLevel = LogLevel.values.firstWhere(
(e) => describeEnum(e).toUpperCase() == logLevelName,
(e) => e.name.toUpperCase() == logLevelName,
orElse: () => LogLevel.info,
);
final msg = args['message'];
_logHandler?.call(logLevel, msg);
}

static Future<bool> presentPaywall() async =>
await _channel.invokeMethod('presentPaywall');

static Future<bool> presentPaywallIfNeeded(
String requiredEntitlementIdentifier,
) async =>
await _channel.invokeMethod(
'presentPaywallIfNeeded',
{'requiredEntitlementIdentifier': requiredEntitlementIdentifier},
);

/// This method will send a purchase to the RevenueCat backend. This function should only be called if you are
/// in Amazon observer mode or performing a client side migration of your current users to RevenueCat.
///
Expand Down
2 changes: 1 addition & 1 deletion macos/purchases_flutter.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Pod::Spec.new do |s|
s.source_files = 'Classes/**/*'
s.public_header_files = 'Classes/**/*.h'
s.dependency 'FlutterMacOS'
s.dependency 'PurchasesHybridCommon', '7.3.3'
s.dependency 'PurchasesHybridCommon', '7.4.0-beta.3'
s.platform = :osx, '10.12'
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
s.swift_version = '5.0'
Expand Down
3 changes: 2 additions & 1 deletion revenuecat_examples/purchase_tester/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ android {

defaultConfig {
applicationId "com.revenuecat.purchases_sample"
minSdkVersion flutter.minSdkVersion
// TODO: change
minSdkVersion 24
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.revenuecat.purchases_sample;

import io.flutter.embedding.android.FlutterActivity;
import com.revenuecat.purchases_flutter.PurchasesFlutterActivity;

public class MainActivity extends FlutterActivity {
public class MainActivity extends PurchasesFlutterActivity {
}
Original file line number Diff line number Diff line change
Expand Up @@ -236,13 +236,15 @@
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
"${BUILT_PRODUCTS_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.framework",
"${BUILT_PRODUCTS_DIR}/RevenueCat/RevenueCat.framework",
"${BUILT_PRODUCTS_DIR}/RevenueCatUI/RevenueCatUI.framework",
"${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework",
"${BUILT_PRODUCTS_DIR}/purchases_flutter/purchases_flutter.framework",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PurchasesHybridCommon.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RevenueCat.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RevenueCatUI.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/purchases_flutter.framework",
);
Expand Down
13 changes: 13 additions & 0 deletions revenuecat_examples/purchase_tester/lib/src/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,19 @@ class _UpsellScreenState extends State<UpsellScreen> {
.expand((i) => i)
.toList();

buttonThings.add(ElevatedButton(
onPressed: () async {
bool result = await Purchases.presentPaywall();

if (result) {
print("Purchase completed");
} else {
print("Purchase failed");
}
},
child: const Text('Present paywall'),
));

return Scaffold(
appBar: AppBar(title: const Text('Upsell Screen')),
body: Center(
Expand Down
Loading