Getting Started

This document provides instructions for embedding PC SDK in Android applications. In order to understand better how to work with PC we recommend you to read the following documents as well:

  • Architecture and functionalty presentation
  • Architecture and functionalty

The PC SDK contains native libraries for Android operating system. These libraries allow to implement all functionality of PC right in your mobile App.

Project Integration

Adding dependency to gradle-script

PC SDK library for Android can be integrated into your project as maven-dependency using SafeTech or Airome repository.

To add PC SDK you need to add external maven repository to top-level gradle script as in the example below.

// Sample of project-level build.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        google()
        mavenCentral()

    }
    dependencies {
        classpath 'com.android.tools.build:gradle:7.4.2'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        mavenCentral()
        maven { url "https://maven.google.com" }

        // PC Libraries
        maven { url "https://repo.paycontrol.org/android/maven" }
    }
}

The next step is to add PC SDK into your project as a dependency. Latest version can be discovered in the repository.

If you want to support GOST algorithms for signing and encryption, you should refer to pcsdk-gost artifact. If support of GOST algorithms is not essential for you, it is better to refer to original artifact called pcsdk. Usage of PCSDK with GOST support leads to increase in your final app size of around 18 Mb.

    // Dependency in your module-level build.gradle (without supporting GOST algorithms)
    implementation 'tech.paycon.sdk.v5:pcsdk:6.0.<LATEST_VERSION>'

    // Dependency in your module-level build.gradle (with supporting GOST algorithms)
    implementation 'tech.paycon.sdk.v5:pcsdk-gost:6.0.<LATEST_VERSION>'

Configuring Android manifest

To use all of the features of PC SDK (like device data collection, device scoring, etc.) PCSDK adds the follwoing permissions to AndroidManifest.xml.

<!-- For ensuring internet connection to interact with PC Server -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- For collecting SIM info and sending it to PC Server -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- For collecting device network settings and sending it to PC Server -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- For obtaining device name from Bluetooth settings for devices running Android 11 or lower -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
<!-- For obtaining device name from Bluetooth settings for devices running Android 12 or igher -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

Note that these permissions are automatically added in your app when merging manifests from your app and PCSDK.

You also need to provide a permission to gather device location by yourself to support sharing device location with server.

<!-- For tracking device location and sending it to PC Server (approximate coordinates) -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- For tracking device location and sending it to PC Server (exact coordinates) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Note that in order to gather device location you are supposed to provide either android.permission.ACCESS_COARSE_LOCATION or android.permission.ACCESS_FINE_LOCATION but not both of them. Consider that android.permission.ACCESS_COARSE_LOCATION provides an approximate location which is based on data from network connection whereas android.permission.ACCESS_FINE_LOCATION supplies more exact coordinates (based on both GPS and network connection) but consumes more power.

Basic Scenarios

SDK Initalization

To initialize PC SDK library you need to call static method PCSDK.init(Context context) passing the context of your application. Your are expected to initialize PC SDK in UI thread in either onCreate() method of your app or onCreate() method of your launcher activity before calling any other methods from PCSDK.

If you want to receive logs in LogCat from the PC SDK, you also should call PCSDK.setLogLevel() method (can be called before init) with desired log level.
Possible log levels are:

  • PCSDK.PC_NO_LOGGING - does not put any data to LogCat
  • PCSDK.PC_LOG_DEBUG - provides sufficient data to LogCat to debug your app behavior when interacting with PC SDK
  • PCSDK.PC_LOG_VERBOSE - provides extended information about what is happening inside the SDK. Consider using it for debug purposes only.
  • PCSDK.PC_LOG_KEY_VALUES - logs values of private keys and passwords. Can be combined with other levels. This log level is for debugging purposes only. Here is an example of initialization in Application's onCreate() method:

    @Override
    public void onCreate() {
        super.onCreate();
        PCSDK.setLogLevel(PCSDK.PC_LOG_DEBUG);
        PCSDK.init(this);
    }    

Your application must use one instance of PCSDK class during operating.

Registering users

Process of user registration includes followings steps:

  1. Getting the personalization data from the PC Server (via QR code, QR code + activation code, or JSON-value)
    see Architecture and functionality document for more details on this.
  2. Registering a PC User on PC Server. During this process PC SDK generates needed key sets
  3. Storing user's keys in internal storage for further usage

Step 1. Import PCUser from an appropriate source

First of all, you should construct a PCUser object (also referenced as key) from String value which contains either JSON extracted from QR-code or JSON delivered to your app with usage of PC Server API.

PCUser user = PCUsersManager.importUser(source);
if (user == null) {
    Log.e(TAG, "Failed to import PCUser");
    return;
}
// PCUser imported successfully - continue registration

Step 2. Check if PCUser requires activation

When delivering personalization data to your clients your infrastructure may use different methods (via QR, QR + activation code, JSON-value) to deliver the key data to the mobile app. The following snippet demonstrates how to import key data to a valid PCUser object:

PCUser user = PCUsersManager.importUser(source);
if (!user.isActivated()) {
    // PCUser requires activation, get activation code according to your rules
    String activationCode = getActivationCode();
    // Activate the PCUser (that is, decrypt the key values)
    PCUsersManager.activate(user, activationCode, new PCActivateCallback() {

        @Override
        public void success() {
            // PCUser was activated successfully
        }

        @Override
        public void error(@NonNull PCAbstractError error) {
            // Activation failed
        }
    });

} else {
    // No activation is required
}

Step 3. Check for information intended to be collected

Your infrastructure might be configured in such a way that some information about the device is collected when interacting with a PC Server. Some of the information requires granting additional permissions on the devices running Android 6 and newer.

You had better check the required permissions when adding a new PCUser to the device. Consider the following code snippet:

ArrayList<String> requiredPermissions = new ArrayList<>();

if (user.isCollectDeviceInfo() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    // For this PCUser the server expects data about the device including the device name.
    // Device name is obtained from Bluetooth settings and for devices rinnung Android 12
    // or newer the permission to access Bluetooth must be asked in runtime
    requiredPermissions.add(Manifest.permission.BLUETOOTH_CONNECT);
}

if (user.isCollectSIMInfo()) {
    // For this PCUser the server expects data about a SIM card to be collected and registered 
    requiredPermissions.add(Manifest.permission.READ_PHONE_STATE);
}

if (mUser.isCollectLocation()) {
    // For this PCUser the server expects a device location to be collected and registered 
    // Note: you can use Manifest.permission.ACCESS_COARSE_LOCATION instead but you will lose precision
    requiredPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
}

// Now request required permissions. It might also be a good idea to check in advance if they are already granted
if (requiredPermissions.size() > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    String[] permissionsArray = new String[requiredPermissions.size()];
    requestPermissions(requiredPermissions.toArray(permissionsArray), REQUEST_PERMISSIONS_CODE);
}

NOTE: If necessary permissions are not granted, the required information will not be collected. This does not result in any errors or misbehavior and process of interaction with PC Server remains intact.

Step 4. Save the PCUser object into permanent storage

Now, you can save the PCUser into storage. Each PCUser object is stored in your app's shared preferences and referenced by a unique name which is supposed to be entered by a client of your app.

Besides, the keys which are used to confirm transactions (HMAC key which is used to calculate a confirmation code and private key used to generate ECDSA-signature) are always additionally protected with a client-password and hardware encryption layer (available from Android 4.4).

So, the process of saving a key will look in this way:

// Prompt a unique name for the key from your client or generate it in your app
String keyName = promptKeyName();
// Prompt a password to protect the key
String password = promptPassword();
// Save the key to storage
int result = PCUsersManager.store(user, keyName, password, new PCStoreCallback() {

    @Override
    public void success() {
        // PCUser is now saved to the storage and can be found in the list of the keys returned by PCUsersManager.listStorage()
    }

    @Override
    public void error(@NonNull PCAbstractError error) {
        // PCUser was not saved - check the error code and handle the error
    }

});

NOTE: You have to check the password policy required by a particular PCUser before prompting a password from a client. Call PCUser.getPasswordPolicy() to find out which requirements must be met.
- Result 0 means that the client is allowed to keep the key without a password protection. In this case you should generate a random or constant password and pass it to PCUsersManager.store(...). The app-generated password must comply with password policy 1. In this particular case, it is your responsibility to save the password and submit it when confirming transactions.
- Result 1 stands for weak password policy which wants client to use at least 6 chars to create a password.
- Result 2 requires submitting passwords which include lower- and upper-case letters and at least 8-chars in length.
- Result 3 implies the strong policy and accepts passwords comprised of digits, lower- and upper-case letters and at least 8-chars in length.

NOTE: The PC SDK does not store submitted passwords anywhere. The client is supposed to remember their password.

NOTE: Instead of directly submitting a password you can design your app so that it uses biometrics mechanisms of authentication (e.g. fingerprint sensor) and generates passwords based on biometrics data. When implementing this functionality take into account that the app must comply with the flag indicated by PCUser.isDenyStoreWithOSProtection().
Consider also, that adding a new fingerprint on a smartphone invalidates all the previously created fingerprints, hence you had better provide a chance to your clients to use a password instead.

Step 5. Registering the key on the PC Server

The final step is to let the server know that the key is installed on the device. When registering a PCUser on the server, you send a device token to get push-notifications. Besides, the key pair is generated and the public key is sent to the server while the private key is encrypted with password and hardware encryption layer. You use PCUsersManager.register() to registered the key on the server side.

PCUsersManager.register(user, pushToken, new PCNetCallback() {
    @Override
    public void success() {
        // The PCUser has been registered sucessfully. Now it can be used to confirm transactions.
    }

    @Override
    public void error(PCNetError pcNetError) {
        // An error occured while registration. Some errors are not critical while some are.
        // Call pcNetError.getType() to check the type of the error in order to handle it properly
        // E.g. PCNetError.PCIS_ERROR_NO_CONNECTION means connectivity problems - you can
        // give the client another try to complete registration.
        // PCNetError.PCIS_ERROR_FINGERPRINT_ALREADY_SET means that this PCUser was already registered 
        // from another device, so it cannot be registered again.
        // Refer to documentation on PCNetError and PCUsersManager classes to investigate 
        // the possible errors and issues.
    }
});

NOTE: You can pass null as a pushToken if the push-token is not available. Then, the device will be registered but the client will not receive any notifications about new transactions.
You can also obtain a push token later and call PCUsersManager.register() again with a new token. Consider also calling it each time, the push token is updated.

NOTE: By default, FCM service is used to send notifications but you can also use HMS service for Huawei devices. Consider calling PCUsersManager.register(PCUser user, String pushID, PCPushServiceType pushServiceType, PCNetCallback callback) with PCPushServiceType.HMS in this case. You also can use PCPushServiceType.Firebase to show explicitly that you want use a Firebase token but it is usually redundant.

Overall key registration flow

The overall process of registration which considers all the possible ways of app personalization may have the following structure:

PCUser user = null;
// 1. Import PCUser in appropriate way
if (appUsesQRcode) {
     PCSDK.PCQRType qrCodeType = PCSDK.analyzeQRValue(qrCodeSource);
     if (qrCodeType == PCQRType.PCUser) {
        user = PCUsersManager.importUser(qrCodeSource);
     }
} else if (appUsesJSON) {
    user = PCUsersManager.importUser(json);
}
if (user == null) {
    // Import failed - handle a error
    return;
}

// 2. Check if PCUser requires to be activated
if (!user.isActivated()) {
    String activationCode = getActivationCode();
    int result = PCUsersManager.activate(user, activationCode, new PCActivateCallback() {

        @Override
        public void success() {
            // PCUser was activated successfully. Go to step 3.
        }

        @Override
        public void error(@NonNull PCAbstractError error) {
            // Activation failed, activation code was wrong - ask the client to resubmit it
        }
    });

} 

// 3. Check if some permissions must be granted
if (user.isCollectSIMInfo()) {
    // Ask for Manifest.permission.READ_PHONE_STATE
}
if (mUser.isCollectLocation()) {
    // Ask for either Manifest.permission.ACCESS_COARSE_LOCATION or Manifest.permission.ACCESS_FINE_LOCATION
}

// 4. Store the key
int result = PCUsersManager.store(user, keyName, password, new PCStoreCallback() {

    @Override
    public void success() {
        // PCUser is now saved to the storage. Go to step 5
    }

    @Override
    public void error(@NonNull PCAbstractError error) {
        // PCUser was not saved - check the error code and handle the error
    }

});

// 5. Register PCUser
PCUsersManager.register(user, pushToken, new PCNetCallback() {
    @Override
    public void success() {
        // The PCUser has been registered successfully
    }

    @Override
    public void error(PCNetError pcNetError) {
        // An error occurred while registration. handle it in a proper way
    }
});

Further considerations

The process of registration described above can be simplified if your infrastructure uses a particular way of delivering personalization information (e.g. only QR-codes without an activation code). If you know in advance what kind of password policy is used, it also makes the logic less complex.

In your app you also can switch steps to adjust them for your logic. E.g. you can call PCUsersManager.register(...) before storing a PCUser into the storage and call PCUsersManager.store(...) only in case the registration was successful.

Process transactions

The overall scenario of processing a transaction includes the following stages:

  1. Call PCUsersManager.listStorage() to obtain a list of users from the storage.
  2. Filter the list of users for which transactions will be loaded.
  3. Call PCTransactionsManager.getTransactionList() for each desired PCUser to find out if there are any transactions to be processed.
  4. Call PCTransactionsManager.getTransaction() to get transaction by its identifier. Consider also calling PCTransactionsManager.getTransactionBinaryData() for transactions which contain attachments.
  5. Display loaded transactions to your client in a desired way with buttons for confirmation and declination.
  6. Handle events from clicking the confirmation/declination buttons:
    • Check if password must be submitted before processing transaction with method PCUser.isReadyToSign();
    • Prompt the password if required and call PCUsersManager.submitPassword(...) to submit it and unlock the keys;
    • Call either PCTransactionsManager.sign() or PCTransactionsManager.decline() to sign or decline the transaction.

Getting transaction data

This code snippet demonstrates how to download available transactions and get ready to display them.

for (PCUser user: PCUsersManager.listStorage()) {
    // Filter list of PUsers in a desired way
    if (user.getUserId().equals(TARGET_USER_ID) || user.getName().equals(TARGET_KEY_NAME)) {
        // Load list of transactions for the user
        PCTransactionsManager.getTransactionList(user, new PCListTransactionsCallback() {
            @Override
            public void success(String[] strings) {
                // List of transactions is loaded. It might be empty
                if (strings.length > 0) {
                    // List is not empty - load transactions
                    for (String transactionId: strings) {
                        // Load each transaction by its id
                        PCTransactionsManager.getTransaction(user,
                                                             transactionId,
                                                             new PCGetTransactionCallback() {
                            @Override
                            public void success(PCTransaction pcTransaction) {
                                // Transaction is loaded. Check if it has an attachment
                                if (pcTransaction.hasBinaryData()) {
                                    PCTransactionsManager.getTransactionBinaryData(
                                        user, 
                                        pcTransaction, 
                                        new PCGetTransactionBinaryDataCallback() {
                                            @Override
                                            public void success(PCTransaction pcTransaction) {
                                                // Transaction is loaded with its attachment and 
                                                // ready to be displayed
                                            }

                                            @Override
                                            public void error(@Nullable PCError pcError, 
                                                              @Nullable PCNetError pcNetError) {
                                                // An error occurred while downloading attachment
                                            }

                                            @Override
                                            public void onProgressChanged(int loaded, int total) {
                                                // Useful for displaying progress of downloading    
                                            }
                                    });
                                } else {
                                    // No attachment for this transaction - display it immediately
                                }
                            }

                            @Override
                            public void error(PCNetError pcNetError) {
                                // Transaction is not loaded - handle an error
                            }
                        });
                    }
                }
            }

            @Override
            public void error(PCNetError pcNetError) {
                // List of transactions is not loaded = handle an error
            }
        });
    }
}

NOTE: Both PCTransactionsManager.getTransactionList() and PCTransactionsManager.getTransaction() are always light-weight methods which result in several kilobytes of the network traffic whereas PCTransactionsManager.getTransactionBinaryData() might seriously affect the traffic when downloading heavy attachments.

Displaying transaction data

Before transaction can be confirmed or declined it should be shown to a client. To show the transaction in more user-friendly way consider the following methods that you can use when working with a particular PCTransaction:

  • PCTransaction.getTransactionText() - returns the text of transaction. Might be null if the transaction contains an attachment only;
  • PCTransaction.getSnippet() - returns short description of the transaction which comes in handy when displaying a transaction in the list. Might be null.
  • PCTransaction.getStoredBinaryData() - returns java.io.File pointing to binary attachment (which is PDF document for example). Might be null if the transaction does not contain any attachment. The attachment stored in app's cache directory and eliminated as soon as the transaction is processed and every time the SDK is initialized.
  • PCTransaction.getTextRenderType() - returns type of transaction text. It makes sense to call this method if PCTransaction.getTransactionText() returns non-null and non-empty value to render the text in a proper way. This method returns either null or PCRenderType.raw for plain-text transactions and PCRenderType.markdown if the text of the transaction follows markdown syntax.
  • PCTransaction.getSnippetRenderType() - the similar method for the transaction snippet.

NOTE: Depending on how your infrastructure is configured, you probably will not need all these methods to be triggered (e.g. if you know exactly that snippets or attachments are not used). Refer to documentation on PCTransaction to find out other methods that might come in handy in particular cases.

Confirm or decline the transaction

As soon as you have downloaded and displayed transaction data, it can be confirmed through sign method or declined with decline method of PCTransactionsManager class. However, before calling these methods you should check whether the password must be submitted or an error might occur. The sample confirmation flow looks like the following:

// 1. Check if the password is required to sign or decline the transaction
public void tryProcessTransaction() {
    if (!user.isReadyToSign()) {
        // Password is required.
        // Depensing on your app configuration, the password may be:
        //  - Generated by the app itself
        //  - Directly submitted by the client
        //  - Generated with usage of Fingerprint of Face ID
        String password = requestPassword();
        int result = PCUsersManager.submitPassword(user, password, new PCSubmitPasswordCallback() {

            @Override
            public void success() {
                // Now the transaction can be processed
                processTransaction();
            }

            @Override
            public void error(@NonNull PCAbstractError error) {
                // An error occurred - handle it in a proper way
            }
        });
    } else {
        // Transaction can be processed without submitting a password
        // This occurs when the password has already been submitted recently
        processTransaction();
    }
}

// 2. Sign or decline a transaction
public void processTransaction() {
    if (confirmationButtonPressed) {
        PCTransactionsManager.sign(user, pcTransaction, new PCSignCallback() {
            @Override
            public void error(PCError pcError, PCNetError pcNetError, PCConfirmation pcConfirmation) {
                // Handle the error if the transaction was not signed
            }

            @Override
            public void success() {
                // Notify the client that the transaction has been signed successfully
            }
        });
    } else {
        PCTransactionsManager.decline(user, transaction, new PCDeclineCallback() {
            @Override
            public void error(PCError pcError, PCNetError pcNetError, PCDeclination pcDeclination) {
                // Handle the error if the transaction was not declined
            }

            @Override
            public void success() {
                // Notify the client that the transaction has been declined successfully
            }
        });
    }
}

NOTE: if the transaction contains an attachment but it was not downloaded with PCTransactionsManager.getTransactionBinaryData(), then the transaction will not be processed.

Getting a transaction from a push notification

PC Server can send a push notification when a transaction was created.
In this case the application can get an identifier of target PCUser and identifier of target PCTransaction from the notification payload and process the transaction.

Default push notification template is the following:

Default android push template (PC Server sends 2 push notifications)

{
 "to":"%DEVICE_TOKEN%",
 "notification":{
    "tag":"%USER_ID%",
    "title":"%PUSH_TITLE%",
    "body":"%PUSH_TEXT%",
    "icon":"pc_push",
    "sound":"default"
 },
 "data":{
    "type":"PayControl_v2"
 }
}

{
 "to":"%DEVICE_TOKEN%",
 "data":{
    "type":"PayControl_v2",
    "userid":"%USER_ID%",
    "transactionid":"%TRANSACTION_ID%"
 }
}

Please, consider that push templates can be changed for your app by PC Server configuration.

Offline confirmation and declination

If the device is offline, then PC SDK can calculate offline confirmation/declination code for a transaction.

In this case PC SDK cannot get the transaction data from the PC Server, hence you should use QR code scanner to get the transaction data (this option is unavailable for transactions with attachments as they obviously cannot be embedded in a QR code).

Calculated confirmation/declination code should be displayed to a client. The client is supposed to input this code manually into the Application web-site. After this the Application can verify it using PC Server API.

The following example demonstrates how to confirm or decline a transaction i offline mode.

// scan QR code with app's QR code scanner

PCSDK.PCQRType qrType = PCSDK.analyzeQR(qrValue);
if (qrType != PCSDK.PCQRType.PCTransaction) {
    // This is not a QR code with transaction data, show an error or skip it
    return;
}
// Import the transaction
PCTransaction transaction = PCTransactionsManager.importTransaction(qrValue);

// Make sure, the transaction was imported correctly
if (transaction == null) {
    // Notify about the error
    return;
}

// Find a PCUser for who the transaction is intended
PCUser targetUser = null;
for (PCUser user: PCUsersManager.listStorage()) {
    if (user.getUserId().equalsIgnoreCase(transaction.getUserId()) {
        targetUser = user;
        break;
    }
}

if (targetUser == null) {
    // There is no such a user in the storage. Notify about the error
    return;
}

// Check if password must be submitted
if (!targetUser.isReadyToSign()) {

    if (targetUser.areOnlineCredentialsRequired()) {
        // You need to perform an online request to submit a password. 
        // Offline confirmation is not available.
        return;
    }

    // Password is required.
    // Depending on your app configuration, the password may be:
    //  - Generated by the app itself
    //  - Directly submitted by the client
    //  - Generated with usage of Fingerprint of Face ID
    String password = requestPassword();
    int result = PCUsersManager.submitPassword(user, password);
    if (result != PCError.PC_ERROR_OK) {
        // An error occurred - handle it in a proper way
        return;
    } 
}

// Process the transaction
if (confirmationButtonPressed) {
    // Confirm the transaction
    PCConfirmation confirmation = PCTransactionsManager.signOffline(toConfirmUser, transaction);
    // Show confirmation code to the client by calling confirmation.getShortConfirmCode()
} else {
    // Decline the transaction
    PCDeclination declination = PCTransactionsManager.declineOffline(toConfirmUser, transaction);
    // Show declination code to the client by calling declination.getShortDeclineCode()
}

Online and Offline transaction processing can be combined in the result scenarios.
Moreover, PCTransactionsManager.sign() and PCTransactionsManager.decline() return PCConfirmation and PCDeclination respectively in case of any error during PC Server interaction so that you can use the mechanism of offline confirmation or declination in this case.

Working with operations

Operation is presented by the instance of PCOperation and includes several transactions as a list of PCTransaction instances. That is, the operation is a group of transactions that can be processed all together within one request resulting in smaller amounts of network traffic and faster calculations.

The algorithm of processing operations can include the following steps:

  1. Calling PCUsersManager.listStorage() to obtain a list of users from the storage and filtering the list.
  2. Calling PCOperationsManager.getOperationList() for each desired PCUser to find out if there are any operations to be processed.
  3. If there are any operations available, each operation can be retrieved by PCOperationsManager.getOperation(). The constructed PCOperation will contain data about all the transactions which comprise the operation. However, the binary data for each transaction must be downloaded separately.
  4. For each transaction listed by PCOperation.getTransactions() check if the transaction requires binary data to be downloaded by calling PCTransaction.hasBinaryData() and PCTransactionsManager.getTransactionBinaryData().
  5. Display the operation in a desired way. See Displaying transaction data for details on proper methods to render transactions. Apart from transactions data, the operation description (accessible by PCOperation.getDescription()) should be displayed.
  6. Operations support partial processing, that is the client can decide to process some transactions immediately, while the decision to process other transactions can be postponed. So, the next step is to offer the interface to allow the client decide which transactions they want to process now and call PCTransaction.processOperation() passing the list of transactions to be confirmed and the list of transactions to be declined.
  7. If there are some transactions left unprocessed, let the client process them later.

Getting operation data

The following code snippet demonstrates how to properly retrieve PCOperation from the PC Server, so that it can be displayed and processed correctly.

/*
 * STEP 1. Find target PCUser according to logic of your application
 */

final PCUser targetUser = PCUsersManager.getById(TARGET_USER_ID);

/*
 * STEP 2. Load list of operations for the user
 */

PCOperationsManager.getOperationList(targetUser, new PCListOperationsCallback() {
    @Override
    public void success(String[] strings) {
        // List of operations loaded successfully
        if (strings.length == 0) {
            // The list is empty
            return;
        }

        /*
         * STEP 3. Load operation data
         */

        // Assume, we want to show first PCOperation
        PCOperationsManager.getOperation(targetUser, strings[0], new PCGetOperationCallback() {
            @Override
            public void success(PCOperation pcOperation) {
                // PCOperation is loaded and can be rendered
                // Extract and show operation description
                String desc = pcOperation.getDescription();
                // Extract and show list of transactions
                List<PCTransaction> transactions = pcOperation.getTransactions();

                /*
                 * STEP 4. Load attachments for transactions. You can load attachments immediately 
                 * before displaying the operation, or you can do it step by step. The main idea
                 * is that the attachment for a particular transaction must be loaded (and shown)
                 * before this transaction can be processed
                 */

                for (PCTransaction transaction: transactions) {
                    if (transaction.hasBinaryData()) {
                        PCTransactionsManager.getTransactionBinaryData(
                                targetUser, 
                                transaction, 
                                new PCGetTransactionBinaryDataCallback() {
                            @Override
                            public void success(PCTransaction pcTransaction) {
                                // Now this transaction is fully loaded and can be
                                // processed
                            }

                            @Override
                            public void error(@Nullable PCError pcError,
                                              @Nullable PCNetError pcNetError) {
                                // Handle a error
                            }
                        });
                    } else {
                        // No need to download an attachment
                    }
                }

            }

            @Override
            public void error(PCNetError pcNetError) {
                // Handle a error
            }
        });
    }

    @Override
    public void error(PCNetError pcNetError) {
        // Handle a error
    }
});

In order to process the operation, the list of transactions to be confirmed and the list of transactions to be declined must be specified. Each list might be empty. but not both of them. The lists must not intersect as well.

Processing an operation

The following code snippet demonstrates how the PCOperation may be processed. Here, we assume that the attachments have been already downloaded for all the transactions intended to be processed.

final PCUser pcUser = PCUsersManager.getById(TARGET_USER_ID);
final PCOperation pcOperation = getTargetOperation();

/*
 * STEP 1. Allow the client to specify the transactions to be confirmed and/or declined.
 * Each transaction must be a reference to an object from the list returned by 
 * pcOperation.getTransactions()
 */

List<PCTransaction> transactionsToConfirm = getTransactionsSelectedForConfirmation();
List<PCTransaction> transactionsToDecline = getTransactionsSelectedForDeclination();

/*
 * STEP 2. Check whether the password is required
 */

if (!pcUser.isReadyToSign()) {
    String password = submittedPassword();
    int result = PCUsersManager.submitPassword(pcUser, password, new PCSubmitPasswordCallback() {

        @Override
        public void success() {
            // Go to STEP 3
        }

        @Override
        public void error(@NonNull PCAbstractError error) {
            // Some error occurred. Probably, the password is incorrect
        // Handle the error
        }
    });

}

/*
 * STEP 3. Process the operation
 */

int initialOperationSize = pcOperation.getTransactions().getSize();

PCOperationsManager.processOperation(
        pcUser,
        pcOperation,
        transactionsToConfirm,
        transactionsToDecline,
        new PCProcessOperationCallback() {
            @Override
            public void result(@NonNull PCOperation operation,
                               @NonNull List<PCConfirmationResult> confirmationResults,
                               @NonNull List<PCDeclinationResult> declinationResults) {

                // Generally, the operation has been processed
                // However, the results must be analyzed to find out whether all the transactions have been processed successfully.

                // A quick check that number of processed transactions coincides with the number of transactions intended to be processed
                if (initialOperationSize - operation.getTransactions().getSize()
                        == transactionsToConfirm.getSize() + transactionsToDecline.getSize()) {
                    // Everything is alright, all transactions have been processed
                    // ...            
                } else {
                    // Some transaction has failed, search for errors

                    for (PCConfirmationResult result: confirmationResults) {
                        if (result.getError() != null) {
                            // Some transaction has not been confirmed
                            // You can process the error and call result.getTransaction() to find out which transaction has failed
                        }
                    }

                    for (PCDeclinationResult result: declinationResults) {
                        if (result.getError() != null) {
                            // Some transaction has not been declined
                            // You can process the error and call result.getTransaction() to find out which transaction has failed
                        }
                    }
                }
            }

            @Override
            public void error(@NonNull PCAbstractError error) {
                // Some critical error has occurred. No one transaction has been confirmed
            }
        });

Logging in a website

PC server and PC SDK provide functionality to log on a website using a QR code instead of submitting any usernames or passwords. The detailed description of this feature can be found here.

PC SDK provides a simple class PCLogin which is responsible for scanning QR code and performing logging into a website.

The sequence of actions which are performed on a mobile device is usually the following:

  1. The application scans QR code for logging in and its content is passed to PC SDK to initiate the process.
  2. PC SDK analyzes the QR code validity (checks that it has not expired yet and at least one PCUser is stored on the device for which the system ID is the same as specified in the QR code). Provided the QR code is valid, PC SDK suggest the list of PCUser objects that can be used to log in.
  3. The client chooses a desired PCUser and the PCLogin downloads the transaction which must be confirmed to log in. The transaction normally contains operating system name, browser name and other details about the device which is used to log into the website.
  4. Client confirms the transaction and after the information between PC Server and the website is synced, the client is authenticated.

The following code sample demonstrates the above-mentioned process.

/*
 * STEP 1. Import data from scanned QR code
 */

PCLogin.importFromJson(qrSource, new PCLogin.GetLoginDataCallback() {
    @Override
    public void error(PCError pcError) {
        // Some error occurred. Probably, the QR code has expired or no
        // appropriate PCUser found in storage
    }

    @Override
    public void success(PCLogin pcLogin) {

        /*
         * STEP 2. Get the list of PCUsers which are eligible to perform logging in. Allow client to
         * pick up the desired key if there are several possible variants
         */

        List<PCUser> users = pcLogin.suggestUsers();
        // Assume, we prefer the first key
        PCUser targetUser = users.get(0);

        /*
         * STEP 3. Request transaction for logging in and display it in your app.
         * You should display it as a regular transaction with both options to confirm and decline
         */
        pcLogin.requestLoginTransaction(targetUser, new PCLogin.GetLoginTransactionCallback() {
            @Override
            public void error(PCError pcError, PCNetError pcNetError) {
                // An error has occurred. Handle it
            }

            @Override
            public void success(PCTransaction pcTransaction) {

                /*
                 * STEP 4. Check if the password must be submitted 
                 */


                if (!targetUser.isReadyToSign()) {
                    String password = submittedPassword();
                    int result = PCUsersManager.submitPassword(targetUser, password, new PCSubmitPasswordCallback() {

                        @Override
                        public void success() {
                            // Go to STEP 5
                        }

                        @Override
                        public void error(@NonNull PCAbstractError error) {
                            // Some error occurred. Probably, the password is incorrect
                            // Handle the error
                        }
                    });
                }

                /*
                 * STEP 5. Confirm or decline the transaction depending on what the client
                 * has selected
                 */

                if (clientDecidedToConfirm) {
                    pcLogin.confirm(new PCGeneralCallback() {
                        @Override
                        public void success() {
                            // The transaction is confirmed. The user will be authenticated
                            // shortly on the website
                        }

                        @Override
                        public void error(@Nullable PCError pcError, @Nullable PCNetError pcNetError) {
                            // Handle the error
                        }
                    });
                } else {
                    pcLogin.decline(new PCGeneralCallback() {
                        @Override
                        public void success() {
                            // Transaction is declined. Authentication will be rejected
                        }

                        @Override
                        public void error(@Nullable PCError pcError, @Nullable PCNetError pcNetError) {
                            // Handle the error
                        }
                    });
                }
            }
        });
    }
});

NOTE: Although the method PCLogin.importFromJson() uses callback for providing results instead of returned value, it is still a synchronized method.

NOTE: Although you can confirm or decline login transaction in a regular way using either PCTransactionsManager.sign() or PCTransactionsManager.decline(), you are encouraged to use pcLogin.confirm() and pcLogin.decline() instead.

Advanced usage

This section contains brief information about other functionality of PC SDK which also may come in handy.

Multiple confirmation and declination

If your application is desined in such a way that the client can confirm or decline several transactions at once, you had better not use a loop with single calls of PCTransactionsManager.sign(PCUser user, PCTransaction transaction, final PCSignCallback callback) but call a designated method for multiple transaction processing like the following:

ArrayList<PCTransaction> mTransactionsToProcess = new ArrayList();
// Consider, mTransactionsToProcess is somehow filled
PCTransactionsManager.sign(
    mUser,
    mTransactionsToProcess,
    PCRegularKeysProcessor.getInstance(),
    new PCMultipleConfirmationCallback() {
        @Override
        public void onNextResult(@NonNull PCConfirmationResult result) {
            // Called every time, a transaction has been processed.
            // If result.getError() is null, the transaction has been confirmed
            // successfully. Otherwise, examine the error code for the reason
            // of failure.
        }

        @Override
        public void error(@NonNull PCAbstractError error) {
            // An error has occurred prior to any transaction has been confirmed
        }
});

NOTE: You can use the similar PCTransactionsManager.decline() method to perform multiple declination.

Keys backup and restoring

In case, some happens with the device and the client has to change it, they will lost their PC keys. The mobile app backup will not resolve this problem and the client will have to personalize the mobile app again.

Fortunately, Pay Control provides the mechanism to backup and restore PCUser objects so that it can be securely transferred from one device to another (or can be reinstalled on the same device).

Check if the key can be backed up and restored

When the key backup is done and it is to be restored, the PC Server runs a procedure called Remote update. This feature can be disabled for your system, so to ensure the key can be backed up, run PCUser.isRemoteUpdateEnabled() first.

Creating a backup

You can create a key backup with just calling one method PCUserRestore.createBackupData() which requires a PCUser instance to be backed up and a password to encrypt the backup content. This password will not be saved anywhere, so that the client must memorize it or save in a secure way.

Here is a sample code snippet that demonstrates how a backup can be created.

// user is an instance of PCUser and password is a password entered by a client. 
// Password must at least 6 chars in length
PCUserRestore.createBackupData(user, password, new PCUserBackupCallback() {
    @Override
    public void success(@NonNull PCUserBackupData pcUserBackupData) {
        // Backup is created successfully
    }

    @Override
    public void error(@Nullable PCError pcError, @Nullable PCNetError pcNetError) {
        // Backup was not created. Handle a error
    }
});

Storing a backup

The created backup contains sensitive data which is used to restore the keys. Although, it is encrypted with a user-provided password, it must be stored in a secure place. The best way to store it is in serialized form. You can serialize the PCUserBackupData object by calling PCUserBackupData.toJson(). The output JSON-string can then be used to restore a backup. The good place to store this string is a text file in a secure area of the client's Google Drive. However you are free to specify your own ways to store clients' key backups.

Restoring a backup

Once the client wants to restore a backup, it can be easily done by calling PCUserRestore.restoreBackup() with a target PCUserBackupData instance and a password as arguments.

Here is an example of how a backup should be restored.

// serializedBackup is a previously created JSON-string
PCUserBackupData backupData = PCUserBackupData.fromJson(serializedBackup);
PCUserRestore.restoreBackup(backupData, password, new PCRestoreCallback() {
    @Override
    public void success(@NonNull PCUser pcUser) {
        // PCUser is restored successfully.
        // After the PCUser is restored it must be saved on the device and new key pair must
        // be registered on the PC Server
    }

    @Override
    public void error(@Nullable PCError pcError, @Nullable PCNetError pcNetError) {
        // Backup was not restored. Handle an error
    }
});

NOTE: After the backup is restored, you should follow all the standard actions related to mobile app personalization (like if the key was imported form a QR code). Refer to Registering users for further details. Some steps will be omitted though (e.g. key importing and key activation).

Task management

To perform network requests to a PC Server the SDK creates a task for each request. You normally receive the results via specified callbacks. The callbacks are invoked shortly for the majority of the requests provided that the internet connection is not too poor.

However, some network requests may involve more intensive data exchange. For instance, PCTransactionsManager.getTransactionBinaryData() might generate a heavy traffic when a large attachment is being downloaded. If the client decides to quit a screen while an attachment is being downloaded, the connection will not be closed automatically. So, you have to close the connection from the code.

Each callback that involves internet connection has default empty implementations of onTaskCreated(PCTask task) and onTaskCancelled(). You can override them to control the tasks manually.

The following example demonstrates how a task can be canceled manually.

PCTransactionsManager.getTransactionBinaryData(user, transaction, new PCGetTransactionBinaryDataCallback() {
    @Override
    public void success(PCTransaction pcTransaction) {

    }

    @Override
    public void error(@Nullable PCError pcError, @Nullable PCNetError pcNetError) {

    }

    @Override
    public void onTaskCreated(@NonNull PCTask task) {
        // This method is called once the internet connection is established
        // Call this method if you want to interrupt the connection immediately 
        // e.g. if the client decides to quit the screen
        PCTasksManager.cancel(task);
    }

    @Override
    public void onTaskCancelled() {
        // This method is invoked when the task is canceled. You can override
        // it if you want to perform some specific actions at this step.
    }
});

Fine-tuning

Once the SDK is initialized, some default settings are applied to ensure the recommended behavior. However, there are some handy methods that can change the default behavior for some special cases.

Password resubmitting after app has been in background

Once the password was submitted for a particular PCUser via PCUsersManager.submitPassword(...) it is not required to be resubmitted until the app is running. By default, if the app was in a background state for less than 15 seconds, the password is still not required again to confirm transactions. You can change this behavior by calling PCSDK.setHandlesBackgroundTimeout(...) at any moment after SDK is initialized.

PCSDK.init(getApplicationContext());
// Allows the app to be in background up to 30 seconds with no need to resubmit password
PCSDK.setHandlesBackgroundTimeout(30_000);

Callbacks execution

By default, all the callbacks are executed in a main thread regardless the thread from which corresponding PCSDK methods were invoked. If you want callbacks to be invoked from the same thread where you pass this callbacks to PCSDK methods, you can use PCSDK.setCallbacksInvokedInMainThread() and pass false:

PCSDK.setCallbacksInvokedInMainThread(false);