@civic/gateway-client-core
v1.1.5
Published
Sample project for creating typescript library with support of iife and types.
Downloads
3,270
Maintainers
Keywords
Readme
Civic Gateway Client Core
Overview
The Civic gateway-client-core library is a state-management library for managing interactions with the Civic Pass system. It listens to inputs from sources such as Civic gatekeeper, on-chain events, Civic Pass data collection iframe, and orchestrates calls to the Civic Gatekeeper API, outputting the gatewayStatus and flowParameters that can be used to present a Civic Pass UI to the user and move forward with pass creation and refresh.
Getting started
npm run build
builds the library to dist
, generating three files:
dist/civic-gateway-client-core.cjs.js
A CommonJS bundle, suitable for use in Node.js, thatrequire
s the external dependency. This corresponds to the"main"
field in package.jsondist/civic-gateway-client-core.esm.js
an ES module bundle, suitable for use in other people's libraries and applications, thatimport
s the external dependency. This corresponds to the"module"
field in package.jsondist/civic-gateway-client-core.umd.js
a UMD build, suitable for use in any environment (including the browser, as a<script>
tag), that includes the external dependency. This corresponds to the"browser"
field in package.json
npm run dev
builds the library, then keeps rebuilding it whenever the source files change using rollup-watch.
npm test
builds the library, then tests it.
How it works
The library uses zustand for inernal state management, where the state is divided into separate concerns:
inputs: ClientCoreInput;
internal: ClientCoreInternal;
output?: ClientCoreOutput;
functions: {
reset: () => void;
};
The high-level flow is that:
- inputs get updated in state via external listeners and changes in the input parameters (on-chain state, gatekeeper record state, events from civic-pass iframe etc)
- the internal.status gets calculated based on the current state of all the inputs
- the internal orchestrator subscribes to status events and for specific status events it performs some orchestration, e.g. requesting a gateway token from the gatekeeper-api when it receives a data-collection payload from civic-pass
- outputs are derived from the current internal state and are used by the instantiater of the gateway-client-core to show a status to the user
ClientCoreInput
This represents the state of different external inputs to the client-core:
civicSign: GatewayInput<CivicSignEventTypeRequestMessage, ChainError>;
civicPass: GatewayInput<CivicPassMessageResponse>;
gatewayToken: GatewayInput<GatewayToken>;
gatekeeperRecord: GatewayInput<GatekeeperRecordResponse>;
parameters: GatewayClientParameters | null;
civicSign
civicSign events are sent and received using postMessage and can be considered 'outside' of the gatewayStatus flow, in that a signature request or did request can be received at any point in any flow. CivicSign events are subscribed to in the listenerManager and handled by 'remoteSign'
civicPass
civicPass is an external frontend application that collects data and posts events that gateway-client-core listens to. These events are subscribed to in the listener manager and normally cause a change in the computed gatewayStatus that in turn can trigger orchestration flows
gatewayToken
gatewayToken represents the on-chain state, where the input chainImplementation is used to query for and listen to token events. Changes to gatewayToken will also trigger gatewayStatus changes and trigger orchestration flows
gatekeeperRecord
gatekeeperRecord represents the civic-gatekeeper-api view of a pass: civic checks are applied on requests to the gatekeeper and can result in token rejections. The orchestrator's main function is to call the gatekeeper-api during different orchestration flows (issuance, refresh etc.) using data collected and provided by civic-pass events.
parameters
The parameters represent the instantiation parameters of gateway-client-core and represent options for things like wallet address, gatekeeper-network etc. that remain constant during a flow.
ClientCoreInternal
export type ClientCoreOutput = {
gatewayStatus: GatewayStatus;
gatewayToken?: GatewayToken;
flowParameters: FlowParameters | null;
pendingRequests: PendingPayload | undefined;
flowState?: {
status?: FlowStatus;
userInteraction: UserInteraction;
};
};
The outputs represent states that the instantiator of the gateway-client-core would be interested in:
- gatewayStatus is the distillation of all the current inputs and can also be thought of as the overall status, representing a derived state from on-chain status, data-collection and gatekeeper record status. This can be used by UI components such as the Civic IdentityButton to show the user what the current state of their pass is
- gatewayToken is the on-chain token object
- flowParameters are used to send to a frontend, representing the different action, state and parameters that the frontend needs to display a flow to the user. They can be merged into e.g. iframe GET parameters
- pendingRequests: applicable to the custom PII-sharing flow only: a pending request means that the instantiator (normally a dApp) would pass this id to some external system to do their own validation on in order to issue a civic pass
- flowState: indicates whether the flow is in progress or complete etc. so that the instantiator of gateway-client-core can decide whether to show a UI to the user or not.
Dynamic inputs
Most of the input parameters to the client-core are static, i.e. any change in them should cause state to be reset and a new instance of the client-core to be used. However, there are some dynamic inputs that should not cause a reset but instead should trigger a new flow.
forceRequireRefresh
In order to allow a dApp to implement custom refresh logic, a boolean flag forceRequireRefresh
can be set by calling the updateDynamicParameters()
function on a gateway-client-core instance.
This flag will only cause a flow if an ACTIVE gatewayToken already exists for a given wallet. Setting it will cause the client-core to view the currrent gatewayToken as expired, and thus trigger set the gatewayStatus to REFRESH_TOKEN_REQUIRED, and to guide the user through a refresh-token flow. When the token expiry is updated to a value in the future, then the flow is viewed as finished and the gatewayStatus will once again be ACTIVE.
UI integration
The core offers optional UI state management using output variables that can be used to control a remote frontend, and providing input methods that can be wired up to user interface elements.
UI Inputs
The UI input consists of 3 methods that are exposed directly on the gateway-client-core instance:
onShow: () => void
: will set the Civic Pass frontend visibility to true and start or resume a user-flow, normally connected to the Civic Identity button, or other UI element that the user starts the Civic Pass flow withonHide: () => void
: will set the Civic Pass frontend visibily to false. Normally connected to the 'close' button on the frontend, if applicableonLoad: () => void
: to trigger when the frontend loads, i.e. on the iframe 'onLoad' method
UI Output
The UI output takes the form of state variables that can be wired to UI elements to show different states:
{
isVisible: boolean; // the frontend visibility
url?: string; // the URL to load frontend content
isLoading: boolean; // current loading status
isLoaded: boolean; // current loaded status
}
UI Example
const inputs = <define core inputs>;
const [clientCoreOutput, setClientCoreOutput] = useState<ReactClientCoreOutput | undefined>(undefined);
const onOutputChange = (output: ClientCoreOutput | undefined) => {
setClientCoreOutput(output);
};
const core = GatewayClientCore.getSingleInstance({ ...inputs, onOutputChange});
{clientCoreOutput?.ui?.url && (
<IframeWrapper>
{ui?.output?.isLoading && <LoaderOverlay />}
<CloseButton onClick={() => core?.ui?.onHide()} />
<iframe
src={clientCoreOutput?.ui?.url}
style={{
pointerEvents: ui?.output?.isLoaded ? 'auto' : 'none', // disables user input during loading
}}
onLoad={() => {
core?.ui?.onLoad?.();
}}
/>
</IframeWrapper>
<IdentityButton onClick={() => core?.ui?.onShow()}>
)}
Error flow handling
Civic-sign errors
If the user rejects a transaction, or the chainImplementation.proveWalletOwnership() function failes for any reason, the gateway-client catches the error and uses the civic-sign remote post message channel to send back the error to the requester so that it can be handled on that side, rather than in the gateway-client.
Transaction send errors
In the 'issuanceClientSends-resume-requested' case:
- the gatekeeper tells the client that a transaction can be retried by including the flag
canRequestFreshTx: true
in the response - the client changes state to
ISSUANCE_CLIENT_SENDS_START_NEW_TX
or REFRESH_CLIENT_SENDS_START_NEW_TX - this change of status causes a new iframe screen to load:
startPreApprovedTransaction
- the user is asked for POWO which successfully sent from the iframe in an issuance/success/payload civicpass event
- the
computeIssuanceStartPreApprovedTransaction
state fires and calculates a new status ofISSUANCE_CLIENT_SENDS_REQUEST_NEW_TX
- this causes the iframe to show the usual 'signTransaction' screen (with fees etc.)
- the orchestrator detects the
ISSUANCE_CLIENT_SENDS_REQUEST_NEW_TX
state and calls a new function on the issuance/refresh servicesfetchFreshTransaction()
- this calls the gatekeeperClient using a new client method
fetchFreshTransaction({ payer, payload })
which updates the gatekeeperRecord.received value in state - the
computeIssuanceInReview()
state function fires and we're back in the normal 'client-sends' flow waiting to send the on-chain transaction
In the 'retries-exhausted' case:
- the user goes through the normal client-sends flow until they get to the point where they have a transaction to sign/send
- the transaction sign/send fails (for whatever reason, either user cancels, not enough funds, or something goes wrong sending)
- the client reties with the same transaction until the 'clientSendsMaxRetries' limit is hit
- the client goes into
ISSUANCE_CLIENT_SENDS_REQUEST_NEW_TX
status - this causes the iframe to show the usual 'signTransaction' screen (with fees etc.)
- the orchestrator detects the
ISSUANCE_CLIENT_SENDS_REQUEST_NEW_TX
state and calls a new function on the issuance/refresh servicesfetchFreshTransaction()
- this calls the gatekeeperClient using a new client method
fetchFreshTransaction({ payer, payload })
which updates the gatekeeperRecord.received value in state - the
computeIssuanceInReview()
state function fires and we're back in the normal 'client-sends' flow waiting to send the on-chain transaction