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

Upgrade Flutter Android APIs #74

Merged
merged 8 commits into from
Jun 26, 2020
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
9 changes: 9 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
include: package:pedantic/analysis_options.1.8.0.yaml
analyzer:
exclude:
# Ignore generated files
- '**/*.g.dart'
- 'lib/src/generated/*.dart'
linter:
rules:
- public_member_api_docs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.revenuecat.purchases_flutter;

import android.app.Activity;
import android.content.Context;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

Expand All @@ -19,6 +22,10 @@
import java.util.List;
import java.util.Map;

import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
Expand All @@ -31,18 +38,27 @@
/**
* PurchasesFlutterPlugin
*/
public class PurchasesFlutterPlugin implements MethodCallHandler {
public class PurchasesFlutterPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware {

private static final String PURCHASER_INFO_UPDATED = "Purchases-PurchaserInfoUpdated";

private final Registrar registrar;
private final MethodChannel channel;
// Only set registrar for v1 embedder.
private PluginRegistry.Registrar registrar;
// Only set activity for v2 embedder. Always access activity from getActivity() method.
private Context applicationContext;
private MethodChannel channel;
private Activity activity;

private static final String PLATFORM_NAME = "flutter";
private static final String PLUGIN_VERSION = "1.2.0-SNAPSHOT";

public PurchasesFlutterPlugin(Registrar registrar, MethodChannel channel) {
this.registrar = registrar;
this.channel = channel;
/**
* Plugin registration.
*/
public static void registerWith(Registrar registrar) {
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 is the old API, but we still need to support it

PurchasesFlutterPlugin instance = new PurchasesFlutterPlugin();
instance.onAttachedToEngine(registrar.messenger(), registrar.context());
instance.registrar = registrar;
registrar.addViewDestroyListener(new PluginRegistry.ViewDestroyListener() {
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 am not sure we need this, but I don't think it would hurt to keep it since we already have it.

@Override
public boolean onViewDestroy(FlutterNativeView flutterNativeView) {
Expand All @@ -56,12 +72,51 @@ public boolean onViewDestroy(FlutterNativeView flutterNativeView) {
});
}

/**
* Plugin registration.
*/
public static void registerWith(Registrar registrar) {
final MethodChannel channel = new MethodChannel(registrar.messenger(), "purchases_flutter");
channel.setMethodCallHandler(new PurchasesFlutterPlugin(registrar, channel));
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
onAttachedToEngine(binding.getBinaryMessenger(), binding.getApplicationContext());
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 is the new API

}

private void onAttachedToEngine(BinaryMessenger messenger, Context applicationContext) {
this.channel = new MethodChannel(messenger, "purchases_flutter");
this.applicationContext = applicationContext;
this.channel.setMethodCallHandler(this);
}

@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
try {
Purchases.getSharedInstance().close();
} catch (UninitializedPropertyAccessException e) {
// there's no instance so all good
}
channel.setMethodCallHandler(null);
this.channel = null;
this.applicationContext = null;
}

@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
this.activity = binding.getActivity();
}

@Override
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
onAttachedToActivity(binding);
}

@Override
public void onDetachedFromActivity() {
this.activity = null;
}

@Override
public void onDetachedFromActivityForConfigChanges() {
onDetachedFromActivity();
}

public Activity getActivity() {
return registrar != null ? registrar.activity() : activity;
Copy link
Contributor Author

@vegaro vegaro Jun 26, 2020

Choose a reason for hiding this comment

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

This is how they do in https://github.com/flutter/plugins/blob/88e85c6a48063cfab9111689179e2582a99b77ff/packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java

Since we still need to support the old API for apps that haven't updated yet, we need to check if the registrar is not null. registrar will be null for apps that use the new API and in that case the activity will be the one saved when onAttachedToActivity gets called.

}

@Override
Expand Down Expand Up @@ -185,7 +240,7 @@ private void sendEvent(String eventName, @Nullable Map<String, Object> params) {

private void setupPurchases(String apiKey, String appUserID, @Nullable Boolean observerMode, final Result result) {
PlatformInfo platformInfo = new PlatformInfo(PLATFORM_NAME, PLUGIN_VERSION);
CommonKt.configure(this.registrar.context(), apiKey, appUserID, observerMode, platformInfo);
CommonKt.configure(this.applicationContext, apiKey, appUserID, observerMode, platformInfo);

Purchases.getSharedInstance().setUpdatedPurchaserInfoListener(new UpdatedPurchaserInfoListener() {
@Override
Expand Down Expand Up @@ -234,7 +289,7 @@ private void purchaseProduct(final String productIdentifier, final String oldSKU
@Nullable final Integer prorationMode, final String type,
final Result result) {
CommonKt.purchaseProduct(
this.registrar.activity(),
getActivity(),
productIdentifier,
oldSKU,
prorationMode,
Expand All @@ -249,7 +304,7 @@ private void purchasePackage(final String packageIdentifier,
@Nullable final Integer prorationMode,
final Result result) {
CommonKt.purchasePackage(
this.registrar.activity(),
getActivity(),
packageIdentifier,
offeringIdentifier,
oldSKU,
Expand Down
4 changes: 3 additions & 1 deletion example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ flutter {
dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
}

// Call passing parameter -PcommonPath="$HOME/Development/repos/purchases-hybrid-common/android"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.revenuecat.purchases_flutter;

import androidx.test.rule.ActivityTestRule;

import com.revenuecat.purchases_flutter_example.EmbeddingV1Activity;

import dev.flutter.plugins.e2e.FlutterRunner;
import org.junit.Rule;
import org.junit.runner.RunWith;

@RunWith(FlutterRunner.class)
public class EmbeddingV1ActivityTest {
@Rule
public ActivityTestRule<EmbeddingV1Activity> rule =
new ActivityTestRule<>(EmbeddingV1Activity.class);

}
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 test makes sure the old APIs are still supported. I copied this from migration docs https://flutter.dev/docs/development/packages-and-plugins/plugin-api-migration#testing-your-plugin

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.revenuecat.purchases_flutter;

import androidx.test.rule.ActivityTestRule;
import dev.flutter.plugins.e2e.FlutterRunner;
import org.junit.Rule;
import org.junit.runner.RunWith;
import io.flutter.embedding.android.FlutterActivity;

@RunWith(FlutterRunner.class)
public class MainActivityTest {
@Rule
public ActivityTestRule<FlutterActivity> rule = new ActivityTestRule<>(FlutterActivity.class);
}
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 test makes sure the new APIs are supported and the plugin is registered successfully. I copied this from migration docs https://flutter.dev/docs/development/packages-and-plugins/plugin-api-migration#testing-your-plugin

13 changes: 10 additions & 3 deletions example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@
android:label="purchases_flutter_example"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:name="io.flutter.embedding.android.FlutterActivity"
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 don't need a MainActivity anymore

android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- This keeps the window background of the activity showing
Expand All @@ -29,5 +28,13 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".EmbeddingV1Activity"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
</activity>
<meta-data android:name="flutterEmbedding" android:value="2"/>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.revenuecat.purchases_flutter_example;

import android.os.Bundle;
import com.revenuecat.purchases_flutter.PurchasesFlutterPlugin;
import io.flutter.app.FlutterActivity;
import io.flutter.view.FlutterMain;

public class EmbeddingV1Activity extends FlutterActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
FlutterMain.startInitialization(this);
super.onCreate(savedInstanceState);
PurchasesFlutterPlugin.registerWith(registrarFor("com.revenuecat.purchases_flutter"));
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

EmbeddingV1Activity.java uses the v1 embedding for the example project in the same folder as MainActivity to keep testing the v1 embedding’s compatibility with your plugin. Note that we have to manually register all the plugins instead of using GeneratedPluginRegistrant, as MainActivity used to do. Also copied from the upgrading docs.

This file was deleted.

2 changes: 1 addition & 1 deletion example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class _MyAppState extends State<InitialScreen> {
Future<void> initPlatformState() async {
await Purchases.setDebugLogsEnabled(true);
await Purchases.setup("api_key");
Purchases.addAttributionData({}, PurchasesAttributionNetwork.facebook);
await Purchases.addAttributionData({}, PurchasesAttributionNetwork.facebook);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

pedantic told me this was missing

Copy link
Member

Choose a reason for hiding this comment

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

nice

PurchaserInfo purchaserInfo = await Purchases.getPurchaserInfo();
Offerings offerings = await Purchases.getOfferings();
// If the widget was removed from the tree while the asynchronous platform
Expand Down
5 changes: 5 additions & 0 deletions example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ dev_dependencies:
purchases_flutter:
path: ../

pedantic: ^1.8.0
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 saw most plugins use this linter tool so I added it

e2e: ^0.2.1
flutter_driver:
sdk: flutter
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 mentioned in the upgrading docs, it's used to create end to end tests.


# For information on the generic Dart part of this file, see the
# following page: https://www.dartlang.org/tools/pub/pubspec

Expand Down
15 changes: 15 additions & 0 deletions example/test_driver/purchases_flutter_e2e_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2019, the Chromium project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:io';
import 'dart:async';
import 'package:flutter_driver/flutter_driver.dart';

Future<void> main() async {
final FlutterDriver driver = await FlutterDriver.connect();
final String result =
await driver.requestData(null, timeout: const Duration(minutes: 1));
await driver.close();
exit(result == 'pass' ? 0 : 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.

I am still not sure what this flutter driver is but I saw a lot of plugins have it (https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in/test_driver)

It's also referenced in the docs of e2e https://pub.dev/packages/e2e#using-flutter-driver-to-run-tests

Running flutter drive --driver=test_driver/purchases_flutter_e2e_test.dart ../test/purchases_flutter_e2e.dart inside the example will run the e2e test in the emulator

Copy link
Member

Choose a reason for hiding this comment

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

I guess it provides for a way to do UI tests or integration tests with a simulator

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, and I think e2e is to run flutter tests in a simulator (using flutter drive), or something like that. Alternatively, someone could write UI tests on the example app in order to test the same

4 changes: 2 additions & 2 deletions lib/purchases_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Purchases {
static final Set<PurchaserInfoUpdateListener> _purchaserInfoUpdateListeners =
Set();

static final _channel = new MethodChannel('purchases_flutter')
static final _channel = MethodChannel('purchases_flutter')
Copy link
Contributor Author

Choose a reason for hiding this comment

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

pedantic told me new is not required

..setMethodCallHandler((MethodCall call) async {
switch (call.method) {
case "Purchases-PurchaserInfoUpdated":
Expand Down Expand Up @@ -141,7 +141,7 @@ class Purchases {
purchaseType = PurchaseType.inapp;
}
return purchaseProduct(productIdentifier,
upgradeInfo: new UpgradeInfo(oldSKU), type: purchaseType);
upgradeInfo: UpgradeInfo(oldSKU), type: purchaseType);
}

/// Makes a purchase. Returns a [PurchaserInfo] object. Throws a
Expand Down
6 changes: 5 additions & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@ documentation: https://docs.revenuecat.com/

environment:
sdk: ">=2.1.0 <3.0.0"
flutter: ">=1.12.0 <2.0.0"
flutter: ">=1.12.13+hotfix.6 <2.0.0"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also taken from the upgrading docs, this is the minimum version they offer support for.


dependencies:
flutter:
sdk: flutter

dev_dependencies:
pedantic: ^1.8.0
flutter_test:
sdk: flutter
e2e: ^0.2.1
flutter_driver:
sdk: flutter

# For information on the generic Dart part of this file, see the
# following page: https://www.dartlang.org/tools/pub/pubspec
Expand Down
16 changes: 16 additions & 0 deletions test/purchases_flutter_e2e.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:e2e/e2e.dart';
import 'package:purchases_flutter/purchases_flutter.dart';

// Execute the following in the `example` folder run these tests:
// `flutter drive --driver=test_driver/purchases_flutter_e2e_test.dart ../test/purchases_flutter_e2e.dart`

void main() {
E2EWidgetsFlutterBinding.ensureInitialized();

testWidgets('Can setup Purchases', (WidgetTester tester) async {
final Future<void> future = Purchases.setup('apiKey', appUserId: 'cesar');
expect(future, completes);
});

}
Copy link
Contributor Author

@vegaro vegaro Jun 26, 2020

Choose a reason for hiding this comment

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

Also copied from the upgrading docs. I ran it by doing ./gradlew app:connectedAndroidTest -Ptarget=pwd/../../test/purchases_flutter_e2e.dartinside example/android and it works. In Android Studio, it doesn't run I don't know why.

Also saw another example here https://github.com/FirebaseExtended/flutterfire/blob/master/packages/firebase_core/firebase_core/test/firebase_core_e2e.dart

This test actually runs in an emulator, so my understanding is we could test a bunch of stuff this way. It only works in Android though. I tried making a purchase and I could see the purchase dialog popping up :)

I am not even adding it to circleci for now, but I am going to leave it since it's part of the upgrading docs.

Copy link
Member

Choose a reason for hiding this comment

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

maybe we could have either a comment or something documenting how to actually run this test so we don't forget? or even a gradle task that wraps the command

25 changes: 23 additions & 2 deletions test/purchases_flutter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,40 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:purchases_flutter/purchases_flutter.dart';

void main() {
TestWidgetsFlutterBinding.ensureInitialized();

const MethodChannel channel = MethodChannel('purchases_flutter');
final List<MethodCall> log = <MethodCall>[];
dynamic response;

setUp(() {
channel.setMockMethodCallHandler((MethodCall methodCall) async {
return '42';
log.add(methodCall);
return response;
});
});

tearDown(() {
channel.setMockMethodCallHandler(null);
log.clear();
response = null;
});

test('setupPurchases', () async {
Purchases.setup("api_key", appUserId: "cesar", observerMode: true);
await Purchases.setup('api_key', appUserId: 'cesar', observerMode: true);
expect(
log,
<Matcher>[
isMethodCall(
'setupPurchases',
arguments: <String, dynamic>{
'apiKey': 'api_key',
'appUserId': 'cesar',
'observerMode': true
},
)
],
);
});

}