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 | Mandatory | Retrieve the authorization code from OAuth service that is used for authentication. | |
PaymentService | Mandatory | Initiate the payment process for purchase scenarios. | |
CodeService | Mandatory | Launch code scanner for mini programs to decode QR codes or barcodes. | |
DeepLinkService | 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](https://ac.alipay.com/storage/2020/5/11/793a3d8d-5270-405b-9362-e6a670b9c842.png)
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](https://ac.alipay.com/storage/2020/5/11/793a3d8d-5270-405b-9362-e6a670b9c842.png)
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.
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()
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()
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()
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](https://ac.alipay.com/storage/2020/5/11/793a3d8d-5270-405b-9362-e6a670b9c842.png)
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](https://ac.alipay.com/storage/2020/5/11/793a3d8d-5270-405b-9362-e6a670b9c842.png)
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](https://ac.alipay.com/storage/2020/5/11/793a3d8d-5270-405b-9362-e6a670b9c842.png)
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. |
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](https://ac.alipay.com/storage/2020/5/11/793a3d8d-5270-405b-9362-e6a670b9c842.png)
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 | 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
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 |
params | Map<String, String> | The parameters that the |
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
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.
}