Alipay, China's leading third-party online payment solutionAlipay, China's leading third-party online payment solution

2. Implement additional services via Wallet API

This document describes the architectural overview that explains how wallet APIs work and the details of each capability provided by wallet APIs.

Interfaces

To be able to use a capability, the wallet first needs to implement the wallet API that represents a capability. To be specific, the following capabilities are available:

  • Mandatory:
    • OAuthService
    • CodeService
    • DeepLinkService

When a capability is consumed, the associated JSAPI is called by the merchant and transformed by Griver AppContainer to be the wallet API that interacts with the wallet app, wallet server, and merchant server.

Below is a mapping table that lists the wallet API (interface), its associated JSAPI, and its description.

Interface

Associated JSAPI

Required

Description

OAuthService

getAuthCode()

Mandatory

Retrieve the authorization code from OAuth service that is used for authentication.

PaymentService

tradePay()

Mandatory

Initiate the payment process for purchase scenarios.

CodeService

scan()

Mandatory

Launch code scanner for mini programs to decode QR codes or barcodes.

DeepLinkService

open()

Mandatory

Open an in-app page, navigate from HTML5 web pages to apps, or navigate across apps.

Note:

  • Customized wallet local logic or JSAPI is out of the scope.
  • See the details for each capability in the Capabilities.

How it works

Architectural overview

Below is the architectural overview of how Wallet APIs work:

2. Implement additional services via Wallet API

The AC SDK 2.0 contains three main components:

  • Griver AppContainer

The Mini Program AppContainer. It handles the JS API requests from mini programs and delegates them to the implementation of Wallet API.

  • Core

The core component that handles the workflows of oAuth and deeplinking.

  • Wallet APIs

A set of common interfaces for Griver AppContainer and Core to program, and these interfaces need to be implemented by the Super App. So the SDK can utilize the Super App's existing features to provide services for Core and Griver AppContainer. For instance, the Super App uses the internal QR Code Scanner to implement CodeService. When Griver AppContainer receives the scan() API request from mini program through the Wallet API delegation, the Wallet API calls the implementation of CodeService and eventually opens the Super App's QR Code scanner to complete the task. With Wallet API, the Super App does not need to understand the workflow details for implementing essential JSAPIs.

Capabilities

Capabilities are sets of JSAPIs and wallet APIs that can work together to help users to complete specific tasks. See the details of each capability below:

OAuthService

OAuthService is a service implemented by Wallet to get auth code and process authorization with getAuthCode() JSAPI.

The following shows the interaction sequence, sample implementations, scopes, and more about the OAuthService interface.

Interaction sequence

Currently, there is no association between the client ID that the developer obtained from the Super App OAuth and the mini program appId, and using appId along with the Super App OAuth process will not be successful. Therefore, we need to amend the existing design accordingly.

Proposed solution:

  • In Mini Program Platform, there will be a new field in the Mini Program Management page to allow the developer to configure the Client ID that he or she obtained from the Super App.
  • The configured Client ID (or authClientId) will be sent to Griver AppContainer as part of the Mini Program MetaData
  • The ACL SDK will receive the authCliendId from Griver AppContainer, and pass it to the ACL SPI Native impl of the Super App , where the Super App can use it to get the auth code from the Super App OAuth Server.

getAuthCode()

2. Implement additional services via Wallet API

Explanation

After the mini program invoking the getAuthCode() JSAPI (Step 1), the Griver AppContainer will invoke the getAuthCode() implementation in the Super App (Step 3). Depending on the type of the mini program, the OAuthCodeFlowType parameter will be either LOCAL_MINI_PROGRAM, if the mini program is published to the local Super App workspace only, or ALIPAY_CONNECT, if the mini program is published from the Alipay connect mini program workspace.

In Step 4, the Super App checks with the Super App Server the authInfo of the requested scopes. If the auth is not silent and the scopes are not cached, the Super App should return errorCode=ERROR_CODE_AUTH_PENDING_AGREEMENT (Step 7). Upon receiving ERROR_CODE_AUTH_PENDING_AGREEMENT, the Griver AppContainer will then invoke OAuthService.showAuthPage() (Step 9) to open up the authorization page.

If the user disagrees with the authorization, the Super App should return errorCode=ERROR_CODE_USER_CANCEL (Step 21) and Griver AppContainer will invoke error() callback to the mini program.

If the user agrees with the authorization, the Super App should first cache the scopes  (Step 13), so next time the mini program requests for the same non-silent scope, the user should not be prompted with the authorization page, and then the Super App returns a successful OAuthPageConfirmResult object (an empty OAuthPageConfirmResult object with referenceAgreementId set to null).

The Griver AppContainer will then call OAuthService.getAuthCode again (Step 16), and since the scopes are already cached, the Super App can then proceed to call applyAuthCode() (Step 24), and eventually return authCode and authSuccessScope(scopes) (Step 27), if OAuth request is successful, otherwise return OAuthResult with errorCode=UNKNOWN_ERROR (Step 31).

The following is the recommended caching logic that the Super App team can use inside the OAuthService.getAuthCode() implementation. The key should be the combination of appId and userId, and the value should be a set of the authorized scopes.

copy
func updateCachedScopesForUser(userId: String, appId: String, scopes: Set<String>) {
    let cacheKey = getCacheKey(for: userId, appId: appId)
    guard let currentScopes = getCachedScopesForUser(userId: userId, appId: appId) else {
        defaults.set(Array(newScopes), forKey: cacheKey)
        return
    }
    let updatedScopes = currentScopes.union(newScopes)
    defaults.set(Array(updatedScopes), forKey: cacheKey)
}

private func getCacheKey(for userId: String, appId: String) -> String {
    return "MPS_scope_\(userId)_\(appId)"
}

Sample implementation of getAuthCode()

copy
override func getAuthCode(clientId: String, 
                          scopes: Set<String>, 
                          type: IAPWalletOAuthCodeFlowType, 
                          extendedInfo: [String: String],
                          in context: IAPWalletAPIContext?,
                          callback: @escaping (IAPWalletOAuthResult) -> Void) {
    // 1. execute auth process
    
    // 2. return results for different cases
    if (userConsentNeeded) {
        let result = IAPWalletOAuthResult(authCode: "", authState: "", authErrorScopes: [:], authSuccessScopes: [], authRedirectUrl: "")
        result.error.code = "2001"
        result.error.localizedDescription = "pending user consent"
        callback(oauthResult)
    }
    
    // If Oauth is successful
    if (oAuthSuccessful) {
        callback(IAPWalletOAuthResult(authCode: "${server-issued-authCode}",
                                      authState: "${server-issued-authState}",
                                      authErrorScopes: ["USER_INFO": "${failed reasons}"],
                                      authSuccessScopes: ["BASE_USER_INFO"]))
    }
    
    // If Oauth is unsuccessful due to other reasons i.e., server error
    if (oAuthUnsuccessful) {
        let result = IAPWalletOAuthResult(authCode: "", authState: "", authErrorScopes: [:], authSuccessScopes: [], authRedirectUrl: "")
        result.error.code = "1000"
        result.error.localizedDescription = "Unkown error"
        callback(oauthResult)
    }
}

Sample implementation of showAuthPage()

copy
override func showAuthPage(clientId: String,
                               name: String,
                               logo: String,
                               scopes: Set<String>,
                               extendedInfo: [String : String]? = nil,
                               in context: IAPWalletAPIContext?,
                               callback: @escaping (IAPWalletAuthPageConfirmResult) -> Void) {
        // override the method to provide a authpage confirmation popover
        var scopeInfo = ""
        for scope in scopes {
            if (scope == OAuthService.SCOPE_BASE_USER_INFO) {
                scopeInfo += "- Access basic user information\n"
            } else if (scope == OAuthService.SCOPE_AGREEMENT_PAY) {
                scopeInfo += "- Conduct aggreement pay automatically\n"
            } else if (scope == OAuthService.SCOPE_USER_NAME) {
                scopeInfo += "- Acess to user's real name\n"
            } else if (scope == OAuthService.SCOPE_USER_LOGIN_ID) {
                scopeInfo += "- Retrieve user login id in the app.\n"
            } // ... iterate through all possible scopes
        }
        
        let authPage = UIAlertController(title: "Authorisation Confirmation",
                                         message: "\(name) wants to access for the following information: \n\(scopeInfo)" ,
                                         preferredStyle: .alert)
        
        authPage.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Default action"),
                                         style: .default,
                                         handler: { _ in
                                            // 1. synchronize with wallet server regarding aggreement check
                                            // ----> async remote call: {
                                                ----- case 1 -----
                                                // 2.1 Option 1: when confirmation is purely checked offline
                                                // then inform the SDK that agreement has ben confirmed
                                                let result = IAPWalletAuthPageConfirmResult()
                                                callback(result)
                                                
                                                ----- case 2 ----- 
                                                // 2.2 Option 2: when confirmation is passed to server for ack
                                                // then inform the SDK with the agreementId from server
                                                let result = IAPWalletAuthPageConfirmResult("${agreementReferenceId}")
                                                callback(result)
                                            // ----> }
        }))
        
        authPage.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "User reject"),
                                         style: .cancel,
                                         handler: { _ in
                                            
                                            // 1. if error is presented, the SDK will assume user abort the operation.
                                            let result = IAPWalletAuthPageConfirmResult()
                                            result.error = NSError(domain: "${Wallet-wallet-domain}",
                                                                   code: IAPWalletBaseServiceResult.ERROR_CODE_USER_CANCEL, // or any ${psp-wallet-domain-code}
                                                                   userInfo: [NSLocalizedDescriptionKey: "user canceled"])
                                            callback(result)
                                            
        }))
    }

Sample implementation of getAuthorizedScopes()

copy
 public func getAuthorisedScopes(userId: String,
                             clientId: String,
                             appId: String,
                             extendedInfo: [String: String],
                             callback: @escaping (IAPWalletOAuthScopeQueryResult) -> Void) -> Void {
     // implement get authorised scopes logic
     let scopes = getCachedScopesForUser(userId: userId, appId: appId);
     let scopeQueryResult = IAPWalletOAuthScopeQueryResult(authorizedScopes: Array(scopes ?? []));
     callback(scopeQueryResult);
 }

func getCachedScopesForUser(userId: String, appId: String) -> Set<String>? {
    let cacheKey = getCacheKey(for: userId, appId: appId);
    guard let storedScopes = defaults.stringArray(forKey: cacheKey) else {
        return nil;
    }
    let scopes: Set<String> = Set(storedScopes);
    return scopes;
}

For Super App Android and iOS engineers

Just focus on the implementation of AuthService as highlighted in blue.

Note that only the implementation of getAuthCode() , showAuthPage() and getAuthorizedScopes() are mandatory, whereas the implementation of other methods in OAuthService are optional for now.

Scopes

The Super App needs to support the following scopes:

Open to 3rd-party developers

Scope

Description

USER_ID

Authorized to obtain the unique user ID.

USER_NICKNAME

Authorized to obtain the user nickname.

USER_NAME

Authorized to obtain the user name.

USER_LOGIN_ID

Authorized to obtain the user login ID.

HASH_LOGIN_ID

Authorized to obtain the hash user login ID.

USER_AVATAR

Authorized to obtain the user avatar.

USER_GENDER

Authorized to obtain the user's gender.

USER_BIRTHDAY

Authorized to obtain the user's birthday.

USER_NATIONALITY

Authorized to obtain the user's nationality.

USER_CONTACTINFO

Authorized to obtain the user's contact.

For internal use only

Scope

Description

auth_user

Invoked by getOpenUserInfo() .

Error codes

Code

Value 

Generic

ERROR_CODE_UNKNOWN_ERROR

1000

ERROR_CODE_USER_CANCEL

1001

ERROR_CODE_APP_SERVICE_ERROR

1002

ERROR_CODE_TIMEOUT

1003

Auth

ERROR_CODE_AUTH_PENDING_AGREEMENT

2001

Use cases

2. Implement additional services via Wallet API

PaymentService

PaymentService is a service implemented by Wallet to initiate the payment process with tradePay() JSAPI.

The following shows the interaction sequence and other details of the PaymentService interface.

Interaction Sequence

2. Implement additional services via Wallet API

Explanation

  • In Step 3, depending on the type of order, the payment info returned in Step 4 can be paymentUrl, order ID, or order string.
  • In Step 9, the Super App presents User a cashier page, and the User can either confirm or reject this payment.
  • If the User confirms the payment, the Super App forwards the paymentInfo to the Super App Server (Step 11).
  • In Step 12, the Super App Server processes the payment request, and informs both the mini program and Merchant Server of the payment result.

For Super App Android and iOS Engineers

Just focus on the implementation of PaymentService as highlighted in blue (Step 9, 11 and 13).

CodeService

CodeService API launches the QR code scanner with the scan() JSAPI. Make sure your app support domain like *.alipaypus.com.

Interaction sequence

2. Implement additional services via Wallet API

Explanation

In (Step 1), the mini program invokes the my.scan() JSAPI with either QR or barcode type. This request is first to be served by Griver AppContainer (Step 2). The scan type is translated into ScannerOption by Griver AppContainer (Step 3) and the request is then passed along and eventually handled by the Super App (Step 5).

The Super App can invoke its existing QR code scanning functionality in the implementation of CodeService (Step 5). Based on the ScannerOption, the Super App can use the appropriate image decoding logic to parse the captured image into either QR code or barcode (Step 9), and return the code via Wallet API (Step 13). The code will be eventually made available to mini program in the success callback (Step 15).

If the user opts to cancel the scanning operation, the Super App should return a ScannerResult with the error code ERROR_CODE_USER_CANCEL(Step 6).

If no QR code or barcode is found in the captured image, the Super App should return a ScannerResult with the error code ERROR_CODE_UNKNOWN_ERROR(Step 10).

Implementation

module: IAPWalletContext

#

Class

Purpose

1

IAPWalletScannerOption

The options specified what the wallet scanner should satisfy, including the scanner compatibility for QR code and barcode, whether to display album entry in the scanner and whether to allow the user to pick up pictures from the album.

Constructor Fields

@param type- type of the scanner @IAPWalletCodeType {qrCode, barCode}

@param hideAlbum - Whether the scanner should provide an album entry to support scanning images from albums.

@param extendedInfo - For future use

2

IAPWalletScannerResult

The result collected from this scanner

Constructor Fields

@param code - The code value extracted by the scanner, null if scan error happens.

copy
final class CodeService: IAPWalletCodeServiceSignature {
    
    // Mark: this API is currently used by Miniprogram combined with AC 2.0 case.
    override func scan(with option: IAPWalletScannerOption, 
                       in context: IAPWalletAPIContext?,
                       callback: @escaping (IAPWalletScannerResult) -> Void) {
        // open up your qrcode scanner in your app based on the following option:
        // option.type = .qrCode  -> qrcode scanner
        // option.type = .barCode -> barcode scanner
        
        // invoke the callback to passback the result, this can be invoked asynchronously.
        // ----> async remote call: {
        callback(IAPWalletScannerResult(code: "${code extracted}"))
        // ----> async remote call: }
    }
}
 

Use cases

2. Implement additional services via Wallet API

DeeplinkService

DeepLinkService is a service implemented by the Super App to open an in-app page of the destination scene or navigate to the destination scene across apps.

The DeepLinkService interface provides two methods for the Super App to implement, which are both called open() with different sets of parameters.

Method 1 - Open with Uri

Input parameters

Parameter

Date type

Description

scheme

Uri

Immutable URI reference. A URI reference includes a URI and a fragment. The component of the URI is followed by a number sign (#). Build and parse URI references that conform to RFC 2396.

apiContext

APIContext

A context object carries the mini program runtime metadata.

Returns

Date Type

Required

Description

boolean

Yes

An indicator of whether the scheme is opened successfully.

Sample implementation of open with Uri

copy
final class DeeplinkService: IAPWalletDeeplinkServiceSignature {
    
    // Mark: this API is currently used by Miniprogram combined with AC 2.0 case.
    override func open(scheme: URL,
                       in context: IAPWalletAPIContext?) -> Bool {
        // the wallet app can choose to process the scheme as deeplink,
        // or ordinary url open in webview
        if UIApplication.shared.canOpenURL(scheme) {
            if #available(iOS 10.0, *) {
                UIApplication.shared.open(scheme, options: [:], completionHandler: nil)
                return true
            } else {
                UIApplication.shared.openURL(scheme)
                return true
            }
        } else {
            return false
        }
    }
}

Method 2 - Open with sceneCode

Input parameters

Parameter

Data type

Description

sceneCode

String

The scene code that presents a scene, such as SCAN and QR_CODE.

params

Map<String, String>

The parameters that the my.navigateToBizScene JSAPI provides, used for data transmission of this JSAPI. 

context

APIContext

A context object carries the mini program runtime metadata.

callback

Callback

The callback allows the developer to send the result back to AC SDK.

Sample implementation of open with sceneCode

copy
final class DeeplinkService: IAPWalletDeeplinkServiceSignature {
    
    public static let SCENE_WEB_PAGE = "WEB_PAGE" // scene for opening a webpage
    public static let SCENE_SCAN = "SCAN" // scene for open the in app scanner
    public static let SCENE_QR_PAY = "QR_PAY" // scene for conduct qr payment
    public static let SCENE_BIND_CARD = "BIND_CARD" // scene for trigger in app card bind flow
    public static let SCENE_DEEPLINK_SCHEME = "SCHEME" // scene for invoke in app deeplink routing
    public static let SCENE_TOP_UP = "TOP_UP" // scene for trigger in app topup flow
    
    override func open(bizSceneCode: String, with params: [String:String], in context: IAPWalletAPIContext? = nil, callback: @escaping (IAPWalletBaseServiceResult) -> Void) {
        
        let result = IAPWalletBaseServiceResult()
        if (bizSceneCode == IAPWalletDeeplinkServiceSignature.SCENE_WEB_PAGE) {
            // handle opening web page
            result.extendedInfo = ["${result-param-1-key}" : "${result-param-1-value}"]
            callback(result)
        } else if (bizSceneCode == IAPWalletDeeplinkServiceSignature.SCENE_SCAN) {
            // handle opening scan page
            result.extendedInfo = ["${result-param-1-key}" : "${result-param-1-value}"]
            callback(result)
        } else if (bizSceneCode == IAPWalletDeeplinkServiceSignature.SCENE_TOP_UP) {
            // handle opening top up page
            result.extendedInfo = ["${result-param-1-key}" : "${result-param-1-value}"]
            callback(result)
        }
        
        // implement all the relevant scene codes here.
    }

Next steps

Support Alipay+ promotion QR code