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

SEPA Launcher API #816

Merged
merged 3 commits into from
Nov 6, 2023
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@
* Card
* Remove `threeDSecureInfo` from `CardNonce`
* Move `ThreeDSecureInfo` to `three-d-secure` module
* SEPA Direct Debit
* Remove `SEPADirectDebitLifecycleObserver` and `SEPADirectDebitListener`
* Add `SEPADirectDebitLauncher`, `SEPADirectDebitLauncherCallback`, `SEPADirectDebitFlowStartedCallback`,
`SEPADirectDebitResponse`, `SEPADirectDebitBrowserSwitchResult`, and `SEPADirectDebitBrowserSwitchResultCallback`
* Remove Fragment or Activity requirement from `SEPADirectDebitClient` constructor
* Modify `SEPADirectDebitClient#tokenize` parameters

## unreleased

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@

import com.braintreepayments.api.BraintreeClient;
import com.braintreepayments.api.PostalAddress;
import com.braintreepayments.api.SEPADirectDebitBrowserSwitchResult;
import com.braintreepayments.api.SEPADirectDebitClient;
import com.braintreepayments.api.SEPADirectDebitListener;
import com.braintreepayments.api.SEPADirectDebitLauncher;
import com.braintreepayments.api.SEPADirectDebitLauncherCallback;
import com.braintreepayments.api.SEPADirectDebitMandateType;
import com.braintreepayments.api.SEPADirectDebitNonce;
import com.braintreepayments.api.SEPADirectDebitRequest;

import java.util.UUID;

public class SEPADirectDebitFragment extends BaseFragment implements SEPADirectDebitListener {
public class SEPADirectDebitFragment extends BaseFragment {

private SEPADirectDebitClient sepaDirectDebitClient;

private SEPADirectDebitLauncher sepaDirectDebitLauncher;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Expand All @@ -31,12 +33,27 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container,
button.setOnClickListener(this::launchSEPADirectDebit);

BraintreeClient braintreeClient = getBraintreeClient();
sepaDirectDebitClient = new SEPADirectDebitClient(this, braintreeClient);
sepaDirectDebitClient.setListener(this);
sepaDirectDebitClient = new SEPADirectDebitClient(braintreeClient);

sepaDirectDebitLauncher = new SEPADirectDebitLauncher(sepaDirectDebitBrowserSwitchResult ->
sepaDirectDebitClient.onBrowserSwitchResult(sepaDirectDebitBrowserSwitchResult, (sepaDirectDebitNonce, error) -> {
if (error != null) {
handleError(error);
} else {
handleSEPANonce(sepaDirectDebitNonce);
}
})
);

return view;
}

@Override
public void onResume() {
super.onResume();
sepaDirectDebitLauncher.handleReturnToAppFromBrowser(requireContext(), requireActivity().getIntent());
}

public void launchSEPADirectDebit(View view) {
PostalAddress billingAddress = new PostalAddress();
billingAddress.setStreetAddress("Kantstraße 70");
Expand All @@ -54,24 +71,26 @@ public void launchSEPADirectDebit(View view) {
request.setBillingAddress(billingAddress);
request.setMerchantAccountId("EUR-sepa-direct-debit");

sepaDirectDebitClient.tokenize(requireActivity(), request);
sepaDirectDebitClient.tokenize(request, (sepaDirectDebitResponse, error) -> {
if (error != null) {
handleError(error);
} else if (sepaDirectDebitResponse.getNonce() != null) { // web-flow mandate not required
handleSEPANonce(sepaDirectDebitResponse.getNonce());
} else { // web-flow mandate required
sepaDirectDebitLauncher.launch(requireActivity(), sepaDirectDebitResponse);
}
});
}

private String generateRandomCustomerId() {
return UUID.randomUUID().toString().substring(0,20);
}

@Override
public void onSEPADirectDebitSuccess(@NonNull SEPADirectDebitNonce sepaDirectDebitNonce) {
private void handleSEPANonce(@NonNull SEPADirectDebitNonce sepaDirectDebitNonce) {
super.onPaymentMethodNonceCreated(sepaDirectDebitNonce);

SEPADirectDebitFragmentDirections.ActionSepaDirectDebitFragmentToDisplayNonceFragment action =
SEPADirectDebitFragmentDirections.actionSepaDirectDebitFragmentToDisplayNonceFragment(sepaDirectDebitNonce);
NavHostFragment.findNavController(this).navigate(action);
}

@Override
public void onSEPADirectDebitFailure(@NonNull Exception error) {
handleError(error);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.braintreepayments.api;

/**
* Result received from the SEPA mandate web flow through {@link SEPADirectDebitBrowserSwitchResultCallback}.
* This result should be passed to
* {@link SEPADirectDebitClient#onBrowserSwitchResult(SEPADirectDebitBrowserSwitchResult, SEPADirectDebitBrowserSwitchResultCallback)} )}
* to complete the SEPA mandate flow.
*/
public class SEPADirectDebitBrowserSwitchResult {

private BrowserSwitchResult browserSwitchResult;
private Exception error;

SEPADirectDebitBrowserSwitchResult(BrowserSwitchResult browserSwitchResult) {
this.browserSwitchResult = browserSwitchResult;
}

SEPADirectDebitBrowserSwitchResult(Exception error) {
this.error = error;
}

BrowserSwitchResult getBrowserSwitchResult() {
return browserSwitchResult;
}

Exception getError() {
return error;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.braintreepayments.api;

import androidx.annotation.Nullable;

/**
* Callback for receiving result of
* {@link SEPADirectDebitClient#onBrowserSwitchResult(SEPADirectDebitBrowserSwitchResult, SEPADirectDebitBrowserSwitchResultCallback)}.
*/
public interface SEPADirectDebitBrowserSwitchResultCallback {

/**
* @param sepaDirectDebitNonce {@link SEPADirectDebitNonce}
* @param error an exception that occurred while processing a PayPal result
*/
void onResult(@Nullable SEPADirectDebitNonce sepaDirectDebitNonce, @Nullable Exception error);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.braintreepayments.api;

import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.webkit.URLUtil;

Expand All @@ -25,65 +27,32 @@ public class SEPADirectDebitClient {

private final SEPADirectDebitApi sepaDirectDebitApi;
private final BraintreeClient braintreeClient;
private SEPADirectDebitListener listener;

/**
* Create a new instance of {@link SEPADirectDebitClient} from within an Activity using a
* {@link BraintreeClient}.
* Create a new instance of {@link SEPADirectDebitClient} using a {@link BraintreeClient}.
*
* @param activity an Android FragmentActivity
* @param braintreeClient a {@link BraintreeClient}
*/
public SEPADirectDebitClient(@NonNull FragmentActivity activity,
@NonNull BraintreeClient braintreeClient) {
this(activity, activity.getLifecycle(), braintreeClient,
new SEPADirectDebitApi(braintreeClient));
}

/**
* Create a new instance of {@link SEPADirectDebitClient} from within a Fragment using a
* {@link BraintreeClient}.
*
* @param fragment an Android Fragment
* @param braintreeClient a {@link BraintreeClient}
*/
public SEPADirectDebitClient(@NonNull Fragment fragment,
@NonNull BraintreeClient braintreeClient) {
this(fragment.getActivity(), fragment.getLifecycle(), braintreeClient,
new SEPADirectDebitApi(braintreeClient));
public SEPADirectDebitClient(@NonNull BraintreeClient braintreeClient) {
this(braintreeClient, new SEPADirectDebitApi(braintreeClient));
}

@VisibleForTesting
SEPADirectDebitClient(FragmentActivity activity, Lifecycle lifecycle,
BraintreeClient braintreeClient, SEPADirectDebitApi sepaDirectDebitApi) {
this.sepaDirectDebitApi = sepaDirectDebitApi;
SEPADirectDebitClient(BraintreeClient braintreeClient, SEPADirectDebitApi sepaDirectDebitApi) {
this.braintreeClient = braintreeClient;
if (activity != null && lifecycle != null) {
SEPADirectDebitLifecycleObserver observer = new SEPADirectDebitLifecycleObserver(this);
lifecycle.addObserver(observer);
}
}

/**
* Add a {@link SEPADirectDebitListener} to your client to receive results or errors from the
* SEPA Direct Debit flow.
*
* @param listener a {@link SEPADirectDebitListener}
*/
public void setListener(SEPADirectDebitListener listener) {
this.listener = listener;
this.sepaDirectDebitApi = sepaDirectDebitApi;
}

/**
* Initiates a browser switch to display a mandate to the user. Upon successful mandate
* creation, tokenizes the payment method and returns a result to the
* {@link SEPADirectDebitListener}.
* Starts the SEPA tokenization process by creating a {@link SEPADirectDebitResponse} to be used
* to launch the SEPA mandate flow in
* {@link SEPADirectDebitLauncher#launch(FragmentActivity, SEPADirectDebitResponse)}
*
* @param activity an Android FragmentActivity
* @param sepaDirectDebitRequest the {@link SEPADirectDebitRequest}.
* @param sepaDirectDebitRequest {@link SEPADirectDebitRequest}
* @param callback {@link SEPADirectDebitFlowStartedCallback}
*/
public void tokenize(final FragmentActivity activity,
final SEPADirectDebitRequest sepaDirectDebitRequest) {
public void tokenize(@NonNull final SEPADirectDebitRequest sepaDirectDebitRequest,
@NonNull final SEPADirectDebitFlowStartedCallback callback) {
braintreeClient.sendAnalyticsEvent("sepa-direct-debit.selected.started");
braintreeClient.sendAnalyticsEvent("sepa-direct-debit.create-mandate.requested");
sepaDirectDebitApi.createMandate(sepaDirectDebitRequest,
Expand All @@ -94,11 +63,13 @@ public void tokenize(final FragmentActivity activity,
braintreeClient.sendAnalyticsEvent(
"sepa-direct-debit.create-mandate.success");
try {
startBrowserSwitch(activity, result);
} catch (JSONException | BrowserSwitchException exception) {
SEPADirectDebitResponse sepaDirectDebitResponse =
new SEPADirectDebitResponse(buildBrowserSwitchOptions(result), null);
callback.onResult(sepaDirectDebitResponse, null);
} catch (JSONException exception) {
braintreeClient.sendAnalyticsEvent(
"sepa-direct-debit.browser-switch.failure");
listener.onSEPADirectDebitFailure(exception);
callback.onResult(null, exception);
}
} else if (result.getApprovalUrl().equals("null")) {
braintreeClient.sendAnalyticsEvent(
Expand All @@ -113,37 +84,58 @@ public void tokenize(final FragmentActivity activity,
if (sepaDirectDebitNonce != null) {
braintreeClient.sendAnalyticsEvent(
"sepa-direct-debit.tokenize.success");
listener.onSEPADirectDebitSuccess(sepaDirectDebitNonce);
SEPADirectDebitResponse sepaDirectDebitResponse =
new SEPADirectDebitResponse(null, sepaDirectDebitNonce);
callback.onResult(sepaDirectDebitResponse, null);
} else if (tokenizeError != null) {
braintreeClient.sendAnalyticsEvent(
"sepa-direct-debit.tokenize.failure");
listener.onSEPADirectDebitFailure(tokenizeError);
callback.onResult(null, tokenizeError);
}
});
} else {
braintreeClient.sendAnalyticsEvent(
"sepa-direct-debit.create-mandate.failure");
listener.onSEPADirectDebitFailure(
new BraintreeException("An unexpected error occurred."));
callback.onResult(null, new BraintreeException("An unexpected error occurred."));
}
} else if (createMandateError != null) {
braintreeClient.sendAnalyticsEvent(
"sepa-direct-debit.create-mandate.failure");
listener.onSEPADirectDebitFailure(createMandateError);
callback.onResult(null, createMandateError);
}
});
}

void onBrowserSwitchResult(FragmentActivity activity) {
// TODO: - The wording in this docstring is confusing to me. Let's improve & align across all clients.
/**
* After receiving a result from the SEPA mandate web flow via
* {@link SEPADirectDebitLauncher#handleReturnToAppFromBrowser(Context, Intent)}, pass the
* {@link SEPADirectDebitBrowserSwitchResult} returned to this method to tokenize the SEPA
* account and receive a {@link SEPADirectDebitNonce} on success.
*
* @param sepaDirectDebitBrowserSwitchResult a {@link SEPADirectDebitBrowserSwitchResult} received
* in the callback of {@link SEPADirectDebitLauncher}
* @param callback {@link SEPADirectDebitBrowserSwitchResultCallback}
*/
public void onBrowserSwitchResult(@NonNull SEPADirectDebitBrowserSwitchResult sepaDirectDebitBrowserSwitchResult,
@NonNull final SEPADirectDebitBrowserSwitchResultCallback callback) {
BrowserSwitchResult browserSwitchResult =
braintreeClient.deliverBrowserSwitchResult(activity);
sepaDirectDebitBrowserSwitchResult.getBrowserSwitchResult();
if (browserSwitchResult == null && sepaDirectDebitBrowserSwitchResult.getError() != null) {
callback.onResult(null, sepaDirectDebitBrowserSwitchResult.getError());
return;
}

if (browserSwitchResult == null) {
callback.onResult(null, new BraintreeException("An unexpected error occurred."));
return;
}

int result = browserSwitchResult.getStatus();
switch (result) {
case BrowserSwitchStatus.CANCELED:
braintreeClient.sendAnalyticsEvent("sepa-direct-debit.browser-switch.canceled");
listener.onSEPADirectDebitFailure(
new UserCanceledException("User canceled SEPA Debit."));
callback.onResult(null, new UserCanceledException("User canceled SEPA Debit."));
break;
case BrowserSwitchStatus.SUCCESS:
Uri deepLinkUri = browserSwitchResult.getDeepLinkUrl();
Expand All @@ -165,34 +157,27 @@ void onBrowserSwitchResult(FragmentActivity activity) {
if (sepaDirectDebitNonce != null) {
braintreeClient.sendAnalyticsEvent(
"sepa-direct-debit.tokenize.success");
listener.onSEPADirectDebitSuccess(sepaDirectDebitNonce);
callback.onResult(sepaDirectDebitNonce, null);
} else if (error != null) {
braintreeClient.sendAnalyticsEvent(
"sepa-direct-debit.tokenize.failure");
listener.onSEPADirectDebitFailure(error);
callback.onResult(null, error);
}
});
} else if (deepLinkUri.getPath().contains("cancel")) {
braintreeClient.sendAnalyticsEvent(
"sepa-direct-debit.browser-switch.failure");
listener.onSEPADirectDebitFailure(
new BraintreeException("An unexpected error occurred."));
callback.onResult(null, new BraintreeException("An unexpected error occurred."));
}
} else {
braintreeClient.sendAnalyticsEvent("sepa-direct-debit.browser-switch.failure");
listener.onSEPADirectDebitFailure(new BraintreeException("Unknown error"));
callback.onResult(null, new BraintreeException("Unknown error"));
}
break;
}
}

BrowserSwitchResult getBrowserSwitchResult(FragmentActivity activity) {
return braintreeClient.getBrowserSwitchResult(activity);
}

private void startBrowserSwitch(FragmentActivity activity,
CreateMandateResult createMandateResult)
throws JSONException, BrowserSwitchException {
private BrowserSwitchOptions buildBrowserSwitchOptions(CreateMandateResult createMandateResult) throws JSONException {
JSONObject metadata = new JSONObject()
.put(IBAN_LAST_FOUR_KEY, createMandateResult.getIbanLastFour())
.put(CUSTOMER_ID_KEY, createMandateResult.getCustomerId())
Expand All @@ -205,7 +190,6 @@ private void startBrowserSwitch(FragmentActivity activity,
.metadata(metadata)
.returnUrlScheme(braintreeClient.getReturnUrlScheme());

braintreeClient.startBrowserSwitch(activity, browserSwitchOptions);
braintreeClient.sendAnalyticsEvent("sepa-direct-debit.browser-switch.started");
return browserSwitchOptions;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.braintreepayments.api;

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

/**
* Callback for receiving result of
* {@link SEPADirectDebitClient#tokenize(SEPADirectDebitRequest, SEPADirectDebitFlowStartedCallback)}.
*/
public interface SEPADirectDebitFlowStartedCallback {

/**
* @param sepaDirectDebitResponse the result of the SEPA create mandate call. If a nonce is present,
* no web-based mandate is required. If a nonce is not present,
* you must trigger the web-based mandate flow via
* {@link SEPADirectDebitLauncher#launch(FragmentActivity, SEPADirectDebitResponse)}
* @param error an exception that occurred while initiating the SEPA transaction
*/
void onResult(@Nullable SEPADirectDebitResponse sepaDirectDebitResponse, @Nullable Exception error);
}
Loading
Loading