No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

widget_ui.js 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.WIDGET_UI = exports.PROVIDERS_LIST_PLACEHOLDER = void 0;
  4. const SCRIPT = `
  5. function assert(condition, error) {
  6. if (!condition) {
  7. if (!(error instanceof Error)) {
  8. error = new Error('Auth Emulator Internal Error: ' + error);
  9. }
  10. // Show error with great visibility AND stops further user interactions.
  11. document.body.textContent = error.stack || error.message;
  12. document.body.style = 'color:red;white-space:pre';
  13. throw error; // Halts current script and prints error to console.
  14. }
  15. }
  16. // TODO: Support older browsers where URLSearchParams is not available.
  17. var query = new URLSearchParams(location.search);
  18. var internalError = query.get('error');
  19. assert(!internalError, internalError);
  20. var apiKey = query.get('apiKey');
  21. var appName = query.get('appName');
  22. var authType = query.get('authType');
  23. var providerId = query.get('providerId');
  24. var redirectUrl = query.get('redirectUrl');
  25. var scopes = query.get('scopes');
  26. var eventId = query.get('eventId');
  27. var storageKey = apiKey + ':' + appName;
  28. var clientId = query.get('clientId');
  29. var firebaseAppId = query.get('appId');
  30. var apn = query.get('apn');
  31. var ibi = query.get('ibi');
  32. var appIdentifier = apn || ibi;
  33. var isSamlProvider = !!providerId.match(/^saml\./);
  34. assert(
  35. appName || clientId || firebaseAppId || appIdentifier,
  36. 'Missing one of appName / clientId / appId / apn / ibi query params.'
  37. );
  38. // Warn the developer of a few flows only available in Auth Emulator.
  39. if ((providerId === 'facebook.com' && appIdentifier) || (providerId === 'apple.com' && ibi)) {
  40. var providerName = (providerId === 'facebook.com') ? 'Facebook' : 'Apple';
  41. var productionMethod = providerName + (apn ? ' Android SDK' : ' iOS SDK');
  42. var warningEl = document.querySelector('.js-signin-warning');
  43. warningEl.querySelector('.content').textContent =
  44. 'Sign-in with ' + providerName + ' via generic IDP is only supported in the Auth Emulator; ' +
  45. 'remember to switch to ' + productionMethod + ' for production Firebase projects.';
  46. warningEl.style.display = 'flex';
  47. }
  48. function saveAuthEvent(authEvent) {
  49. if (/popup/i.test(authType)) {
  50. sendAuthEventViaIframeRelay(authEvent, function (err) {
  51. assert(!err, err);
  52. });
  53. } else {
  54. if (apn) {
  55. redirectToAndroid(authEvent);
  56. } else if (ibi) {
  57. redirectToIos(authEvent);
  58. } else if (redirectUrl) {
  59. saveAuthEventToStorage(authEvent);
  60. window.location = redirectUrl;
  61. } else {
  62. assert(false, 'This feature is not implemented in Auth Emulator yet. Please use signInWithCredential for now.');
  63. }
  64. }
  65. }
  66. function saveAuthEventToStorage(authEvent) {
  67. sessionStorage['firebase:redirectEvent:' + storageKey] =
  68. JSON.stringify(authEvent);
  69. }
  70. function sendAuthEventViaIframeRelay(authEvent, cb) {
  71. var sent = false;
  72. if (window.opener) {
  73. for (var i = 0; i < window.opener.frames.length; i++) {
  74. // Try/catch is necessary because without it, the code will crash the first time one of the frames does not have
  75. // the same origin (from the iframeWin.location.search) and the loop will not reach the other frames.
  76. try {
  77. var iframeWin = window.opener.frames[i];
  78. var query = new URLSearchParams(iframeWin.location.search);
  79. if (query.get('apiKey') === apiKey && query.get('appName') === appName) {
  80. iframeWin.postMessage({
  81. data: {authEvent: authEvent, storageKey: storageKey},
  82. eventId: Math.floor(Math.random() * Math.pow(10, 20)).toString(),
  83. eventType: "sendAuthEvent",
  84. }, '*');
  85. sent = true;
  86. }
  87. } catch (e) {
  88. // The frame does not have the same origin
  89. }
  90. }
  91. }
  92. if (!sent) {
  93. return cb('No matching frame');
  94. }
  95. return cb();
  96. }
  97. function redirectToAndroid(authEvent) {
  98. // This is shown when no app handles the link and displays an error.
  99. var fallbackUrl = window.location.href + '&error=App+not+found+for+intent';
  100. var link = 'intent://firebase.auth/#Intent;scheme=genericidp;' +
  101. 'package=' + apn + ';' +
  102. 'S.authType=' + authEvent.type + ';';
  103. if (authEvent.eventId) {
  104. link += 'S.eventId=' + authEvent.eventId + ';';
  105. }
  106. link += 'S.link=' + encodeURIComponent(authEvent.urlResponse) + ';';
  107. link += 'B.encryptionEnabled=false;';
  108. link += 'S.browser_fallback_url=' + encodeURIComponent(fallbackUrl) + ';';
  109. link += 'end;';
  110. window.location.replace(link);
  111. }
  112. function redirectToIos(authEvent) {
  113. // This URL format is based on production widget and known to work with the
  114. // iOS SDK. It does not matter that /__/auth/callback is not an actual page
  115. // served by the Auth Emulator -- only the format and query params matter.
  116. var url = window.location.protocol + '//' + window.location.host +
  117. '/__/auth/callback?authType=' + encodeURIComponent(authEvent.type) +
  118. '&link=' + encodeURIComponent(authEvent.urlResponse);
  119. if (authEvent.eventId) {
  120. url += '&eventId=' + authEvent.eventId;
  121. }
  122. var scheme;
  123. if (clientId) {
  124. scheme = clientId.split('.').reverse().join('.');
  125. } else if (firebaseAppId) {
  126. scheme = 'app-' + firebaseAppId.replace(/:/g, '-');
  127. } else {
  128. scheme = appIdentifier;
  129. }
  130. var deepLink = scheme + '://' +
  131. (clientId || firebaseAppId ? 'firebaseauth' : 'google') + '/link';
  132. deepLink += '?deep_link_id=' + encodeURIComponent(url);
  133. window.location.replace(deepLink);
  134. }
  135. // DOM logic
  136. var formattedProviderId = providerId[0].toUpperCase() + providerId.substring(1);
  137. document.querySelectorAll('.js-provider-id').forEach(function(e) {
  138. e.textContent = formattedProviderId;
  139. });
  140. var reuseAccountEls = document.querySelectorAll('.js-reuse-account');
  141. if (reuseAccountEls.length) {
  142. [].forEach.call(reuseAccountEls, function (el) {
  143. var urlEncodedIdToken = el.dataset.idToken;
  144. const decoded = JSON.parse(decodeURIComponent(urlEncodedIdToken));
  145. el.addEventListener('click', function (e) {
  146. e.preventDefault();
  147. finishWithUser(urlEncodedIdToken, decoded.email);
  148. });
  149. });
  150. } else {
  151. document.querySelector('.js-accounts-help-text').textContent = "No " + formattedProviderId + " accounts exist in the Auth Emulator.";
  152. }
  153. function finishWithUser(urlEncodedIdToken, email) {
  154. // Use widget URL, but replace all query parameters (no apiKey etc.).
  155. var url = window.location.href.split('?')[0];
  156. // Avoid URLSearchParams for browser compatibility.
  157. url += '?providerId=' + encodeURIComponent(providerId);
  158. url += '&id_token=' + urlEncodedIdToken;
  159. // Save reasonable defaults for SAML providers
  160. if (isSamlProvider) {
  161. url += '&SAMLResponse=' + encodeURIComponent(JSON.stringify({
  162. assertion: {
  163. subject: {
  164. nameId: email,
  165. },
  166. },
  167. }));
  168. }
  169. saveAuthEvent({
  170. type: authType,
  171. eventId: eventId,
  172. urlResponse: url,
  173. sessionId: "ValueNotUsedByAuthEmulator",
  174. postBody: "",
  175. tenantId: null,
  176. error: null,
  177. });
  178. }
  179. document.querySelector('.js-new-account').addEventListener('click', function (e) {
  180. e.preventDefault();
  181. toggleForm(true);
  182. });
  183. var inputs = document.querySelectorAll('.mdc-text-field');
  184. // Set up styling and reactivity for inputs
  185. inputs.forEach(function (input) {
  186. input.querySelector('input').addEventListener('input', function(e) {
  187. var display = 'none';
  188. if (!e.target.value) display = 'block';
  189. input.querySelector('.custom-label').style.display = display;
  190. validateForm();
  191. });
  192. window.mdc && mdc.textField.MDCTextField.attachTo(input);
  193. });
  194. document.getElementById('autogen-button').addEventListener('click', function() {
  195. runAutogen();
  196. });
  197. // Handle form validation and submission
  198. document.getElementById('main-form').addEventListener('submit', function(e) {
  199. e.preventDefault();
  200. var valid = validateForm();
  201. if (valid) {
  202. var email = document.getElementById('email-input').value;
  203. var displayName = document.getElementById('display-name-input').value;
  204. var screenName = document.getElementById('screen-name-input').value;
  205. var photoUrl = document.getElementById('profile-photo-input').value;
  206. var claims = {};
  207. if (email) claims.email = email;
  208. if (displayName) claims.displayName = displayName;
  209. if (screenName) claims.screenName = screenName;
  210. if (photoUrl) claims.photoUrl = photoUrl;
  211. finishWithUser(createFakeClaims(claims), claims.email);
  212. }
  213. });
  214. document.getElementById('back-button').addEventListener('click', function() {
  215. toggleForm(false);
  216. });
  217. function createFakeClaims(info) {
  218. return encodeURIComponent(JSON.stringify({
  219. sub: randomProviderRawId(),
  220. iss: "",
  221. aud: "",
  222. exp: 0,
  223. iat: 0,
  224. name: info.displayName,
  225. screen_name: info.screenName,
  226. email: info.email,
  227. email_verified: true, // TODO: Shall we allow changing this?
  228. picture: info.photoUrl,
  229. }));
  230. }
  231. function randomProviderRawId() {
  232. var str = '';
  233. for (var i = 0; i < 40; i++) {
  234. str += Math.floor(Math.random() * 10).toString();
  235. }
  236. return str;
  237. }
  238. // For now form validation only checks the email field.
  239. function validateForm() {
  240. var emailInput = document.getElementById('email-input');
  241. var valid = true;
  242. var value = emailInput.value;
  243. if (!value) {
  244. valid = false;
  245. emailErrorMessage('Email required');
  246. } else if (value.indexOf('@') < 0) {
  247. valid = false;
  248. emailErrorMessage('Missing "@"');
  249. } else {
  250. emailErrorMessage('');
  251. }
  252. document.querySelector('#sign-in').disabled = !valid;
  253. return valid;
  254. }
  255. // Generates random info for user creation
  256. function runAutogen() {
  257. var emailInput = document.getElementById('email-input');
  258. var displayInput = document.getElementById('display-name-input');
  259. var screenInput = document.getElementById('screen-name-input');
  260. var nameOptions = [
  261. 'raccoon',
  262. 'olive',
  263. 'orange',
  264. 'chicken',
  265. 'mountain',
  266. 'peach',
  267. 'panda',
  268. 'grass',
  269. 'algae',
  270. 'otter'
  271. ];
  272. var randomNumber = Math.floor(Math.random() * 1000);
  273. var givenName = nameOptions[Math.floor(Math.random() * nameOptions.length)];
  274. var familyName = nameOptions[Math.floor(Math.random() * nameOptions.length)];
  275. emailInput.value = givenName + '.' + familyName + '.' + randomNumber + '@example.com';
  276. displayInput.value = capitalize(givenName) + ' ' + capitalize(familyName);
  277. screenInput.value = familyName + '_' + givenName;
  278. emailInput.dispatchEvent(new Event('input'));
  279. displayInput.dispatchEvent(new Event('input'));
  280. screenInput.dispatchEvent(new Event('input'));
  281. }
  282. function emailErrorMessage(value) {
  283. document.getElementById('email-error').innerText = value;
  284. }
  285. function capitalize(a) {
  286. return a.charAt(0).toUpperCase() + a.slice(1);
  287. }
  288. function toggleForm(showForm) {
  289. document.getElementById('add-user').style.display =
  290. showForm ? 'block' : 'none';
  291. document.getElementById('accounts-list').style.display =
  292. showForm ? 'none' : 'block';
  293. }
  294. `;
  295. const STYLE = `
  296. :root {
  297. --mdc-theme-text-secondary-on-background: rgba(0,0,0,.56);
  298. }
  299. body {
  300. font-family: "Roboto", sans-serif;
  301. margin: 0;
  302. padding: 0;
  303. width: 100%;
  304. }
  305. p {
  306. margin-block-end: 0em;
  307. margin-block-start: 0em;
  308. }
  309. li {
  310. padding: 8px 16px;
  311. list-style-type: none;
  312. }
  313. ul {
  314. padding-inline-start: 0;
  315. }
  316. button {
  317. text-transform: none !important;
  318. letter-spacing: 0 !important;
  319. }
  320. #title {
  321. align-items: center;
  322. display: flex;
  323. flex-direction: row;
  324. font-size: 24px;
  325. font-weight: 500;
  326. margin-bottom: 16px;
  327. margin-top: 32px;
  328. }
  329. #title > span {
  330. flex: 1;
  331. }
  332. #title > button {
  333. color: #858585;
  334. }
  335. .subtitle {
  336. color: var(--mdc-theme-text-secondary-on-background);
  337. font-size: 14px;
  338. line-height: 20px;
  339. margin-block-end: 0em;
  340. margin-block-start: 0em;
  341. }
  342. #content {
  343. box-sizing: border-box;
  344. margin: 16px auto;
  345. max-width: 515px;
  346. min-width: 300px;
  347. }
  348. .content-wrapper, .mdc-list--avatar-list .mdc-list-item {
  349. padding: 0 24px;
  350. }
  351. .mdc-list .mdc-list-item__graphic {
  352. align-items: center;
  353. background-color: #c5c5c5;
  354. background-size: contain;
  355. border-radius: 50%;
  356. color: #fff;
  357. fill: currentColor;
  358. flex-shrink: 0;
  359. height: 36px;
  360. justify-content: center;
  361. margin-left: 0;
  362. margin-right: 16px;
  363. width: 36px;
  364. }
  365. #add-account-button {
  366. height: 56px !important;
  367. }
  368. .callout {
  369. background: #e5eaf0;
  370. color: #476282;
  371. display: flex;
  372. flex-direction: row;
  373. padding: 12px 24px;
  374. }
  375. .callout-warning {
  376. background: #fff3e0;
  377. color: #bf360c;
  378. }
  379. .callout .content {
  380. flex: 1;
  381. align-self: center;
  382. font-size: 14px;
  383. font-weight: 500;
  384. margin-left: 8px;
  385. }
  386. /* Vertical Spaced */
  387. .vs {
  388. margin-bottom: 16px;
  389. }
  390. .mdc-text-field {
  391. height: 40px !important;
  392. width: 100%;
  393. }
  394. .form-label {
  395. color: rgba(0,0,0,.54);
  396. display: block;
  397. font-size: 12px;
  398. margin: 0 0 4px 1px;
  399. }
  400. .custom-label {
  401. color: rgba(0,0,0,.38);
  402. display: inline-block;
  403. margin-left: 4px;
  404. transform: translateY(50%);
  405. }
  406. .error-info {
  407. color: #C62828;
  408. display: block;
  409. font-size: 12px;
  410. padding-top: 4px;
  411. }
  412. #main-action {
  413. display: flex;
  414. flex-direction: row;
  415. justify-content: space-between;
  416. margin-top: 15px;
  417. width: 100%;
  418. }
  419. #back-button {
  420. left: -8px;
  421. position: relative;
  422. }
  423. #add-user {
  424. display: none;
  425. }
  426. .fallback-secondary-text {
  427. color: var(--mdc-theme-text-secondary-on-background);
  428. }
  429. `;
  430. exports.PROVIDERS_LIST_PLACEHOLDER = "__PROVIDERS__";
  431. exports.WIDGET_UI = `
  432. <!DOCTYPE html>
  433. <meta charset="utf-8">
  434. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  435. <title>Auth Emulator IDP Login Widget</title>
  436. <link href="https://unpkg.com/material-components-web@10/dist/material-components-web.min.css" rel="stylesheet">
  437. <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
  438. <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
  439. <style>${STYLE}</style>
  440. <div id="content">
  441. <div class="content-wrapper">
  442. <div id="title">
  443. <span>Sign-in with <span class="js-provider-id provider-name">Provider</span></span>
  444. </div>
  445. </div>
  446. <div id="accounts-list">
  447. <div class="content-wrapper">
  448. <div class="callout callout-warning vs js-signin-warning" style="display:none">
  449. <i class="material-icons">error</i>
  450. <div class="content"></div>
  451. </div>
  452. <p class="subtitle js-accounts-help-text">Please select an existing account in the Auth Emulator or add a new one:</p>
  453. </div>
  454. <ul class="mdc-list list mdc-list--two-line mdc-list--avatar-list">
  455. ${exports.PROVIDERS_LIST_PLACEHOLDER}
  456. <li id="add-account-button" class="js-new-account mdc-list-item">
  457. <button class="mdc-button mdc-button--outlined">
  458. <div class="mdc-button__ripple"></div>
  459. <i class="material-icons mdc-button__icon" aria-hidden="true">add</i>
  460. <span class="mdc-button__label">Add new account</span>
  461. </button>
  462. </li>
  463. </ul>
  464. </div>
  465. <div id="add-user">
  466. <div class="content-wrapper" id="form-content">
  467. <div class="callout vs">
  468. <i class="material-icons">info</i>
  469. <div class="content">
  470. Custom claims can be added after an account is created
  471. </div>
  472. </div>
  473. <button id="autogen-button" class="vs mdc-button mdc-button--outlined" type="button">
  474. <div class="mdc-button__ripple"></div>
  475. <span class="mdc-button__label">Auto-generate user information</span>
  476. </button>
  477. <form id="main-form">
  478. <span class="form-label">Email</span>
  479. <label class="mdc-text-field mdc-text-field--outlined">
  480. <input id="email-input" type="text"
  481. class="mdc-text-field__input test" aria-labelledby="my-label-id">
  482. <span class="mdc-notched-outline">
  483. <span class="mdc-notched-outline__leading"></span>
  484. <span class="mdc-notched-outline__notch">
  485. <span class="custom-label" id="email-label">Email</span>
  486. </span>
  487. <span class="mdc-notched-outline__trailing"></span>
  488. </span>
  489. </label>
  490. <span class="error-info vs" id="email-error"></span>
  491. <span class="form-label">Display name (optional)</span>
  492. <label class="mdc-text-field mdc-text-field--outlined vs">
  493. <input id="display-name-input" type="text"
  494. class="mdc-text-field__input test" aria-labelledby="my-label-id">
  495. <span class="mdc-notched-outline">
  496. <span class="mdc-notched-outline__leading"></span>
  497. <span class="mdc-notched-outline__notch">
  498. <span class="custom-label" id="email-label">Display name</span>
  499. </span>
  500. <span class="mdc-notched-outline__trailing"></span>
  501. </span>
  502. </label>
  503. <span class="form-label">Screen name (optional)</span>
  504. <label class="mdc-text-field mdc-text-field--outlined vs">
  505. <input id="screen-name-input" type="text"
  506. class="mdc-text-field__input test" aria-labelledby="my-label-id">
  507. <span class="mdc-notched-outline">
  508. <span class="mdc-notched-outline__leading"></span>
  509. <span class="mdc-notched-outline__notch">
  510. <span class="custom-label" id="email-label">Screen name</span>
  511. </span>
  512. <span class="mdc-notched-outline__trailing"></span>
  513. </span>
  514. </label>
  515. <span class="form-label">Profile photo URL (optional)</span>
  516. <label class="mdc-text-field mdc-text-field--outlined vs">
  517. <input id="profile-photo-input" type="text"
  518. class="mdc-text-field__input test" aria-labelledby="my-label-id">
  519. <span class="mdc-notched-outline">
  520. <span class="mdc-notched-outline__leading"></span>
  521. <span class="mdc-notched-outline__notch">
  522. <span class="custom-label" id="email-label">Profile photo URL</span>
  523. </span>
  524. <span class="mdc-notched-outline__trailing"></span>
  525. </span>
  526. </label>
  527. <div id="main-action" class="vs">
  528. <button class="mdc-button" id="back-button" type="button">
  529. <div class="mdc-button__ripple"></div>
  530. <i class="material-icons mdc-button__icon" aria-hidden="true"
  531. >arrow_back</i
  532. >
  533. <span class="mdc-button__label">Back</span>
  534. </button>
  535. <button class="mdc-button mdc-button--raised" id="sign-in" type="submit">
  536. <span class="mdc-button__label">
  537. Sign in with <span class="js-provider-id provider-name">Provider</span>
  538. </span>
  539. </button>
  540. </div>
  541. </form>
  542. </div>
  543. </div>
  544. </div>
  545. <script src="https://unpkg.com/material-components-web@10/dist/material-components-web.min.js"></script>
  546. <script>${SCRIPT}</script>
  547. `;