# SDK Event Listeners
The Wallet emits events when properties change, for example, when the authenticated user changes their network. Events allow your dApp to keep in sync. You can hook onto event listeners via methods exposed in the SDK, writing your own handling logic that you want to fire when the event is received.
You can only register one event listener per message listener - if you do try to listen to a listener which has already been registered, it will throw a error. You can cancel listeners as necessary, and you'll then be able to register them again without an error being thrown.
The SDK exposes two different ways to register an event listener:
- Event listeners: listen to all events of a specified type until the listener is cancelled explicitly.
- One-off listeners: subscribe only to the first event of a specified type. Once the event has fired, your dApp will be unsubscribed and will receive no further events of the type. An example use case of a one-off listener might be when your dApp wants to update its state based on the next block mined, but isn't interested in subsequent changes to the blockchain.
Once the SDK is initialized, you should hook onto all the event listeners, as each one is important to the integration. We recommend that the callback you write for each message listener is just a simple state change to your Redux store, allowing that state change to be reflected in your UI components. Reactive programming (e.g. RxJS) will simplify the integration, and while it's not required (the SDK itself doesn't impose any constraints on this), it is our recommended integration approach for a clean solution.
# Registering an Event Listener
# Registering a One-Off Listener
# Event listeners response formats
Results are returned to the listener as follows:
{
data: TStronglyTypedResponse,
origin: string,
source: Window
}
This is the result of the postMessage
operation performed by the SDK.
As well as indicating the origin and the source of the received data, the returned result will always have a data property which specifies the data for the event being received.
data
The object passed from the other window.
origin
The origin of the window that sent the message at the time postMessage
was called. This string is the concatenation of the protocol and "://"
, the host name if one exists, and ":"
followed by a port number if a port is present and differs from the default port for the given protocol. Examples of typical origins are https://example.org/ (implying port 443), http://example.net/ (implying port 80), and http://example.com:8080/. The origin will always put the /
at the end so make sure you include that in your compare.
Note that this origin is not guaranteed to be the current or future origin of that window, which might have been navigated to a different location since postMessage was called.
You should always check that its https://wallet.funfair.io/
sending the post messages to be sure its came from us.
source
A reference to the window object that sent the message. You can use this to establish two-way communication between two windows with different origins.
# Security Concerns
💡 Always verify the sender's identity using the origin and possibly source properties. Any window (including, for example, http://evil.example.com) can send a message to any other window, and you have no guarantee that an unknown sender will not send malicious messages. Having verified identity, however, you still should always verify the syntax of the received message. Otherwise, a security hole in the site you trusted to send only trusted messages could then open a cross-site scripting hole in your site.
💡 To protect your dApp's users from cross-site scripting attacks, make sure you NEVER assign the data result from the postMessage
to any HTML elements:
window.funwallet.sdk.once(MessageListeners.TypeYouWantToUse, (result) => {
if (result.origin === 'https://wallet.funfair.io/') {
// OUCH!! YOU HAVE OPENED YOURSELF UP FOR THE TRUSTED DOMAINS TO
// INJECT BAD SCRIPTS INTO YOUR PAGE. AS RULE OF THUMB, NEVER, *EVER*
// DO THIS (DON'T WORRY WE WOULD NEVER DO SOMETHING SO MEAN :D)
document.getElementById('message').innerHTML = result.data;
}
});
Always specify an exact target origin, not "*", when you use postMessage
to send data to other windows. A malicious site can change the location of the window without your knowledge, and therefore it can intercept the data sent using postMessage
.
Please note, the SDK does check this as well and only connects messages from the Wallet but as the SDK is hosted on your side and exported globally on the window we suggest you check the origin as well. The messages the Wallet sends you are just information based anyway, we never register a event which says "go and execute this script on the parent site" so this makes it a lot less to worry about but we still suggest you abide by the security concerns addressed. If you fail to check this you can not be sure that the message has came from the Wallet.
# List of All Available Listeners
{
restoreAuthenticationCompleted = 'restoreAuthenticationCompleted',
// @deprecated Please use `window.funwallet.sdk.login` promise instead.
authenticationCompleted = 'authenticationCompleted',
followerAuthenticationCompleted = 'followerAuthenticationCompleted',
changeNetwork = 'changeNetwork',
walletInactivityLoggedOut = 'walletInactivityLoggedOut',
walletDeviceDeletedLoggedOut = 'walletDeviceDeletedLoggedOut',
pendingTransaction = 'pendingTransaction',
completedTransaction = 'completedTransaction',
erc20TokenBalanceChanged = 'erc20TokenBalanceChanged',
erc20TokenFiatPriceChanged = 'erc20TokenFiatPriceChanged',
ethBalanceChanged = 'ethBalanceChanged',
ethFiatPriceChanged = 'ethFiatPriceChanged',
changeCurrency = 'changeCurrency',
isKycVerified = 'isKycVerified',
kycProcessCancelled = 'kycProcessCancelled',
websocketConnected = 'websocketConnected',
websocketDisconnected = 'websocketDisconnected',
newBlock = 'newBlock',
playerProtectionUpdated = 'playerProtectionUpdated',
walletTracking = 'walletTracking',
authenticationPopUpClosed = 'authenticationPopUpClosed',
transactionReplaced = 'transactionReplaced',
appJwtExtended = 'appJwtExtended',
}
All the examples of code here will use the on
but in all of these cases, you can also use once
if you require one-off listening functionality.
# restoreAuthenticationCompleted
To allow restoring someone to be logged in after they refresh on initial load the Wallet tries to restore a session. Upon success, it will emit restoreAuthenticationCompleted
telling you if it's restored a user's session or not with any authentication data. You should disable any sign-in/up click button until you get this event.
result.data
returns:
{
// this will be defined if `isAuthenticated` is true
result?: AuthenticationCompletedResponeData | undefined,
isAuthenticated: boolean,
}
export interface AuthenticationCompletedResponeData {
authenticationCompleted: {
playerProtection: ExclusionStatusResponse;
ethereumAddress: string;
currentCurrency: string;
// details of this interface is just below in `changeNetwork` response data
currentNetwork: NetworkDetails;
userAccountId: string;
};
}
ExclusionStatusResponse
:
{
status: ExclusionStatusType;
startTimestamp?: number | undefined;
durationDays?: number | undefined;
activeTimestamp?: number | undefined;
}
ExclusionStatusType
:
export enum ExclusionStatusType {
ACTIVE = 'ACTIVE',
ON_BREAK = 'ON_BREAK',
EXCLUDED = 'EXCLUDED',
}
NetworkDetails
:
{
selectedNode: string;
backupNodes: string[];
enabled: boolean;
name: string;
id: number;
chainId: number;
nativeCurrency: string;
networkBlockExplorer?: NetworkBlockExplorer | undefined;
feeWarningThreshold: BigNumber;
supportsFiatPrices: boolean;
supportsWalletConnect: boolean;
isProductionNetwork: boolean;
fixedGasPrice?: string | undefined;
}
# authenticationCompleted
DEPRECATED
Please use window.funwallet.sdk.login
promise instead.
This will fire when the leader instance has been authenticated by a user. It is a much better flow in your dApp to use the login
async method now so we do not recommend using this approach.
result.data
returns:
{
// can just be ignored if your dapp does not care about player protection
playerProtection: ExclusionStatusResponse;
ethereumAddress: string;
currentCurrency: string;
currentNetwork: NetworkDetails;
// the fun wallets id for this user
userAccountId: string;
}
ExclusionStatusResponse
:
{
status: ExclusionStatusType;
startTimestamp?: number | undefined;
durationDays?: number | undefined;
activeTimestamp?: number | undefined;
}
ExclusionStatusType
:
export enum ExclusionStatusType {
ACTIVE = 'ACTIVE',
ON_BREAK = 'ON_BREAK',
EXCLUDED = 'EXCLUDED',
}
NetworkDetails
:
{
selectedNode: string;
backupNodes: string[];
enabled: boolean;
name: string;
id: number;
chainId: number;
nativeCurrency: string;
networkBlockExplorer?: NetworkBlockExplorer | undefined;
feeWarningThreshold: BigNumber;
supportsFiatPrices: boolean;
supportsWalletConnect: boolean;
isProductionNetwork: boolean;
fixedGasPrice?: string | undefined;
}
# followerAuthenticationCompleted
This will fire when the follower instance has authenticated itself successfully and indicates that you should re-enable any disabled Wallet buttons.
result.data
returns:
{
followerAuthenticationCompleted: boolean,
}
# changeNetwork
This will fire when the Wallet network has been changed. Note: this will always fire upon initial authentication of the leader as networks will update as a result of authentication.
result.data
returns:
{
network: NetworkDetails,
}
NetworkDetails
:
{
selectedNode: string;
backupNodes: string[];
enabled: boolean;
name: string;
id: number;
chainId: number;
nativeCurrency: string;
networkBlockExplorer?: NetworkBlockExplorer | undefined;
feeWarningThreshold: BigNumber;
supportsFiatPrices: boolean;
supportsWalletConnect: boolean;
isProductionNetwork: boolean;
fixedGasPrice?: string | undefined;
}
# walletInactivityLoggedOut
This will fire when the inactivity timeout has expired, meaning all authenticated instances have now been logged out.
result.data
returns:
{
loggedOut: boolean,
}
# walletDeviceDeletedLoggedOut
This will fire when the current device the user is using has been deleted, meaning all authenticated instances have now been logged out.
result.data
returns:
{
loggedOut: boolean,
}
# appJwtExtended
This will fire when the appJwt
token has been extended.
result.data
returns:
{
jwtToken: string;
}
# pendingTransaction
This will fire when a pending transaction has occurred on the Wallet. We suggest if your dApp has sent this transaction and wants to hook onto certain notifications, e.g. the transaction hash, receipt etc just use the framework your using to get that data (ethers/web3).
result.data
returns:
{
transactionHash: string,
transaction: {
to: string;
from: string;
nonce: string;
gasLimit: string;
gasPrice: string;
data: string;
value: string;
chainId: number;
}
}
# completedTransaction
This will fire when a completed transaction has occurred on the Wallet (i.e. upon the first confirmation). We suggest if your dApp has sent this transaction and wants to hook onto certain notifications, e.g. the transaction hash, receipt etc just use the framework your using to get that data (ethers/web3).
result.data
returns:
{
transactionReceipt: {
to: string;
from: string;
contractAddress: string;
transactionIndex: number;
root?: string;
gasUsed: string;
logsBloom: string;
blockHash: string;
transactionHash: string;
logs: Array<{
blockNumber: number;
blockHash: string;
transactionIndex: number;
removed: boolean;
address: string;
data: string;
topics: Array<string>;
transactionHash: string;
logIndex: number;
}>;
blockNumber: number;
confirmations: number;
cumulativeGasUsed: string;
byzantium: boolean;
status?: number;
},
blockTimestamp: number,
}
# transactionReplaced
This will fire when the user cancels the transaction or speeds it up within the Wallet itself. Most wallets do not handle this meaning that your dApp polls forever but if it happens when using the FunFair Wallet, we will throw an error if you're waiting for the receipt. Also, we will emit this event which tells you the oldHash
the newHash
and the reason it got replaced allowing you to link the old transaction to the new one without having to monitor events.
result.data
returns:
{
oldHash: string,
newHash: string,
replacedReason: 'gasIncreased' | 'cancelled',
}
# erc20TokenBalanceChanged
This will fire when an ERC20 token balance changes for the authenticated user.
result.data
returns:
{
symbol: string;
contractAddress: string;
networkId: number;
// the balance is pre-formatted
// to the correct maximum decimal
tokenBalance: string;
tokenIndex: number;
}
# erc20TokenFiatPriceChanged
This will fire when an ERC20 token's fiat price changes. Fiat prices are monitored by the Wallet server and updated regularly. Any change will trigger this event.
result.data
returns:
{
symbol: string;
contractAddress: string;
networkId: number;
fiatPrice: number;
tokenIndex: number;
}
# ethBalanceChanged
This will fire when the ETH balance changes for the authenticated user.
result.data
returns:
{
// the ethBalance is pre-formatted
// to the correct maximum decimal
ethBalance: string,
}
# ethFiatPriceChanged
This will fire when the ETH fiat price changes. Fiat prices are monitored by the Wallet server and updated regularly. Any change will trigger this event.
result.data
returns:
{
fiatPrice: number,
}
# changeCurrency
This will fire when the authenticated user's selected currency has changed.
result.data
returns:
{
currency: string,
}
# isKycVerified
This will fire upon initial login, whether the account is KYC-verified or not. This allows you to pop up the KYC modal automatically when a user logs in, if necessary.
result.data
returns:
{
isVerified: boolean;
}
# kycProcessCancelled
This will fire when the authenticated account going through the KYC process cancels the modal and goes back to the dApp website.
result.data
returns:
{
cancelled: boolean;
}
# websocketConnected
This will fire when a successful WebSocket connection is made, including any time a dApp reconnects after being disconnected. Please keep the WebSocket connected status in memory, and when websocketDisconnected
is received, update the value of the WebSocket connection status.
result.data
returns:
{
connected: boolean;
}
# websocketDisconnected
This will fire when the WebSocket disconnects or gets closed.
result.data
returns:
{
disconnected: boolean;
}
# newBlock
This will fire when the Wallet receives a new block alert through the WebSocket connection. This removes the need for any polling - the dApp can just listen for these events. You can hook onto this even if the user is not authenticated.
result.data
returns:
{
networkId: Networks;
blockNumber: number;
blockHash: string;
bloomFilter: string;
timestamp: number;
}
# playerProtectionUpdated
This will fire when the player protection data has been updated, for example if they have self-excluded or changed their exclusion reactivation date.
result.data
returns:
{
status: ExclusionStatusType;
startTimestamp?: number | undefined;
durationDays?: number | undefined;
activeTimestamp?: number | undefined;
}
ExclusionStatusType
export enum ExclusionStatusType {
ACTIVE = 'ACTIVE',
ON_BREAK = 'ON_BREAK',
EXCLUDED = 'EXCLUDED',
}
# walletTracking
Due to the Wallet holding private keys in memory, we don't allow Google analytics scripts in the Wallet itself. However, this event emits tracking data back to the client so the you can pass them to any tracking software you want to use.
result.data
returns:
{
eventCategory: TrackingEventCategory;
eventAction: TrackingEventAction;
eventLabel?: TrackingEventLabel | undefined;
eventValue?: number | undefined;
}
export declare enum TrackingEventCategory {
walletRegistration = 'walletRegistration',
walletRecovery = 'walletRecovery',
wallet = 'wallet',
accountDeposit = 'accountDeposit',
accountWithdrawal = 'accountWithdrawal',
accountTokenTransfers = 'accountTokenTransfers',
accountTransactions = 'accountTransactions',
accountSettings = 'accountSettings',
accountSecurity = 'accountSecurity',
accountResponsibleGaming = 'accountResponsibleGaming',
approveTransaction = 'approveTransaction',
approveSigningText = 'approveSigningText',
}
export declare enum TrackingEventAction {
step = 'step',
click = 'click',
}
export declare enum TrackingEventLabel {
emailValidatingRequest = 'emailValidatingRequest',
resendEmail = 'resendEmail',
emailValidationComplete = 'emailValidationComplete',
backToSignIn = 'backToSignIn',
emailValidationFailed = 'emailValidationFailed',
closeAndReturnToApp = 'closeAndReturnToApp',
passwordSetup = 'passwordSetup',
registrationComplete = 'registrationComplete',
startKyc = 'startKyc',
closeKyc = 'closeKyc',
verifyMyId = 'verifyMyId',
selectCountry = 'selectCountry',
kycLoaded = 'kycLoaded',
kycCompleted = 'kycCompleted',
kycFailed = 'kycFailed',
kycFallbackLoaded = 'kycFallbackLoaded',
recoveryRequest = 'recoveryRequest',
noRecoveryPossible = 'noRecoveryPossible',
recoveryEmailSent = 'recoveryEmailSent',
recoveryEmailValidated = 'recoveryEmailValidated',
recoveryComplete = 'recoveryComplete',
createAccount = 'createAccount',
signIn = 'signIn',
signInFailed = 'signInFailed',
signInCompleted = 'signInCompleted',
forgotPassword = 'forgotPassword',
deposit = 'deposit',
ethOrFun = 'ethOrFun',
copyAddress = 'copyAddress',
coinSwap = 'coinSwap',
createdCoinSwap = 'createdCoinSwap',
withdrawal = 'withdrawal',
withdrawalMax = 'withdrawalMax',
withdrawalAdvanced = 'widhtrawalAdvanced',
withdrawalChangeGasPrice = 'withdrawalChangeGasPrice',
withdrawalClick = 'withdrawalClick',
withdrawalCompleted = 'withdrawalCompleted',
withdrawalFailed = 'withdrawalFailed',
withdrawalCancelled = 'withdrawalCancelled',
tokenTransfers = 'tokenTransfers',
tokenTransfersNext = 'tokenTransfersNext',
tokenTransfersPrevious = 'tokenTransfersPrevious',
transactions = 'transactions',
transactoinViewMoreInfo = 'transactoinViewMoreInfo',
transactionGoToEtherscan = 'transactionGoToEtherscan',
transactionSpeedUp = 'transactionSpeedUp',
transactionCancel = 'transactionCancel',
transactionsNext = 'transactionsNext',
transactionsPrevious = 'transactionsPrevious',
accountSettings = 'accountSettings',
networkChange = 'networkChange',
currencyChange = 'currencyChange',
signerChange = 'signerChange',
exportWallet = 'exportWallet',
accountSecurity = 'accountSecurity',
changeEmail = 'changeEmail',
changeEmailcompleted = 'changeEmailcompleted',
changePassword = 'changePassword',
changePasswordCompleted = 'changePasswordCompleted',
secondaryEmail = 'secondaryEmail',
secondaryEmailCompleted = 'secondaryEmailCompleted',
secondaryEmailDeleted = 'secondaryEmailDeleted',
twoFactorAuthentication = 'twoFactorAuthentication',
twoFactorAuthenticationEnabled = 'twoFactorAuthenticationEnabled',
twoFactorAuthenticationDisabled = 'twoFactorAuthenticationDisabled',
deviceActivity = 'deviceActivity',
deviceDeleted = 'deviceDeleted',
thirdPartySigners = 'thirdPartySigners',
thirdPartyAddSigner = 'thirdPartyAddSigner',
thirdPartyAddSignerCompleted = 'thirdPartyAddSignerCompleted',
thirdPartyAddSignerFailed = 'thirdPartyAddSignerFailed',
responsibleGaming = 'responsibleGaming',
takeABreak = 'takeABreak',
takeABreakClick = 'takeABreakClick',
takeABreakCompleted = 'takeABreakCompleted',
takeABreakCancelled = 'takeABreakCancelled',
extendBreak = 'extendBreak',
extendBreakClick = 'extendBreakClick',
extendBreakCompleted = 'extendBreakCompleted',
extendBreakFailed = 'extendBreakFailed',
extendBreakCancelled = 'extendBreakCancelled',
selfExcluse = 'selfExcluse',
selfExcluseCompleted = 'selfExcluseCompleted',
selfExcluseCancelled = 'selfExcluseCancelled',
reactivateAccountModal = 'reactivateAccountModal',
reactivateAccount = 'reactivateAccount',
approveTransaction = 'approveTransaction',
approveTransactionAdvanced = 'approveTransactionAdvanced',
approveTransactionApproved = 'approveTransactionApproved',
approveTransactionRejected = 'approveTransactionRejected',
approveSigningText = 'approveSigningText',
approveSigningTextApproved = 'approveSigningTextApproved',
approveSigningTextRejected = 'approveSigningTextRejected',
}
# authenticationPopUpClosed
This emits an event when the authentication popup is closed. If isAuthenticated
in the response is true, it means the close happened after a successful login; if it's false it means the user didn't go through the whole login process (for example, if they closed the popup).
result.data
returns:
{
isAuthenticated: boolean;
}
# Cancelling event listeners
This works the same as once
:
Now if you try to listen to that message listener again it will work, as you have cancelled the other listener. On the once
calls, once the once
has been fired you will be able to register a new listener without an error being thrown.