1/* global ga */ 2 3/** 4 * @param {string} key 5 * @param {string} [defaultValue] 6 * @param {string|null} 7 */ 8function getQueryParam(key, defaultValue) { 9 if (typeof URLSearchParams === "undefined") return null; 10 11 var q = new URLSearchParams(location.search); 12 var value = q.get(key); 13 14 return value == null ? (defaultValue == null ? null : defaultValue.toLocaleLowerCase()) : value.toLocaleLowerCase(); 15} 16 17//#region Google one tap login 18window.onGoogleLibraryLoad = () => { 19 if (!isMobile.any) { 20 google.accounts.id.initialize({ 21 client_id: Auth().GOOGLE_CLIENT_ID, 22 prompt_parent_id: "google-one-tap-login-container", 23 callback: handleGoogleLoginSuccess, 24 cancel_on_tap_outside: false, 25 use_fedcm_for_prompt: true, 26 }); 27 google.accounts.id.prompt(); 28 } 29}; 30 31function handleGoogleLoginSuccess(event) { 32 Auth() 33 .loginGoogle(event.credential) 34 .then((token) => { 35 Auth().redirectToDesktop(token); 36 }); 37} 38//#endregion 39 40/** 41 * @typedef {Object} IsMobile 42 * @property {boolean} any 43 * @property {{ phone: boolean, tablet: boolean, device: boolean }} android 44 * @property {{ phone: boolean, ipod: boolean, tablet: boolean, device: boolean }} apple 45 */ 46 47// general functionality 48 49jQuery(function ($) { 50 "use strict"; 51 52 /** 53 * Detect mobile 54 */ 55 56 if (isMobile.apple.device) { 57 document.documentElement.classList.add("ios-device"); 58 } 59 if (isMobile.android.device) { 60 document.documentElement.classList.add("android-device"); 61 } 62 63 (function shoHideGetStarted() { 64 // get started button show/hide 65 66 if ($("#get-started-btn").length > 0) { 67 $("#get-started-btn") 68 .scrollSpy({ 69 offsetTop: $(".site-header").outerHeight(), 70 offsetBottom: 0, 71 }) 72 .on("scrollSpy:enter", function () { 73 $("body").addClass("hide-get-started"); 74 }) 75 .on("scrollSpy:exit", function () { 76 $("body").removeClass("hide-get-started"); 77 }); 78 } 79 })(); 80 81 //simple tabs 82 (function simpleTabs() { 83 $(".tabs .tabs-nav").on("click", "li:not(.active)", function (e) { 84 e.preventDefault(); 85 var $this = $(this); 86 $this.addClass("active").siblings().removeClass("active"); 87 var $pane = $this 88 .closest(".tabs") 89 .find(".tabs-pane") 90 .hide() 91 .removeClass("active") 92 .eq($this.index()) 93 .addClass("active"); 94 $pane.fadeIn(150); 95 }); 96 })(); 97 98 //scroll to element function 99 $.fn.scrollView = function () { 100 var siteHeader = ".fixed-header", 101 offset = 0, 102 headerHeight = 0; 103 104 if ($(siteHeader).length > 0) { 105 headerHeight = $(siteHeader).outerHeight(); 106 } 107 offset = offset + headerHeight; 108 return this.each(function () { 109 $("html, body").animate( 110 { 111 scrollTop: $(this).offset().top - offset, 112 }, 113 600, 114 ); 115 }); 116 }; 117 118 // scroll to element via data scroll-to 119 function scrollToElement(e) { 120 e.preventDefault(); 121 var $this = $(this), 122 target = $this.data("scroll-to"); 123 $(target).scrollView(); 124 } 125 126 $("body").on("click", "[data-scroll-to]", scrollToElement); 127 128 // add classes to body according to page scroll 129 function handleScroll() { 130 $("body").toggleClass("page-top", $(window).scrollTop() == 0); 131 $("body").toggleClass( 132 "show-back-to-top", 133 $(window).scrollTop() / ($(document).height() - $(window).height()) > 0.4, 134 ); 135 } 136 $(window).on("load scroll touchmove", function () { 137 handleScroll(); 138 }); 139 140 if ($().scrollSpy) { 141 var $sidebox = $(".sticky-side-box-container"); 142 143 $(".sticky-sidebox-top-trigger-show") 144 .scrollSpy({ 145 offsetTop: 0, 146 offsetBottom: 0, 147 }) 148 .on("scrollSpy:enter", function () { 149 $sidebox.addClass("show"); 150 }) 151 .on("scrollSpy:exit", function () { 152 $sidebox.removeClass("show"); 153 }); 154 155 $(".sticky-sidebox-top-trigger-hide") 156 .scrollSpy({ 157 offsetTop: 0, 158 offsetBottom: 0, 159 }) 160 .on("scrollSpy:enter", function () { 161 $sidebox.removeClass("show"); 162 }); 163 164 $(".site-footer") 165 .scrollSpy({ 166 offsetTop: 0, 167 offsetBottom: 0, 168 }) 169 .on("scrollSpy:enter", function () { 170 $sidebox.removeClass("show"); 171 }) 172 .on("scrollSpy:exit", function () { 173 $sidebox.addClass("show"); 174 }); 175 } 176 177 $(".sticky-side-box-container").on("click", ".sticky-side-box-close", function () { 178 $(".sticky-side-box-container").addClass("close"); 179 }); 180 181 //Main nav 182 183 $(".mobile-nav-btn").on("click", function () { 184 $("body").toggleClass("open-nav").removeClass("open-user-nav open-languages"); 185 $(".language-selector .languages-list").slideUp(); 186 $(".main-nav .site-menu").slideToggle(); 187 }); 188 189 $(".language-selector").on("click", ".current-language", function () { 190 $("body").toggleClass("open-languages").removeClass("open-user-nav open-nav"); 191 $(".main-nav .site-menu").slideUp(); 192 $(".language-selector .languages-list").slideToggle(); 193 }); 194 195 var $mainNav = $(".main-nav"); 196 197 $mainNav.on("click", ".subdrop-arrow", function (e) { 198 e.preventDefault(); 199 var $subdropContainer = $(this).closest(".subdrop-container"); 200 $subdropContainer.siblings(".subdrop-container").find(".sub-dropdown").slide
200Up(); 201 $subdropContainer.toggleClass("open-dropdown").find(".sub-dropdown").slideToggle(); 202 }); 203 $mainNav.on("click", ".drop-arrow, .dummy-link", function (e) { 204 e.preventDefault(); 205 var $dropdownContainer = $(this).closest("li"); 206 $dropdownContainer.siblings("li").find(".sub-dropdown").slideUp(); 207 $dropdownContainer.toggleClass("open-dropdown").find(".dropdown").slideToggle(); 208 }); 209 210 //premium plan selectors 211 function updatePlanSelector(plan) { 212 $("#premium-plan-selector").find(".payment-plan").removeClass("-selected").eq(plan).addClass("-selected"); 213 } 214 215 (function planSelector() { 216 $("#premium-plan-selector").on("click", ".payment-plan", function () { 217 var $this = $(this), 218 plan = $this.data("plan"); 219 updatePlanSelector(plan); 220 }); 221 222 $("#premium-plans-list") 223 .on("click", ".button", function () { 224 var plan = $(this).data("plan"); 225 updatePlanSelector(plan); 226 }) 227 .on("click", ".payment-plan", function () { 228 var $this = $(this), 229 plan = $this.data("plan"); 230 $this.closest(".plan-selector").find(".payment-plan").removeClass("-selected").eq(plan).addClass("-selected"); 231 updatePlanSelector(plan); 232 }); 233 })(); 234 235 // sideboxregister-form 236 237 $(".sideboxregister-form").on("submit", function (e) { 238 e.preventDefault(); 239 var email = $(this).find("input[type='text']").val(); 240 console.log(email); 241 location.href = "https://app.any.do/?website_registration_flow=team&website_registration_email=" + email; 242 }); 243 244 if ($().slick) { 245 $(".teams-slider").slick({ 246 autoplay: true, 247 slidesToShow: 1, 248 slidesToScroll: 1, 249 swipeToSlide: true, 250 dots: true, 251 arrows: false, 252 infinite: true, 253 focusOnSelect: false, 254 variableWidth: false, 255 adaptiveHeight: false, 256 }); 257 258 $(".quotes-slider").slick({ 259 autoplay: true, 260 autoplaySpeed: 500000, 261 slidesToShow: 1, 262 slidesToScroll: 1, 263 swipeToSlide: true, 264 dots: false, 265 arrows: true, 266 infinite: true, 267 focusOnSelect: false, 268 variableWidth: false, 269 adaptiveHeight: false, 270 }); 271 272 $(".marketing-carousel").slick({ 273 autoplay: true, 274 autoplaySpeed: 5000, 275 slidesToShow: 5, 276 slidesToScroll: 5, 277 swipeToSlide: true, 278 dots: false, 279 arrows: true, 280 infinite: true, 281 focusOnSelect: false, 282 variableWidth: false, 283 adaptiveHeight: false, 284 responsive: [ 285 { 286 breakpoint: 1920, 287 settings: { 288 slidesToShow: 4, 289 slidesToScroll: 4, 290 }, 291 }, 292 { 293 breakpoint: 1700, 294 settings: { 295 slidesToShow: 3, 296 slidesToScroll: 3, 297 }, 298 }, 299 { 300 breakpoint: 1025, 301 settings: { 302 slidesToShow: 1, 303 slidesToScroll: 1, 304 centerMode: true, 305 centerPadding: "25%", 306 }, 307 }, 308 { 309 breakpoint: 768, 310 settings: { 311 slidesToShow: 1, 312 slidesToScroll: 1, 313 }, 314 }, 315 ], 316 }); 317 318 $(".live-feed-slider").slick({ 319 autoplay: true, 320 vertical: true, 321 pauseOnFocus: false, 322 autoplaySpeed: 3000, 323 slidesToShow: 3, 324 slidesToScroll: 1, 325 swipe: false, 326 swipeToSlide: false, 327 dots: false, 328 arrows: false, 329 infinite: true, 330 focusOnSelect: false, 331 variableWidth: false, 332 adaptiveHeight: true, 333 }); 334 } //end if slick 335 336 (function playTeamsVideos() { 337 // trigger play on teams page videos on page scroll 338 339 if ($(".block-video-container").length > 0) { 340 $(".block-video-container") 341 .scrollSpy({ 342 offsetTop: $(".site-header").outerHeight(), 343 offsetBottom: 0, 344 }) 345 .on("scrollSpy:enter", function () { 346 var $video = $(this).find("video"); 347 $video.prop("loop"); 348 $video.get(0).play(); 349 }); 350 } 351 })(); 352 353 (function teamsGetStarted() { 354 // trigger play on teams page videos on page scroll 355 356 if ($(".teams-get-started-button").length > 0) { 357 $(".teams-get-started-button").on("click", function () { 358 var $teamsForm = $("#teams-signup-form"); 359 $teamsForm.scrollView($(".site-header").outerHeight() + 30); 360 $teamsForm.find("input").focus(); 361 }); 362 } 363 })(); 364 365 // faq 366 367 $(".faq").on("click", ".faq-title", function () { 368 $(this).toggleClass("open").next(".faq-description").slideToggle(); 369 }); 370 371 // contact sales form submit 372 373 $("#contact-sales-form").submit(function (e) { 374 e.preventDefault(); 375 376 const SERVER_DOMAIN = __DEV__ ? "sm-dev-teams.any.do" : "sm-prod4.any.do"; 377 378 var formData = { 379 firstname: $("#first-name").val(), 380 lastname: $("#last-name").val(), 381 email: $("#email").val(), 382 teamsize: $("#teamsize").val(), 383 }; 384 385 return new Promise(function (resolve, reject) { 386 grecaptcha.enterprise.ready(function () { 387 const siteKey = "6Le0784oAAAAAKlcpM4NH00ZAnGJSrra5Cd-RTQc"; 388 389 return grecaptcha.enterprise 390 .execute(siteKey, { action: "submit_contact_sales_form" }) 391 .then(function (token) { 392 formData["captcha_method"] = "google_recaptcha"; 393 formData["captcha_params"] = { 394 token, 395 site_key: siteKey, 396 action: "submit_contact_sales_form", 397 }; 398 resolve( 399 $.ajax({ 400 type: "POST", 401 url: "https://" + SERVER_DOMAIN + "/submit_contact_sales_form", 402 contentType: "application/json", 403 data: JSON.stringify(formData), 404 encode: true, 405 success: function (data) { 406 console.log(data); 407 $("#contacSalesModal").modal("toggle"); 408 }, 409 }).fail(function (jqxhr, textStatus, error) { 410 var err = textStatus + ", " + error; 411 console.log("Failed: " + err); 412 }), 413 ); 414 }) 415 .catch(reject); 416 }); 417 }); 418 }); 419 420 $.ajax({ 421 url: "https://sm-dev-teams.any.do/api/board_templates", 422 type: "GET", 423 //contentType: "application/json; charset=utf-8", 424 dataType: "json", 425 data: "PROJECT_MANAGMENT", 426 success: function (data) { 427 console.log(data); 428 }, 429 }).fail(function (jqxhr, textStatus, error) { 430 var err = textStatus + ", " + error; 431 console.log("Failed: " + err); 432 }); 433 434 /** 435 * Open todo tracking 436 */ 437 438 Array.prototype.slice.apply(document.querySelectorAll("[data-open-todo]")).forEac
438h( 439 /** @param {HTMLAnchorElement} $e */ 440 function ($e) { 441 var eventValue = Number($e.dataset.openTodo); 442 if (isNaN(eventValue)) return; 443 444 $e.addEventListener( 445 "click", 446 function onClick(event) { 447 event.preventDefault(); 448 $e.removeEventListener("click", onClick); 449 450 if (typeof ga !== "undefined") { 451 ga("send", { 452 hitType: "event", 453 eventCategory: "Open todo", 454 eventAction: "click", 455 eventLabel: $e.href, 456 eventValue: eventValue, 457 transport: "beacon", 458 }); 459 } 460 461 setTimeout(function () { 462 $e.click(); 463 }, 300); 464 }, 465 false, 466 ); 467 }, 468 ); 469 470 /** handle open todo click */ 471 Array.prototype.slice.apply(document.querySelectorAll(".js-open-todo")).forEach( 472 /** @param {HTMLAnchorElement} $e */ 473 function ($e) { 474 $e.addEventListener("click", function (e) { 475 e.preventDefault(); 476 Auth().redirectToDesktop(); 477 }); 478 }, 479 ); 480 481 /** handle open checkout click */ 482 Array.prototype.slice.apply(document.querySelectorAll(".js-open-checkout")).forEach( 483 /** @param {HTMLAnchorElement} $e */ 484 function ($e) { 485 $e.addEventListener("click", function (e) { 486 Auth().redirectToCheckout(); 487 }); 488 }, 489 ); 490 491 /** 492 * Sticky header 493 */ 494 /* 495 var $header = $("header.site-header"); 496 var $headerSticky = $header.clone(); 497 498 $headerSticky 499 .attr("tabindex", "-1") 500 .css({ 501 opacity: 0, 502 visibility: "hidden" 503 }) 504 .find("#mobile-nav-btn") 505 .removeAttr("id"); 506 507 $header.css({ 508 position: "fixed", 509 top: 0, 510 left: 0, 511 width: "100%", 512 background: "white" 513 }); 514 515 $headerSticky.insertAfter($header); 516 517*/ 518 519 //#region Teams 520 function simpleTabs() { 521 $(".js-tab-underline").css("width", $(".js-active-tab .js-tab-link").outerWidth()); 522 523 $(".tabs .tabs-nav").on("click", "li:not(.js-active-tab)", function (e) { 524 e.preventDefault(); 525 var $this = $(this), 526 position = $this.position(), 527 newIndex = $this.index(), 528 oldIndex = $this.closest(".tabs-nav").find(".js-active-tab").index(), 529 animateClass = newIndex < oldIndex ? "animate-from-right" : "animate-from-left"; 530 531 $(".js-tab-underline").css({ left: position.left, width: $(this).outerWidth() }); 532 533 $this.addClass("js-active-tab").siblings().removeClass("js-active-tab"); 534 var $pane = $this 535 .closest(".tabs") 536 .find(".tabs-pane") 537 .removeClass("js-active-pane animate-from-left animate-from-right") 538 .eq($this.index() - 1) 539 .addClass(animateClass); 540 541 setTimeout(function () { 542 $pane.addClass("js-active-pane"); 543 }, 10); 544 }); 545 } 546 547 /** 548 * @param {Array.<string>} keys 549 * @returns {*} 550 */ 551 function createEnum(keys) { 552 var ENUM = {}; 553 554 for (var i = 0; i < keys.length; i++) { 555 var key = keys[i]; 556 var value = i + 1; 557 558 ENUM[key] = value; 559 ENUM[value] = key; 560 } 561 562 return Object.freeze(ENUM); 563 } 564 565 /** 566 * @param {HTMLDivElement} $container 567 */ 568 function initTeamForm($container) { 569 var STATE = createEnum([ 570 // 571 "LOCK", 572 "CHECK_EMAIL", 573 "CHECK_EMAIL_LOADING", 574 "JOIN", 575 "JOIN_LOADING", 576 "LOGIN", 577 "LOGIN_LOADING", 578 ]); 579 580 var auth = Auth(); 581 /** @type {HTMLInputElement} */ 582 var $tfEmail = $container.querySelector("input[name='email']"); 583 /** @type {HTMLInputElement} */ 584 var $tfPasswordLogin = $container.querySelector("input[name='password_login']"); 585 /** @type {HTMLInputElement} */ 586 var $tfPasswordJoin = $container.querySelector("input[name='password_join']"); 587 /** @type {HTMLButtonElement} */ 588 var $emailButton = $container.querySelector(".button-spinner"); 589 /** @type {HTMLFormElement} */ 590 var $loginForm = $container.querySelector(".teams-register-pass"); 591 /** @type {HTMLFormElement} */ 592 var $joinForm = $container.querySelector(".teams-create-account"); 593 594 var state = STATE.CHECK_EMAIL; 595 596 Array.prototype.slice.apply($container.querySelectorAll("form")).forEac
596h(function ($form) { 597 $form.addEventListener("submit", function (event) { 598 event.preventDefault(); 599 render(); 600 }); 601 }); 602 603 render(); 604 605 function render() { 606 var email = $tfEmail.value; 607 var passwordLogin = $tfPasswordLogin.value; 608 var passwordJoin = $tfPasswordJoin.value; 609 610 switch (state) { 611 case STATE.LOCK: 612 break; 613 614 case STATE.CHECK_EMAIL: 615 state = STATE.CHECK_EMAIL_LOADING; 616 $tfEmail.removeAttribute("disabled"); 617 $emailButton.classList.remove("running"); 618 $emailButton.removeAttribute("disabled"); 619 break; 620 621 case STATE.CHECK_EMAIL_LOADING: 622 state = STATE.LOCK; 623 $tfEmail.setAttribute("disabled", "disabled"); 624 $emailButton.classList.add("running"); 625 $emailButton.setAttribute("disabled", "disabled"); 626 auth.checkEmail(email).then( 627 function (isExisted) { 628 var $el = isExisted ? $loginForm : $joinForm; 629 630 if (!isMobile.any) $($el).slideDown(); 631 632 setTimeout(function () { 633 state = isExisted ? STATE.LOGIN : STATE.JOIN; 634 635 $($el).show(); 636 $emailButton.classList.add("form-success"); 637 $el.querySelector("input").focus(); 638 639 render(); 640 }, 300); 641 }, 642 function () { 643 alert("There is a connectivity issue. Please check if you're network"); 644 }, 645 ); 646 break; 647 648 case STATE.JOIN: 649 state = STATE.JOIN_LOADING; 650 break; 651 652 case STATE.LOGIN: 653 state = STATE.LOGIN_LOADING; 654 break; 655 656 case STATE.JOIN_LOADING: 657 state = STATE.LOCK; 658 $joinForm.querySelector("input").readOnly = true; 659 $joinForm.querySelector("button").classList.add("running"); 660 661 auth 662 .join(email, getUserNameFromEmail(email), passwordJoin) 663 .then(function (token) { 664 auth.redirectToTeams(token); 665 }) 666 .catch(function (error) { 667 state = STATE.JOIN; 668 // eslint-disable-next-line no-console 669 console.error(error); 670 alert("Something went wrong"); 671 672 $joinForm.querySelector("input").readOnly = false; 673 $joinForm.querySelector("button").classList.remove("running"); 674 }); 675 break; 676 677 case STATE.LOGIN_LOADING: 678 state = STATE.LOCK; 679 $loginForm.querySelector("input").readOnly = true; 680 $loginForm.querySelector("button").classList.add("running"); 681 682 auth 683 .login(email, passwordLogin) 684 .then(function (token) { 685 auth.redirectToTeams(token); 686 }) 687 .catch(function (error) { 688 state = STATE.LOGIN; 689 // eslint-disable-next-line no-console 690 console.error(error); 691 alert("Password is incorrect"); 692 693 $loginForm.querySelector("input").readOnly = false; 694 $loginForm.querySelector("button").classList.remove("running"); 695 }); 696 break; 697 } 698 } 699 700 /** 701 * @param {string} email 702 */ 703 function getUserNameFromEmail(email) { 704 var name = email.split("@")[0]; 705 706 return name 707 .replace(/[+-.]/g, " ") 708 .toLowerCase() 709 .replace(/\b([a-z])/g, (m) => m.toUpperCase()); 710 } 711 } 712 713 function initTeamsForm() { 714 var formsContainers = Array.prototype.slice.apply(document.querySelectorAll(".js-teams")); 715 716 formsContainers.forEach(initTeamForm); 717 } 718 719 $(function () { 720 if (document.querySelector(".js-teams")) { 721 initTeamsForm(); 722 } 723 }); 724 simpleTabs(); 725 726 //#endregion 727 728 // #region assistant-lp 729 function initAssistantLp($container) { 730 var stripeHelper = new StripeHelper(); 731 stripeHelper.initStripeSingleInput(stripeHelper.ASSISTANT_STRIPE_KEY); 732 var assistantSubscriptionHelper = new AssistantSubscriptionHelper(); 733 734 var $paymentForm = $container.querySelector("#assistant-form"); 735 var $errorContainer = $container.querySelector(".error"); 736 var $paymentSubmit = $paymentForm.querySelector(".button.submit"); 737 738 // set up show + hide of checkout popup
739 [...$container.querySelectorAll(".js-cta")].forEach((e) => { 740 e.addEventListener("click", showCheckout); 741 }); 742 743 [...$container.querySelectorAll(".js-hide-checkout")].forEach((e) => { 744 e.addEventListener("click", hideCheckout); 745 }); 746 747 // switch between sign in / sign up in checkout 748 $container.querySelector(".js-switch-to-signup").addEventListener("click", switchToSignUp); 749 $container.querySelector(".js-switch-to-signin").addEventListener("click", switchToSignIn); 750 751 // plan selection radio button UI set up 752 $(".js-plan-selector").click(() => { 753 $(".js-plan-selector").each((i, e) => { 754 if (e.checked) { 755 e.closest(".assistant-plan-option").classList.add("selected"); 756 } else { 757 e.closest(".assistant-plan-option").classList.remove("selected"); 758 } 759 }); 760 }); 761 762 // checkout submit 763 $paymentForm.addEventListener("submit", function (event) { 764 event.preventDefault(); 765 766 enterButtonLoading($paymentSubmit); 767 768 var isSignUpMode = $paymentForm.classList.contains("signup"); 769 770 var formData = new FormData($paymentForm); 771 var email = formData.get("email"); 772 var password = formData.get("password"); 773 var selectedPlan = formData.get("assistant-plan"); 774 775 Auth() 776 .checkEmail(email) 777 .then(function (doesUserExist) { 778 if (isAuthStateMismatch(doesUserExist, isSignUpMode, $errorContainer)) { 779 exitButtonLoading($paymentSubmit); 780 return; 781 } 782 stripeHelper.stripe.createToken(stripeHelper.card).then(function (stripeResponse) { 783 if (stripeResponse.error || !stripeResponse.token) { 784 $errorContainer.textContent = stripeResponse.error && stripeResponse.error.message; 785 exitButtonLoading($paymentSubmit); 786 return; 787 } 788 789 var authPromise = isSignUpMode 790 ? Auth().join( 791 email, 792 formData.get("name"), 793 password, 794 assistantSubscriptionHelper.ASSISTANT_APPLICATION_NAME, 795 ) 796 : Auth().login(email, password); 797 798 authPromise.then(subscribeToAssistant.bind(null, stripeResponse.token.id)).catch(handleSignUpError); 799 }); 800 }); 801 802 function subscribeToAssistant(stripeToken) { 803 assistantSubscriptionHelper 804 .createCustomer(stripeToken) 805 .then(() => { 806 assistantSubscriptionHelper 807 .subscribe(selectedPlan) 808 .then(redirectToAssistantForm) 809 .catch(handleAssistantSubscribeError); 810 }) 811 .catch(handleAssistantSubscribeError); 812 } 813 814 function handleAssistantSubscribeError(errorMessage) { 815 console.error(errorMessage); 816 exitButtonLoading($paymentSubmit); 817 switchToSignIn(); 818 $errorContainer.textContent = errorMessage || "An error has occurred"; 819 } 820 821 function redirectToAssistantForm() { 822 location.href = "https://anydo.typeform.com/to/dZBHwSX5#puid=" + encodeURIComponent(Auth().getUserGlobalId()); 823 } 824 }); 825 826 function switchToSignUp() { 827 $paymentForm.classList.add("signup"); 828 $paymentForm.classList.remove("signin"); 829 $errorContainer.textContent = ""; 830 } 831 832 function switchToSignIn() { 833 $paymentForm.classList.add("signin"); 834 $paymentForm.classList.remove("signup"); 835 $errorContainer.textContent = ""; 836 } 837 838 function hideCheckout() { 839 $container.querySelector("#checkout").classList.add("hidden"); 840 $container.querySelector("#overlay").classList.add("hidden"); 841 $container.querySelector("#info").classList.remove("hide-on-mobile"); 842 } 843 844 function showCheckout() { 845 $container.querySelector("#checkout").classList.remove("hidden"); 846 $container.querySelector("#overlay").classList.remove("hidden"); 847 $container.querySelector("#info").classList.add("hide-on-mobile"); 848 } 849 } 850 851 $(function () { 852 var $formContainer = document.querySelector(".js-assistant-lp"); 853 if ($formContainer) { 854 initAssistantLp($formContainer); 855 } 856 }); 857 858 function AssistantSubscriptionHelper() { 859 this.ASSISTANT_APPLICATION_NAME = "assistant"; 860 this._SERVER_DOMAIN = __DEV__ ? "sm-dev2.any.do" : "sm-prod2.any.do"; 861 this._ASSISTANT_CUSTOMER_URI = "https://" + this._SERVER_DOMAIN + "/me/assistant/create_customer"; 862 this._ASSISTANT_SUB_URI = "https://" + this._SERVER_DOMAIN + "/me/assistant/packages/subscribe"; 863 } 864 865 AssistantSubscriptionHelper.prototype.createCustomer = function createCustomer(stripeToken) { 866 return fetch(this._ASSISTANT_CUSTOMER_URI, { 867 method: "POST", 868 headers: Auth().createHeaders(true),
869 body: JSON.stringify({ 870 token: stripeToken, 871 sendEmail: true, 872 }), 873 }).then(function (response) { 874 return Promise.resolve(response); 875 }); 876 }; 877 878 AssistantSubscriptionHelper.prototype.subscribe = function subscribe(stripePlan) { 879 return fetch(this._ASSISTANT_SUB_URI, { 880 method: "POST", 881 headers: Auth().createHeaders(true), 882 body: JSON.stringify({ 883 plan: stripePlan, 884 }), 885 }).then(function (response) { 886 return response.ok 887 ? Promise.resolve() 888 : Promise.reject( 889 new Error(`AssistantSubscriptionHelper.subscribe(): Any.do server status "${response.status}"`), 890 ); 891 }); 892 }; 893 894 // #endregion assistant-lp 895 896 // #region checkout 897 function handleCheckoutSubmit(event, stripeHelper, subscribeFunction, login_app_name) { 898 event.preventDefault(); 899 900 var $paymentForm = event.target; 901 var $paymentSubmit = $paymentForm.querySelector(".button.submit"); 902 var $errorContainer = $paymentForm.querySelector(".error"); 903 904 enterButtonLoading($paymentSubmit); 905 906 var isSignUpMode = $paymentForm.classList.contains("signup"); 907 908 var formData = new FormData($paymentForm); 909 var email = formData.get("email"); 910 var password = formData.get("password"); 911 var selectedPlan = $paymentForm.querySelector(".payment-plan.-selected").getAttribute("data-plan-id"); 912 913 stripeHelper.stripe.createToken(stripeHelper.card).then(function (stripeResponse) { 914 if (stripeResponse.error || !stripeResponse.token) { 915 $errorContainer.textContent = stripeResponse.error && stripeResponse.error.message; 916 exitButtonLoading($paymentSubmit); 917 return; 918 } 919 if (Auth().getAuthToken()) { 920 subscribeFunction(stripeResponse, selectedPlan, $paymentForm); 921 } else { 922 Auth() 923 .checkEmail(email) 924 .then(function (doesUserExist) { 925 if (isAuthStateMismatch(doesUserExist, isSignUpMode, $errorContainer)) { 926 exitButtonLoading($paymentSubmit); 927 return; 928 } 929 930 var authPromise = isSignUpMode 931 ? Auth().join(email, formData.get("name"), password, login_app_name) 932 : Auth().login(email, password); 933 934 authPromise 935 .then(subscribeFunction.bind(null, stripeResponse, selectedPlan, $paymentForm)) 936 .catch(handleSignUpError.bind(null, $paymentForm)); 937 }); 938 } 939 }); 940 } 941 942 function handleSignUpError($form, errorMessage) { 943 console.error(errorMessage); 944 var $submitButton = $form.querySelector(".button.submit"); 945 var $errorContainer = $form.querySelector(".error"); 946 var isSignUpMode = $form.classList.contains("signup"); 947 948 exitButtonLoading($submitButton); 949 $errorContainer.textContent = isSignUpMode ? "Something went wrong." : "Password is incorrect"; 950 } 951 952 function switchToSignUp($form) { 953 $form.classList.add("signup"); 954 $form.classList.remove("signin"); 955 $form.querySelector(".error").textContent = ""; 956 } 957 958 function switchToSignIn($form) { 959 $form.classList.add("signin"); 960 $form.classList.remove("signup"); 961 $form.querySelector(".error").textContent = ""; 962 } 963 // #endregion checkout 964 965 // #region premium 966 function subscribeToPremium(stripeResponse, plan, $paymentForm) { 967 var $submitButton = $paymentForm.querySelector(".button.submit"); 968 // check for active subscription 969 Auth() 970 .getPremiumData() 971 .then(function (premiumData) { 972 if (premiumData) { 973 alert("You are already subscribed to Any.do Premium! press OK to go to Any.do."); 974 Auth().redirectToDesktop(); 975 } else { 976 Auth() 977 .subscribeToPremium(stripeResponse, plan) 978 .then(function () { 979 $submitButton.classList.remove("running"); 980 $submitButton.textContent = "Success! Sending you to Any.do..."; 981 setTimeout(function () { 982 Auth().redirectToDesktop(Auth().getAuthToken()); 983 }, 3000); 984 }) 985 .catch(function (message) { 986 exitButtonLoading($submitButton); 987 $paymentForm.querySelector(".error").textContent = message || "An error occurred"; 988 }); 989 } 990 }) 991 .catch(function (message) { 992 exitButtonLoading($submitButton); 993 $paymentForm.querySelector(".error").textContent = message || "An error occurred"; 994 }); 995 } 996 997 async function initPremiumCheckoutModal($checkoutContainer) { 998 var access_token = null; 999 var google_login_success = null; 1000 var fb_login_success = null; 1001
1002 new URLSearchParams(location.search).forEach(function (value, key) { 1003 if (key == "google-login-result" && value == "1") { 1004 google_login_success = true; 1005 } 1006 if (key == "fb-login-result" && value == "1") { 1007 fb_login_success = true; 1008 } 1009 }); 1010 1011 new URLSearchParams(location.hash.slice(1)).forEach(function (value, key) { 1012 if (key == "access_token") { 1013 access_token = value; 1014 } 1015 }); 1016 1017 if (Auth().getAuthToken() || google_login_success || fb_login_success || (await authPromise)) { 1018 $('#go-premium input[type="email"]').removeAttr("required"); 1019 $('#go-premium input[type="password"]').removeAttr("required"); 1020 $("#go-premium .hide-if-logged-in").hide(); 1021 $("#go-premium").modal("show"); 1022 } 1023 1024 if (google_login_success && access_token) { 1025 await Auth().loginGoogle(null, access_token); 1026 } 1027 if (fb_login_success && access_token) { 1028 await Auth().loginFacebook(access_token); 1029 } 1030 1031 $(".js-google-button").on("click", () => Auth().redirectLoginWithGoogle()); 1032 $(".login .button.facebook").on("click", () => Auth().redirectLoginWithFacebook()); 1033 1034 var stripeHelper = new StripeHelper(); 1035 stripeHelper.initStripeSingleInput(stripeHelper.ANYDO_STRIPE_KEY); 1036 var $paymentForm = $checkoutContainer.querySelector(".js-payment-form"); 1037 1038 // switch between sign in / sign up in checkout 1039 $checkoutContainer 1040 .querySelector(".js-switch-to-signup") 1041 .addEventListener("click", () => switchToSignUp($paymentForm)); 1042 $checkoutContainer 1043 .querySelector(".js-switch-to-signin") 1044 .addEventListener("click", () => switchToSignIn($paymentForm)); 1045 1046 $("#go-premium").on("hide.bs.modal", () => $("#go-premium").removeClass("hide-unselected-plans")); 1047 1048 $paymentForm.addEventListener("submit", function () { 1049 handleCheckoutSubmit(event, stripeHelper, subscribeToPremium, "web"); 1050 }); 1051 } 1052 1053 $(function () { 1054 var $checkoutContainer = document.querySelector(".js-go-premium"); 1055 if ($checkoutContainer) { 1056 initPremiumCheckoutModal($checkoutContainer); 1057 } 1058 }); 1059 // #endregion premium 1060 1061 //#region Teams Checkout 1062 function Business() { 1063 this._SERVER_DOMAIN = __DEV__ ? "sm-dev-teams.any.do" : "sm-prod4.any.do"; 1064 this._BUSINESS_URI = "https://" + this._SERVER_DOMAIN + "/business"; 1065 } 1066 1067 Business.prototype.createBusiness = function createBusiness(options) { 1068 var stripeHelper = options.stripeHelper; 1069 var stripePlan = options.stripePlan; 1070 var businessName = options.businessName; 1071 var members = options.members; 1072 var self = this; 1073 1074 return stripeHelper.stripe 1075 .createToken(stripeHelper.cardNumber) 1076 .then(function (stripeResponse) { 1077 if (stripeResponse.error || !stripeResponse.token) { 1078 return Promise.reject(stripeResponse.error && stripeResponse.error.message); 1079 } 1080 1081 return fetch(self._BUSINESS_URI, { 1082 method: "POST", 1083 headers: Auth().createHeaders(true), 1084 body: JSON.stringify({ 1085 name: businessName, 1086 members, 1087 is_trial: true, 1088 provider_data: stripeResponse.token, 1089 stripe_plan: stripePlan, 1090 currency: "usd", 1091 payment_provider: "Stripe", 1092 }), 1093 }); 1094 }) 1095 .then(function (response) { 1096 if (response.ok) { 1097 return Auth().getPremiumData(); 1098 } 1099 1100 return Promise.reject(new Error(`createBusiness(): Any.do server status "${response.status}"`)); 1101 }) 1102 .then(function (data) { 1103 return { 1104 businessId: data.businessId, 1105 members, 1106 }; 1107 }); 1108 }; 1109 1110 Business.prototype.updateBusiness = async function updateBusiness(businessId, businessName, members) { 1111 var response = await fetch(this._BUSINESS_URI + "/" + businessId, { 1112 method: "PUT", 1113 headers: Auth().createHeaders(true), 1114 body: JSON.stringify({ 1115 name: businessName, 1116 members, 1117 }), 1118 }); 1119 if (!response.ok) { 1120 throw new Error(`updateBusiness(): Any.do server status "${response.status}"`); 1121 } else { 1122 return { 1123 businessId, 1124 members, 1125 }; 1126 } 1127 }; 1128 1129 Business.prototype.getBusinessData = async function getBusinessData(businessId) { 1130 var response = await fetch(this._BUSINESS_URI + "/" + businessId, { 1131 method: "GET", 1132 headers: Auth().createHeaders(true),
1133 }); 1134 if (response.ok) { 1135 var data = await response.json(); 1136 return data; 1137 } else { 1138 throw new Error(`getBusinessData(): Any.do server status "${response.status}"`); 1139 } 1140 }; 1141 1142 Business.prototype.buildMembersObject = function buildMembersObject(adminEmail, membersEmails) { 1143 return membersEmails 1144 .map(function (email) { 1145 return { 1146 email, 1147 is_admin: false, 1148 }; 1149 }) 1150 .concat({ 1151 email: adminEmail, 1152 is_admin: true, 1153 }); 1154 }; 1155 1156 function StripeHelper() { 1157 this.ANYDO_STRIPE_KEY = __DEV__ ? "pk_test_ibci0S0N18vCEROPRoY786nk" : "pk_live_PpUdKgmIH1mJ388r4uOIqJPb"; 1158 this.ASSISTANT_STRIPE_KEY = __DEV__ ? "pk_test_crYdbmMDSFbqzEOgD4wAQDCN" : "pk_live_MDMT2N9MUCxGKoamBrmA79BT"; 1159 this.STRIPE_TEAMS_PLAN = "anydo_teams_yearly"; 1160 this.TEAMS_PLAN_USD_PRICE_IN_CENTS = 5999; 1161 this.TEAMS_PLAN_USD_PRICE_MONTHLY_IN_CENTS = 499; 1162 this.TEAMS_FREE_TRIAL_LENGTH = 14; 1163 this.stripeLoadingPromise = undefined; 1164 } 1165 1166 StripeHelper.prototype.initStripeMultiInput = async function initStripeMultiInput(key) { 1167 this.stripe = Stripe(key); 1168 const elements = this.stripe.elements({ 1169 locale: "auto", 1170 }); 1171 1172 const elementStyles = { 1173 base: { 1174 fontSize: "16px", 1175 1176 "::placeholder": { 1177 color: "#afafaf", 1178 }, 1179 ":-webkit-autofill": { 1180 color: "#e39f48", 1181 }, 1182 }, 1183 invalid: { 1184 color: "#E25950", 1185 1186 "::placeholder": { 1187 color: "#FFCCA5", 1188 }, 1189 }, 1190 }; 1191 1192 const elementClasses = { 1193 focus: "focused", 1194 empty: "empty", 1195 invalid: "invalid", 1196 }; 1197 1198 this.cardNumber = elements.create("cardNumber", { 1199 style: elementStyles, 1200 classes: elementClasses, 1201 }); 1202 this.cardNumber.mount("#card-number"); 1203 this.cardNumber.on("change", this.handleStripeChange.bind(this)); 1204 1205 this.cardExpiry = elements.create("cardExpiry", { 1206 style: elementStyles, 1207 classes: elementClasses, 1208 }); 1209 this.cardExpiry.mount("#card-expiry"); 1210 this.cardExpiry.on("change", this.handleStripeChange.bind(this)); 1211 1212 this.cardCvc = elements.create("cardCvc", { 1213 style: elementStyles, 1214 classes: elementClasses, 1215 }); 1216 this.cardCvc.mount("#card-cvc"); 1217 this.cardCvc.on("change", this.handleStripeChange.bind(this)); 1218 1219 this.cardNumber.on("change", (event) => event && event.complete && this.cardExpiry.focus()); 1220 this.cardExpiry.on("change", (event) => event && event.complete && this.cardCvc.focus()); 1221 }; 1222 1223 StripeHelper.prototype.initStripeSingleInput = async function initStripeSingleInput(key) { 1224 this.stripe = Stripe(key); 1225 const elements = this.stripe.elements({ 1226 locale: "auto", 1227 }); 1228 1229 const elementStyles = { 1230 base: { 1231 "::placeholder": { 1232 color: "#cfcfcf", 1233 }, 1234 }, 1235 invalid: { 1236 color: "#E25950", 1237 1238 "::placeholder": { 1239 color: "#FFCCA5", 1240 }, 1241 }, 1242 }; 1243 1244 const elementClasses = { 1245 focus: "focused", 1246 empty: "empty", 1247 invalid: "invalid", 1248 }; 1249 1250 this.card = elements.create("card", { 1251 style: elementStyles, 1252 classes: elementClasses, 1253 }); 1254 this.card.mount("#card"); 1255 this.card.on("change", this.handleStripeChange.bind(this)); 1256 }; 1257 1258 StripeHelper.prototype.handleStripeChange = function handleStripeChange(event) { 1259 let $errorContainer = document.querySelector(".stripe-error"); 1260 1261 if (event && event.error) { 1262 $errorContainer.textContent = event.error.message || ""; 1263 } else { 1264 $errorContainer.textContent = ""; 1265 } 1266 }; 1267 1268 /** 1269 * @param {HTMLDivElement} $container 1270 * @package {String} flowExprGroup - group name of the flow experiment 1271 */ 1272 async function initTeamsCheckoutForm($container, flowExprGroup) { 1273 var stripeHelper = new StripeHelper(); 1274 stripeHelper.initStripeMultiInput(stripeHelper.ANYDO_STRIPE_KEY); 1275 var businessHelper = new Business(); 1276 1277 var STATE = createEnum([ 1278 "LOCK", 1279 "INIT", 1280 "LOAD_PAYMENT", 1281 "PAY", 1282 "LOAD_MANAGE_TEAM", 1283 "VALIDATE_TEAM_MEMBERS", 1284 "MANAGE_TEAM", 1285 "LOAD_SUCCESS", 1286 "REDIRECT_TO_ANYDO", 1287 ]); 1288 1289 var userEmail = Auth().getUserEmail(); 1290 var businessId = null; 1291 var serverMemberCount = null; // for analytics 1292 var premiumData = await Auth().getPremiumData(); 1293 1294 /** @type {HTMLInputElement} */ 1295 var $tcTeamName = $container.querySelector("input#team-name"); 1296 1297 /** @type {HTMLSectionElement} */ 1298 var $tcInfoSection = $container.querySelector("section#tc-info"); 1299 /** @type {HTMLSectionElement} */ 1300 var $tcTeamNameSection = $container.querySelector("section#tc-team-name"); 1301 /** @type {HTMLSectionElement} */ 1302 var $tcPaymentSection = $container.querySelector("section#tc-payment"); 1303 /** @type {HTMLSectionElement} */ 1304 var $tcTeamManagementSection = $container.querySelector("section#tc-team-management"); 1305 /** @type {HTMLSectionElement} */ 1306 var $tcSuccessSection = $container.querySelector("section#tc-success"); 1307 1308 /** @type {HTMLElement} */ 1309 var $teamManageErrorContainers = $container.querySelectorAll(".js-team-member-form div.error"); 1310 /** @type {HTMLElement} */ 1311 var $successMessageContainers = $container.querySelectorAll(".js-team-member-form .success"); 1312 /** @type {HTMLButtonElement} */ 1313 var $teamManageSubmit = $container.querySelectorAll(".js-team-member-form .js-tc-submit"); 1314 /** @type {HTMLButtonElement} */ 1315 var $tcPaymentSubmit = $tcPaymentSection.querySelector(".js-tc-submit"); 1316 1317 setLayoutPerFlowExprGroup(flowExprGroup); 1318 var state = await determineStateByPremiumStatus(premiumData); 1319 1320 [...$container.querySelectorAll(".js-team-member.insert input")].forEac
1320h(function ($field) { 1321 $field.addEventListener("input", handleMemberInputChange); 1322 }); 1323 1324 [...$container.querySelectorAll(".js-teams-checkout-form")].forEach(function ($form) { 1325 $form.addEventListener("submit", function (event) { 1326 event.preventDefault(); 1327 render(); 1328 }); 1329 }); 1330 1331 render(); 1332 1333 function render() { 1334 switch (state) { 1335 case STATE.LOCK: 1336 break; 1337 1338 case STATE.REDIRECT_TO_ANYDO: 1339 window.location.replace("/open-todo"); 1340 break; 1341 1342 case STATE.INIT: 1343 state = STATE.LOCK; 1344 loadTeamNameView(); 1345 state = flowExprGroup == "invite-first" ? STATE.LOAD_MANAGE_TEAM : STATE.LOAD_PAYMENT; 1346 break; 1347 1348 case STATE.LOAD_PAYMENT: 1349 state = STATE.LOCK; 1350 loadPaymentView(); 1351 state = STATE.PAY; 1352 break; 1353 1354 case STATE.PAY: 1355 state = STATE.LOCK; 1356 toggleButtonRunning($tcPaymentSubmit, true); 1357 performBusinessCreate() 1358 .then((responseData) => { 1359 handleCreateBusinessSuccess(responseData); 1360 toggleButtonRunning($tcPaymentSubmit, false); 1361 state = flowExprGroup == "invite-first" ? STATE.LOAD_SUCCESS : STATE.LOAD_MANAGE_TEAM; 1362 render(); 1363 }) 1364 .catch((message) => { 1365 handleCreateBusinessFailure(message); 1366 toggleButtonRunning($tcPaymentSubmit, false); 1367 state = STATE.PAY; 1368 }); 1369 break; 1370 1371 case STATE.LOAD_MANAGE_TEAM: 1372 state = STATE.LOCK; 1373 loadTeamManageView(); 1374 state = STATE.VALIDATE_TEAM_MEMBERS; 1375 break; 1376 1377 case STATE.VALIDATE_TEAM_MEMBERS: 1378 state = STATE.LOCK; 1379 setElementsText([...$successMessageContainers, ...$teamManageErrorContainers], ""); 1380 if (validateInvitees()) { 1381 state = flowExprGroup == "invite-first" && !businessId ? STATE.LOAD_PAYMENT : STATE.MANAGE_TEAM; 1382 render(); 1383 } else { 1384 setElementsText([...$teamManageErrorContainers], "Some of the emails you have entered are invalid"); 1385 state = STATE.VALIDATE_TEAM_MEMBERS; 1386 } 1387 break; 1388 1389 case STATE.MANAGE_TEAM: 1390 state = STATE.LOCK; 1391 toggleButtonsRunning([...$teamManageSubmit], true); 1392 performBusinessUpdate() 1393 .then((responseData) => { 1394 toggleButtonsRunning([...$teamManageSubmit], false); 1395 handleBusinessUpdateSuccess(responseData); 1396 state = STATE.VALIDATE_TEAM_MEMBERS; 1397 }) 1398 .catch(() => { 1399 toggleButtonsRunning([...$teamManageSubmit], false); 1400 handleBusinessUpdateFailure(); 1401 state = STATE.VALIDATE_TEAM_MEMBERS; 1402 }); 1403 break; 1404 1405 case STATE.LOAD_SUCCESS: 1406 state = STATE.LOCK; 1407 loadSuccessView(); 1408 state = STATE.VALIDATE_TEAM_MEMBERS; 1409 break; 1410 } 1411 } 1412 1413 async function determineStateByPremiumStatus(premiumData) { 1414 if (premiumData && premiumData.serverExpirationDate > Date.now()) { 1415 // user is premium 1416 if (premiumData.activePlan == "BUSINESS") { 1417 // business plan 1418 if (premiumData.isBusinessAdmin) { 1419 // business admin => direct to team management section 1420 await fillBusinessData(premiumData.businessId); 1421 return STATE.LOAD_SUCCESS; 1422 } else { 1423 // business team member => can't use, direct to anydo 1424 alert( 1425 "You are already a member of an existing team. Please contact your admin in order to manage the team.", 1426 ); 1427 return STATE.REDIRECT_TO_ANYDO; 1428 } 1429 } else { 1430 // other premium plan => treatment TBD 1431 return STATE.INIT; 1432 } 1433 } else { 1434 // not premium 1435 return STATE.INIT; 1436 } 1437 } 1438 1439 async function fillBusinessData(businessIdArg) { 1440 businessId = businessIdArg; 1441 var businessData = await businessHelper.getBusinessData(businessId); 1442 $tcTeamName.value = businessData.name; 1443 var $teamMemberElement = $container.querySelector("#team-members .js-team-member.insert");
1444 businessData.members.forEach((member) => { 1445 if (!member.is_admin) addTeamMember($teamMemberElement, member.email); 1446 }); 1447 serverMemberCount = businessData.members.length; 1448 } 1449 1450 function setLayoutPerFlowExprGroup(group) { 1451 if (group == "invite-first") { 1452 $tcTeamManagementSection.querySelector(".js-tc-submit .button-text").textContent = "Continue"; 1453 } 1454 } 1455 1456 function loadTeamNameView() { 1457 $tcTeamNameSection.querySelector(".disclaimer .date").textContent = addToToday( 1458 stripeHelper.TEAMS_FREE_TRIAL_LENGTH, 1459 ) 1460 .toDateString() 1461 .substring(4); 1462 showElement($tcTeamNameSection); 1463 hideElement($tcPaymentSection); 1464 hideElement($tcTeamManagementSection); 1465 1466 ga("send", { 1467 hitType: "event", 1468 eventCategory: "teams_checkout", 1469 eventAction: "team_name_step_shown", 1470 }); 1471 } 1472 1473 function loadPaymentView() { 1474 showElement($tcPaymentSection); 1475 hideElement($tcTeamNameSection); 1476 hideElement($tcTeamManagementSection); 1477 stripeHelper.cardNumber.focus(); 1478 1479 ga("send", { 1480 hitType: "event", 1481 eventCategory: "teams_checkout", 1482 eventAction: "enter_cc_step_shown", 1483 }); 1484 } 1485 1486 function loadTeamManageView() { 1487 $tcTeamManagementSection.querySelector(".admin input").value = userEmail + " (That's you)"; 1488 $(".hide-team-manage").hide(); 1489 hideElement($tcPaymentSection); 1490 showElement($tcTeamNameSection); 1491 showElement($tcTeamManagementSection); 1492 1493 ga("send", { 1494 hitType: "event", 1495 eventCategory: "teams_checkout", 1496 eventAction: "manage_team_step_shown", 1497 }); 1498 } 1499 1500 function loadSuccessView() { 1501 $tcSuccessSection.querySelector(".js-instruction").textContent = SUCCESS_DESCRIPTION.replace( 1502 "(#team-name)", 1503 teamNameFromContainer(), 1504 ); 1505 1506 hideElement($tcInfoSection); 1507 hideElement($tcPaymentSection); 1508 showElement($tcSuccessSection); 1509 } 1510 1511 function performBusinessCreate() { 1512 var membersObject = businessHelper.buildMembersObject(userEmail, getInviteesFromContainer()); 1513 return businessHelper.createBusiness({ 1514 stripeHelper, 1515 stripePlan: stripeHelper.STRIPE_TEAMS_PLAN, 1516 businessName: teamNameFromContainer(), 1517 members: membersObject, 1518 }); 1519 } 1520 1521 function performBusinessUpdate() { 1522 var membersObject = businessHelper.buildMembersObject(userEmail, getInviteesFromContainer()); 1523 return businessHelper.updateBusiness(businessId, teamNameFromContainer(), membersObject); 1524 } 1525 1526 function handleCreateBusinessSuccess(businessData) { 1527 businessId = businessData.businessId; 1528 serverMemberCount = businessData.members.length; 1529 var purchaseValueInCents = serverMemberCount * stripeHelper.TEAMS_PLAN_USD_PRICE_IN_CENTS; 1530 1531 ga("send", { 1532 hitType: "event", 1533 eventCategory: "teams_checkout", 1534 eventAction: "purchase", 1535 eventValue: purchaseValueInCents, 1536 }); 1537 1538 if (!__DEV__) { 1539 fbq("track", "StartTrial", { 1540 currency: "USD", 1541 value: purchaseValueInCents / 100, 1542 }); 1543 } 1544 } 1545 1546 function handleCreateBusinessFailure(errorMessage) { 1547 $tcPaymentSection.querySelector(".error").textContent = errorMessage || "An error occurred"; 1548 1549 ga("send", { 1550 hitType: "event", 1551 eventCategory: "teams_checkout", 1552 eventAction: "purchase_error", 1553 eventLabel: errorMessage || "unknown error", 1554 }); 1555 } 1556 1557 function handleBusinessUpdateSuccess(businessData) { 1558 setElementsText([...$successMessageContainers], "Invitations have been sent to your team!"); 1559 1560 var updatedMemberCount = businessData.members.length; 1561 1562 // report to ga 1563 ga("send", { 1564 hitType: "event", 1565 eventCategory: "teams_checkout", 1566 eventAction: "teams_plan_updated", 1567 eventValue: Math.max(updatedMemberCount - serverMemberCount, 0) * stripeHelper.TEAMS_PLAN_USD_PRICE_IN_CENTS, 1568 }); // eventValue must be non-negative. negative value events will be reported with value = 0 for lack of a better option 1569 1570 if (updatedMemberCount != serverMemberCount) { 1571 var manageEvent = 1572 "team_manage_" + (updatedMemberCount < serverMemberCount ? "removed_members" : "added_members"); 1573 ga("send", { 1574 hitType: "event", 1575 eventCategory: "teams_checkout", 1576 eventAction: manageEvent, 1577 eventValue: Math.abs(serverMemberCount - updatedMemberCount), 1578 }); 1579 } 1580 1581 // report to fb 1582 if (!__DEV__ && updatedMemberCount > serverMemberCount) { 1583 // no concept of "negative conversions" exists on fb 1584 fbq("track", "StartTrial", { 1585 currency: "USD", 1586 value: ((updatedMemberCount - serverMemberCount) * stripeHelper.TEAMS_PLAN_USD_PRICE_IN_CENTS) / 100, 1587 }); 1588 } 1589 1590 serverMemberCount = updatedMemberCount; 1591 } 1592 1593 function handleBusinessUpdateFailure() { 1594 setElementsText([...$teamManageErrorContainers], "An error has occurred"); 1595 toggleButtonsRunning([...$teamManageSubmit], false); 1596 1597 ga("send", { 1598 hitType: "event", 1599 eventCategory: "teams_checkout", 1600 eventAction: "teams_plan_update_failed", 1601 }); 1602 } 1603 1604 function tmUpdateTotalPricePerYear() { 1605 var memberCount = getTeamMembersFromContainer().length; 1606 var total_price_per_year = (memberCount * stripeHelper.TEAMS_PLAN_USD_PRICE_IN_CENTS) / 100;
1607 $tcTeamManagementSection.querySelector(".plan-details").textContent = TM_PAYMENT_TOTAL.replace( 1608 "(#yearly)", 1609 total_price_per_year, 1610 ).replace("(#monthly)", stripeHelper.TEAMS_PLAN_USD_PRICE_MONTHLY_IN_CENTS / 100); 1611 } 1612 1613 function handleMemberInputChange(event) { 1614 var $targetTeamMember = event.target.closest(".js-team-member"); 1615 if ($targetTeamMember.classList.contains("insert") && event.target.value) { 1616 var $newMember = $targetTeamMember.cloneNode(true); 1617 var $newMemberInput = $newMember.querySelector("input"); 1618 $newMemberInput.value = ""; 1619 $newMemberInput.addEventListener("input", handleMemberInputChange); 1620 1621 $targetTeamMember.classList.remove("insert"); 1622 $targetTeamMember.classList.add("invitee"); 1623 $targetTeamMember.querySelector("input").addEventListener("blur", (e) => { 1624 if (!e.target.value) { 1625 e.target.closest(".js-team-member").remove(); 1626 tmUpdateTotalPricePerYear(); 1627 } 1628 }); 1629 $targetTeamMember.querySelector("button").addEventListener("click", (e) => { 1630 e.target.closest(".js-team-member").remove(); 1631 tmUpdateTotalPricePerYear(); 1632 }); 1633 $targetTeamMember.onkeypress = (event) => { 1634 if (event.keyCode == 13) { 1635 event.preventDefault; 1636 event.target.closest("ul.js-team-member-list").querySelector(".insert input").focus(); 1637 return false; 1638 } 1639 }; 1640 1641 $targetTeamMember.closest("ul.js-team-member-list").appendChild($newMember); 1642 tmUpdateTotalPricePerYear(); 1643 $targetTeamMember.closest("form").querySelector(".js-tc-submit").removeAttribute("disabled"); 1644 } 1645 } 1646 1647 function addTeamMember($teamMemberElement, newMemberEmail) { 1648 var $insertedTeamMember = $teamMemberElement.cloneNode(true); 1649 var $insertedTeamMemberInput = $insertedTeamMember.querySelector("input"); 1650 var $insertedTeamMemberButton = $insertedTeamMember.querySelector(".member-button"); 1651 if (newMemberEmail) $insertedTeamMemberInput.value = newMemberEmail; 1652 1653 $insertedTeamMemberInput.setAttribute("disabled", "disabled"); 1654 $insertedTeamMember.classList.remove("insert"); 1655 $insertedTeamMember.classList.add("invitee"); 1656 $teamMemberElement.closest("ul.js-team-member-list").insertBefore($insertedTeamMember, $teamMemberElement); 1657 $insertedTeamMemberButton.addEventListener("click", (e) => e.target.closest(".js-team-member").remove()); 1658 $insertedTeamMemberButton.removeAttribute("disabled"); 1659 tmUpdateTotalPricePerYear(); 1660 } 1661 1662 function getInviteeElementsFromContainer() { 1663 return [...$container.querySelectorAll(".js-team-member.invitee input")]; 1664 } 1665 1666 function getInviteesFromContainer() { 1667 return getInviteeElementsFromContainer().map((e) => e.value); 1668 } 1669 1670 function validateInvitees() { 1671 var is_valid = true; 1672 getInviteeElementsFromContainer().forEach((e) => { 1673 var $teamMember = e.closest(".js-team-member"); 1674 if (isValidTeamsEmail(e.value)) { 1675 $teamMember.classList.remove("error"); 1676 } else { 1677 $teamMember.classList.add("error"); 1678 is_valid = false; 1679 } 1680 }); 1681 return is_valid; 1682 } 1683 1684 function getTeamMembersFromContainer() { 1685 var teamMembers = getInviteesFromContainer(); 1686 teamMembers.push(userEmail); // add admin 1687 return teamMembers; 1688 } 1689 1690 function teamNameFromContainer() { 1691 return $tcTeamName.value; 1692 } 1693 1694 function isValidTeamsEmail(str) { 1695 if (!isValidEmail(str)) { 1696 return false; 1697 } 1698 var teamMembers = getTeamMembersFromContainer(); 1699 return teamMembers.reduce((n, member) => n + (member === str), 0) == 1; 1700 } 1701 } 1702 1703 $(function () { 1704 var formContainer = document.querySelector(".js-teams-checkout"); 1705 if (formContainer) { 1706 updateAuthFromQueryParams(); 1707 if (!Auth().getAuthToken()) window.location.replace("/teams"); 1708 1709 var flowExprGroup = getQueryParam("flow") === "b" ? "invite-first" : "payment-first"; 1710 1711 initTeamsCheckoutForm(formContainer, flowExprGroup); 1712 } 1713 1714 function updateAuthFromQueryParams() { 1715 if (typeof URLSearchParams === "undefined") return null; 1716 1717 var q = new URLSearchParams(location.search); 1718 var token = q.get("token"); 1719 var email = q.get("email"); 1720 1721 if (token && email) Auth().setAuth(token, email); 1722 1723 // remove the auth params from the url 1724 q.delete("token"); 1725 q.delete("email"); 1726 const url = `${location.href.split("?")[0]}?${q.toString()}`; 1727 history.replaceState({}, document.title, url); 1728 } 1729 }); 1730 1731 // utils 1732 function addToToday(days) { 1733 var res = new Date(); 1734 res.setDate(res.getDate() + days); 1735 return res; 1736 } 1737 1738 function isValidEmail(str) { 1739 const re = 1740 /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 1741 return re.test(str); 1742 } 1743 1744 function setElementsText(elements, text) {
1745 elements.forEach((e) => (e.textContent = text)); 1746 } 1747 1748 function toggleButtonRunning(button, buttonState) { 1749 var runningClass = "running"; 1750 if (buttonState) button.classList.add(runningClass); 1751 else button.classList.remove(runningClass); 1752 } 1753 1754 function toggleButtonsRunning(buttons, buttonState) { 1755 buttons.forEach((button) => toggleButtonRunning(button, buttonState)); 1756 } 1757 1758 function hideElement(e) { 1759 e.style.display = "none"; 1760 e.style.visibility = "hidden"; 1761 } 1762 1763 function showElement(e) { 1764 e.style.display = "block"; 1765 e.style.visibility = "visible"; 1766 } 1767 1768 function enterButtonLoading($el) { 1769 $el.classList.add("running"); 1770 $el.setAttribute("disabled", "disabled"); 1771 } 1772 1773 function exitButtonLoading($el) { 1774 $el.classList.remove("running"); 1775 $el.removeAttribute("disabled"); 1776 } 1777 1778 function isAuthStateMismatch(doesUserExist, isSignUpMode, $errorContainer) { 1779 if (doesUserExist && isSignUpMode) { 1780 $errorContainer.textContent = "A user with this email address is already registered, please sign in."; 1781 } else if (!doesUserExist && !isSignUpMode) { 1782 $errorContainer.textContent = "Oops.. we don't recognize this email address, please sign up."; 1783 } else { 1784 $errorContainer.textContent = ""; 1785 return false; 1786 } 1787 return true; 1788 } 1789 1790 $(function () { 1791 document.querySelector(".js-book-demo-redirect").addEventListener("click", function () { 1792 window.location.href = "https://app.any.do/?website_registration_flow=team"; 1793 }); 1794 1795 window.addEventListener("message", (evt) => { 1796 if ( 1797 // 1798 evt.origin !== "https://calendly.com" || 1799 evt.data === null || 1800 !("event" in evt.data) 1801 ) { 1802 return; 1803 } 1804 1805 if (evt.data.event === "calendly.event_scheduled") { 1806 window.location.href = "https://app.any.do/?website_registration_flow=team"; 1807 } 1808 }); 1809 1810 const bookDemo = document.querySelector(".js-book-demo-schedule"); 1811 1812 bookDemo.addEventListener("click", function () { 1813 const params = new URLSearchParams({ 1814 hide_gdpr_banner: "1", 1815 1816 // theme 1817 background_color: "fffff", 1818 text_color: "030303", 1819 primary_color: "0083ff", 1820 }); 1821 1822 window.Calendly.initPopupWidget({ 1823 url: `${bookDemo.dataset.calendlyLink}?${params.toString()}`, 1824 utm: {}, 1825 }); 1826 }); 1827 }); 1828 //#endregion 1829}); //end jQuery
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.