UserInterface
An instance of a UserInterface
is required for the SessionKit and is responsible for rendering out information to the end user and facilitating interactions when required. The SessionKit does this by exporting an interface named UserInterface
, which defines the patterns used for interactions during the Login and Transact method calls.
Usage
A typical developer will never make calls directly against the UserInterface
, with the exception of developers who:
- Create any type of Plugin, such as a LoginPlugin, TransactPlugin, or WalletPlugin package.
- Create a custom
UserInterface
instance for inclusion in the SessionKit.
All developers who are building interactive applications, however, will be required to include an instance of a UserInterface
to the SessionKit during instantiation, an example of which is below:
import { SessionKit } from "@wharfkit/session"
import { WebRenderer } from "@wharfkit/web-renderer"
import { WalletPluginAnchor } from "@wharfkit/wallet-plugin-anchor"
const sessionKit = new SessionKit({
appName: "myapp",
chains: [
{
id: "73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d",
url: "http://jungle4.greymass.com",
},
],
ui: new WebRenderer(), // <-- An instance of a `UserInterface`
walletPlugins: [new WalletPluginAnchor()],
})
Wharf maintains and distributes a core package called the WebRenderer that acts as a default implementation of a UserInterface
. More details about this implementation can be found in the WebRenderer documentation or on the Github repository.
Purpose
The UserInterface
is primarily responsible for two major aspects of the SessionKit: communication of information to the user, and prompting of user interactions. These responsibilities are carried out through various other components, including:
- During calls to the Login and Transact methods of the SessionKit
- During interactions with the active WalletPlugin
- While processing hooks defined by LoginPlugin or TransactPlugin packages
Communication
During SessionKit operations, there is often benefit to providing the end user with status updates and information about transaction activity. The UserInterface
defines multiple event-based methods in which external operations are able to call out to the user interface to provide relevant information to the user.
Interactions
The interaction between the end user and the SessionKit are facilitated by the UserInterface
through a number of predefined events and method calls. During these interactions, the SessionKit will trigger asynchronous operations against the UserInterface
while waiting for the user’s response before proceeding.
Specification
The remainder of this document will target advanced developers who wish to further customize the SessionKit through the creation of a custom UserInterface
for use in applications.
Concepts
A developer who is creating a custom UserInterface
instance will need to understand:
- The Architecture design to integrate with the SessionKit and various [Plugins
- The Life Cycle events and methods for Login and Transact
- The Prompt method and how to facilitate interactivity
- The Translation interface to allow localization of the UI
- The Logging and [Error Handling events
A full example of how a UserInterface
can be implemented can be found in the WebRenderer package.
Architecture
The UserInterface
components of the SessionKit are designed with inheritance in mind, to optionally allow developers to extend existing core code in order to more effectively build custom implementations. This inheritance structure can roughly be viewed as:
DefinedUserInterface (e.g. WebRenderer)
↳ extends AbstractUserInterface
↳ implements UserInterface
The UserInterface
(docs) is defined as an interface with the data structures that the SessionKit requires in order to function. One layer above this is an AbstractUserInterface
(docs) declared as an abstract class from which complete implementations may extend in order to inherit base functionality.
Life Cycle
Through the Session Kit, Wharf currently has two major life cycle processes in which the UserInterface
is utilized:
- The Login call on an instance of the SessionKit.
- The Transact call on an individual Session.
When developers make calls to these methods in their applications, a life cycle event is started and a context object is spawned for its duration. This context contains an instance of the UserInterface
, which the SessionKit and various Plugins may call to interact with the user.
Login
When an application calls the Login method of the SessionKit, a series of events is triggered against an implemented UserInterface
and its methods.
Listed below are all of the methods this sequence will call, in chronological order:
onLogin
onLogin: () => Promise<void>
Immediately upon the developer’s call to Login against the SessionKit, the onLogin
call is made against the UserInterface
. This call passes no data to the interface and expects no response, but gives the opportunity to the UserInterface
to prepare itself for the incoming login
call.
This time can be used to prepare UI elements (in DOM or other mediums) before any actual user interaction.
login
login(context: LoginContext): Promise<UserInterfaceLoginResponse>
After initial processing has been completed by the SessionKit, the login
method of the given UserInterface
is called. The SessionKit will pass in an instance of a LoginContext to the UserInterface
to provide information about the request and how to interact with the user. This information will include which WalletPlugin instances are available and any metadata values the application developer has defined.
This data will be used in order to facilitate a number of scenarios based on the various capabilities of the WalletPlugin instances in use:
- Prompt the user to select a WalletPlugin, if multiple are provided and it was not defined during the Login call.
- Prompt the user to select a blockchain, if multiple are provided and the selected WalletPlugin has requiresChainSelect set to
true
. If the WalletPlugin also has an array ofsupportedChains
defined, the list of available chains must be filtered down to match this list. - Prompt the user to enter an account name manually, if the selected WalletPlugin has requiresPermissionEntry set to
true
. - Prompt the user to select a permission associated to a PublicKey, if the selected WalletPlugin has requiresPermissionSelect set to
true
.
The SessionKit will await a response from the UserInterface
conforming to the UserInterfaceLoginResponse
pattern, or until an Error
is thrown.
onLoginComplete
onLoginComplete: () => Promise<void>
After the WalletPlugin successfully completes its login operations, the SessionKit will issue the onLoginComplete
call against the UserInterface
, which gives it the opportunity to reset itself and perform any clean-up required.
Transact
When an application calls the Transact method on a Session, a series of events is triggered against an implemented UserInterface
and its methods.
Listed below are all of the methods this sequence will call in chronological order:
onTransact
onTransact: () => Promise<void>
Immediately upon the developer’s call to Transact against the SessionKit, the onTransact
call is made against the UserInterface
. This call, similar to onLogin
, passes no data and expects no specific response, but gives the implemented user interface time to prepare for the incoming request.
onSign
onSign: () => Promise<void>
After any included TransactPlugin instances have had their opportunity to process their beforeSign hooks, the onSign
call is made to the user interface to indicate that the transaction is about to be signed by the WalletPlugin.
onSignComplete
onSignComplete: () => Promise<void>
Following a successful call to the WalletPlugin to sign the transaction and all TransactPlugin instances have executed their afterSign hooks, the onSignComplete
call is made to indicate a signature has been retrieved.
onBroadcast
onBroadcast: () => Promise<void>
If the Transact call has the default flag of broadcast: true
, the onBroadcast
call is made to the user interface as it prepares to broadcast the transaction to the network.
Note that if the broadcast: false
flag is set, this event will not be called.
onBroadcastComplete
onBroadcastComplete: () => Promise<void>
Following a successful call to broadcast the transaction and execution of the TransactPlugin instances afterBroadcast
hooks, the onBroadcastComplete
call is made to indicate that the transaction has been successfully broadcast to the designated blockchain.
Note that if the broadcast: false
flag is set, this event will not be called.
onTransactComplete
onTransactComplete: () => Promise<void>
Upon success of the entire Transact life cycle flow, the onTransactComplete
call is made to indicate to the UserInterface
that this transaction has now completed. Any cleanup operations required by the user interface itself can now be called.
Prompt
Outside of the event-driven life cycle methods above, one of the most important abilities a UserInterface
provides is for Plugins to interact with users. This is done using the prompt
call made available through the context given to every plugin to either display information or await some form of user interaction.
Examples of instances where prompt
may be called are:
- A WalletPlugin during the Login and Transact calls
- A LoginPlugin during the Login call
- A TransactPlugin during the Transact call
Each plugin that makes the call needs to provide arguments that match the PromptArgs
interface and await a response, which will come in the form of a CancelablePromise
. This special type of promise allows the prompt to either be accepted, rejected, or canceled.
Rendering
It is the responsibility of the UserInterface
to implement the prompt
call, interpret its request, and render the appropriate elements to the end user.
The arguments passed to the UserInterface
that are accepted by the prompt
call will match:
interface PromptArgs {
title: string
body?: string
elements: PromptElement[]
}
It’s up to the renderer to decide the best use of this information, but in general the elements are meant for the following purposes:
- The
title
is provided as a header for the display that will be rendered to the user - A
body
may optionally be defined with a text-based description of what you are prompting for - The
elements
array consists of one or morePromptElement
instances, which instruct theUserInterface
on how to present this prompt to the user
The elements
array is populated by one or more PromptElement
objects that make up the desired layout presented to the user.
interface PromptElement {
type:
| "accept"
| "asset"
| "button"
| "close"
| "countdown"
| "link"
| "qr"
| "textarea"
label?: string
data?: unknown
}
These should be pre-built elements internal to the UserInterface
that can accommodate for certain types of interactive events.
- The
type
field must match one of the strings included. Each string represents a different type of prompt. - A
label
for the element, typically providing context to the element. - A
data
object for the element, which is specific to the element itself and how it’s rendered.
Responding
The prompt
call is asynchronous and the SessionKit or Plugin will await a response from the UserInterface
before moving on. Based on how the user responds to the rendered elements, the UserInterface
is responsible for responding to the promise by:
- Resolving the promise (which acts as an “accept and move on” response)
- Rejecting the promise (which acts as “reject and move on” response)
- Canceling the promise (which aborts the entire login/transact request)
This response contains no specific data but instead offers the three above logic paths using the CancelablePromise
pattern.
Logging
status
status: (message: string) => void
The status
method is a generic string-based system on the user interface which allows any process or plugin to provide basic messages directly to the user interface. These can be included in the UI to update the user about details of what process is occurring, or for the developer to output as logs for debugging.
Translation
Part of the Communication responsibility of the UserInterface
is also handling the translation of all the content being passed to the user. The UserInterface
library should implement an i18n compatible translation library which allows the use of key-based translation strings, which can be provided by either the SessionKit or any type of Plugin.
translate
translate: UserInterfaceTranslateFunction
This method must follow the UserInterfaceTranslateFunction
interface and utilize whichever technologies are best suited to the task of facilitating content translation. The WebRenderer serves as an example of how this can be done, as it translates Wharf’s expectation of a translation string into the i18n library it’s using.
export type UserInterfaceTranslateFunction = (
key: string,
options?: UserInterfaceTranslateOptions,
namespace?: string
) => string
getTranslate
getTranslate: (namespace?: string) => UserInterfaceTranslateFunction
This method defines how Wharf or Plugins should retrieve an instance of the UserInterfaceTranslateFunction
of a given namespace. Plugins specifically will make use of this call to access and provide translations for their content within a given user interface.
By default the AbstractUserInterface class will define this method for use in plugins.
addTranslations
addTranslations: (translations: LocaleDefinitions) => void
The final translation responsibility of the UserInterface
is the ability for Wharf or a plugin to be able to dynamically add translation strings into the user interface. This method is added so that the SessionKit and various Plugins can programmatically add translation strings to the dictionary.
The WebRenderer again serves as an example of how this can be done.
Error Handling
onError
onError: (error: Error) => Promise<void>
A UserInterface
must define an onError
method to define how the user interface handles errors thrown by the SessionKit. These can be displayed directly to users, interpreted and handled based on their content, and logged externally to help troubleshoot issues.