/*
* Copyright (C) 2016 Payworks GmbH (http://www.payworks.com)
*
* The MIT License (MIT)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package io.mpos.ui.paybutton.view;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
import java.util.List;
import io.mpos.accessories.parameters.AccessoryParameters;
import io.mpos.errors.ErrorType;
import io.mpos.errors.MposError;
import io.mpos.paymentdetails.ApplicationInformation;
import io.mpos.paymentdetails.PaymentDetailsScheme;
import io.mpos.platform.LocalizationToolbox;
import io.mpos.shared.errors.DefaultMposError;
import io.mpos.transactionprovider.TransactionProcessDetails;
import io.mpos.transactionprovider.TransactionProcessDetailsState;
import io.mpos.transactionprovider.TransactionProvider;
import io.mpos.transactionprovider.processparameters.TransactionProcessParameters;
import io.mpos.transactions.Transaction;
import io.mpos.transactions.TransactionStatus;
import io.mpos.transactions.TransactionType;
import io.mpos.transactions.parameters.TransactionParameters;
import io.mpos.ui.R;
import io.mpos.ui.acquirer.MposUiAccountManager;
import io.mpos.ui.acquirer.view.LoginFragment;
import io.mpos.ui.paybutton.controller.StatefulTransactionProviderProxy;
import io.mpos.ui.shared.MposUi;
import io.mpos.ui.shared.controller.StatefulPrintingProcessProxy;
import io.mpos.ui.shared.model.MposUiConfiguration;
import io.mpos.ui.shared.model.TransactionDataHolder;
import io.mpos.ui.shared.util.ErrorHolder;
import io.mpos.ui.shared.util.ParametersHelper;
import io.mpos.ui.shared.util.UiHelper;
import io.mpos.ui.shared.util.UiState;
import io.mpos.ui.shared.view.ErrorFragment;
import io.mpos.ui.shared.view.PrintReceiptFragment;
import io.mpos.ui.shared.view.SendReceiptFragment;
import io.mpos.ui.shared.view.SummaryFragment;
public class TransactionActivity extends AbstractTransactionActivity implements StatefulTransactionProviderProxy.Callback,
AbstractTransactionFragment.Interaction,
ErrorFragment.Interaction,
SummaryFragment.Interaction,
SendReceiptFragment.Interaction,
PrintReceiptFragment.Interaction,
LoginFragment.Interaction {
private final static String TAG = "TransactionActivity";
public final static String BUNDLE_EXTRA_SESSION_IDENTIFIER = "io.mpos.ui.paybutton.view.TransactionActivity.SESSION_IDENTIFIER";
public final static String BUNDLE_EXTRA_TRANSACTION_PARAMETERS = "io.mpos.ui.paybutton.view.TransactionActivity.TRANSACTION_PARAMETERS";
public final static String BUNDLE_EXTRA_TRANSACTION_PROCESS_PARAMETERS = "io.mpos.ui.paybutton.view.TransactionActivity.TRANSACTION_PROCESS_PARAMETERS";
public final static String BUNDLE_EXTRA_ACQUIRER_LOGIN = "io.mpos.ui.paybutton.TransactionActivity.BUNDLE_EXTRA_ACQUIRER_LOGIN";
public final static String BUNDLE_EXTRA_ACQUIRER_APPLICATION_ID = "io.mpos.ui.paybutton.TransactionActivity.BUNDLE_EXTRA_ACQUIRER_LOGIN";
private static final int REQUEST_CODE_SIGNATURE = 1;
private static final String SAVED_INSTANCE_STATE_FORMATTED_AMOUNT = "io.mpos.ui.paybutton.view.TransactionActivity.FORMATTED_AMOUNT";
private static final String SAVED_INSTANCE_STATE_TITLE_TRANSACTION_TYPE = "io.mpos.ui.paybutton.view.TransactionActivity.TITLE_TRANSACTION_TYPE";
public final static String SAVED_INSTANCE_STATE_UI_STATE = "io.mpos.ui.paybutton.view.TransactionActivity.UI_STATE";
private String mFormattedAmount;
private String mTitleTransactionType;
private MposUiAccountManager mMposUiAccountManager;
private TransactionParameters mTransactionParameters;
private AccessoryParameters mAccessoryParameters;
private TransactionProcessParameters mTransactionProcessParameters;
private String mSessionIdentifier;
private String mApplicationIdentifier;
private boolean mIsAcquirerMode;
private boolean mHasSessionIdentifier;
private TransactionType mTransactionType;
private TransactionParameters.Type mTransactionParametersType;
private boolean mSummaryInteractionOccured;
private CountDownTimer mAutoCloseCountDownTimer;
private LocalizationToolbox mLocalizationToolbox;
private StatefulTransactionProviderProxy mStatefulTransactionProviderProxy = StatefulTransactionProviderProxy.getInstance();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.mpu_activity_payment);
if (getCallingActivity() == null) {
Log.w(TAG, "The transaction activity was started without startActivityForResult() and will not return a result code.");
}
UiHelper.setActionbarWithCustomColors(this, (android.support.v7.widget.Toolbar) findViewById(R.id.mpu_toolbar));
ErrorHolder.getInstance().clear();
parseExtras();
if (savedInstanceState == null) {
if (mHasSessionIdentifier) {
setTitle("");
} else {
constructTransactionTypeTitle();
setTitle(constructTitle());
}
mTransactionProcessParameters = (TransactionProcessParameters) getIntent().getSerializableExtra(BUNDLE_EXTRA_TRANSACTION_PROCESS_PARAMETERS);
} else {
mFormattedAmount = savedInstanceState.getString(SAVED_INSTANCE_STATE_FORMATTED_AMOUNT);
mTitleTransactionType = savedInstanceState.getString(SAVED_INSTANCE_STATE_TITLE_TRANSACTION_TYPE);
setUiState((UiState) savedInstanceState.getSerializable(SAVED_INSTANCE_STATE_UI_STATE));
setTitle(constructTitle());
}
if (!mStatefulTransactionProviderProxy.isTransactionOnGoing() && savedInstanceState == null) {
if (mIsAcquirerMode) {
mMposUiAccountManager = MposUiAccountManager.getInitializedInstance();
if (mMposUiAccountManager.isLoggedIn()) {
// Already logged in. Proceed with the transaction.
setTitle(constructTitle());
startTransaction();
} else {
// Show the acquirer UI and continue only if logged in.
mStatefulTransactionProviderProxy.clearForNewTransaction(); // This needs to be done!
showLoginFragment(mApplicationIdentifier);
}
} else {
startTransaction();
}
}
if (MposUi.getInitializedInstance().getTransactionProvider() != null) {
mLocalizationToolbox = MposUi.getInitializedInstance().getTransactionProvider().getLocalizationToolbox();
}
}
private void parseExtras() {
// 1. Check if Acquirer Mode.
if (getIntent().hasExtra(BUNDLE_EXTRA_ACQUIRER_LOGIN)) {
mIsAcquirerMode = getIntent().hasExtra(BUNDLE_EXTRA_ACQUIRER_LOGIN);
mApplicationIdentifier = getIntent().getStringExtra(BUNDLE_EXTRA_ACQUIRER_APPLICATION_ID);
}
// 2. Get the Accessory Parameters.
mAccessoryParameters = MposUi.getInitializedInstance().getConfiguration().getTerminalParameters();
// 3. Get the Transaction Parameters / Session Identifier for the transaction.
if (getIntent().hasExtra(BUNDLE_EXTRA_SESSION_IDENTIFIER)) {
mHasSessionIdentifier = true;
mSessionIdentifier = getIntent().getStringExtra(BUNDLE_EXTRA_SESSION_IDENTIFIER);
} else {
mTransactionParameters = (TransactionParameters) getIntent().getSerializableExtra(BUNDLE_EXTRA_TRANSACTION_PARAMETERS);
mTransactionType = mTransactionParameters.getType();
mTransactionParametersType = mTransactionParameters.getParametersType();
}
// 4. Get the Transaction Process Parameters
mTransactionProcessParameters = (TransactionProcessParameters) getIntent().getSerializableExtra(BUNDLE_EXTRA_TRANSACTION_PROCESS_PARAMETERS);
}
private String modifyCustomIdentifier(String integratorIdentifier, String customIdentifier) {
if (!TextUtils.isEmpty(customIdentifier)) {
return integratorIdentifier + "-" + customIdentifier;
}
return integratorIdentifier;
}
private void startTransaction() {
if (mIsAcquirerMode) {
String integratorIdentifier = MposUiAccountManager.getInitializedInstance().getIntegratorIdentifier();
String modifiedCustomIdentifier = modifyCustomIdentifier(integratorIdentifier, mTransactionParameters.getCustomIdentifier());
mTransactionParameters = ParametersHelper.getTransactionParametersWithNewCustomIdentifier(mTransactionParameters, modifiedCustomIdentifier);
}
if (mHasSessionIdentifier) {
mStatefulTransactionProviderProxy.startTransactionWithSessionIdentifier(mAccessoryParameters, mSessionIdentifier, mTransactionProcessParameters);
} else if (mTransactionParametersType == TransactionParameters.Type.CAPTURE || mTransactionParametersType == TransactionParameters.Type.REFUND) {
mStatefulTransactionProviderProxy.amendTransaction(mTransactionParameters);
} else {
mStatefulTransactionProviderProxy.startTransaction(mAccessoryParameters, mTransactionParameters, mTransactionProcessParameters);
}
}
private void constructTransactionTypeTitle() {
mTitleTransactionType = getString(R.string.MPUSale);
if (mTransactionType != null && mTransactionType == TransactionType.REFUND) {
mTitleTransactionType = getString(R.string.MPURefund);
}
}
@Override
protected void onResume() {
super.onResume();
mStatefulTransactionProviderProxy.attachCallback(this);
}
@Override
protected void onPause() {
super.onPause();
mStatefulTransactionProviderProxy.attachCallback(null);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putString(SAVED_INSTANCE_STATE_FORMATTED_AMOUNT, mFormattedAmount);
outState.putString(SAVED_INSTANCE_STATE_TITLE_TRANSACTION_TYPE, mTitleTransactionType);
outState.putSerializable(SAVED_INSTANCE_STATE_UI_STATE, getUiState());
super.onSaveInstanceState(outState);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_SIGNATURE) {
if (resultCode == RESULT_CANCELED) {
mStatefulTransactionProviderProxy.abortTransaction();
} else {
byte[] byteArraySignature = data.getByteArrayExtra(SignatureActivity.BUNDLE_KEY_SIGNATURE_IMAGE);
Bitmap signature = BitmapFactory.decodeByteArray(byteArraySignature, 0, byteArraySignature.length);
mStatefulTransactionProviderProxy.continueWithSignature(signature, true);
}
}
}
@Override
public void onBackPressed() {
navigateBack();
}
@Override
public void navigateBack() {
hideSoftKeyboard();
if (getUiState() == UiState.RECEIPT_SENDING || getUiState() == UiState.RECEIPT_PRINTING_ERROR) {
showSummaryFragment(mStatefulTransactionProviderProxy.getCurrentTransaction());
} else if (getUiState() == UiState.TRANSACTION_ERROR) {
processErrorState();
} else if (getUiState() == UiState.SUMMARY_DISPLAYING || getUiState() == UiState.LOGIN_DISPLAYING) {
finishWithResult();
} else if (getUiState() == UiState.FORGOT_PASSWORD_DISPLAYING) {
LoginFragment loginFragment = (LoginFragment) getFragmentManager().findFragmentByTag(LoginFragment.TAG);
if (loginFragment != null) {
loginFragment.setLoginMode(true);
}
} else {
super.navigateBack();
}
}
private void processErrorState() {
if (mIsAcquirerMode &&
MposUi.getInitializedInstance().getError() != null &&
MposUi.getInitializedInstance().getError().getErrorType() == ErrorType.SERVER_AUTHENTICATION_FAILED) {
showLoginFragment(MposUiAccountManager.getInitializedInstance().getApplicationData().getIdentifier());
} else {
finishWithResult();
}
}
@Override
public void onApplicationSelectionRequired(List<ApplicationInformation> applicationInformations) {
Log.d(TAG, "onApplicationSelectionRequired");
showApplicationSelectionFragment(applicationInformations);
}
@Override
public void onCustomerSignatureRequired() {
Log.d(TAG, "onCustomerSignatureRequired");
if (MposUi.getInitializedInstance().getConfiguration().getSignatureCapture() == MposUiConfiguration.SignatureCapture.ON_SCREEN) {
showSignatureActivity();
} else {
mStatefulTransactionProviderProxy.continueWithCustomerSignatureOnReceipt();
}
}
@Override
public void onStatusChanged(TransactionProcessDetails details, Transaction transaction) {
Log.d(TAG, "onStatusChanged=" + details);
if (transaction != null && mLocalizationToolbox != null) {
mFormattedAmount = mLocalizationToolbox.formatAmount(transaction.getAmount(), transaction.getCurrency());
constructTransactionTypeTitle();
setTitle(constructTitle());
}
TransactionFragment paymentFragment = (TransactionFragment) getFragmentManager().findFragmentByTag(TransactionFragment.TAG);
if (paymentFragment == null) {
paymentFragment = TransactionFragment.newInstance();
getFragmentManager().beginTransaction().replace(R.id.mpu_fragment_container, paymentFragment, TransactionFragment.TAG).commit();
}
showFragment(paymentFragment, TransactionFragment.TAG, UiState.TRANSACTION_ONGOING, false);
paymentFragment.updateStatus(details, transaction);
}
@Override
public void onCompleted(Transaction transaction, MposError error) {
TransactionProcessDetails details = mStatefulTransactionProviderProxy.getLastTransactionProcessDetails();
if (transaction == null && error == null) {
MposError e = new DefaultMposError(ErrorType.TRANSACTION_ABORTED);
showErrorFragment(UiState.TRANSACTION_ERROR, true, e, details);
} else if (transaction != null && transaction.getStatus() == TransactionStatus.ABORTED) {
MposError e = new DefaultMposError(ErrorType.TRANSACTION_ABORTED);
showErrorFragment(UiState.TRANSACTION_ERROR, true, e, details);
} else if (error == null) {
showSummaryFragment(transaction);
} else {
ErrorHolder.getInstance().setError(error);
if (mIsAcquirerMode && error.getErrorType() == ErrorType.SERVER_AUTHENTICATION_FAILED) {
mMposUiAccountManager.logout(false);
}
if (mStatefulTransactionProviderProxy.getLastTransactionProcessDetails().getState() == TransactionProcessDetailsState.NOT_REFUNDABLE) {
showErrorFragment(UiState.TRANSACTION_ERROR, false, error, details);
} else if (error.getErrorType() == ErrorType.ACCESSORY_BUSY) {
showErrorFragment(UiState.TRANSACTION_ERROR, true, error, details);
} else {
showErrorFragment(UiState.TRANSACTION_ERROR, !getIntent().hasExtra(BUNDLE_EXTRA_SESSION_IDENTIFIER), error, details);
}
}
}
@Override
public void onAbortTransactionButtonClicked() {
boolean result = mStatefulTransactionProviderProxy.abortTransaction();
if (!result) {
Toast.makeText(this, R.string.MPUBackButtonDisabled, Toast.LENGTH_LONG).show();
}
}
@Override
public void onApplicationSelected(ApplicationInformation applicationInformation) {
mStatefulTransactionProviderProxy.continueWithApplicationSelection(applicationInformation);
}
@Override
public void onSummaryRetryButtonClicked() {
stopAutoCloseTimer();
mFormattedAmount = null;
setTitle(constructTitle());
startTransaction();
}
@Override
public void onSummaryRefundButtonClicked(String transactionIdentifier) {
// noop
}
@Override
public void onSummaryCaptureButtonClicked(String transactionIdentifier) {
// NO -OP
}
@Override
public void onSummarySendReceiptButtonClicked(String transactionIdentifier) {
mSummaryInteractionOccured = true;
stopAutoCloseTimer();
showSendReceiptFragment(transactionIdentifier);
}
@Override
public void onSummaryPrintReceiptButtonClicked(String transactionIdentifier) {
mSummaryInteractionOccured = true;
stopAutoCloseTimer();
showPrintReceiptFragment(transactionIdentifier);
}
@Override
public void onSummaryCloseButtonClicked() {
finishWithResult();
}
@Override
public void onErrorRetryButtonClicked() {
mFormattedAmount = null;
if (getUiState() == UiState.TRANSACTION_ERROR) {
stopAutoCloseTimer();
startTransaction();
} else if (getUiState() == UiState.RECEIPT_PRINTING_ERROR) {
showPrintReceiptFragment(mStatefulTransactionProviderProxy.getCurrentTransaction().getIdentifier());
}
}
@Override
public void onErrorCloseButtonClicked() {
finishWithResult();
}
@Override
public void onReceiptSent() {
showSummaryFragment(mStatefulTransactionProviderProxy.getCurrentTransaction());
}
@Override
public void onReceiptPrintCompleted(MposError error) {
StatefulPrintingProcessProxy.getInstance().teardown();
if (error != null) {
ErrorHolder.getInstance().setError(error);
showErrorFragment(UiState.RECEIPT_PRINTING_ERROR, true, error, null);
} else {
showSummaryFragment(mStatefulTransactionProviderProxy.getCurrentTransaction());
}
}
@Override
public void onAbortPrintingClicked() {
StatefulPrintingProcessProxy.getInstance().requestAbort();
}
// Login Interaction
@Override
public void onLoginCompleted() {
setTitle(constructTitle());
mLocalizationToolbox = MposUi.getInitializedInstance().getTransactionProvider().getLocalizationToolbox();
startTransaction();
}
@Override
public void onLoginModeChanged(boolean loginMode) {
// handle back button behaviour
if (loginMode) {
setUiState(UiState.LOGIN_DISPLAYING);
} else {
setUiState(UiState.FORGOT_PASSWORD_DISPLAYING);
}
}
@Override
public TransactionProvider getTransactionProvider() {
return MposUi.getInitializedInstance().getTransactionProvider();
}
private void finishWithResult() {
stopAutoCloseTimer();
Transaction transaction = mStatefulTransactionProviderProxy.getCurrentTransaction();
int resultCode;
String transactionIdentifier = null;
if (transaction != null) {
boolean approved = (transaction.getStatus() == TransactionStatus.APPROVED);
resultCode = approved ? MposUi.RESULT_CODE_APPROVED : MposUi.RESULT_CODE_FAILED;
transactionIdentifier = transaction.getIdentifier();
} else {
resultCode = MposUi.RESULT_CODE_FAILED;
}
mStatefulTransactionProviderProxy.teardown();
Intent resultIntent = new Intent();
resultIntent.putExtra(MposUi.RESULT_EXTRA_TRANSACTION_IDENTIFIER, transactionIdentifier);
setResult(resultCode, resultIntent);
finish();
}
private void showApplicationSelectionFragment(List<ApplicationInformation> applicationInformations) {
ApplicationSelectionFragment fragment = ApplicationSelectionFragment.newInstance(applicationInformations);
showFragment(fragment, ApplicationSelectionFragment.TAG, UiState.TRANSACTION_WAITING_APPLICATION_SELECTION, false);
}
private void showSignatureActivity() {
setUiState(UiState.TRANSACTION_WAITING_SIGNATURE);
Intent intent = new Intent(this, SignatureActivity.class);
intent.putExtra(SignatureActivity.BUNDLE_KEY_AMOUNT, mFormattedAmount);
PaymentDetailsScheme scheme = mStatefulTransactionProviderProxy.getCurrentTransaction().getPaymentDetails().getScheme();
if (scheme != null) {
int resId = UiHelper.getDrawableIdForCardScheme(scheme);
intent.putExtra(SignatureActivity.BUNDLE_KEY_CARD_SCHEME_ID, resId);
}
startActivityForResult(intent, REQUEST_CODE_SIGNATURE);
}
private void showSendReceiptFragment(String transactionIdentifier) {
SendReceiptFragment fragment = SendReceiptFragment.newInstance(transactionIdentifier);
showFragment(fragment, SendReceiptFragment.TAG, UiState.RECEIPT_SENDING, true);
}
private void showSummaryFragment(Transaction transaction) {
TransactionDataHolder dataHolder = new TransactionDataHolder(transaction);
SummaryFragment summaryFragment = SummaryFragment.newInstance(!getIntent().hasExtra(BUNDLE_EXTRA_SESSION_IDENTIFIER), false, false, dataHolder);
showFragment(summaryFragment, SummaryFragment.TAG, UiState.SUMMARY_DISPLAYING, true);
handleResultDisplayBehavior();
}
private void showErrorFragment(UiState uiState, boolean retryEnabled, MposError error, TransactionProcessDetails transactionProcessDetails) {
ErrorFragment fragment = ErrorFragment.newInstance(retryEnabled, error, transactionProcessDetails);
showFragment(fragment, ErrorFragment.TAG, uiState, true);
handleResultDisplayBehavior();
}
private void showPrintReceiptFragment(String transactionIdentifier) {
PrintReceiptFragment fragment = PrintReceiptFragment.newInstance(transactionIdentifier);
showFragment(fragment, PrintReceiptFragment.TAG, UiState.RECEIPT_PRINTING, false);
}
private void showLoginFragment(String applicationIdentifier) {
LoginFragment fragment = LoginFragment.newInstance(applicationIdentifier);
showFragment(fragment, LoginFragment.TAG, UiState.LOGIN_DISPLAYING, true);
}
private String constructTitle() {
if (mFormattedAmount == null && mTitleTransactionType == null) { // Initialized with Session identifier;
return "";
}
if (mFormattedAmount == null) {
return mTitleTransactionType;
} else {
return mTitleTransactionType + ": " + mFormattedAmount;
}
}
private void hideSoftKeyboard() {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
View containerView = findViewById(R.id.mpu_fragment_container);
if (containerView != null) {
imm.hideSoftInputFromWindow(containerView.getWindowToken(), 0);
}
}
private void handleResultDisplayBehavior() {
if (isAutoCloseSummary()) {
startAutoCloseTimer();
}
}
private boolean isAutoCloseSummary() {
MposUiConfiguration.ResultDisplayBehavior displayResultBehavior = MposUi.getInitializedInstance().getConfiguration().getDisplayResultBehavior();
return !mSummaryInteractionOccured && (displayResultBehavior == MposUiConfiguration.ResultDisplayBehavior.CLOSE_AFTER_TIMEOUT);
}
private void startAutoCloseTimer() {
mAutoCloseCountDownTimer = new CountDownTimer(MposUiConfiguration.RESULT_DISPLAY_BEHAVIOUR_TIMEOUT, 1000) {
@Override
public void onTick(long millisUntilFinished) {
// NO-OP
}
@Override
public void onFinish() {
finishWithResult();
}
}.start();
}
private void stopAutoCloseTimer() {
if (mAutoCloseCountDownTimer != null) {
mAutoCloseCountDownTimer.cancel();
mAutoCloseCountDownTimer = null;
}
}
}