Skip to content

Commit 44e3e3f

Browse files
Merge pull request #1 from fhdeodato/main
Merge PR pikaju#165
2 parents 5cd6dd3 + 1bc42ab commit 44e3e3f

20 files changed

+445
-111
lines changed

.vscode/settings.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"editor.formatOnSave": true
3+
}

README.md

+26
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ dependencies {
3333

3434
#### PayPal / Venmo / 3D Secure
3535

36+
##### With Drop-In
37+
3638
In order for this plugin to support PayPal, Venmo or 3D Secure payments, you must allow for the
3739
browser switch by adding an intent filter to your `AndroidManifest.xml` (inside the `<application>` body):
3840

@@ -53,6 +55,30 @@ browser switch by adding an intent filter to your `AndroidManifest.xml` (inside
5355
**Important:** Your app's URL scheme must begin with your app's package ID and end with `.braintree`. For example, if the Package ID is `com.your-company.your-app`, then your URL scheme should be `com.your-company.your-app.braintree`. `${applicationId}` is automatically applied with your app's package when using Gradle.
5456
**Note:** The scheme you define must use all lowercase letters. If your package contains underscores, the underscores should be removed when specifying the scheme in your Android Manifest.
5557

58+
##### Without Drop-In
59+
60+
If you want to use PayPal without Drop-In, you need to declare another intent filter to your `AndroidManifest.xml` (inside the `<application>` body):
61+
62+
```xml
63+
<activity android:name="com.example.flutter_braintree.FlutterBraintreeCustom"
64+
android:theme="@style/bt_transparent_activity" android:exported="true"
65+
android:launchMode="singleInstance">
66+
<intent-filter>
67+
<action android:name="android.intent.action.VIEW" />
68+
<category android:name="android.intent.category.DEFAULT" />
69+
<category android:name="android.intent.category.BROWSABLE" />
70+
<data android:scheme="${applicationId}.return.from.braintree" />
71+
</intent-filter>
72+
</activity>
73+
<activity android:name="com.braintreepayments.api.ThreeDSecureActivity" android:theme="@style/Theme.AppCompat.Light" android:exported="true">
74+
</activity>
75+
```
76+
77+
Please note that intent filter scheme is different from drop-in's one.
78+
79+
**Important:** Your app's URL scheme must begin with your app's package ID and end with `.return.from.braintree`. For example, if the Package ID is `com.your-company.your-app`, then your URL scheme should be `com.your-company.your-app.return.from.braintree`. `${applicationId}` is automatically applied with your app's package when using Gradle.
80+
**Note:** The scheme you define must use all lowercase letters. If your package contains underscores, the underscores should be removed when specifying the scheme in your Android Manifest.
81+
5682
#### Google Pay
5783

5884
Add the wallet enabled meta-data tag to your `AndroidManifest.xml` (inside the `<application>` body):

android/src/main/AndroidManifest.xml

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3-
package="com.example.flutter_braintree">
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
43

54
<application>
65
<activity android:name=".DropInActivity"

android/src/main/java/com/example/flutter_braintree/FlutterBraintreeCustom.java

+99-12
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
package com.example.flutter_braintree;
2+
import static com.braintreepayments.api.PayPalCheckoutRequest.USER_ACTION_COMMIT;
3+
import static com.braintreepayments.api.PayPalCheckoutRequest.USER_ACTION_DEFAULT;
4+
25
import androidx.annotation.NonNull;
6+
import androidx.annotation.Nullable;
37
import androidx.appcompat.app.AppCompatActivity;
48

59
import android.content.Intent;
610
import android.os.Bundle;
11+
import android.util.Log;
12+
import org.json.JSONException;
13+
import org.json.JSONObject;
714

815
import com.braintreepayments.api.BraintreeClient;
916
import com.braintreepayments.api.Card;
@@ -14,33 +21,43 @@
1421
import com.braintreepayments.api.PayPalCheckoutRequest;
1522
import com.braintreepayments.api.PayPalClient;
1623
import com.braintreepayments.api.PayPalListener;
24+
import com.braintreepayments.api.PayPalPaymentIntent;
1725
import com.braintreepayments.api.PayPalRequest;
1826
import com.braintreepayments.api.PayPalVaultRequest;
1927
import com.braintreepayments.api.PaymentMethodNonce;
28+
import com.braintreepayments.api.PostalAddress;
2029
import com.braintreepayments.api.UserCanceledException;
2130

2231

2332
import java.util.HashMap;
33+
import java.util.Objects;
2434

2535
public class FlutterBraintreeCustom extends AppCompatActivity implements PayPalListener {
2636
private BraintreeClient braintreeClient;
2737
private PayPalClient payPalClient;
2838
private Boolean started = false;
39+
private long creationTimestamp = -1;
2940

3041
@Override
3142
protected void onCreate(Bundle savedInstanceState) {
3243
super.onCreate(savedInstanceState);
44+
45+
creationTimestamp = System.currentTimeMillis();
46+
3347
setContentView(R.layout.activity_flutter_braintree_custom);
3448
try {
3549
Intent intent = getIntent();
36-
braintreeClient = new BraintreeClient(this, intent.getStringExtra("authorization"));
50+
String returnUrlScheme = (getPackageName() + ".return.from.braintree").replace("_", "").toLowerCase();
51+
braintreeClient = new BraintreeClient(this, intent.getStringExtra("authorization"), returnUrlScheme);
52+
3753
String type = intent.getStringExtra("type");
3854
if (type.equals("tokenizeCreditCard")) {
3955
tokenizeCreditCard();
4056
} else if (type.equals("requestPaypalNonce")) {
4157
payPalClient = new PayPalClient(this, braintreeClient);
4258
payPalClient.setListener(this);
4359
requestPaypalNonce();
60+
4461
} else {
4562
throw new Exception("Invalid request type: " + type);
4663
}
@@ -59,16 +76,15 @@ protected void onNewIntent(Intent newIntent) {
5976
setIntent(newIntent);
6077
}
6178

62-
@Override
63-
protected void onStart() {
64-
super.onStart();
79+
// @Override
80+
// protected void onStart() {
81+
// super.onStart();
82+
// }
6583

66-
}
67-
68-
@Override
69-
protected void onResume() {
70-
super.onResume();
71-
}
84+
// @Override
85+
// protected void onResume() {
86+
// super.onResume();
87+
// }
7288

7389
protected void tokenizeCreditCard() {
7490
Intent intent = getIntent();
@@ -104,6 +120,53 @@ protected void requestPaypalNonce() {
104120
// Checkout flow
105121
PayPalCheckoutRequest checkOutRequest = new PayPalCheckoutRequest(intent.getStringExtra("amount"));
106122
checkOutRequest.setCurrencyCode(intent.getStringExtra("currencyCode"));
123+
checkOutRequest.setDisplayName(intent.getStringExtra("displayName"));
124+
checkOutRequest.setBillingAgreementDescription(intent.getStringExtra("billingAgreementDescription"));
125+
checkOutRequest.setShouldRequestBillingAgreement(Objects.requireNonNull(intent.getExtras()).getBoolean("requestBillingAgreement"));
126+
checkOutRequest.setShippingAddressEditable(Objects.requireNonNull(intent.getExtras()).getBoolean("shippingAddressEditable"));
127+
checkOutRequest.setShippingAddressRequired(Objects.requireNonNull(intent.getExtras()).getBoolean("shippingAddressRequired"));
128+
129+
// handles the shipping address override
130+
if (intent.getStringExtra("shippingAddressOverride") != null) {
131+
try {
132+
JSONObject obj = new JSONObject(intent.getStringExtra("shippingAddressOverride"));
133+
PostalAddress shippingAddressOverride = new PostalAddress();
134+
shippingAddressOverride.setRecipientName(obj.getString("recipientName"));
135+
shippingAddressOverride.setStreetAddress(obj.getString("streetAddress"));
136+
shippingAddressOverride.setExtendedAddress(obj.getString("extendedAddress"));
137+
shippingAddressOverride.setLocality(obj.getString("locality"));
138+
shippingAddressOverride.setCountryCodeAlpha2(obj.getString("countryCodeAlpha2"));
139+
shippingAddressOverride.setPostalCode(obj.getString("postalCode"));
140+
shippingAddressOverride.setRegion(obj.getString("region"));
141+
checkOutRequest.setShippingAddressOverride(shippingAddressOverride);
142+
} catch (JSONException e) {
143+
throw new RuntimeException(e);
144+
}
145+
}
146+
147+
String userAction;
148+
switch (intent.getStringExtra("payPalPaymentUserAction")) {
149+
case "commit":
150+
userAction = USER_ACTION_COMMIT;
151+
break;
152+
default:
153+
userAction = USER_ACTION_DEFAULT;
154+
}
155+
checkOutRequest.setUserAction(userAction);
156+
157+
String paymentIntent;
158+
switch (intent.getStringExtra("payPalPaymentIntent")) {
159+
case "order":
160+
paymentIntent = PayPalPaymentIntent.ORDER;
161+
break;
162+
case "sale":
163+
paymentIntent = PayPalPaymentIntent.SALE;
164+
break;
165+
default:
166+
paymentIntent = PayPalPaymentIntent.AUTHORIZE;
167+
}
168+
checkOutRequest.setIntent(paymentIntent);
169+
107170
payPalClient.tokenizePayPalAccount(this, checkOutRequest);
108171
}
109172
}
@@ -117,6 +180,14 @@ public void onPaymentMethodNonceCreated(PaymentMethodNonce paymentMethodNonce) {
117180
nonceMap.put("paypalPayerId", paypalAccountNonce.getPayerId());
118181
nonceMap.put("typeLabel", "PayPal");
119182
nonceMap.put("description", paypalAccountNonce.getEmail());
183+
184+
nonceMap.put("firstName", paypalAccountNonce.getFirstName());
185+
nonceMap.put("lastName", paypalAccountNonce.getLastName());
186+
nonceMap.put("email", paypalAccountNonce.getEmail());
187+
188+
HashMap<String, Object> billingAddressMap = getResultBillingAddress(paypalAccountNonce);
189+
190+
nonceMap.put("billingAddress", billingAddressMap);
120191
}else if(paymentMethodNonce instanceof CardNonce){
121192
CardNonce cardNonce = (CardNonce) paymentMethodNonce;
122193
nonceMap.put("typeLabel", cardNonce.getCardType());
@@ -129,6 +200,20 @@ public void onPaymentMethodNonceCreated(PaymentMethodNonce paymentMethodNonce) {
129200
finish();
130201
}
131202

203+
@NonNull
204+
private static HashMap<String, Object> getResultBillingAddress(PayPalAccountNonce paypalAccountNonce) {
205+
PostalAddress btBillingAddress = paypalAccountNonce.getBillingAddress();
206+
HashMap<String, Object> billingAddressMap = new HashMap<String, Object>();
207+
billingAddressMap.put("recipientName",btBillingAddress.getRecipientName());
208+
billingAddressMap.put("streetAddress",btBillingAddress.getStreetAddress());
209+
billingAddressMap.put("extendedAddress",btBillingAddress.getExtendedAddress());
210+
billingAddressMap.put("locality",btBillingAddress.getLocality());
211+
billingAddressMap.put("countryCodeAlpha2",btBillingAddress.getCountryCodeAlpha2());
212+
billingAddressMap.put("postalCode",btBillingAddress.getPostalCode());
213+
billingAddressMap.put("region",btBillingAddress.getRegion());
214+
return billingAddressMap;
215+
}
216+
132217
public void onCancel() {
133218
setResult(RESULT_CANCELED);
134219
finish();
@@ -149,12 +234,14 @@ public void onPayPalSuccess(@NonNull PayPalAccountNonce payPalAccountNonce) {
149234
@Override
150235
public void onPayPalFailure(@NonNull Exception error) {
151236
if (error instanceof UserCanceledException) {
152-
if(((UserCanceledException) error).isExplicitCancelation()){
237+
if(((UserCanceledException) error).isExplicitCancelation() || System.currentTimeMillis() - creationTimestamp > 500)
238+
{
239+
// PayPal sometimes sends a UserCanceledException early for no reason: filter it out
240+
// Otherwise take every cancellation event
153241
onCancel();
154242
}
155243
} else {
156244
onError(error);
157245
}
158-
159246
}
160247
}

android/src/main/java/com/example/flutter_braintree/FlutterBraintreePlugin.java

+26
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import android.content.Intent;
66

77

8+
import org.json.JSONException;
9+
import org.json.JSONObject;
810

911
import java.util.Map;
1012

@@ -108,6 +110,30 @@ public void onMethodCall(MethodCall call, Result result) {
108110
intent.putExtra("payPalPaymentIntent", (String) request.get("payPalPaymentIntent"));
109111
intent.putExtra("payPalPaymentUserAction", (String) request.get("payPalPaymentUserAction"));
110112
intent.putExtra("billingAgreementDescription", (String) request.get("billingAgreementDescription"));
113+
114+
if(request.containsKey("shippingAddressOverride")){
115+
Map shippingAddress = (Map) request.get("shippingAddressOverride");
116+
JSONObject shippingAddressJson;
117+
shippingAddressJson = new JSONObject();
118+
assert shippingAddress != null;
119+
try {
120+
shippingAddressJson.put("recipientName",shippingAddress.get("recipientName"));
121+
shippingAddressJson.put("streetAddress",shippingAddress.get("streetAddress"));
122+
shippingAddressJson.put("extendedAddress",shippingAddress.get("extendedAddress"));
123+
shippingAddressJson.put("locality",shippingAddress.get("locality"));
124+
shippingAddressJson.put("countryCodeAlpha2",shippingAddress.get("countryCodeAlpha2"));
125+
shippingAddressJson.put("postalCode",shippingAddress.get("postalCode"));
126+
shippingAddressJson.put("region",shippingAddress.get("region"));
127+
} catch (JSONException e) {
128+
throw new RuntimeException(e);
129+
}
130+
intent.putExtra("shippingAddressOverride", (String) shippingAddressJson.toString());
131+
}
132+
133+
intent.putExtra("requestBillingAgreement", (Boolean) request.get("requestBillingAgreement"));
134+
intent.putExtra("shippingAddressEditable", (Boolean) request.get("shippingAddressEditable"));
135+
intent.putExtra("shippingAddressRequired", (Boolean) request.get("shippingAddressRequired"));
136+
111137
activity.startActivityForResult(intent, CUSTOM_ACTIVITY_REQUEST_CODE);
112138
} else {
113139
result.notImplemented();

example/.vscode/launch.json

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": "example",
9+
"request": "launch",
10+
"type": "dart"
11+
},
12+
{
13+
"name": "example (profile mode)",
14+
"request": "launch",
15+
"type": "dart",
16+
"flutterMode": "profile"
17+
},
18+
{
19+
"name": "example (release mode)",
20+
"request": "launch",
21+
"type": "dart",
22+
"flutterMode": "release"
23+
}
24+
]
25+
}

example/android/app/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ apply plugin: 'com.android.application'
2525
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
2626

2727
android {
28-
compileSdkVersion 31
28+
compileSdkVersion 33
2929

3030
defaultConfig {
3131
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).

example/android/app/src/main/AndroidManifest.xml

+19-8
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
<application
1010
android:icon="@mipmap/ic_launcher">
1111
<activity
12-
android:name="io.flutter.embedding.android.FlutterActivity"
12+
android:name=".MainActivity"
1313
android:theme="@style/LaunchTheme"
1414
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
1515
android:hardwareAccelerated="true"
16-
android:windowSoftInputMode="adjustResize">
16+
android:windowSoftInputMode="adjustResize"
17+
android:exported="true"
18+
android:launchMode="singleInstance">
1719
<!-- This keeps the window background of the activity showing
1820
until Flutter renders its first frame. It can be removed if
1921
there is no splash screen (such as the default splash screen
@@ -22,8 +24,8 @@
2224
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
2325
android:value="true" />
2426
<intent-filter>
25-
<action android:name="android.intent.action.MAIN"/>
26-
<category android:name="android.intent.category.LAUNCHER"/>
27+
<action android:name="android.intent.action.MAIN" />
28+
<category android:name="android.intent.category.LAUNCHER" />
2729
</intent-filter>
2830
</activity>
2931

@@ -36,15 +38,24 @@
3638
<data android:scheme="com.example.flutterbraintreeexample.braintree" />
3739
</intent-filter>
3840
</activity>
39-
<activity android:name="com.braintreepayments.api.ThreeDSecureActivity" android:exported="true">
41+
<activity android:name="com.braintreepayments.api.ThreeDSecureActivity"
42+
android:exported="true">
4043
</activity>
41-
<activity android:name="com.example.flutter_braintree.FlutterBraintreeCustom" android:theme="@style/Theme.AppCompat.Light" android:exported="true">
44+
<activity android:name="com.example.flutter_braintree.FlutterBraintreeCustom"
45+
android:theme="@style/bt_transparent_activity" android:exported="true"
46+
android:launchMode="singleInstance">
47+
<intent-filter>
48+
<action android:name="android.intent.action.VIEW" />
49+
<category android:name="android.intent.category.DEFAULT" />
50+
<category android:name="android.intent.category.BROWSABLE" />
51+
<data android:scheme="com.example.flutterbraintreeexample.return.from.braintree" />
52+
</intent-filter>
4253
</activity>
4354

44-
<meta-data android:name="com.google.android.gms.wallet.api.enabled" android:value="true"/>
55+
<meta-data android:name="com.google.android.gms.wallet.api.enabled" android:value="true" />
4556

4657
<meta-data
4758
android:name="flutterEmbedding"
4859
android:value="2" />
4960
</application>
50-
</manifest>
61+
</manifest>

example/android/build.gradle

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ buildscript {
55
}
66

77
dependencies {
8-
classpath 'com.android.tools.build:gradle:4.1.0'
8+
classpath 'com.android.tools.build:gradle:7.4.2'
99
}
1010
}
1111

@@ -24,6 +24,6 @@ subprojects {
2424
project.evaluationDependsOn(':app')
2525
}
2626

27-
task clean(type: Delete) {
27+
tasks.register("clean", Delete) {
2828
delete rootProject.buildDir
2929
}

0 commit comments

Comments
 (0)