index.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import Bowser from 'bowser';
  2. const ONBOARDING_STATE = {
  3. INSTALLED: 'INSTALLED' as const,
  4. NOT_INSTALLED: 'NOT_INSTALLED' as const,
  5. REGISTERED: 'REGISTERED' as const,
  6. REGISTERING: 'REGISTERING' as const,
  7. RELOADING: 'RELOADING' as const,
  8. };
  9. const EXTENSION_DOWNLOAD_URL = {
  10. CHROME:
  11. 'https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn',
  12. FIREFOX: 'https://addons.mozilla.org/firefox/addon/ether-metamask/',
  13. DEFAULT: 'https://metamask.io',
  14. };
  15. // sessionStorage key
  16. const REGISTRATION_IN_PROGRESS = 'REGISTRATION_IN_PROGRESS';
  17. // forwarder iframe id
  18. const FORWARDER_ID = 'FORWARDER_ID';
  19. export default class Onboarding {
  20. static FORWARDER_MODE = {
  21. INJECT: 'INJECT' as const,
  22. OPEN_TAB: 'OPEN_TAB' as const,
  23. };
  24. private readonly forwarderOrigin: string;
  25. private readonly downloadUrl: string;
  26. private readonly forwarderMode: keyof typeof Onboarding.FORWARDER_MODE;
  27. private state: keyof typeof ONBOARDING_STATE;
  28. constructor({
  29. forwarderOrigin = 'https://fwd.metamask.io',
  30. forwarderMode = Onboarding.FORWARDER_MODE.INJECT,
  31. } = {}) {
  32. this.forwarderOrigin = forwarderOrigin;
  33. this.forwarderMode = forwarderMode;
  34. this.state = Onboarding.isMetaMaskInstalled()
  35. ? ONBOARDING_STATE.INSTALLED
  36. : ONBOARDING_STATE.NOT_INSTALLED;
  37. const browser = Onboarding._detectBrowser();
  38. if (browser) {
  39. this.downloadUrl = EXTENSION_DOWNLOAD_URL[browser];
  40. } else {
  41. this.downloadUrl = EXTENSION_DOWNLOAD_URL.DEFAULT;
  42. }
  43. this._onMessage = this._onMessage.bind(this);
  44. this._onMessageFromForwarder = this._onMessageFromForwarder.bind(this);
  45. this._openForwarder = this._openForwarder.bind(this);
  46. this._openDownloadPage = this._openDownloadPage.bind(this);
  47. this.startOnboarding = this.startOnboarding.bind(this);
  48. this.stopOnboarding = this.stopOnboarding.bind(this);
  49. window.addEventListener('message', this._onMessage);
  50. if (
  51. forwarderMode === Onboarding.FORWARDER_MODE.INJECT &&
  52. sessionStorage.getItem(REGISTRATION_IN_PROGRESS) === 'true'
  53. ) {
  54. Onboarding._injectForwarder(this.forwarderOrigin);
  55. }
  56. }
  57. _onMessage(event: MessageEvent) {
  58. if (event.origin !== this.forwarderOrigin) {
  59. // Ignoring non-forwarder message
  60. return undefined;
  61. }
  62. if (event.data.type === 'metamask:reload') {
  63. return this._onMessageFromForwarder(event);
  64. }
  65. console.debug(
  66. `Unknown message from '${event.origin}' with data ${JSON.stringify(
  67. event.data,
  68. )}`,
  69. );
  70. return undefined;
  71. }
  72. _onMessageUnknownStateError(state: never): never {
  73. throw new Error(`Unknown state: '${state}'`);
  74. }
  75. async _onMessageFromForwarder(event: MessageEvent) {
  76. switch (this.state) {
  77. case ONBOARDING_STATE.RELOADING:
  78. console.debug('Ignoring message while reloading');
  79. break;
  80. case ONBOARDING_STATE.NOT_INSTALLED:
  81. console.debug('Reloading now to register with MetaMask');
  82. this.state = ONBOARDING_STATE.RELOADING;
  83. location.reload();
  84. break;
  85. case ONBOARDING_STATE.INSTALLED:
  86. console.debug('Registering with MetaMask');
  87. this.state = ONBOARDING_STATE.REGISTERING;
  88. await Onboarding._register();
  89. this.state = ONBOARDING_STATE.REGISTERED;
  90. (event.source as Window).postMessage(
  91. { type: 'metamask:registrationCompleted' },
  92. event.origin,
  93. );
  94. this.stopOnboarding();
  95. break;
  96. case ONBOARDING_STATE.REGISTERING:
  97. console.debug('Already registering - ignoring reload message');
  98. break;
  99. case ONBOARDING_STATE.REGISTERED:
  100. console.debug('Already registered - ignoring reload message');
  101. break;
  102. default:
  103. this._onMessageUnknownStateError(this.state);
  104. }
  105. }
  106. /**
  107. * Starts onboarding by opening the MetaMask download page and the Onboarding forwarder
  108. */
  109. startOnboarding() {
  110. sessionStorage.setItem(REGISTRATION_IN_PROGRESS, 'true');
  111. this._openDownloadPage();
  112. this._openForwarder();
  113. }
  114. /**
  115. * Stops onboarding registration, including removing the injected forwarder (if any)
  116. *
  117. * Typically this function is not necessary, but it can be useful for cases where
  118. * onboarding completes before the forwarder has registered.
  119. */
  120. stopOnboarding() {
  121. if (sessionStorage.getItem(REGISTRATION_IN_PROGRESS) === 'true') {
  122. if (this.forwarderMode === Onboarding.FORWARDER_MODE.INJECT) {
  123. console.debug('Removing forwarder');
  124. Onboarding._removeForwarder();
  125. }
  126. sessionStorage.setItem(REGISTRATION_IN_PROGRESS, 'false');
  127. }
  128. }
  129. _openForwarder() {
  130. if (this.forwarderMode === Onboarding.FORWARDER_MODE.OPEN_TAB) {
  131. window.open(this.forwarderOrigin, '_blank');
  132. } else {
  133. Onboarding._injectForwarder(this.forwarderOrigin);
  134. }
  135. }
  136. _openDownloadPage() {
  137. window.open(this.downloadUrl, '_blank');
  138. }
  139. /**
  140. * Checks whether the MetaMask extension is installed
  141. */
  142. static isMetaMaskInstalled() {
  143. return Boolean(
  144. (window as any).ethereum && (window as any).ethereum.isMetaMask,
  145. );
  146. }
  147. static _register() {
  148. return (window as any).ethereum.request({
  149. method: 'wallet_registerOnboarding',
  150. });
  151. }
  152. static _injectForwarder(forwarderOrigin: string) {
  153. const container = document.body;
  154. const iframe = document.createElement('iframe');
  155. iframe.setAttribute('height', '0');
  156. iframe.setAttribute('width', '0');
  157. iframe.setAttribute('style', 'display: none;');
  158. iframe.setAttribute('src', forwarderOrigin);
  159. iframe.setAttribute('id', FORWARDER_ID);
  160. container.insertBefore(iframe, container.children[0]);
  161. }
  162. static _removeForwarder() {
  163. document.getElementById(FORWARDER_ID)?.remove();
  164. }
  165. static _detectBrowser() {
  166. const browserInfo = Bowser.parse(window.navigator.userAgent);
  167. if (browserInfo.browser.name === 'Firefox') {
  168. return 'FIREFOX';
  169. } else if (
  170. ['Chrome', 'Chromium'].includes(browserInfo.browser.name || '')
  171. ) {
  172. return 'CHROME';
  173. }
  174. return null;
  175. }
  176. }