-
-
Notifications
You must be signed in to change notification settings - Fork 1
UserInterface Implementation
As of tag v0.3.0-ui-4
on Feb 1, 2023.
This WILL change rapidly, and this document will be out of date within weeks.
- The
SessionKit
accepts aui
parameter that implementsUserInterface
. - When
.login()
is called on theSessionKit
, the.onLogin
call is triggered on theUserInterface
. - The
SessionKit
determines which other calls it needs to make, including:- Check the
.walletPlugins
array and if more than one, call.onSelectWallet()
on theUserInterface.
- The
.walletPlugins
converted to aWalletPluginMetadata
array and is passed to this call via theLoginContext
. - This is awaited until the
UserInterface
returns the index of the array that matches theWalletPlugin
the user selects. - The
.login()
call continues and checks to see if theWalletPlugin
requires the user to select a blockchain. - If no blockchain is preselected and the user is required to select, call
.onSelectChain()
on theUserInterface
. - The list of blockchains is passed to the
.onSelectChain()
call again via theLoginContext
. - This is awaited until the
UserInterface
returns the chain ID (Checksum256) of the blockchain the user selects.
- Check the
- The
WalletPlugin.login()
method is triggered asking the wallet to return a selected chain ID and permission level. - A
Session
is created and returned as part of theLoginResult
object from the original.login()
call. - The
SessionKit
makes a.onLoginResult
call against theUserInterface
to indicate that the login is completed. - The resulting
Session
object is configured to use the selected wallet, chain, and permission. - The
.transact()
call can now be used on theSession
object by the app. - When
.transact()
is called on theSession
, it interacts with the UI by doing the following:- ...more to come about transactions
A UserInterface
is an interface that species a number of methods that are called during either the login
or transact
calls on a Session.
/**
* Interface which a [[UserInteface]] plugins must implement.
*/
export interface UserInterface {
// Inform the UI that a login call has started
onLogin: (options?: LoginOptions) => void
// Inform the UI that a login call has completed
onLoginResult: () => void
// Ask the user to select a blockchain, and return the chain id
onSelectChain: (context: LoginContext) => Promise<Checksum256>
// Ask the user to select an account, and return the PermissionLevel
onSelectPermissionLevel: (context: LoginContext) => Promise<PermissionLevel>
// Ask the user to select a wallet, and return the index based on the metadata
onSelectWallet: (context: LoginContext) => Promise<number>
// Inform the UI that a transact call has started
onTransact: (context: TransactContext) => void
// Inform the UI that a transact call has completed
onTransactResult: (context: TransactResult) => void
// Update the displayed modal status from a TransactPlugin
status: (message: string) => void
}
The LoginContext
is passed to many of the .login()
calls to provide information to the UI about the login process and facilitate decision making.
/**
* Temporary context created for the duration of a [[Kit.login]] call.
*
* This context is used to store the state of the login request and
* provide a way for plugins to add hooks into the process.
*/
export class LoginContext {
// client: APIClient
chain: ChainDefinition
chains: ChainDefinition[] = []
hooks: LoginHooks = {
afterLogin: [],
beforeLogin: [],
}
ui: UserInterface
walletPlugins: WalletPluginMetadata[] = []
constructor(options: LoginContextOptions) {
// this.client = options.client
if (options.chains) {
this.chains = options.chains
}
this.chain = options.chain || this.chains[0]
this.walletPlugins = options.walletPlugins || []
this.ui = options.ui
// options.loginPlugins?.forEach((plugin: AbstractLoginPlugin) => {
// plugin.register(this)
// })
}
addHook(t: LoginHookTypes, hook: LoginHook) {
this.hooks[t].push(hook)
}
}
Just like the LoginContext
being used in .login()
calls, the TransactContext
is used in the .transact()
calls to facilitate UI interactions during a transaction.
/**
* Temporary context created for the duration of a [[Session.transact]] call.
*
* This context is used to store the state of the transact request and
* provide a way for plugins to add hooks into the process.
*/
export class TransactContext {
readonly abiProvider: AbiProvider
readonly client: APIClient
readonly fetch: Fetch
readonly hooks: TransactHooks = {
afterBroadcast: [],
afterSign: [],
beforeSign: [],
}
readonly permissionLevel: PermissionLevel
readonly transactPluginsOptions: TransactPluginsOptions
readonly ui: UserInterface
constructor(options: TransactContextOptions) {
this.abiProvider = options.abiProvider
this.client = options.client
this.fetch = options.fetch
this.permissionLevel = options.permissionLevel
this.transactPluginsOptions = options.transactPluginsOptions || {}
this.ui = options.ui
options.transactPlugins?.forEach((plugin: AbstractTransactPlugin) => {
plugin.register(this)
})
}
get accountName(): Name {
return this.permissionLevel.actor
}
get permissionName(): Name {
return this.permissionLevel.permission
}
get esrOptions(): SigningRequestEncodingOptions {
return {
abiProvider: this.abiProvider,
zlib,
}
}
addHook(t: TransactHookTypes, hook: TransactHook) {
this.hooks[t].push(hook)
}
async resolve(request: SigningRequest, expireSeconds = 120): Promise<ResolvedSigningRequest> {
// TODO: Cache the info/header first time the context resolves?
// If multiple plugins resolve the same request and call get_info, tapos might change
const info = await this.client.v1.chain.get_info()
const header = info.getTransactionHeader(expireSeconds)
// Load ABIs required to resolve this request
const abis = await request.fetchAbis(this.abiProvider)
// Resolve the request and return
return request.resolve(abis, this.permissionLevel, header)
}
}
This class implements UserInterface
and primarily just adds some potential logging for testing purposes. It also serves as an example of how the interface above can be implemented.
export class UserInterfaceHeadless implements UserInterface {
public consoleLog = false
public messages: string[] = []
public log(message: string) {
if (this.consoleLog) {
// eslint-disable-next-line no-console
console.info('UserInterfaceHeadless', message)
}
}
onLogin(options?: LoginOptions) {
this.log('onLogin: ' + JSON.stringify(options))
}
onLoginResult() {
this.log('onLoginResult')
}
public async onSelectPermissionLevel(context: LoginContext): Promise<PermissionLevel> {
throw new Error('The headless user interface does not support permission selection')
}
public async onSelectChain(context: LoginContext): Promise<Checksum256> {
throw new Error('The headless user interface does not support chain selection')
}
public async onSelectWallet(context: LoginContext): Promise<number> {
throw new Error('The headless user interface does not support wallet selection')
}
public async onTransact(context: TransactContext) {
this.log('onTransact' + String(context.accountName))
}
public async onTransactResult(context: TransactResult) {
this.log('onTransactResult' + String(context.transaction))
}
public status(message: string) {
this.messages.push(message)
this.log(message)
}
}
https://github.com/wharfkit/web-ui-renderer
The Web UI Renderer is a prototype that takes these interfaces and implements them as a SvelteKit rendered HTML element. All of the UserInterface
methods are implemented here:
https://github.com/wharfkit/web-ui-renderer/blob/master/src/index.ts
In order to create elements on the page and react to the events happening within the SessionKit.
The web-ui-renderer package also has a live development mode that you can run after cloning down the repo:
yarn dev
This will serve the following static HTML project, which implements both the Session Kit and the Web UI Renderer.
https://github.com/wharfkit/web-ui-renderer/blob/master/test/public/index.html