From 512bc14aebd29a77e3bb7ba2b1c36a57ac03a420 Mon Sep 17 00:00:00 2001 From: Evgenii Evstropov Date: Wed, 30 Dec 2020 16:54:24 +0100 Subject: [PATCH] Redirect component (#9) * Implement redirectComponent support for android and ios. Small refactoring to unify code and extract constants * Update types declarations, remove unused and commented code --- README.md | 6 + android/build.gradle | 1 + .../reactlibrary/RNAdyenEncryptorModule.java | 98 ++++++++++-- index.d.ts | 29 ++-- .../AdyenThreeDS2Wrapper.swift | 5 + ios/RNAdyenEncryptor/HelperThreeDS2.swift | 34 +++-- ios/RNAdyenEncryptor/RNAdyenEncryptor.m | 5 + lib/AdyenEncryptor.ts | 143 ++++++++---------- package.json | 2 +- 9 files changed, 211 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index f2d5ba8..9fd8e6f 100644 --- a/README.md +++ b/README.md @@ -42,3 +42,9 @@ promise.then((data: EncryptedCard) => { // do something neat }) ``` + +### Redirect component + +Check documentation for native code changes required to support redirects +https://docs.adyen.com/checkout/android/components#redirect-component + diff --git a/android/build.gradle b/android/build.gradle index a76841e..e878889 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -74,6 +74,7 @@ repositories { dependencies { //noinspection GradleDynamicVersion implementation 'com.facebook.react:react-native:+' // From node_modules + implementation "com.adyen.checkout:redirect:3.8.0" implementation "com.adyen.checkout:card-ui:3.8.0" implementation "com.adyen.checkout:3ds2:3.8.0" implementation 'org.jdeferred.v2:jdeferred-android-aar:2.0.0' diff --git a/android/src/main/java/com/reactlibrary/RNAdyenEncryptorModule.java b/android/src/main/java/com/reactlibrary/RNAdyenEncryptorModule.java index 4709e0f..79040af 100644 --- a/android/src/main/java/com/reactlibrary/RNAdyenEncryptorModule.java +++ b/android/src/main/java/com/reactlibrary/RNAdyenEncryptorModule.java @@ -1,22 +1,26 @@ package com.reactlibrary; import android.app.Activity; + +import androidx.lifecycle.Observer; + import android.os.Handler; import android.os.Looper; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; -import androidx.lifecycle.Observer; import com.adyen.checkout.base.ActionComponentData; import com.adyen.checkout.base.ComponentError; import com.adyen.checkout.base.model.payments.response.Action; +import com.adyen.checkout.base.model.payments.response.RedirectAction; import com.adyen.checkout.base.model.payments.response.Threeds2ChallengeAction; import com.adyen.checkout.base.model.payments.response.Threeds2FingerprintAction; import com.adyen.checkout.cse.Card; import com.adyen.checkout.cse.CardEncryptor; import com.adyen.checkout.cse.EncryptedCard; import com.adyen.checkout.cse.internal.CardEncryptorImpl; +import com.adyen.checkout.redirect.RedirectComponent; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; @@ -33,8 +37,11 @@ import com.adyen.checkout.adyen3ds2.Adyen3DS2Component; + public class RNAdyenEncryptorModule extends ReactContextBaseJavaModule { + private final String SUCCESS_CALLBACK = "AdyenCardEncryptedSuccess"; + private final String ERROR_CALLBACK = "AdyenCardEncryptedError"; private final ReactApplicationContext reactContext; public RNAdyenEncryptorModule(ReactApplicationContext reactContext) { @@ -71,19 +78,22 @@ public void encryptWithData(ReadableMap cardData) throws Exception { encryptedCardMap.putString("encryptedExpiryYear", encryptedCard.getEncryptedExpiryYear()); } catch (Exception e) { encryptedCardMap.putString("error", e.toString()); - reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("AdyenCardEncryptedError", encryptedCardMap); + reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(ERROR_CALLBACK, encryptedCardMap); } - reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("AdyenCardEncryptedSuccess", encryptedCardMap); + reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(SUCCESS_CALLBACK, encryptedCardMap); } private Adyen3DS2Component authenticator; + private RedirectComponent redirectComponent; private Callback callback; private interface Callback { void onSuccess(ActionComponentData data); + void onError(ComponentError error); } + private Deferred getAuthenticator(Callback callback) { final Deferred deferred = new DeferredObject<>(); @@ -134,22 +144,22 @@ private void dispatchAction(final Action action, final String resultKey) { public void onSuccess(ActionComponentData data) { final JSONObject details = data.getDetails(); final String result = details.optString(resultKey); - if (!result.isEmpty()){ - reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("AdyenCardEncryptedSuccess", details.toString()); - }else{ - reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("AdyenCardEncryptedError","String is empty?"); + if (!result.isEmpty()) { + reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(SUCCESS_CALLBACK, details.toString()); + } else { + reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(ERROR_CALLBACK, "String is empty?"); } } @Override public void onError(ComponentError error) { - reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("AdyenCardEncryptedError", error.getErrorMessage()); + reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(ERROR_CALLBACK, error.getErrorMessage()); } }).then(new DoneCallback() { @Override public void onDone(Adyen3DS2Component authenticator) { if (authenticator == null) { - reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("AdyenCardEncryptedError", "activity is null"); + reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(ERROR_CALLBACK, "activity is null"); return; } @@ -160,9 +170,9 @@ public void onDone(Adyen3DS2Component authenticator) { @Override public void onFail(Throwable result) { if (result != null) { - reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("AdyenCardEncryptedError", result.toString()); + reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(ERROR_CALLBACK, result.toString()); } else { - reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("AdyenCardEncryptedError", "error null"); + reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(ERROR_CALLBACK, "error null"); } } @@ -185,4 +195,70 @@ public void challenge(final String token, final String paymentData) { dispatchAction(action, "threeds2.challengeResult"); } + @ReactMethod + public void redirect(final String url, final String paymentData) { + final RedirectAction action = new RedirectAction(); + action.setUrl(url); + action.setType(RedirectAction.ACTION_TYPE); + action.setPaymentData(paymentData); + + final Deferred deferred = new DeferredObject<>(); + new Handler(Looper.getMainLooper()) + .post(new Runnable() { + @Override + public void run() { + try { + final Activity activity = getCurrentActivity(); + + if (activity == null) { + deferred.reject(null); + } + + final FragmentActivity fragmentActivity = (FragmentActivity) activity; + redirectComponent = RedirectComponent.PROVIDER.get(fragmentActivity); + + redirectComponent.observe(fragmentActivity, new Observer() { + @Override + public void onChanged(@Nullable ActionComponentData actionComponentData) { + final JSONObject details = actionComponentData.getDetails(); + if (details != null) { + reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(SUCCESS_CALLBACK, details.toString()); + } else { + reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(ERROR_CALLBACK, "empty return"); + } + } + }); + + redirectComponent.observeErrors(fragmentActivity, new Observer() { + @Override + public void onChanged(@Nullable ComponentError componentError) { + reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(ERROR_CALLBACK, componentError.getErrorMessage()); + } + }); + deferred.resolve(redirectComponent); + } catch (Throwable e) { + deferred.reject(e); + } + } + }); + + deferred.then(new DoneCallback() { + @Override + public void onDone(RedirectComponent redirectComponent) { + if (redirectComponent == null) { + reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(ERROR_CALLBACK, "activity is null"); + return; + } + + final Activity activity = getCurrentActivity(); + redirectComponent.handleAction(activity, action); + } + }); + } + + @ReactMethod + public void getRedirectUrl() { + reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(SUCCESS_CALLBACK, RedirectComponent.getReturnUrl(getReactApplicationContext())); + } + } diff --git a/index.d.ts b/index.d.ts index 39de79b..9c83a6a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,17 +1,22 @@ declare module 'react-native-adyen-encrypt' { interface CardForm { - cardNumber: string; - securityCode: string; - expiryMonth: string; - expiryYear: string; + cardNumber: string; + securityCode: string; + expiryMonth: string; + expiryYear: string; } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars + declare class AdyenEncryptor { - constructor(adyenPublicKey: string); - encryptCard(cardForm: CardForm); - identify(token: String, paymentData: String): Promise; - challenge(token: String, paymentData: String): Promise; + constructor(adyenPublicKey: string); + + encryptCard(cardForm: CardForm); + + identify(token: String, paymentData: String): Promise; + + challenge(token: String, paymentData: String): Promise; + + redirect(url: String, paymentData: String): Promise; + + getRedirectUrl(): Promise; } - } - \ No newline at end of file +} diff --git a/ios/RNAdyenEncryptor/AdyenThreeDS2Wrapper.swift b/ios/RNAdyenEncryptor/AdyenThreeDS2Wrapper.swift index ca927fb..6a5ea73 100644 --- a/ios/RNAdyenEncryptor/AdyenThreeDS2Wrapper.swift +++ b/ios/RNAdyenEncryptor/AdyenThreeDS2Wrapper.swift @@ -17,4 +17,9 @@ let helperSingleton = HelperThreeDS2() guard let token = token, let paymentData = paymentData else { return } helperSingleton.challenger(token, paymentData) } + + @objc public func redirect(_ url: String?, paymentData: String?) { + guard let url = url, let paymentData = paymentData else { return } + helperSingleton.redirect(url, paymentData) + } } diff --git a/ios/RNAdyenEncryptor/HelperThreeDS2.swift b/ios/RNAdyenEncryptor/HelperThreeDS2.swift index 0356707..5a45771 100644 --- a/ios/RNAdyenEncryptor/HelperThreeDS2.swift +++ b/ios/RNAdyenEncryptor/HelperThreeDS2.swift @@ -9,38 +9,48 @@ import Foundation import Adyen let threeDS2ComponentSingleton = ThreeDS2Component() +let redirectComponentSingleton = RedirectComponent() public class HelperThreeDS2: NSObject, ActionComponentDelegate { - + let threeDS2Component: ThreeDS2Component - + let redirectComponent: RedirectComponent + public override init() { threeDS2Component = threeDS2ComponentSingleton + redirectComponent = redirectComponentSingleton super.init() self.threeDS2Component.delegate = self + self.redirectComponent.delegate = self } - + public func identify(_ token: String?, _ paymentData: String?) { guard let token = token, let paymentData = paymentData else { return } let action = ThreeDS2FingerprintAction(token: token, paymentData: paymentData) self.threeDS2Component.handle(action) } - + public func challenger(_ token: String?, _ paymentData: String?) { guard let token = token, let paymentData = paymentData else { return } let action = ThreeDS2ChallengeAction(token: token, paymentData: paymentData) self.threeDS2Component.handle(action) } - + + public func redirect(_ url: String?, _ paymentData: String?) { + guard let url = url, let paymentData = paymentData else { return } + let action = RedirectAction(url: URL(string: url)!, paymentData: paymentData) + self.redirectComponent.handle(action) + } + public func didProvide(_ data: ActionComponentData, from component: ActionComponent) { - let dictionary = data.details.dictionaryRepresentation; - if let jsonData = try? JSONSerialization.data( withJSONObject: dictionary, options: .prettyPrinted ) { - if let jsonString = String(data: jsonData, encoding: .utf8) { - RNAdyenEventEmitter.sharedInstance().emitEncryptedCard(jsonString) + let dictionary = data.details.dictionaryRepresentation; + if let jsonData = try? JSONSerialization.data( withJSONObject: dictionary, options: .prettyPrinted ) { + if let jsonString = String(data: jsonData, encoding: .utf8) { + RNAdyenEventEmitter.sharedInstance().emitEncryptedCard(jsonString) + } } - } } - + public func didFail(with error: Error, from component: ActionComponent) { var json: [String: String] = [:] json["error"] = error.localizedDescription @@ -50,5 +60,5 @@ public class HelperThreeDS2: NSObject, ActionComponentDelegate { } RNAdyenEventEmitter.sharedInstance().emitEncryptedCardError(json) } - + } diff --git a/ios/RNAdyenEncryptor/RNAdyenEncryptor.m b/ios/RNAdyenEncryptor/RNAdyenEncryptor.m index d78f4bf..bdec2c7 100644 --- a/ios/RNAdyenEncryptor/RNAdyenEncryptor.m +++ b/ios/RNAdyenEncryptor/RNAdyenEncryptor.m @@ -40,6 +40,11 @@ @implementation RNAdyenEncryptor [wrapper challenger:token paymentData:paymentData]; } +RCT_EXPORT_METHOD(redirect: (NSString*)url setPaymentData:(NSString*)paymentData) { + AdyenThreeDS2Wrapper *wrapper = [AdyenThreeDS2Wrapper new]; + [wrapper redirect:url paymentData:paymentData]; +} + - (NSString *)safeString:(id)object { if ([object isKindOfClass:[NSString class]]) { return (NSString *)object; diff --git a/lib/AdyenEncryptor.ts b/lib/AdyenEncryptor.ts index 8317513..c14e98e 100644 --- a/lib/AdyenEncryptor.ts +++ b/lib/AdyenEncryptor.ts @@ -1,98 +1,89 @@ -import { NativeModules, NativeEventEmitter } from "react-native" +import {NativeModules, NativeEventEmitter} from "react-native" interface CardForm { - cardNumber: string - securityCode: string - expiryMonth: string - expiryYear: string + cardNumber: string + securityCode: string + expiryMonth: string + expiryYear: string } interface EncryptedCard { - encryptedCardNumber: string - encryptedExpiryMonth: string - encryptedExpiryYear: string - encryptedSecurityCode: string + encryptedCardNumber: string + encryptedExpiryMonth: string + encryptedExpiryYear: string + encryptedSecurityCode: string } const { - AdyenEncryptor: NativeAdyenEncryptor, - RNAdyenEventEmitter + AdyenEncryptor: NativeAdyenEncryptor, + RNAdyenEventEmitter } = NativeModules +const SUCCESS_CALLBACK = "AdyenCardEncryptedSuccess" +const ERROR_CALLBACK = "AdyenCardEncryptedError" + class AdyenEncryptor { - private emitter: NativeEventEmitter - constructor(private adyenPublicKey: string) { - this.emitter = new NativeEventEmitter(RNAdyenEventEmitter) - } + private emitter: NativeEventEmitter + + constructor(private adyenPublicKey: string) { + this.emitter = new NativeEventEmitter(RNAdyenEventEmitter) + } - encryptCard(cardForm: CardForm): Promise { - const data = { - ...cardForm, - publicKey: this.adyenPublicKey + generatePromise(): Promise { + return new Promise((resolve, reject) => { + const successSubscription = this.emitter.addListener( + SUCCESS_CALLBACK, + (result: T) => { + successSubscription.remove() + resolve(result) + } + ) + const errorSubscription = this.emitter.addListener( + ERROR_CALLBACK, + (result: T) => { + errorSubscription.remove() + reject(result) + } + ) + }) } - const promise = new Promise((resolve, reject) => { - const successSubscription = this.emitter.addListener( - "AdyenCardEncryptedSuccess", - (result: EncryptedCard) => { - successSubscription.remove() - resolve(result) + + encryptCard(cardForm: CardForm): Promise { + const data = { + ...cardForm, + publicKey: this.adyenPublicKey } - ) - const errorSubscription = this.emitter.addListener( - "AdyenCardEncryptedError", - (result: EncryptedCard) => { - errorSubscription.remove() - reject(result) - } - ) - NativeAdyenEncryptor.encryptWithData(data) - }) - return promise - } + const promise = this.generatePromise(); + NativeAdyenEncryptor.encryptWithData(data) + return promise + } - identify(token: String, paymentData: String): Promise { - const promise = new Promise((resolve, reject) => { - const successSubscription = this.emitter.addListener( - "AdyenCardEncryptedSuccess", - (result: any) => { - successSubscription.remove(); - resolve(result) - } - ) - const errorSubscription = this.emitter.addListener( - "AdyenCardEncryptedError", - (result: any) => { - errorSubscription.remove(); - reject(result); - } - ) + identify(token: String, paymentData: String): Promise { + const promise = this.generatePromise(); NativeAdyenEncryptor.identify(token, paymentData); - }); - return promise; -} + return promise; + } -challenge(token: String, paymentData: String): Promise { - const promise = new Promise((resolve, reject) => { - const successSubscription = this.emitter.addListener( - "AdyenCardEncryptedSuccess", - (result: String) => { - successSubscription.remove() - resolve(result) - } - ); - const errorSubscription = this.emitter.addListener( - "AdyenCardEncryptedError", - (result: String) => { - errorSubscription.remove() - reject(result) - } - ); + challenge(token: String, paymentData: String): Promise { + const promise = this.generatePromise(); NativeAdyenEncryptor.challenge(token, paymentData); - }); - return promise; -} + return promise; + } + + redirect(url: String, paymentData: String): Promise { + const promise = this.generatePromise(); + NativeAdyenEncryptor.redirect(url, paymentData); + return promise; + } + + getRedirectUrl(): Promise { + const promise = this.generatePromise(); + NativeAdyenEncryptor.getRedirectUrl(); + return promise; + } + } export default AdyenEncryptor -export { CardForm } +export {CardForm} diff --git a/package.json b/package.json index 33bdc80..76116e2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-native-adyen-encrypt", "title": "React Native Adyen Encryption", - "version": "1.2.4", + "version": "1.2.5", "description": "Cross platform way to encrypt cards for use with Adyen (www.adyen.com) on react native", "main": "index.js", "repository": "github:voomflights/react-native-adyen-encrypt",