From 66282eabfc96bb23ca4ac7d0f37b73afe3e25c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A6=8A=E5=8E=9F=E6=98=8C=E5=BD=A6?= Date: Thu, 5 Dec 2024 13:51:51 +0900 Subject: [PATCH] feat(terminal): kotlin version to 2.0.+ --- packages/terminal/android/build.gradle | 2 +- .../stripe/terminal/StripeTerminal.java | 872 ----------------- .../stripe/terminal/StripeTerminal.kt | 919 ++++++++++++++++++ .../stripe/terminal/StripeTerminalPlugin.java | 193 ---- .../stripe/terminal/StripeTerminalPlugin.kt | 205 ++++ .../stripe/terminal/TokenProvider.java | 105 -- .../stripe/terminal/TokenProvider.kt | 85 ++ .../stripe/terminal/helper/MetaData.java | 28 - .../stripe/terminal/helper/MetaData.kt | 24 + .../terminal/helper/TerminalMappers.java | 96 -- .../stripe/terminal/helper/TerminalMappers.kt | 90 ++ .../stripe/terminal/models/Executor.java | 33 - .../stripe/terminal/models/Executor.kt | 22 + 13 files changed, 1346 insertions(+), 1328 deletions(-) delete mode 100644 packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminal.java create mode 100644 packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminal.kt delete mode 100644 packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminalPlugin.java create mode 100644 packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminalPlugin.kt delete mode 100644 packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/TokenProvider.java create mode 100644 packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/TokenProvider.kt delete mode 100644 packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/helper/MetaData.java create mode 100644 packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/helper/MetaData.kt delete mode 100644 packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/helper/TerminalMappers.java create mode 100644 packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/helper/TerminalMappers.kt delete mode 100644 packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/models/Executor.java create mode 100644 packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/models/Executor.kt diff --git a/packages/terminal/android/build.gradle b/packages/terminal/android/build.gradle index 12d5f7d2f..c753b9375 100644 --- a/packages/terminal/android/build.gradle +++ b/packages/terminal/android/build.gradle @@ -11,7 +11,7 @@ ext { } buildscript { - ext.kotlin_version = project.hasProperty("kotlin_version") ? rootProject.ext.kotlin_version : '1.8.20' + ext.kotlin_version = project.hasProperty("kotlin_version") ? rootProject.ext.kotlin_version : '20.0.+' repositories { google() mavenCentral() diff --git a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminal.java b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminal.java deleted file mode 100644 index e751cde93..000000000 --- a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminal.java +++ /dev/null @@ -1,872 +0,0 @@ -package com.getcapacitor.community.stripe.terminal; - -import android.Manifest; -import android.app.Activity; -import android.app.Application; -import android.bluetooth.BluetoothAdapter; -import android.content.Context; -import android.content.pm.PackageManager; -import android.os.Build; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.ActivityCompat; -import androidx.core.util.Supplier; -import com.getcapacitor.JSArray; -import com.getcapacitor.JSObject; -import com.getcapacitor.PluginCall; -import com.getcapacitor.community.stripe.terminal.helper.TerminalMappers; -import com.getcapacitor.community.stripe.terminal.models.Executor; -import com.google.android.gms.common.util.BiConsumer; -import com.stripe.stripeterminal.Terminal; -import com.stripe.stripeterminal.TerminalApplicationDelegate; -import com.stripe.stripeterminal.external.callable.Callback; -import com.stripe.stripeterminal.external.callable.Cancelable; -import com.stripe.stripeterminal.external.callable.DiscoveryListener; -import com.stripe.stripeterminal.external.callable.InternetReaderListener; -import com.stripe.stripeterminal.external.callable.MobileReaderListener; -import com.stripe.stripeterminal.external.callable.PaymentIntentCallback; -import com.stripe.stripeterminal.external.callable.ReaderCallback; -//import com.stripe.stripeterminal.external.callable.ReaderReconnectionListener; -import com.stripe.stripeterminal.external.callable.TapToPayReaderListener; -import com.stripe.stripeterminal.external.callable.TerminalListener; -import com.stripe.stripeterminal.external.models.BatteryStatus; -import com.stripe.stripeterminal.external.models.CardPresentDetails; -import com.stripe.stripeterminal.external.models.Cart; -import com.stripe.stripeterminal.external.models.CartLineItem; -import com.stripe.stripeterminal.external.models.CollectConfiguration; -import com.stripe.stripeterminal.external.models.ConnectionConfiguration; -import com.stripe.stripeterminal.external.models.ConnectionConfiguration.BluetoothConnectionConfiguration; -import com.stripe.stripeterminal.external.models.ConnectionConfiguration.InternetConnectionConfiguration; -import com.stripe.stripeterminal.external.models.ConnectionConfiguration.UsbConnectionConfiguration; -import com.stripe.stripeterminal.external.models.ConnectionStatus; -import com.stripe.stripeterminal.external.models.DisconnectReason; -import com.stripe.stripeterminal.external.models.DiscoveryConfiguration; -import com.stripe.stripeterminal.external.models.PaymentIntent; -import com.stripe.stripeterminal.external.models.PaymentMethod; -import com.stripe.stripeterminal.external.models.PaymentStatus; -import com.stripe.stripeterminal.external.models.Reader; -import com.stripe.stripeterminal.external.models.ReaderDisplayMessage; -import com.stripe.stripeterminal.external.models.ReaderEvent; -import com.stripe.stripeterminal.external.models.ReaderInputOptions; -import com.stripe.stripeterminal.external.models.ReaderSoftwareUpdate; -import com.stripe.stripeterminal.external.models.SimulateReaderUpdate; -import com.stripe.stripeterminal.external.models.SimulatedCard; -import com.stripe.stripeterminal.external.models.SimulatedCardType; -import com.stripe.stripeterminal.external.models.SimulatorConfiguration; -import com.stripe.stripeterminal.external.models.TerminalException; -import com.stripe.stripeterminal.log.LogLevel; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import org.jetbrains.annotations.NotNull; -import org.json.JSONException; -import org.json.JSONObject; - -public class StripeTerminal extends Executor { - - private TokenProvider tokenProvider; - private Cancelable discoveryCancelable; - private Cancelable collectCancelable; - private Cancelable installUpdateCancelable; - private Cancelable cancelReaderConnectionCancellable; - private List discoveredReadersList; - private String locationId; - private PluginCall collectCall; - private PluginCall confirmPaymentIntentCall; - private final JSObject emptyObject = new JSObject(); - private Boolean isTest; - private TerminalConnectTypes terminalConnectType; - private PaymentIntent paymentIntentInstance; - - private final TerminalMappers terminalMappers = new TerminalMappers(); - - public StripeTerminal( - Supplier contextSupplier, - Supplier activitySupplier, - BiConsumer notifyListenersFunction, - String pluginLogTag - ) { - super(contextSupplier, activitySupplier, notifyListenersFunction, pluginLogTag, "StripeTerminalExecutor"); - this.contextSupplier = contextSupplier; - this.discoveredReadersList = new ArrayList<>(); - } - - public void initialize(final PluginCall call) throws TerminalException { - this.isTest = call.getBoolean("isTest", true); - - BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter(); - if (!bluetooth.isEnabled()) { - if ( - ActivityCompat.checkSelfPermission(this.contextSupplier.get(), Manifest.permission.BLUETOOTH_CONNECT) == - PackageManager.PERMISSION_GRANTED - ) { - bluetooth.enable(); - } - } - - this.activitySupplier.get() - .runOnUiThread(() -> { - TerminalApplicationDelegate.onCreate((Application) this.contextSupplier.get().getApplicationContext()); - notifyListeners(TerminalEnumEvent.Loaded.getWebEventName(), emptyObject); - call.resolve(); - }); - TerminalListener listener = new TerminalListener() { - @Override - public void onConnectionStatusChange(@NonNull ConnectionStatus status) { - notifyListeners( - TerminalEnumEvent.ConnectionStatusChange.getWebEventName(), - new JSObject().put("status", status.toString()) - ); - } - - @Override - public void onPaymentStatusChange(@NonNull PaymentStatus status) { - notifyListeners(TerminalEnumEvent.PaymentStatusChange.getWebEventName(), new JSObject().put("status", status.toString())); - } - }; - LogLevel logLevel = LogLevel.VERBOSE; - this.tokenProvider = new TokenProvider( - this.contextSupplier, - call.getString("tokenProviderEndpoint", ""), - this.notifyListenersFunction - ); - if (!Terminal.isInitialized()) { - Terminal.initTerminal(this.contextSupplier.get().getApplicationContext(), logLevel, this.tokenProvider, listener); - } - Terminal.getInstance(); - } - - public void setConnectionToken(PluginCall call) { - this.tokenProvider.setConnectionToken(call); - } - - public void setSimulatorConfiguration(PluginCall call) { - try { - Terminal.getInstance() - .setSimulatorConfiguration( - new SimulatorConfiguration( - SimulateReaderUpdate.valueOf(call.getString("update", "UPDATE_AVAILABLE")), - new SimulatedCard(SimulatedCardType.valueOf(call.getString("simulatedCard", "VISA"))), - call.getLong("simulatedTipAmount", null), - false - ) - ); - - call.resolve(); - } catch (Exception ex) { - call.reject(ex.getMessage()); - } - } - - public void onDiscoverReaders(final PluginCall call) { - if ( - ActivityCompat.checkSelfPermission(this.contextSupplier.get(), Manifest.permission.ACCESS_FINE_LOCATION) != - PackageManager.PERMISSION_GRANTED - ) { - Log.d(this.logTag, "android.permission.ACCESS_FINE_LOCATION permission is not granted."); - call.reject("android.permission.ACCESS_FINE_LOCATION permission is not granted."); - return; - } - - this.locationId = call.getString("locationId"); - final DiscoveryConfiguration config; - if (Objects.equals(call.getString("type"), TerminalConnectTypes.TapToPay.getWebEventName())) { - config = new DiscoveryConfiguration.TapToPayDiscoveryConfiguration(this.isTest); - this.terminalConnectType = TerminalConnectTypes.TapToPay; - } else if (Objects.equals(call.getString("type"), TerminalConnectTypes.Internet.getWebEventName())) { - config = new DiscoveryConfiguration.InternetDiscoveryConfiguration(0, this.locationId, this.isTest); - this.terminalConnectType = TerminalConnectTypes.Internet; - } else if (Objects.equals(call.getString("type"), TerminalConnectTypes.Usb.getWebEventName())) { - config = new DiscoveryConfiguration.UsbDiscoveryConfiguration(0, this.isTest); - this.terminalConnectType = TerminalConnectTypes.Usb; - } else if ( - Objects.equals(call.getString("type"), TerminalConnectTypes.Bluetooth.getWebEventName()) || - Objects.equals(call.getString("type"), TerminalConnectTypes.Simulated.getWebEventName()) - ) { - config = new DiscoveryConfiguration.BluetoothDiscoveryConfiguration(0, this.isTest); - this.terminalConnectType = TerminalConnectTypes.Bluetooth; - } else { - call.unimplemented(call.getString("type") + " is not support now"); - return; - } - - final DiscoveryListener discoveryListener = readers -> { - // 検索したReaderの一覧をListenerで渡す - Log.d(logTag, String.valueOf(readers.get(0).getSerialNumber())); - this.discoveredReadersList = readers; - JSArray readersJSObject = new JSArray(); - - int i = 0; - for (Reader reader : this.discoveredReadersList) { - readersJSObject.put(convertReaderInterface(reader).put("index", String.valueOf(i))); - } - this.notifyListeners(TerminalEnumEvent.DiscoveredReaders.getWebEventName(), new JSObject().put("readers", readersJSObject)); - call.resolve(new JSObject().put("readers", readersJSObject)); - }; - discoveryCancelable = Terminal.getInstance() - .discoverReaders( - config, - discoveryListener, - new Callback() { - @Override - public void onSuccess() { - Log.d(logTag, "Finished discovering readers"); - } - - @Override - public void onFailure(@NonNull TerminalException ex) { - Log.d(logTag, ex.getLocalizedMessage()); - } - } - ); - } - - public void connectReader(final PluginCall call) { - if (this.terminalConnectType == TerminalConnectTypes.TapToPay) { - this.connectTapToPayReader(call); - } else if (this.terminalConnectType == TerminalConnectTypes.Internet) { - this.connectInternetReader(call); - } else if (this.terminalConnectType == TerminalConnectTypes.Usb) { - this.connectUsbReader(call); - } else if (this.terminalConnectType == TerminalConnectTypes.Bluetooth) { - this.connectBluetoothReader(call); - } else { - call.reject("type is not defined."); - } - } - - public void getConnectedReader(final PluginCall call) { - Reader reader = Terminal.getInstance().getConnectedReader(); - if (reader == null) { - call.resolve(new JSObject().put("reader", JSObject.NULL)); - } else { - call.resolve(new JSObject().put("reader", convertReaderInterface(reader))); - } - } - - public void disconnectReader(final PluginCall call) { - if (Terminal.getInstance().getConnectedReader() == null) { - call.resolve(); - return; - } - - Terminal.getInstance() - .disconnectReader( - new Callback() { - @Override - public void onSuccess() { - notifyListeners(TerminalEnumEvent.DisconnectedReader.getWebEventName(), emptyObject); - call.resolve(); - } - - @Override - public void onFailure(@NonNull TerminalException ex) { - call.reject(ex.getLocalizedMessage(), ex); - } - } - ); - } - - private void connectTapToPayReader(final PluginCall call) { - JSObject reader = call.getObject("reader"); - String serialNumber = reader.getString("serialNumber"); - this.locationId = call.getString("locationId", this.locationId); - - Reader foundReader = this.findReader(this.discoveredReadersList, serialNumber); - - if (serialNumber == null || foundReader == null) { - call.reject("The reader value is not set correctly."); - return; - } - - Boolean autoReconnectOnUnexpectedDisconnect = Objects.requireNonNullElse( - call.getBoolean("autoReconnectOnUnexpectedDisconnect", false), - false - ); - - ConnectionConfiguration.TapToPayConnectionConfiguration config = new ConnectionConfiguration.TapToPayConnectionConfiguration( - this.locationId, - autoReconnectOnUnexpectedDisconnect, - this.tapToPayReaderListener - ); - Terminal.getInstance().connectReader(foundReader, config, this.readerCallback(call)); - } - - TapToPayReaderListener tapToPayReaderListener = new TapToPayReaderListener() { - @Override - public void onReaderReconnectFailed(@NonNull Reader reader) { - notifyListeners( - TerminalEnumEvent.ReaderReconnectFailed.getWebEventName(), - new JSObject().put("reader", convertReaderInterface(reader)) - ); - } - - @Override - public void onReaderReconnectStarted( - @NonNull Reader reader, - @NonNull Cancelable cancelReconnect, - @NonNull DisconnectReason reason - ) { - cancelReaderConnectionCancellable = cancelReconnect; - notifyListeners( - TerminalEnumEvent.ReaderReconnectStarted.getWebEventName(), - new JSObject().put("reason", reason.toString()).put("reader", convertReaderInterface(reader)) - ); - } - - @Override - public void onReaderReconnectSucceeded(@NonNull Reader reader) { - notifyListeners( - TerminalEnumEvent.ReaderReconnectSucceeded.getWebEventName(), - new JSObject().put("reader", convertReaderInterface(reader)) - ); - } - - @Override - public void onDisconnect(@NonNull DisconnectReason reason) { - notifyListeners(TerminalEnumEvent.DisconnectedReader.getWebEventName(), new JSObject().put("reason", reason.toString())); - } - }; - - InternetReaderListener internetReaderListener = new InternetReaderListener() { - @Override - public void onDisconnect(@NonNull DisconnectReason reason) { - notifyListeners(TerminalEnumEvent.DisconnectedReader.getWebEventName(), new JSObject().put("reason", reason.toString())); - } - }; - - // ReaderReconnectionListener readerReconnectionListener = new ReaderReconnectionListener() { - // @Override - // public void onReaderReconnectStarted(@NonNull Reader reader, @NonNull Cancelable cancelable, @NonNull DisconnectReason reason) { - // cancelReaderConnectionCancellable = cancelable; - // notifyListeners( - // TerminalEnumEvent.ReaderReconnectStarted.getWebEventName(), - // new JSObject().put("reason", reason.toString()).put("reader", convertReaderInterface(reader)) - // ); - // } - // - // @Override - // public void onReaderReconnectSucceeded(@NonNull Reader reader) { - // notifyListeners( - // TerminalEnumEvent.ReaderReconnectSucceeded.getWebEventName(), - // new JSObject().put("reader", convertReaderInterface(reader)) - // ); - // } - // - // @Override - // public void onReaderReconnectFailed(@NonNull Reader reader) { - // notifyListeners( - // TerminalEnumEvent.ReaderReconnectFailed.getWebEventName(), - // new JSObject().put("reader", convertReaderInterface(reader)) - // ); - // } - // }; - - private void connectInternetReader(final PluginCall call) { - JSObject reader = call.getObject("reader"); - String serialNumber = reader.getString("serialNumber"); - this.locationId = call.getString("locationId", this.locationId); - - Reader foundReader = this.findReader(this.discoveredReadersList, serialNumber); - - if (serialNumber == null || foundReader == null) { - call.reject("The reader value is not set correctly."); - return; - } - - InternetConnectionConfiguration config = new InternetConnectionConfiguration(true, this.internetReaderListener); - Terminal.getInstance().connectReader(foundReader, config, this.readerCallback(call)); - } - - private void connectUsbReader(final PluginCall call) { - JSObject reader = call.getObject("reader"); - String serialNumber = reader.getString("serialNumber"); - this.locationId = call.getString("locationId", this.locationId); - - Reader foundReader = this.findReader(this.discoveredReadersList, serialNumber); - - if (serialNumber == null || foundReader == null) { - call.reject("The reader value is not set correctly."); - return; - } - - UsbConnectionConfiguration config = new UsbConnectionConfiguration(this.locationId, true, this.readerListener()); - Terminal.getInstance().connectReader(foundReader, config, this.readerCallback(call)); - } - - private void connectBluetoothReader(final PluginCall call) { - JSObject reader = call.getObject("reader"); - String serialNumber = reader.getString("serialNumber"); - this.locationId = call.getString("locationId", this.locationId); - - Reader foundReader = this.findReader(this.discoveredReadersList, serialNumber); - - if (serialNumber == null || foundReader == null) { - call.reject("The reader value is not set correctly."); - return; - } - Boolean autoReconnectOnUnexpectedDisconnect = Objects.requireNonNullElse( - call.getBoolean("autoReconnectOnUnexpectedDisconnect", false), - false - ); - - BluetoothConnectionConfiguration config = new BluetoothConnectionConfiguration( - this.locationId, - autoReconnectOnUnexpectedDisconnect, - this.readerListener() - ); - Terminal.getInstance().connectReader(foundReader, config, this.readerCallback(call)); - } - - public void cancelDiscoverReaders(final PluginCall call) { - if (discoveryCancelable == null || discoveryCancelable.isCompleted()) { - call.resolve(); - return; - } - discoveryCancelable.cancel( - new Callback() { - @Override - public void onSuccess() { - notifyListeners(TerminalEnumEvent.CancelDiscoveredReaders.getWebEventName(), emptyObject); - call.resolve(); - } - - @Override - public void onFailure(@NonNull TerminalException ex) { - call.reject(ex.getLocalizedMessage(), ex); - } - } - ); - } - - public void collectPaymentMethod(final PluginCall call) { - String paymentIntent = call.getString("paymentIntent"); - if (paymentIntent == null) { - call.reject("The value of paymentIntent is not set correctly."); - return; - } - this.collectCall = call; - Terminal.getInstance().retrievePaymentIntent(paymentIntent, createPaymentIntentCallback); - } - - public void cancelCollectPaymentMethod(final PluginCall call) { - if (this.collectCancelable == null || this.collectCancelable.isCompleted()) { - call.resolve(); - return; - } - - this.collectCancelable.cancel( - new Callback() { - @Override - public void onSuccess() { - notifyListeners(TerminalEnumEvent.Canceled.getWebEventName(), emptyObject); - call.resolve(); - } - - @Override - public void onFailure(@NonNull TerminalException e) { - call.reject(e.getLocalizedMessage()); - } - } - ); - } - - private final PaymentIntentCallback createPaymentIntentCallback = new PaymentIntentCallback() { - @Override - public void onSuccess(@NonNull PaymentIntent paymentIntent) { - CollectConfiguration collectConfig = new CollectConfiguration.Builder().updatePaymentIntent(true).build(); - collectCancelable = Terminal.getInstance().collectPaymentMethod(paymentIntent, collectPaymentMethodCallback, collectConfig); - } - - @Override - public void onFailure(@NonNull TerminalException exception) { - notifyListeners(TerminalEnumEvent.Failed.getWebEventName(), emptyObject); - var returnObject = new JSObject(); - returnObject.put("message", exception.getLocalizedMessage()); - if (exception.getApiError() != null) { - returnObject.put("code", exception.getApiError().getCode()); - returnObject.put("declineCode", exception.getApiError().getDeclineCode()); - } - collectCall.reject(exception.getLocalizedMessage(), (String) null, returnObject); - } - }; - - private final PaymentIntentCallback collectPaymentMethodCallback = new PaymentIntentCallback() { - @Override - public void onSuccess(PaymentIntent paymentIntent) { - paymentIntentInstance = paymentIntent; - notifyListeners(TerminalEnumEvent.CollectedPaymentIntent.getWebEventName(), emptyObject); - - PaymentMethod pm = paymentIntent.getPaymentMethod(); - CardPresentDetails card = null; - - if (pm != null) { - card = pm.getCardPresentDetails() != null ? pm.getCardPresentDetails() : pm.getInteracPresentDetails(); - } - - if (card != null) { - collectCall.resolve( - new JSObject() - .put("brand", card.getBrand()) - .put("cardholderName", card.getCardholderName()) - .put("country", card.getCountry()) - .put("emvAuthData", card.getEmvAuthData()) - .put("expMonth", card.getExpMonth()) - .put("expYear", card.getExpYear()) - .put("funding", card.getFunding()) - .put("generatedCard", card.getGeneratedCard()) - .put("incrementalAuthorizationStatus", card.getIncrementalAuthorizationStatus()) - .put("last4", card.getLast4()) - .put("networks", card.getNetworks()) - .put("readMethod", card.getReadMethod()) - ); - } else { - collectCall.resolve(); - } - } - - @Override - public void onFailure(@NonNull TerminalException exception) { - notifyListeners(TerminalEnumEvent.Failed.getWebEventName(), emptyObject); - String errorCode = "generic_error"; - if (exception.getApiError() != null && exception.getApiError().getCode() != null) { - errorCode = exception.getApiError().getCode(); - } - var returnObject = new JSObject(); - returnObject.put("message", exception.getLocalizedMessage()); - if (exception.getApiError() != null) { - returnObject.put("code", exception.getApiError().getCode()); - returnObject.put("declineCode", exception.getApiError().getDeclineCode()); - } - collectCall.reject(exception.getLocalizedMessage(), errorCode, returnObject); - } - }; - - public void confirmPaymentIntent(final PluginCall call) { - if (this.paymentIntentInstance == null) { - call.reject("PaymentIntent not found for confirmPaymentIntent. Use collect method first and try again."); - return; - } - - this.confirmPaymentIntentCall = call; - Terminal.getInstance().confirmPaymentIntent(this.paymentIntentInstance, confirmPaymentMethodCallback); - } - - public void installAvailableUpdate(final PluginCall call) { - Terminal.getInstance().installAvailableUpdate(); - call.resolve(emptyObject); - } - - public void cancelInstallUpdate(final PluginCall call) { - if (this.installUpdateCancelable == null || this.installUpdateCancelable.isCompleted()) { - call.resolve(); - return; - } - - this.installUpdateCancelable.cancel( - new Callback() { - @Override - public void onSuccess() { - call.resolve(); - } - - @Override - public void onFailure(@NonNull TerminalException e) { - call.reject(e.getLocalizedMessage()); - } - } - ); - } - - public void setReaderDisplay(final PluginCall call) { - String currency = call.getString("currency", null); - if (currency == null) { - call.reject("You must provide a currency value"); - return; - } - - int tax = Objects.requireNonNullElse(call.getInt("tax", 0), 0); - int total = Objects.requireNonNullElse(call.getInt("total", 0), 0); - if (total == 0) { - call.reject("You must provide a total value"); - return; - } - - JSArray lineItems = call.getArray("lineItems"); - List lineItemsList; - try { - lineItemsList = lineItems.toList(); - } catch (JSONException e) { - call.reject(e.getLocalizedMessage()); - return; - } - - List cartLineItems = new ArrayList<>(); - for (JSONObject item : lineItemsList) { - try { - JSObject itemObj = JSObject.fromJSONObject(item); - cartLineItems.add( - new CartLineItem( - Objects.requireNonNull(itemObj.getString("displayName")), - Objects.requireNonNull(itemObj.getInteger("quantity")), - Objects.requireNonNull(itemObj.getInteger("amount")) - ) - ); - } catch (JSONException e) { - call.reject(e.getLocalizedMessage()); - return; - } - } - - Cart cart = new Cart.Builder(currency, tax, total, cartLineItems).build(); - - Terminal.getInstance() - .setReaderDisplay( - cart, - new Callback() { - @Override - public void onSuccess() { - call.resolve(); - } - - @Override - public void onFailure(@NonNull TerminalException e) { - call.reject(e.getErrorMessage()); - } - } - ); - } - - public void clearReaderDisplay(final PluginCall call) { - Terminal.getInstance() - .clearReaderDisplay( - new Callback() { - @Override - public void onSuccess() { - call.resolve(); - } - - @Override - public void onFailure(@NonNull TerminalException e) { - call.reject(e.getErrorMessage()); - } - } - ); - } - - public void rebootReader(final PluginCall call) { - Terminal.getInstance() - .rebootReader( - new Callback() { - @Override - public void onSuccess() { - paymentIntentInstance = null; - call.resolve(emptyObject); - } - - @Override - public void onFailure(@NonNull TerminalException e) { - call.reject(e.getLocalizedMessage()); - } - } - ); - } - - public void cancelReaderReconnection(final PluginCall call) { - if (cancelReaderConnectionCancellable == null || cancelReaderConnectionCancellable.isCompleted()) { - call.resolve(); - return; - } - cancelReaderConnectionCancellable.cancel( - new Callback() { - @Override - public void onSuccess() { - call.resolve(); - } - - @Override - public void onFailure(@NonNull TerminalException ex) { - call.reject(ex.getLocalizedMessage(), ex); - } - } - ); - } - - private final PaymentIntentCallback confirmPaymentMethodCallback = new PaymentIntentCallback() { - @Override - public void onSuccess(@NonNull PaymentIntent paymentIntent) { - notifyListeners(TerminalEnumEvent.ConfirmedPaymentIntent.getWebEventName(), emptyObject); - paymentIntentInstance = null; - confirmPaymentIntentCall.resolve(); - } - - @Override - public void onFailure(TerminalException exception) { - notifyListeners(TerminalEnumEvent.Failed.getWebEventName(), emptyObject); - var returnObject = new JSObject(); - returnObject.put("message", exception.getLocalizedMessage()); - if (exception.getApiError() != null) { - returnObject.put("code", exception.getApiError().getCode()); - returnObject.put("declineCode", exception.getApiError().getDeclineCode()); - } - confirmPaymentIntentCall.reject(exception.getLocalizedMessage(), (String) null, returnObject); - } - }; - - private ReaderCallback readerCallback(final PluginCall call) { - return new ReaderCallback() { - @Override - public void onSuccess(@NonNull Reader r) { - notifyListeners(TerminalEnumEvent.ConnectedReader.getWebEventName(), emptyObject); - call.resolve(); - } - - @Override - public void onFailure(@NonNull TerminalException ex) { - ex.printStackTrace(); - call.reject(ex.getLocalizedMessage(), ex); - } - }; - } - - private MobileReaderListener readerListener() { - return new MobileReaderListener() { - @Override - public void onStartInstallingUpdate(@NotNull ReaderSoftwareUpdate update, @NotNull Cancelable cancelable) { - // Show UI communicating that a required update has started installing - installUpdateCancelable = cancelable; - notifyListeners( - TerminalEnumEvent.StartInstallingUpdate.getWebEventName(), - new JSObject().put("update", convertReaderSoftwareUpdate(update)) - ); - } - - @Override - public void onReportReaderSoftwareUpdateProgress(float progress) { - // Update the progress of the install - notifyListeners(TerminalEnumEvent.ReaderSoftwareUpdateProgress.getWebEventName(), new JSObject().put("progress", progress)); - } - - @Override - public void onFinishInstallingUpdate(@Nullable ReaderSoftwareUpdate update, @Nullable TerminalException error) { - JSObject eventObject = new JSObject(); - - if (error != null) { - // note: Since errorCode cannot be obtained in iOS, use errorMessage for unification. - eventObject.put("error", error.getLocalizedMessage()); - notifyListeners(TerminalEnumEvent.FinishInstallingUpdate.getWebEventName(), eventObject); - return; - } - - eventObject.put("update", update == null ? null : convertReaderSoftwareUpdate(update)); - notifyListeners(TerminalEnumEvent.FinishInstallingUpdate.getWebEventName(), eventObject); - } - - @Override - public void onBatteryLevelUpdate(float batteryLevel, @NonNull BatteryStatus batteryStatus, boolean isCharging) { - notifyListeners( - TerminalEnumEvent.BatteryLevel.getWebEventName(), - new JSObject().put("level", batteryLevel).put("charging", isCharging).put("status", batteryStatus.toString()) - ); - } - - @Override - public void onReportLowBatteryWarning() {} - - @Override - public void onReportAvailableUpdate(@NotNull ReaderSoftwareUpdate update) { - // An update is available for the connected reader. Show this update in your application. - // This update can be installed using `Terminal.getInstance().installAvailableUpdate`. - notifyListeners( - TerminalEnumEvent.ReportAvailableUpdate.getWebEventName(), - new JSObject().put("update", convertReaderSoftwareUpdate(update)) - ); - } - - @Override - public void onReportReaderEvent(@NotNull ReaderEvent event) { - notifyListeners(TerminalEnumEvent.ReaderEvent.getWebEventName(), new JSObject().put("event", event.toString())); - } - - @Override - public void onRequestReaderDisplayMessage(@NotNull ReaderDisplayMessage message) { - notifyListeners( - TerminalEnumEvent.RequestDisplayMessage.getWebEventName(), - new JSObject().put("messageType", message.name()).put("message", message.toString()) - ); - } - - @Override - public void onRequestReaderInput(@NotNull ReaderInputOptions options) { - List optionsList = options.getOptions(); - JSArray jsOptions = new JSArray(); - for (ReaderInputOptions.ReaderInputOption optionType : optionsList) { - jsOptions.put(optionType.name()); - } - - notifyListeners( - TerminalEnumEvent.RequestReaderInput.getWebEventName(), - new JSObject().put("options", jsOptions).put("message", options.toString()) - ); - } - - public void onDisconnect(@NotNull DisconnectReason reason) { - notifyListeners(TerminalEnumEvent.DisconnectedReader.getWebEventName(), new JSObject().put("reason", reason.toString())); - } - }; - } - - private JSObject convertReaderInterface(Reader reader) { - return new JSObject() - .put("label", reader.getLabel()) - .put("serialNumber", reader.getSerialNumber()) - .put("id", reader.getId()) - .put("locationId", reader.getLocation() != null ? reader.getLocation().getId() : null) - .put("deviceSoftwareVersion", reader.getDeviceSwVersion$public_release()) - .put("simulated", reader.isSimulated()) - .put("serialNumber", reader.getSerialNumber()) - .put("ipAddress", reader.getIpAddress()) - .put("baseUrl", reader.getBaseUrl()) - .put("bootloaderVersion", reader.getBootloaderVersion()) - .put("configVersion", reader.getConfigVersion()) - .put("emvKeyProfileId", reader.getEmvKeyProfileId()) - .put("firmwareVersion", reader.getFirmwareVersion()) - .put("hardwareVersion", reader.getHardwareVersion()) - .put("macKeyProfileId", reader.getMacKeyProfileId()) - .put("pinKeyProfileId", reader.getPinKeyProfileId()) - .put("trackKeyProfileId", reader.getTrackKeyProfileId()) - .put("settingsVersion", reader.getSettingsVersion()) - .put("pinKeysetId", reader.getPinKeysetId()) - .put("deviceType", terminalMappers.mapFromDeviceType(reader.getDeviceType())) - .put("status", terminalMappers.mapFromNetworkStatus(reader.getNetworkStatus())) - .put("locationStatus", terminalMappers.mapFromLocationStatus(reader.getLocationStatus())) - .put("batteryLevel", reader.getBatteryLevel() != null ? reader.getBatteryLevel().doubleValue() : null) - .put("availableUpdate", terminalMappers.mapFromReaderSoftwareUpdate(reader.getAvailableUpdate())) - .put("location", terminalMappers.mapFromLocation(reader.getLocation())); - } - - private JSObject convertReaderSoftwareUpdate(ReaderSoftwareUpdate update) { - return terminalMappers.mapFromReaderSoftwareUpdate(update); - } - - private Reader findReader(List discoveredReadersList, String serialNumber) { - Reader foundReader = null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - foundReader = discoveredReadersList - .stream() - .filter(device -> serialNumber != null && serialNumber.equals(device.getSerialNumber())) - .findFirst() - .orElse(null); - } else { - for (Reader device : discoveredReadersList) { - if (serialNumber != null && serialNumber.equals(device.getSerialNumber())) { - foundReader = device; - break; - } - } - } - - return foundReader; - } -} diff --git a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminal.kt b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminal.kt new file mode 100644 index 000000000..cbd71d40f --- /dev/null +++ b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminal.kt @@ -0,0 +1,919 @@ +package com.getcapacitor.community.stripe.terminal + +import android.Manifest +import android.app.Activity +import android.app.Application +import android.bluetooth.BluetoothAdapter +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.util.Log +import androidx.core.app.ActivityCompat +import androidx.core.util.Supplier +import com.getcapacitor.JSArray +import com.getcapacitor.JSObject +import com.getcapacitor.PluginCall +import com.getcapacitor.community.stripe.terminal.helper.TerminalMappers +import com.getcapacitor.community.stripe.terminal.models.Executor +import com.google.android.gms.common.util.BiConsumer +import com.stripe.stripeterminal.Terminal +import com.stripe.stripeterminal.Terminal.Companion.initTerminal +import com.stripe.stripeterminal.Terminal.Companion.isInitialized +import com.stripe.stripeterminal.TerminalApplicationDelegate.onCreate +import com.stripe.stripeterminal.external.callable.Callback +import com.stripe.stripeterminal.external.callable.Cancelable +import com.stripe.stripeterminal.external.callable.DiscoveryListener +import com.stripe.stripeterminal.external.callable.InternetReaderListener +import com.stripe.stripeterminal.external.callable.MobileReaderListener +import com.stripe.stripeterminal.external.callable.PaymentIntentCallback +import com.stripe.stripeterminal.external.callable.ReaderCallback +import com.stripe.stripeterminal.external.callable.TapToPayReaderListener +import com.stripe.stripeterminal.external.callable.TerminalListener +import com.stripe.stripeterminal.external.models.BatteryStatus +import com.stripe.stripeterminal.external.models.CardPresentDetails +import com.stripe.stripeterminal.external.models.Cart +import com.stripe.stripeterminal.external.models.CartLineItem +import com.stripe.stripeterminal.external.models.CollectConfiguration +import com.stripe.stripeterminal.external.models.ConnectionConfiguration +import com.stripe.stripeterminal.external.models.ConnectionStatus +import com.stripe.stripeterminal.external.models.DisconnectReason +import com.stripe.stripeterminal.external.models.DiscoveryConfiguration +import com.stripe.stripeterminal.external.models.PaymentIntent +import com.stripe.stripeterminal.external.models.PaymentStatus +import com.stripe.stripeterminal.external.models.Reader +import com.stripe.stripeterminal.external.models.ReaderDisplayMessage +import com.stripe.stripeterminal.external.models.ReaderEvent +import com.stripe.stripeterminal.external.models.ReaderInputOptions +import com.stripe.stripeterminal.external.models.ReaderSoftwareUpdate +import com.stripe.stripeterminal.external.models.SimulateReaderUpdate +import com.stripe.stripeterminal.external.models.SimulatedCard +import com.stripe.stripeterminal.external.models.SimulatorConfiguration +import com.stripe.stripeterminal.external.models.TerminalException +import com.stripe.stripeterminal.log.LogLevel +import org.json.JSONException +import org.json.JSONObject +import java.util.Objects + +//import com.stripe.stripeterminal.external.callable.ReaderReconnectionListener; +class StripeTerminal( + contextSupplier: Supplier, + activitySupplier: Supplier, + notifyListenersFunction: BiConsumer, + pluginLogTag: String +) : Executor( + contextSupplier, + activitySupplier, + notifyListenersFunction, + pluginLogTag, + "StripeTerminalExecutor" +) { + private var tokenProvider: TokenProvider? = null + private var discoveryCancelable: Cancelable? = null + private var collectCancelable: Cancelable? = null + private var installUpdateCancelable: Cancelable? = null + private var cancelReaderConnectionCancellable: Cancelable? = null + private var discoveredReadersList: List + private var locationId: String? = null + private var collectCall: PluginCall? = null + private var confirmPaymentIntentCall: PluginCall? = null + private val emptyObject = JSObject() + private var isTest: Boolean? = null + private var terminalConnectType: TerminalConnectTypes? = null + private var paymentIntentInstance: PaymentIntent? = null + + private val terminalMappers = TerminalMappers() + + @Throws(TerminalException::class) + fun initialize(call: PluginCall) { + this.isTest = call.getBoolean("isTest", true) + + val bluetooth = BluetoothAdapter.getDefaultAdapter() + if (!bluetooth.isEnabled) { + if (ActivityCompat.checkSelfPermission( + contextSupplier.get()!!, + Manifest.permission.BLUETOOTH_CONNECT + ) == + PackageManager.PERMISSION_GRANTED + ) { + bluetooth.enable() + } + } + + activitySupplier.get() + .runOnUiThread { + onCreate((contextSupplier.get()!!.applicationContext as Application)) + notifyListeners(TerminalEnumEvent.Loaded.webEventName, emptyObject) + call.resolve() + } + val listener: TerminalListener = object : TerminalListener { + override fun onConnectionStatusChange(status: ConnectionStatus) { + notifyListeners( + TerminalEnumEvent.ConnectionStatusChange.webEventName, + JSObject().put("status", status.toString()) + ) + } + + override fun onPaymentStatusChange(status: PaymentStatus) { + notifyListeners( + TerminalEnumEvent.PaymentStatusChange.webEventName, + JSObject().put("status", status.toString()) + ) + } + } + val logLevel = LogLevel.VERBOSE + this.tokenProvider = TokenProvider( + this.contextSupplier, + call.getString("tokenProviderEndpoint", ""), + this.notifyListenersFunction + ) + if (!isInitialized()) { + initTerminal( + contextSupplier.get()!!.applicationContext, + logLevel, + this.tokenProvider!!, + listener + ) + } + Terminal.getInstance() + } + + fun setConnectionToken(call: PluginCall) { + tokenProvider!!.setConnectionToken(call) + } + + fun setSimulatorConfiguration(call: PluginCall) { + try { + val updateString = call.getString("update", "UPDATE_AVAILABLE") + val simulateReaderUpdate = SimulateReaderUpdate.values().find { it.name == updateString } + + Terminal.getInstance() + .simulatorConfiguration = SimulatorConfiguration( + simulateReaderUpdate!!, + SimulatedCard(call.getString("simulatedCard", "VISA")!!), + call.getLong("simulatedTipAmount", null), + false + ) + + + call.resolve() + } catch (ex: Exception) { + call.reject(ex.message) + } + } + + fun onDiscoverReaders(call: PluginCall) { + if (ActivityCompat.checkSelfPermission( + contextSupplier.get()!!, + Manifest.permission.ACCESS_FINE_LOCATION + ) != + PackageManager.PERMISSION_GRANTED + ) { + Log.d(this.logTag, "android.permission.ACCESS_FINE_LOCATION permission is not granted.") + call.reject("android.permission.ACCESS_FINE_LOCATION permission is not granted.") + return + } + + this.locationId = call.getString("locationId") + val config: DiscoveryConfiguration + if (call.getString("type") == TerminalConnectTypes.TapToPay.webEventName) { + config = DiscoveryConfiguration.TapToPayDiscoveryConfiguration(this.isTest!!) + this.terminalConnectType = TerminalConnectTypes.TapToPay + } else if (call.getString("type") == TerminalConnectTypes.Internet.webEventName) { + config = DiscoveryConfiguration.InternetDiscoveryConfiguration( + 0, + this.locationId, + this.isTest!! + ) + this.terminalConnectType = TerminalConnectTypes.Internet + } else if (call.getString("type") == TerminalConnectTypes.Usb.webEventName) { + config = DiscoveryConfiguration.UsbDiscoveryConfiguration(0, this.isTest!!) + this.terminalConnectType = TerminalConnectTypes.Usb + } else if (call.getString("type") == TerminalConnectTypes.Bluetooth.webEventName || call.getString( + "type" + ) == TerminalConnectTypes.Simulated.webEventName + ) { + config = DiscoveryConfiguration.BluetoothDiscoveryConfiguration(0, this.isTest!!) + this.terminalConnectType = TerminalConnectTypes.Bluetooth + } else { + call.unimplemented(call.getString("type") + " is not support now") + return + } + + discoveryCancelable = Terminal.getInstance() + .discoverReaders( + config, + object : DiscoveryListener { + override fun onUpdateDiscoveredReaders(readers: List) { + Log.d(logTag, readers[0].serialNumber.toString()) + discoveredReadersList = readers + val readersJSObject = JSArray() + + val i = 0 + for (reader in discoveredReadersList) { + readersJSObject.put(convertReaderInterface(reader).put("index", i.toString())) + } + notifyListeners( + TerminalEnumEvent.DiscoveredReaders.webEventName, + JSObject().put("readers", readersJSObject) + ) + call.resolve(JSObject().put("readers", readersJSObject)) + } + }, + object : Callback { + override fun onSuccess() { + Log.d(logTag, "Finished discovering readers") + } + + override fun onFailure(ex: TerminalException) { + Log.d(logTag, ex.localizedMessage) + } + } + ) + } + + fun connectReader(call: PluginCall) { + if (this.terminalConnectType == TerminalConnectTypes.TapToPay) { + this.connectTapToPayReader(call) + } else if (this.terminalConnectType == TerminalConnectTypes.Internet) { + this.connectInternetReader(call) + } else if (this.terminalConnectType == TerminalConnectTypes.Usb) { + this.connectUsbReader(call) + } else if (this.terminalConnectType == TerminalConnectTypes.Bluetooth) { + this.connectBluetoothReader(call) + } else { + call.reject("type is not defined.") + } + } + + fun getConnectedReader(call: PluginCall) { + val reader: Reader? = Terminal.getInstance().connectedReader + if (reader == null) { + call.resolve(JSObject().put("reader", JSObject.NULL)) + } else { + call.resolve(JSObject().put("reader", convertReaderInterface(reader))) + } + } + + fun disconnectReader(call: PluginCall) { + if (Terminal.getInstance().connectedReader == null) { + call.resolve() + return + } + + Terminal.getInstance() + .disconnectReader( + object : Callback { + override fun onSuccess() { + notifyListeners( + TerminalEnumEvent.DisconnectedReader.webEventName, + emptyObject + ) + call.resolve() + } + + override fun onFailure(ex: TerminalException) { + call.reject(ex.localizedMessage, ex) + } + } + ) + } + + private fun connectTapToPayReader(call: PluginCall) { + val reader = call.getObject("reader") + val serialNumber = reader.getString("serialNumber") + this.locationId = call.getString("locationId", this.locationId) + + val foundReader = this.findReader(this.discoveredReadersList, serialNumber) + + if (serialNumber == null || foundReader == null) { + call.reject("The reader value is not set correctly.") + return + } + + val autoReconnectOnUnexpectedDisconnect: Boolean = Objects.requireNonNullElse( + call.getBoolean("autoReconnectOnUnexpectedDisconnect", false), + false + ) + + val config: ConnectionConfiguration.TapToPayConnectionConfiguration = ConnectionConfiguration.TapToPayConnectionConfiguration( + this.locationId!!, + autoReconnectOnUnexpectedDisconnect, + this.tapToPayReaderListener + ) + Terminal.getInstance().connectReader(foundReader, config, this.readerCallback(call)) + } + + var tapToPayReaderListener: TapToPayReaderListener = object : TapToPayReaderListener { + override fun onReaderReconnectFailed(reader: Reader) { + notifyListeners( + TerminalEnumEvent.ReaderReconnectFailed.webEventName, + JSObject().put("reader", convertReaderInterface(reader)) + ) + } + + override fun onReaderReconnectStarted( + reader: Reader, + cancelReconnect: Cancelable, + reason: DisconnectReason + ) { + cancelReaderConnectionCancellable = cancelReconnect + notifyListeners( + TerminalEnumEvent.ReaderReconnectStarted.webEventName, + JSObject().put("reason", reason.toString()) + .put("reader", convertReaderInterface(reader)) + ) + } + + override fun onReaderReconnectSucceeded(reader: Reader) { + notifyListeners( + TerminalEnumEvent.ReaderReconnectSucceeded.webEventName, + JSObject().put("reader", convertReaderInterface(reader)) + ) + } + + override fun onDisconnect(reason: DisconnectReason) { + notifyListeners( + TerminalEnumEvent.DisconnectedReader.webEventName, + JSObject().put("reason", reason.toString()) + ) + } + } + + var internetReaderListener: InternetReaderListener = object : InternetReaderListener { + override fun onDisconnect(reason: DisconnectReason) { + notifyListeners( + TerminalEnumEvent.DisconnectedReader.webEventName, + JSObject().put("reason", reason.toString()) + ) + } + } + + // ReaderReconnectionListener readerReconnectionListener = new ReaderReconnectionListener() { + // @Override + // public void onReaderReconnectStarted(@NonNull Reader reader, @NonNull Cancelable cancelable, @NonNull DisconnectReason reason) { + // cancelReaderConnectionCancellable = cancelable; + // notifyListeners( + // TerminalEnumEvent.ReaderReconnectStarted.getWebEventName(), + // new JSObject().put("reason", reason.toString()).put("reader", convertReaderInterface(reader)) + // ); + // } + // + // @Override + // public void onReaderReconnectSucceeded(@NonNull Reader reader) { + // notifyListeners( + // TerminalEnumEvent.ReaderReconnectSucceeded.getWebEventName(), + // new JSObject().put("reader", convertReaderInterface(reader)) + // ); + // } + // + // @Override + // public void onReaderReconnectFailed(@NonNull Reader reader) { + // notifyListeners( + // TerminalEnumEvent.ReaderReconnectFailed.getWebEventName(), + // new JSObject().put("reader", convertReaderInterface(reader)) + // ); + // } + // }; + private fun connectInternetReader(call: PluginCall) { + val reader = call.getObject("reader") + val serialNumber = reader.getString("serialNumber") + this.locationId = call.getString("locationId", this.locationId) + + val foundReader = this.findReader(this.discoveredReadersList, serialNumber) + + if (serialNumber == null || foundReader == null) { + call.reject("The reader value is not set correctly.") + return + } + + val config: ConnectionConfiguration.InternetConnectionConfiguration = + ConnectionConfiguration.InternetConnectionConfiguration( + true, + this.internetReaderListener + ) + Terminal.getInstance().connectReader(foundReader, config, this.readerCallback(call)) + } + + private fun connectUsbReader(call: PluginCall) { + val reader = call.getObject("reader") + val serialNumber = reader.getString("serialNumber") + this.locationId = call.getString("locationId", this.locationId) + + val foundReader = this.findReader(this.discoveredReadersList, serialNumber) + + if (serialNumber == null || foundReader == null) { + call.reject("The reader value is not set correctly.") + return + } + + val config: ConnectionConfiguration.UsbConnectionConfiguration = + ConnectionConfiguration.UsbConnectionConfiguration( + this.locationId!!, + true, + this.readerListener() + ) + Terminal.getInstance().connectReader(foundReader, config, this.readerCallback(call)) + } + + private fun connectBluetoothReader(call: PluginCall) { + val reader = call.getObject("reader") + val serialNumber = reader.getString("serialNumber") + this.locationId = call.getString("locationId", this.locationId) + + val foundReader = this.findReader(this.discoveredReadersList, serialNumber) + + if (serialNumber == null || foundReader == null) { + call.reject("The reader value is not set correctly.") + return + } + val autoReconnectOnUnexpectedDisconnect: Boolean = Objects.requireNonNullElse( + call.getBoolean("autoReconnectOnUnexpectedDisconnect", false), + false + ) + + val config: ConnectionConfiguration.BluetoothConnectionConfiguration = + ConnectionConfiguration.BluetoothConnectionConfiguration( + this.locationId!!, + autoReconnectOnUnexpectedDisconnect, + this.readerListener() + ) + Terminal.getInstance().connectReader(foundReader, config, this.readerCallback(call)) + } + + fun cancelDiscoverReaders(call: PluginCall) { + if (discoveryCancelable == null || discoveryCancelable!!.isCompleted) { + call.resolve() + return + } + discoveryCancelable!!.cancel( + object : Callback { + override fun onSuccess() { + notifyListeners( + TerminalEnumEvent.CancelDiscoveredReaders.webEventName, + emptyObject + ) + call.resolve() + } + + override fun onFailure(ex: TerminalException) { + call.reject(ex.localizedMessage, ex) + } + } + ) + } + + fun collectPaymentMethod(call: PluginCall) { + val paymentIntent = call.getString("paymentIntent") + if (paymentIntent == null) { + call.reject("The value of paymentIntent is not set correctly.") + return + } + this.collectCall = call + Terminal.getInstance().retrievePaymentIntent(paymentIntent, createPaymentIntentCallback) + } + + fun cancelCollectPaymentMethod(call: PluginCall) { + if (this.collectCancelable == null || collectCancelable!!.isCompleted) { + call.resolve() + return + } + + collectCancelable!!.cancel( + object : Callback { + override fun onSuccess() { + notifyListeners(TerminalEnumEvent.Canceled.webEventName, emptyObject) + call.resolve() + } + + override fun onFailure(e: TerminalException) { + call.reject(e.localizedMessage) + } + } + ) + } + + private val createPaymentIntentCallback: PaymentIntentCallback = + object : PaymentIntentCallback { + override fun onSuccess(paymentIntent: PaymentIntent) { + val collectConfig: CollectConfiguration = + CollectConfiguration.Builder().updatePaymentIntent(true).build() + collectCancelable = Terminal.getInstance().collectPaymentMethod( + paymentIntent, + collectPaymentMethodCallback, + collectConfig + ) + } + + override fun onFailure(exception: TerminalException) { + notifyListeners(TerminalEnumEvent.Failed.webEventName, emptyObject) + val returnObject = JSObject() + returnObject.put("message", exception.localizedMessage) + if (exception.apiError != null) { + returnObject.put("code", exception.apiError!!.code) + returnObject.put("declineCode", exception.apiError!!.declineCode) + } + collectCall!!.reject(exception.localizedMessage, null as String?, returnObject) + } + } + + private val collectPaymentMethodCallback: PaymentIntentCallback = + object : PaymentIntentCallback { + override fun onSuccess(paymentIntent: PaymentIntent) { + paymentIntentInstance = paymentIntent + notifyListeners(TerminalEnumEvent.CollectedPaymentIntent.webEventName, emptyObject) + + val pm = paymentIntent.paymentMethod + var card: CardPresentDetails? = null + + if (pm != null) { + card = + if (pm.cardPresentDetails != null) pm.cardPresentDetails else pm.interacPresentDetails + } + + if (card != null) { + collectCall!!.resolve( + JSObject() + .put("brand", card.brand) + .put("cardholderName", card.cardholderName) + .put("country", card.country) + .put("emvAuthData", card.emvAuthData) + .put("expMonth", card.expMonth) + .put("expYear", card.expYear) + .put("funding", card.funding) + .put("generatedCard", card.generatedCard) + .put( + "incrementalAuthorizationStatus", + card.incrementalAuthorizationStatus + ) + .put("last4", card.last4) + .put("networks", card.networks) + .put("readMethod", card.readMethod) + ) + } else { + collectCall!!.resolve() + } + } + + override fun onFailure(exception: TerminalException) { + notifyListeners(TerminalEnumEvent.Failed.webEventName, emptyObject) + var errorCode: String? = "generic_error" + if (exception.apiError != null && exception.apiError!!.code != null) { + errorCode = exception.apiError!!.code + } + val returnObject = JSObject() + returnObject.put("message", exception.localizedMessage) + if (exception.apiError != null) { + returnObject.put("code", exception.apiError!!.code) + returnObject.put("declineCode", exception.apiError!!.declineCode) + } + collectCall!!.reject(exception.localizedMessage, errorCode, returnObject) + } + } + + fun confirmPaymentIntent(call: PluginCall) { + if (this.paymentIntentInstance == null) { + call.reject("PaymentIntent not found for confirmPaymentIntent. Use collect method first and try again.") + return + } + + this.confirmPaymentIntentCall = call + Terminal.getInstance() + .confirmPaymentIntent(this.paymentIntentInstance!!, confirmPaymentMethodCallback) + } + + fun installAvailableUpdate(call: PluginCall) { + Terminal.getInstance().installAvailableUpdate() + call.resolve(emptyObject) + } + + fun cancelInstallUpdate(call: PluginCall) { + if (this.installUpdateCancelable == null || installUpdateCancelable!!.isCompleted) { + call.resolve() + return + } + + installUpdateCancelable!!.cancel( + object : Callback { + override fun onSuccess() { + call.resolve() + } + + override fun onFailure(e: TerminalException) { + call.reject(e.localizedMessage) + } + } + ) + } + + fun setReaderDisplay(call: PluginCall) { + val currency = call.getString("currency", null) + if (currency == null) { + call.reject("You must provide a currency value") + return + } + + val tax = Objects.requireNonNullElse(call.getInt("tax", 0), 0) + val total = Objects.requireNonNullElse(call.getInt("total", 0), 0) + if (total == 0) { + call.reject("You must provide a total value") + return + } + + val lineItems = call.getArray("lineItems") + val lineItemsList: List + try { + lineItemsList = lineItems.toList() + } catch (e: JSONException) { + call.reject(e.localizedMessage) + return + } + + val cartLineItems: MutableList = ArrayList() + for (item in lineItemsList) { + try { + val itemObj = JSObject.fromJSONObject(item) + cartLineItems.add( + CartLineItem.Builder( + Objects.requireNonNull(itemObj.getString("displayName")!!), + Objects.requireNonNull(itemObj.getInteger("quantity")!!), + Objects.requireNonNull(itemObj.getInteger("amount")!!).toLong() + ).build() + ) + } catch (e: JSONException) { + call.reject(e.localizedMessage) + return + } + } + + val cart: Cart = Cart.Builder(currency, tax!!.toLong(), total!!.toLong(), cartLineItems).build() + + Terminal.getInstance() + .setReaderDisplay( + cart, + object : Callback { + override fun onSuccess() { + call.resolve() + } + + override fun onFailure(e: TerminalException) { + call.reject(e.errorMessage) + } + } + ) + } + + fun clearReaderDisplay(call: PluginCall) { + Terminal.getInstance() + .clearReaderDisplay( + object : Callback { + override fun onSuccess() { + call.resolve() + } + + override fun onFailure(e: TerminalException) { + call.reject(e.errorMessage) + } + } + ) + } + + fun rebootReader(call: PluginCall) { + Terminal.getInstance() + .rebootReader( + object : Callback { + override fun onSuccess() { + paymentIntentInstance = null + call.resolve(emptyObject) + } + + override fun onFailure(e: TerminalException) { + call.reject(e.localizedMessage) + } + } + ) + } + + fun cancelReaderReconnection(call: PluginCall) { + if (cancelReaderConnectionCancellable == null || cancelReaderConnectionCancellable!!.isCompleted) { + call.resolve() + return + } + cancelReaderConnectionCancellable!!.cancel( + object : Callback { + override fun onSuccess() { + call.resolve() + } + + override fun onFailure(ex: TerminalException) { + call.reject(ex.localizedMessage, ex) + } + } + ) + } + + private val confirmPaymentMethodCallback: PaymentIntentCallback = + object : PaymentIntentCallback { + override fun onSuccess(paymentIntent: PaymentIntent) { + notifyListeners(TerminalEnumEvent.ConfirmedPaymentIntent.webEventName, emptyObject) + paymentIntentInstance = null + confirmPaymentIntentCall!!.resolve() + } + + override fun onFailure(exception: TerminalException) { + notifyListeners(TerminalEnumEvent.Failed.webEventName, emptyObject) + val returnObject = JSObject() + returnObject.put("message", exception.localizedMessage) + if (exception.apiError != null) { + returnObject.put("code", exception.apiError!!.code) + returnObject.put("declineCode", exception.apiError!!.declineCode) + } + confirmPaymentIntentCall!!.reject( + exception.localizedMessage, + null as String?, + returnObject + ) + } + } + + init { + this.contextSupplier = contextSupplier + this.discoveredReadersList = ArrayList() + } + + private fun readerCallback(call: PluginCall): ReaderCallback { + return object : ReaderCallback { + override fun onSuccess(r: Reader) { + notifyListeners(TerminalEnumEvent.ConnectedReader.webEventName, emptyObject) + call.resolve() + } + + override fun onFailure(ex: TerminalException) { + ex.printStackTrace() + call.reject(ex.localizedMessage, ex) + } + } + } + + private fun readerListener(): MobileReaderListener { + return object : MobileReaderListener { + override fun onStartInstallingUpdate( + update: ReaderSoftwareUpdate, + cancelable: Cancelable? + ) { + // Show UI communicating that a required update has started installing + installUpdateCancelable = cancelable + notifyListeners( + TerminalEnumEvent.StartInstallingUpdate.webEventName, + JSObject().put("update", convertReaderSoftwareUpdate(update)) + ) + } + + override fun onReportReaderSoftwareUpdateProgress(progress: Float) { + // Update the progress of the install + notifyListeners( + TerminalEnumEvent.ReaderSoftwareUpdateProgress.webEventName, + JSObject().put("progress", progress.toDouble()) + ) + } + + override fun onFinishInstallingUpdate( + update: ReaderSoftwareUpdate?, + error: TerminalException? + ) { + val eventObject = JSObject() + + if (error != null) { + // note: Since errorCode cannot be obtained in iOS, use errorMessage for unification. + eventObject.put("error", error.localizedMessage) + notifyListeners( + TerminalEnumEvent.FinishInstallingUpdate.webEventName, + eventObject + ) + return + } + + eventObject.put( + "update", + if (update == null) null else convertReaderSoftwareUpdate(update) + ) + notifyListeners(TerminalEnumEvent.FinishInstallingUpdate.webEventName, eventObject) + } + + override fun onBatteryLevelUpdate( + batteryLevel: Float, + batteryStatus: BatteryStatus, + isCharging: Boolean + ) { + notifyListeners( + TerminalEnumEvent.BatteryLevel.webEventName, + JSObject().put("level", batteryLevel.toDouble()).put("charging", isCharging) + .put("status", batteryStatus.toString()) + ) + } + + override fun onReportLowBatteryWarning() {} + + override fun onReportAvailableUpdate(update: ReaderSoftwareUpdate) { + // An update is available for the connected reader. Show this update in your application. + // This update can be installed using `Terminal.getInstance().installAvailableUpdate`. + notifyListeners( + TerminalEnumEvent.ReportAvailableUpdate.webEventName, + JSObject().put("update", convertReaderSoftwareUpdate(update)) + ) + } + + override fun onReportReaderEvent(event: ReaderEvent) { + notifyListeners( + TerminalEnumEvent.ReaderEvent.webEventName, + JSObject().put("event", event.toString()) + ) + } + + override fun onRequestReaderDisplayMessage(message: ReaderDisplayMessage) { + notifyListeners( + TerminalEnumEvent.RequestDisplayMessage.webEventName, + JSObject().put("messageType", message.name).put("message", message.toString()) + ) + } + + override fun onRequestReaderInput(options: ReaderInputOptions) { + val optionsList: List = options.options + val jsOptions = JSArray() + for (optionType in optionsList) { + jsOptions.put(optionType.name) + } + + notifyListeners( + TerminalEnumEvent.RequestReaderInput.webEventName, + JSObject().put("options", jsOptions).put("message", options.toString()) + ) + } + + override fun onDisconnect(reason: DisconnectReason) { + notifyListeners( + TerminalEnumEvent.DisconnectedReader.webEventName, + JSObject().put("reason", reason.toString()) + ) + } + } + } + + private fun convertReaderInterface(reader: Reader?): JSObject { + return JSObject() + .put("label", reader!!.label) + .put("serialNumber", reader.serialNumber) + .put("id", reader.id) + .put("locationId", if (reader.location != null) reader.location!!.id else null) + .put("deviceSoftwareVersion", reader.softwareVersion) + .put("simulated", reader.isSimulated) + .put("serialNumber", reader.serialNumber) + .put("ipAddress", reader.ipAddress) + .put("baseUrl", reader.baseUrl) + .put("bootloaderVersion", reader.bootloaderVersion) + .put("configVersion", reader.configVersion) + .put("emvKeyProfileId", reader.emvKeyProfileId) + .put("firmwareVersion", reader.firmwareVersion) + .put("hardwareVersion", reader.hardwareVersion) + .put("macKeyProfileId", reader.macKeyProfileId) + .put("pinKeyProfileId", reader.pinKeyProfileId) + .put("trackKeyProfileId", reader.trackKeyProfileId) + .put("settingsVersion", reader.settingsVersion) + .put("pinKeysetId", reader.pinKeysetId) + .put("deviceType", terminalMappers.mapFromDeviceType(reader.deviceType)) + .put("status", terminalMappers.mapFromNetworkStatus(reader.networkStatus)) + .put("locationStatus", terminalMappers.mapFromLocationStatus(reader.locationStatus)) + .put( + "batteryLevel", + if (reader.batteryLevel != null) reader.batteryLevel!!.toDouble() else null + ) + .put( + "availableUpdate", + terminalMappers.mapFromReaderSoftwareUpdate(reader.availableUpdate) + ) + .put("location", terminalMappers.mapFromLocation(reader.location)) + } + + private fun convertReaderSoftwareUpdate(update: ReaderSoftwareUpdate): JSObject? { + return terminalMappers.mapFromReaderSoftwareUpdate(update) + } + + private fun findReader(discoveredReadersList: List, serialNumber: String?): Reader? { + var foundReader: Reader? = null + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + foundReader = discoveredReadersList + .stream() + .filter { device: Reader? -> serialNumber != null && serialNumber == device!!.serialNumber } + .findFirst() + .orElse(null) + } else { + for (device in discoveredReadersList) { + if (serialNumber != null && serialNumber == device!!.serialNumber) { + foundReader = device + break + } + } + } + + return foundReader + } +} diff --git a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminalPlugin.java b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminalPlugin.java deleted file mode 100644 index e39c2a8a8..000000000 --- a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminalPlugin.java +++ /dev/null @@ -1,193 +0,0 @@ -package com.getcapacitor.community.stripe.terminal; - -import android.Manifest; -import android.os.Build; -import android.util.Log; -import androidx.annotation.RequiresApi; -import com.getcapacitor.PermissionState; -import com.getcapacitor.Plugin; -import com.getcapacitor.PluginCall; -import com.getcapacitor.PluginMethod; -import com.getcapacitor.annotation.CapacitorPlugin; -import com.getcapacitor.annotation.Permission; -import com.getcapacitor.annotation.PermissionCallback; -import com.stripe.stripeterminal.external.models.TerminalException; -import java.util.Objects; - -@RequiresApi(api = Build.VERSION_CODES.S) -@CapacitorPlugin( - name = "StripeTerminal", - permissions = { - @Permission(alias = "location", strings = { Manifest.permission.ACCESS_FINE_LOCATION }), - @Permission(alias = "bluetooth_old", strings = { Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN }), - @Permission( - alias = "bluetooth", - strings = { Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_ADVERTISE } - ) - } -) -public class StripeTerminalPlugin extends Plugin { - - private final StripeTerminal implementation = new StripeTerminal( - this::getContext, - this::getActivity, - this::notifyListeners, - getLogTag() - ); - - @PluginMethod - public void initialize(PluginCall call) throws TerminalException { - this._initialize(call); - } - - @PluginMethod - public void setConnectionToken(PluginCall call) { - this.implementation.setConnectionToken(call); - } - - @PluginMethod - public void setSimulatorConfiguration(PluginCall call) { - this.implementation.setSimulatorConfiguration(call); - } - - @PermissionCallback - private void locationPermsCallback(PluginCall call) throws TerminalException { - if (getPermissionState("location") == PermissionState.GRANTED) { - this._initialize(call); - } else { - requestPermissionForAlias("location", call, "locationPermsCallback"); - } - } - - @PermissionCallback - private void bluetoothOldPermsCallback(PluginCall call) throws TerminalException { - if (getPermissionState("bluetooth_old") == PermissionState.GRANTED) { - if (Objects.equals(call.getMethodName(), "discoverReaders")) { - this.discoverReaders(call); - } else { - this.connectReader(call); - } - } else { - requestPermissionForAlias("bluetooth_old", call, "bluetoothOldPermsCallback"); - } - } - - @PermissionCallback - private void bluetoothPermsCallback(PluginCall call) throws TerminalException { - if (getPermissionState("bluetooth") == PermissionState.GRANTED) { - if (Objects.equals(call.getMethodName(), "discoverReaders")) { - this.discoverReaders(call); - } else { - this.connectReader(call); - } - } else { - requestPermissionForAlias("bluetooth", call, "bluetoothPermsCallback"); - } - } - - private void _initialize(PluginCall call) throws TerminalException { - if (getPermissionState("location") != PermissionState.GRANTED) { - requestPermissionForAlias("location", call, "locationPermsCallback"); - } else { - Log.d("Capacitor:permission location", getPermissionState("location").toString()); - this.implementation.initialize(call); - } - } - - @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK) - public void discoverReaders(PluginCall call) { - if ( - Objects.equals(call.getString("type"), TerminalConnectTypes.Bluetooth.getWebEventName()) || - Objects.equals(call.getString("type"), TerminalConnectTypes.Simulated.getWebEventName()) - ) { - Log.d("Capacitor:permission bluetooth_old", getPermissionState("bluetooth_old").toString()); - Log.d("Capacitor:permission bluetooth", getPermissionState("bluetooth").toString()); - if (Build.VERSION.SDK_INT <= 30 && getPermissionState("bluetooth_old") != PermissionState.GRANTED) { - requestPermissionForAlias("bluetooth_old", call, "bluetoothOldPermsCallback"); - } else if (Build.VERSION.SDK_INT > 30 && getPermissionState("bluetooth") != PermissionState.GRANTED) { - requestPermissionForAlias("bluetooth", call, "bluetoothPermsCallback"); - } else { - this.implementation.onDiscoverReaders(call); - } - } else { - this.implementation.onDiscoverReaders(call); - } - } - - @PluginMethod - public void cancelDiscoverReaders(PluginCall call) { - this.implementation.cancelDiscoverReaders(call); - } - - @PluginMethod - public void connectReader(PluginCall call) { - if (Objects.equals(call.getString("type"), TerminalConnectTypes.Bluetooth.getWebEventName())) { - Log.d("Capacitor:permission bluetooth_old", getPermissionState("bluetooth_old").toString()); - Log.d("Capacitor:permission bluetooth", getPermissionState("bluetooth").toString()); - if (Build.VERSION.SDK_INT <= 30 && getPermissionState("bluetooth_old") != PermissionState.GRANTED) { - requestPermissionForAlias("bluetooth_old", call, "bluetoothOldPermsCallback"); - } else if (Build.VERSION.SDK_INT > 30 && getPermissionState("bluetooth") != PermissionState.GRANTED) { - requestPermissionForAlias("bluetooth", call, "bluetoothPermsCallback"); - } else { - this.implementation.connectReader(call); - } - } else { - this.implementation.connectReader(call); - } - } - - @PluginMethod - public void getConnectedReader(final PluginCall call) { - this.implementation.getConnectedReader(call); - } - - @PluginMethod - public void disconnectReader(final PluginCall call) { - this.implementation.disconnectReader(call); - } - - @PluginMethod - public void collectPaymentMethod(PluginCall call) { - this.implementation.collectPaymentMethod(call); - } - - @PluginMethod - public void cancelCollectPaymentMethod(final PluginCall call) { - this.implementation.cancelCollectPaymentMethod(call); - } - - @PluginMethod - public void confirmPaymentIntent(PluginCall call) { - this.implementation.confirmPaymentIntent(call); - } - - @PluginMethod - public void installAvailableUpdate(PluginCall call) { - this.implementation.installAvailableUpdate(call); - } - - @PluginMethod - public void cancelInstallUpdate(PluginCall call) { - this.implementation.cancelInstallUpdate(call); - } - - @PluginMethod - public void setReaderDisplay(PluginCall call) { - this.implementation.setReaderDisplay(call); - } - - @PluginMethod - public void clearReaderDisplay(PluginCall call) { - this.implementation.clearReaderDisplay(call); - } - - @PluginMethod - public void rebootReader(PluginCall call) { - this.implementation.rebootReader(call); - } - - @PluginMethod - public void cancelReaderReconnection(PluginCall call) { - this.implementation.cancelReaderReconnection(call); - } -} diff --git a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminalPlugin.kt b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminalPlugin.kt new file mode 100644 index 000000000..2d079a34c --- /dev/null +++ b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/StripeTerminalPlugin.kt @@ -0,0 +1,205 @@ +package com.getcapacitor.community.stripe.terminal + +import android.Manifest +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import com.getcapacitor.JSObject +import com.getcapacitor.PermissionState +import com.getcapacitor.Plugin +import com.getcapacitor.PluginCall +import com.getcapacitor.PluginMethod +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.getcapacitor.annotation.PermissionCallback +import com.stripe.stripeterminal.external.models.TerminalException + +@RequiresApi(api = Build.VERSION_CODES.S) +@CapacitorPlugin( + name = "StripeTerminal", + permissions = [Permission( + alias = "location", + strings = [Manifest.permission.ACCESS_FINE_LOCATION] + ), Permission( + alias = "bluetooth_old", + strings = [Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN] + ), Permission( + alias = "bluetooth", + strings = [Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_ADVERTISE] + )] +) +class StripeTerminalPlugin : Plugin() { + private val implementation = StripeTerminal( + { this.context }, + { this.activity }, + { eventName: String?, data: JSObject? -> this.notifyListeners(eventName, data) }, + logTag + ) + + @PluginMethod + @Throws(TerminalException::class) + fun initialize(call: PluginCall) { + this._initialize(call) + } + + @PluginMethod + fun setConnectionToken(call: PluginCall) { + implementation.setConnectionToken(call) + } + + @PluginMethod + fun setSimulatorConfiguration(call: PluginCall) { + implementation.setSimulatorConfiguration(call) + } + + @PermissionCallback + @Throws(TerminalException::class) + private fun locationPermsCallback(call: PluginCall) { + if (getPermissionState("location") == PermissionState.GRANTED) { + this._initialize(call) + } else { + requestPermissionForAlias("location", call, "locationPermsCallback") + } + } + + @PermissionCallback + @Throws(TerminalException::class) + private fun bluetoothOldPermsCallback(call: PluginCall) { + if (getPermissionState("bluetooth_old") == PermissionState.GRANTED) { + if (call.methodName == "discoverReaders") { + this.discoverReaders(call) + } else { + this.connectReader(call) + } + } else { + requestPermissionForAlias("bluetooth_old", call, "bluetoothOldPermsCallback") + } + } + + @PermissionCallback + @Throws(TerminalException::class) + private fun bluetoothPermsCallback(call: PluginCall) { + if (getPermissionState("bluetooth") == PermissionState.GRANTED) { + if (call.methodName == "discoverReaders") { + this.discoverReaders(call) + } else { + this.connectReader(call) + } + } else { + requestPermissionForAlias("bluetooth", call, "bluetoothPermsCallback") + } + } + + @Throws(TerminalException::class) + private fun _initialize(call: PluginCall) { + if (getPermissionState("location") != PermissionState.GRANTED) { + requestPermissionForAlias("location", call, "locationPermsCallback") + } else { + Log.d("Capacitor:permission location", getPermissionState("location").toString()) + implementation.initialize(call) + } + } + + @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK) + fun discoverReaders(call: PluginCall) { + if (call.getString("type") == TerminalConnectTypes.Bluetooth.webEventName || call.getString( + "type" + ) == TerminalConnectTypes.Simulated.webEventName + ) { + Log.d( + "Capacitor:permission bluetooth_old", + getPermissionState("bluetooth_old").toString() + ) + Log.d("Capacitor:permission bluetooth", getPermissionState("bluetooth").toString()) + if (Build.VERSION.SDK_INT <= 30 && getPermissionState("bluetooth_old") != PermissionState.GRANTED) { + requestPermissionForAlias("bluetooth_old", call, "bluetoothOldPermsCallback") + } else if (Build.VERSION.SDK_INT > 30 && getPermissionState("bluetooth") != PermissionState.GRANTED) { + requestPermissionForAlias("bluetooth", call, "bluetoothPermsCallback") + } else { + implementation.onDiscoverReaders(call) + } + } else { + implementation.onDiscoverReaders(call) + } + } + + @PluginMethod + fun cancelDiscoverReaders(call: PluginCall) { + implementation.cancelDiscoverReaders(call) + } + + @PluginMethod + fun connectReader(call: PluginCall) { + if (call.getString("type") == TerminalConnectTypes.Bluetooth.webEventName) { + Log.d( + "Capacitor:permission bluetooth_old", + getPermissionState("bluetooth_old").toString() + ) + Log.d("Capacitor:permission bluetooth", getPermissionState("bluetooth").toString()) + if (Build.VERSION.SDK_INT <= 30 && getPermissionState("bluetooth_old") != PermissionState.GRANTED) { + requestPermissionForAlias("bluetooth_old", call, "bluetoothOldPermsCallback") + } else if (Build.VERSION.SDK_INT > 30 && getPermissionState("bluetooth") != PermissionState.GRANTED) { + requestPermissionForAlias("bluetooth", call, "bluetoothPermsCallback") + } else { + implementation.connectReader(call) + } + } else { + implementation.connectReader(call) + } + } + + @PluginMethod + fun getConnectedReader(call: PluginCall) { + implementation.getConnectedReader(call) + } + + @PluginMethod + fun disconnectReader(call: PluginCall) { + implementation.disconnectReader(call) + } + + @PluginMethod + fun collectPaymentMethod(call: PluginCall) { + implementation.collectPaymentMethod(call) + } + + @PluginMethod + fun cancelCollectPaymentMethod(call: PluginCall) { + implementation.cancelCollectPaymentMethod(call) + } + + @PluginMethod + fun confirmPaymentIntent(call: PluginCall) { + implementation.confirmPaymentIntent(call) + } + + @PluginMethod + fun installAvailableUpdate(call: PluginCall) { + implementation.installAvailableUpdate(call) + } + + @PluginMethod + fun cancelInstallUpdate(call: PluginCall) { + implementation.cancelInstallUpdate(call) + } + + @PluginMethod + fun setReaderDisplay(call: PluginCall) { + implementation.setReaderDisplay(call) + } + + @PluginMethod + fun clearReaderDisplay(call: PluginCall) { + implementation.clearReaderDisplay(call) + } + + @PluginMethod + fun rebootReader(call: PluginCall) { + implementation.rebootReader(call) + } + + @PluginMethod + fun cancelReaderReconnection(call: PluginCall) { + implementation.cancelReaderReconnection(call) + } +} diff --git a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/TokenProvider.java b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/TokenProvider.java deleted file mode 100644 index cc114b271..000000000 --- a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/TokenProvider.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.getcapacitor.community.stripe.terminal; - -import android.content.Context; -import android.util.Log; -import androidx.core.util.Supplier; -import com.android.volley.Request; -import com.android.volley.RequestQueue; -import com.android.volley.Response; -import com.android.volley.VolleyError; -import com.android.volley.toolbox.StringRequest; -import com.android.volley.toolbox.Volley; -import com.getcapacitor.JSObject; -import com.getcapacitor.PluginCall; -import com.google.android.gms.common.util.BiConsumer; -import com.stripe.stripeterminal.external.callable.ConnectionTokenCallback; -import com.stripe.stripeterminal.external.callable.ConnectionTokenProvider; -import com.stripe.stripeterminal.external.models.ConnectionTokenException; -import java.util.ArrayList; -import java.util.Objects; -import org.json.JSONException; -import org.json.JSONObject; - -public class TokenProvider implements ConnectionTokenProvider { - - protected Supplier contextSupplier; - protected final String tokenProviderEndpoint; - protected BiConsumer notifyListenersFunction; - ArrayList pendingCallback = new ArrayList<>(); - - public TokenProvider( - Supplier contextSupplier, - String tokenProviderEndpoint, - BiConsumer notifyListenersFunction - ) { - this.contextSupplier = contextSupplier; - this.tokenProviderEndpoint = tokenProviderEndpoint; - this.notifyListenersFunction = notifyListenersFunction; - } - - public void setConnectionToken(PluginCall call) { - String token = call.getString("token", ""); - if (!pendingCallback.isEmpty()) { - ConnectionTokenCallback pending = pendingCallback.remove(0); - if (Objects.equals(token, "")) { - pending.onFailure(new ConnectionTokenException("Missing `token` is empty")); - call.reject("Missing `token` is empty"); - } else { - pending.onSuccess(token); - call.resolve(); - } - } else { - call.reject("Stripe Terminal do not pending fetchConnectionToken"); - } - } - - @Override - public void fetchConnectionToken(ConnectionTokenCallback callback) { - if (Objects.equals(this.tokenProviderEndpoint, "")) { - pendingCallback.add(callback); - this.notifyListeners(TerminalEnumEvent.RequestedConnectionToken.getWebEventName(), new JSObject()); - } else { - try { - RequestQueue queue = Volley.newRequestQueue(this.contextSupplier.get()); - StringRequest postRequest = new StringRequest( - Request.Method.POST, - this.tokenProviderEndpoint, - new Response.Listener() { - @Override - public void onResponse(String response) { - try { - JSONObject jsonObject = new JSONObject(response); - Log.d("TokenProvider", jsonObject.getString("secret")); - callback.onSuccess(jsonObject.getString("secret")); - } catch (JSONException e) { - callback.onFailure(new ConnectionTokenException(Objects.requireNonNull(e.getLocalizedMessage()))); - throw new RuntimeException(e); - } - } - }, - new Response.ErrorListener() { - @Override - public void onErrorResponse(VolleyError e) { - throw new RuntimeException(e); - } - } - ) { - // @Override - // protected Map getParams(){ - // Map params = new HashMap<>(); - // params.put("word","test"); - // return params; - // } - }; - - queue.add(postRequest); - } catch (Exception e) { - callback.onFailure(new ConnectionTokenException("Failed to fetch connection token", e)); - } - } - } - - protected void notifyListeners(String eventName, JSObject data) { - notifyListenersFunction.accept(eventName, data); - } -} diff --git a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/TokenProvider.kt b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/TokenProvider.kt new file mode 100644 index 000000000..14028bfc3 --- /dev/null +++ b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/TokenProvider.kt @@ -0,0 +1,85 @@ +package com.getcapacitor.community.stripe.terminal + +import android.content.Context +import android.util.Log +import androidx.core.util.Supplier +import com.android.volley.Response +import com.android.volley.toolbox.StringRequest +import com.android.volley.toolbox.Volley +import com.getcapacitor.JSObject +import com.getcapacitor.PluginCall +import com.google.android.gms.common.util.BiConsumer +import com.stripe.stripeterminal.external.callable.ConnectionTokenCallback +import com.stripe.stripeterminal.external.callable.ConnectionTokenProvider +import com.stripe.stripeterminal.external.models.ConnectionTokenException +import org.json.JSONException +import org.json.JSONObject +import java.util.Objects + +class TokenProvider( + protected var contextSupplier: Supplier?, + protected val tokenProviderEndpoint: String?, + protected var notifyListenersFunction: BiConsumer +) : ConnectionTokenProvider { + var pendingCallback: ArrayList = ArrayList() + + fun setConnectionToken(call: PluginCall) { + val token = call.getString("token", "") + if (!pendingCallback.isEmpty()) { + val pending = pendingCallback.removeAt(0) + if (token == "") { + pending.onFailure(ConnectionTokenException("Missing `token` is empty")) + call.reject("Missing `token` is empty") + } else { + pending.onSuccess(token!!) + call.resolve() + } + } else { + call.reject("Stripe Terminal do not pending fetchConnectionToken") + } + } + + override fun fetchConnectionToken(callback: ConnectionTokenCallback) { + if (this.tokenProviderEndpoint == "") { + pendingCallback.add(callback) + this.notifyListeners( + TerminalEnumEvent.RequestedConnectionToken.webEventName, + JSObject() + ) + } else { + try { + val queue = Volley.newRequestQueue(contextSupplier!!.get()) + val postRequest: StringRequest = object : StringRequest( + Method.POST, + this.tokenProviderEndpoint, + Response.Listener { response -> + try { + val jsonObject = JSONObject(response) + Log.d("TokenProvider", jsonObject.getString("secret")) + callback.onSuccess(jsonObject.getString("secret")) + } catch (e: JSONException) { + callback.onFailure(ConnectionTokenException(Objects.requireNonNull(e.localizedMessage))) + throw RuntimeException(e) + } + }, + Response.ErrorListener { e -> throw RuntimeException(e) } + ) { + // @Override + // protected Map getParams(){ + // Map params = new HashMap<>(); + // params.put("word","test"); + // return params; + // } + } + + queue.add(postRequest) + } catch (e: Exception) { + callback.onFailure(ConnectionTokenException("Failed to fetch connection token", e)) + } + } + } + + protected fun notifyListeners(eventName: String?, data: JSObject?) { + notifyListenersFunction!!.accept(eventName!!, data!!) + } +} diff --git a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/helper/MetaData.java b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/helper/MetaData.java deleted file mode 100644 index c17787a50..000000000 --- a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/helper/MetaData.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.getcapacitor.community.stripe.terminal.helper; - -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import androidx.core.util.Supplier; -import com.getcapacitor.Logger; - -public class MetaData { - - protected Supplier contextSupplier; - - public boolean enableIdentifier; - - public MetaData(Supplier contextSupplier) { - this.contextSupplier = contextSupplier; - try { - ApplicationInfo appInfo = contextSupplier - .get() - .getPackageManager() - .getApplicationInfo(contextSupplier.get().getPackageName(), PackageManager.GET_META_DATA); - - enableIdentifier = appInfo.metaData.getBoolean("com.getcapacitor.community.stripe.enableIdentifier"); - } catch (Exception ignored) { - Logger.info("MetaData didn't be prepare fore Google Pay."); - } - } -} diff --git a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/helper/MetaData.kt b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/helper/MetaData.kt new file mode 100644 index 000000000..05af97451 --- /dev/null +++ b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/helper/MetaData.kt @@ -0,0 +1,24 @@ +package com.getcapacitor.community.stripe.terminal.helper + +import android.content.Context +import android.content.pm.PackageManager +import androidx.core.util.Supplier +import com.getcapacitor.Logger + +class MetaData(protected var contextSupplier: Supplier) { + var enableIdentifier: Boolean = false + + init { + try { + val appInfo = contextSupplier + .get() + .packageManager + .getApplicationInfo(contextSupplier.get().packageName, PackageManager.GET_META_DATA) + + enableIdentifier = + appInfo.metaData.getBoolean("com.getcapacitor.community.stripe.enableIdentifier") + } catch (ignored: Exception) { + Logger.info("MetaData didn't be prepare fore Google Pay.") + } + } +} diff --git a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/helper/TerminalMappers.java b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/helper/TerminalMappers.java deleted file mode 100644 index 4c5a20e71..000000000 --- a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/helper/TerminalMappers.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.getcapacitor.community.stripe.terminal.helper; - -import com.getcapacitor.JSObject; -import com.stripe.stripeterminal.external.models.DeviceType; -import com.stripe.stripeterminal.external.models.Location; -import com.stripe.stripeterminal.external.models.LocationStatus; -import com.stripe.stripeterminal.external.models.Reader; -import com.stripe.stripeterminal.external.models.ReaderSoftwareUpdate; - -public class TerminalMappers { - - public JSObject mapFromLocation(Location location) { - if (location == null) { - return new JSObject(); - } - - JSObject address = new JSObject(); - - if (location.getAddress() != null) { - address - .put("country", location.getAddress().getCountry()) - .put("city", location.getAddress().getCity()) - .put("postalCode", location.getAddress().getPostalCode()) - .put("line1", location.getAddress().getLine1()) - .put("line2", location.getAddress().getLine2()) - .put("state", location.getAddress().getState()); - } - - return new JSObject() - .put("id", location.getId()) - .put("displayName", location.getDisplayName()) - .put("address", address) - .put("livemode", location.getLivemode()); - } - - public JSObject mapFromReaderSoftwareUpdate(ReaderSoftwareUpdate update) { - if (update == null) { - return new JSObject(); - } - - return new JSObject() - .put("deviceSoftwareVersion", update.getVersion()) - .put("estimatedUpdateTime", update.getDurationEstimate().toString()) - .put("requiredAt", update.getRequiredAtMs()); - } - - public String mapFromLocationStatus(LocationStatus status) { - if (status == null) { - return "UNKNOWN"; - } - - return switch (status) { - case NOT_SET -> "NOT_SET"; - case SET -> "SET"; - case UNKNOWN -> "UNKNOWN"; - default -> "UNKNOWN"; - }; - } - - public String mapFromNetworkStatus(Reader.NetworkStatus status) { - if (status == null) { - return "unknown"; - } - - return switch (status) { - case OFFLINE -> "OFFLINE"; - case ONLINE -> "ONLINE"; - default -> "UNKNOWN"; - }; - } - - public String mapFromDeviceType(DeviceType type) { - return switch (type) { - case CHIPPER_1X -> "chipper1X"; - case CHIPPER_2X -> "chipper2X"; - case TAP_TO_PAY_DEVICE -> "tapToPayDevice"; - case ETNA -> "etna"; - case STRIPE_M2 -> "stripeM2"; - case STRIPE_S700 -> "stripeS700"; - case STRIPE_S700_DEVKIT -> "stripeS700Devkit"; - // React Native has this model. deprecated? - // case STRIPE_S710: - // return "stripeS710"; - // case STRIPE_S710_DEVKIT: - // return "stripeS710Devkit"; - case UNKNOWN -> "unknown"; - case VERIFONE_P400 -> "verifoneP400"; - case WISECUBE -> "wiseCube"; - case WISEPAD_3 -> "wisePad3"; - case WISEPAD_3S -> "wisePad3s"; - case WISEPOS_E -> "wisePosE"; - case WISEPOS_E_DEVKIT -> "wisePosEDevkit"; - default -> throw new IllegalArgumentException("Unknown DeviceType: " + type); - }; - } -} diff --git a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/helper/TerminalMappers.kt b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/helper/TerminalMappers.kt new file mode 100644 index 000000000..5eae9af7b --- /dev/null +++ b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/helper/TerminalMappers.kt @@ -0,0 +1,90 @@ +package com.getcapacitor.community.stripe.terminal.helper + +import com.getcapacitor.JSObject +import com.stripe.stripeterminal.external.models.DeviceType +import com.stripe.stripeterminal.external.models.Location +import com.stripe.stripeterminal.external.models.LocationStatus +import com.stripe.stripeterminal.external.models.Reader +import com.stripe.stripeterminal.external.models.ReaderSoftwareUpdate + +class TerminalMappers { + fun mapFromLocation(location: Location?): JSObject { + if (location == null) { + return JSObject() + } + + val address = JSObject() + + if (location.address != null) { + address + .put("country", location.address!!.country) + .put("city", location.address!!.city) + .put("postalCode", location.address!!.postalCode) + .put("line1", location.address!!.line1) + .put("line2", location.address!!.line2) + .put("state", location.address!!.state) + } + + return JSObject() + .put("id", location.id) + .put("displayName", location.displayName) + .put("address", address) + .put("livemode", location.getLivemode()) + } + + fun mapFromReaderSoftwareUpdate(update: ReaderSoftwareUpdate?): JSObject { + if (update == null) { + return JSObject() + } + + return JSObject() + .put("deviceSoftwareVersion", update.version) + .put("estimatedUpdateTime", update.durationEstimate.toString()) + .put("requiredAt", update.requiredAtMs) + } + + fun mapFromLocationStatus(status: LocationStatus?): String { + if (status == null) { + return "UNKNOWN" + } + + return when (status) { + LocationStatus.NOT_SET -> "NOT_SET" + LocationStatus.SET -> "SET" + LocationStatus.UNKNOWN -> "UNKNOWN" + else -> "UNKNOWN" + } + } + + fun mapFromNetworkStatus(status: NetworkStatus?): String { + if (status == null) { + return "unknown" + } + + return when (status) { + Reader.NetworkStatus.OFFLINE -> "OFFLINE" + Reader.NetworkStatus.ONLINE -> "ONLINE" + else -> "UNKNOWN" + } + } + + fun mapFromDeviceType(type: DeviceType): String { + return when (type) { + DeviceType.CHIPPER_1X -> "chipper1X" + DeviceType.CHIPPER_2X -> "chipper2X" + DeviceType.TAP_TO_PAY_DEVICE -> "tapToPayDevice" + DeviceType.ETNA -> "etna" + DeviceType.STRIPE_M2 -> "stripeM2" + DeviceType.STRIPE_S700 -> "stripeS700" + DeviceType.STRIPE_S700_DEVKIT -> "stripeS700Devkit" + DeviceType.UNKNOWN -> "unknown" + DeviceType.VERIFONE_P400 -> "verifoneP400" + DeviceType.WISECUBE -> "wiseCube" + DeviceType.WISEPAD_3 -> "wisePad3" + DeviceType.WISEPAD_3S -> "wisePad3s" + DeviceType.WISEPOS_E -> "wisePosE" + DeviceType.WISEPOS_E_DEVKIT -> "wisePosEDevkit" + else -> throw IllegalArgumentException("Unknown DeviceType: $type") + } + } +} diff --git a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/models/Executor.java b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/models/Executor.java deleted file mode 100644 index de25963ee..000000000 --- a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/models/Executor.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.getcapacitor.community.stripe.terminal.models; - -import android.app.Activity; -import android.content.Context; -import androidx.core.util.Supplier; -import com.getcapacitor.JSObject; -import com.google.android.gms.common.util.BiConsumer; - -public abstract class Executor { - - protected Supplier contextSupplier; - protected final Supplier activitySupplier; - protected BiConsumer notifyListenersFunction; - protected final String logTag; - - // Eventually we can change the notification directly here! - protected void notifyListeners(String eventName, JSObject data) { - notifyListenersFunction.accept(eventName, data); - } - - public Executor( - Supplier contextSupplier, - Supplier activitySupplier, - BiConsumer notifyListenersFunction, - String pluginLogTag, - String executorTag - ) { - this.contextSupplier = contextSupplier; - this.activitySupplier = activitySupplier; - this.notifyListenersFunction = notifyListenersFunction; - this.logTag = pluginLogTag + "|" + executorTag; - } -} diff --git a/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/models/Executor.kt b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/models/Executor.kt new file mode 100644 index 000000000..ca4ac7013 --- /dev/null +++ b/packages/terminal/android/src/main/java/com/getcapacitor/community/stripe/terminal/models/Executor.kt @@ -0,0 +1,22 @@ +package com.getcapacitor.community.stripe.terminal.models + +import android.app.Activity +import android.content.Context +import androidx.core.util.Supplier +import com.getcapacitor.JSObject +import com.google.android.gms.common.util.BiConsumer + +abstract class Executor( + protected var contextSupplier: Supplier, + protected val activitySupplier: Supplier, + protected var notifyListenersFunction: BiConsumer, + pluginLogTag: String, + executorTag: String +) { + protected val logTag: String = "$pluginLogTag|$executorTag" + + // Eventually we can change the notification directly here! + protected fun notifyListeners(eventName: String, data: JSObject) { + notifyListenersFunction.accept(eventName, data) + } +}