@omega.js/web 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +98 -0
- package/README.md +640 -0
- package/bin/omega +2 -0
- package/bin/omega-web +2 -0
- package/bin/omg +2 -0
- package/core/_includes/admin/sections/sidebar.json +100 -0
- package/core/_includes/admin/sections/topbar.json +11 -0
- package/core/_includes/backend/sections/sidebar.json +39 -0
- package/core/_includes/backend/sections/topbar.json +16 -0
- package/core/_includes/core/body.html +102 -0
- package/core/_includes/core/foot.html +536 -0
- package/core/_includes/core/head.html +423 -0
- package/core/_includes/core/pricing/price-per-unit.html +33 -0
- package/core/_includes/core/showcase/entry-body.html +94 -0
- package/core/_includes/core/showcase/index-body.html +43 -0
- package/core/_includes/core/showcase/nav.html +60 -0
- package/core/_includes/frontend/sections/footer.json +74 -0
- package/core/_includes/frontend/sections/nav.json +70 -0
- package/core/_includes/global/sections/account.json +31 -0
- package/core/_includes/modules/engagement/giscus.html +26 -0
- package/core/_layouts/blueprint/404.html +10 -0
- package/core/_layouts/blueprint/about.html +10 -0
- package/core/_layouts/blueprint/account/index.html +11 -0
- package/core/_layouts/blueprint/admin/calendar/index.html +590 -0
- package/core/_layouts/blueprint/admin/dashboard/index.html +301 -0
- package/core/_layouts/blueprint/admin/firebase/index.html +214 -0
- package/core/_layouts/blueprint/admin/posts/editor.html +157 -0
- package/core/_layouts/blueprint/admin/posts/index.html +126 -0
- package/core/_layouts/blueprint/admin/stackblitz/index.html +30 -0
- package/core/_layouts/blueprint/admin/studio/index.html +85 -0
- package/core/_layouts/blueprint/admin/users/index.html +272 -0
- package/core/_layouts/blueprint/admin/users/new.html +82 -0
- package/core/_layouts/blueprint/admin/verts/index.html +204 -0
- package/core/_layouts/blueprint/alternatives/alternative.html +15 -0
- package/core/_layouts/blueprint/alternatives/index.html +10 -0
- package/core/_layouts/blueprint/app.html +10 -0
- package/core/_layouts/blueprint/auth/reset.html +19 -0
- package/core/_layouts/blueprint/auth/signin.html +19 -0
- package/core/_layouts/blueprint/auth/signup.html +19 -0
- package/core/_layouts/blueprint/auth/token.html +19 -0
- package/core/_layouts/blueprint/blog/categories/category.html +8 -0
- package/core/_layouts/blueprint/blog/categories/index.html +11 -0
- package/core/_layouts/blueprint/blog/index.html +10 -0
- package/core/_layouts/blueprint/blog/post.html +5 -0
- package/core/_layouts/blueprint/blog/tags/index.html +11 -0
- package/core/_layouts/blueprint/blog/tags/tag.html +8 -0
- package/core/_layouts/blueprint/careers.md +13 -0
- package/core/_layouts/blueprint/collection/category.html +9 -0
- package/core/_layouts/blueprint/collection/document.html +9 -0
- package/core/_layouts/blueprint/collection/index.html +9 -0
- package/core/_layouts/blueprint/connections/callback.html +11 -0
- package/core/_layouts/blueprint/contact.html +14 -0
- package/core/_layouts/blueprint/dashboard/index.html +10 -0
- package/core/_layouts/blueprint/download.html +20 -0
- package/core/_layouts/blueprint/extension/index.html +14 -0
- package/core/_layouts/blueprint/extension/installed.html +11 -0
- package/core/_layouts/blueprint/feedback.html +10 -0
- package/core/_layouts/blueprint/index.html +16 -0
- package/core/_layouts/blueprint/legal/cookies.md +57 -0
- package/core/_layouts/blueprint/legal/privacy.md +109 -0
- package/core/_layouts/blueprint/legal/terms.md +155 -0
- package/core/_layouts/blueprint/payment/checkout.html +15 -0
- package/core/_layouts/blueprint/payment/confirmation.html +14 -0
- package/core/_layouts/blueprint/portal/email-preferences.html +15 -0
- package/core/_layouts/blueprint/pricing.html +20 -0
- package/core/_layouts/blueprint/status.html +10 -0
- package/core/_layouts/blueprint/team/index.html +10 -0
- package/core/_layouts/blueprint/team/member.html +5 -0
- package/core/_layouts/blueprint/updates/index.html +10 -0
- package/core/_layouts/blueprint/updates/update.html +10 -0
- package/core/_layouts/core/root.html +45 -0
- package/core/_layouts/modules/utilities/redirect.html +129 -0
- package/core/css/_studio.scss +237 -0
- package/core/css/components/_index.scss +146 -0
- package/core/css/core/_.scss +0 -0
- package/core/css/core/_alert.scss +87 -0
- package/core/css/core/_animations.scss +343 -0
- package/core/css/core/_bindings.scss +116 -0
- package/core/css/core/_blog-search.scss +57 -0
- package/core/css/core/_chatsy.scss +61 -0
- package/core/css/core/_consent.scss +424 -0
- package/core/css/core/_custom-font-awesome.scss +146 -0
- package/core/css/core/_exit-popup.scss +42 -0
- package/core/css/core/_generated-badge.scss +33 -0
- package/core/css/core/_initialize.scss +29 -0
- package/core/css/core/_lazy-loading.scss +104 -0
- package/core/css/core/_social-sharing.scss +41 -0
- package/core/css/core/_tones.scss +32 -0
- package/core/css/core/_utilities.scss +157 -0
- package/core/css/main.scss +54 -0
- package/core/css/motion/_index.scss +368 -0
- package/core/css/pages/404/index.scss +0 -0
- package/core/css/pages/admin/calendar/index.scss +692 -0
- package/core/css/pages/admin/firebase/index.scss +33 -0
- package/core/css/pages/admin/index.scss +9 -0
- package/core/css/pages/admin/users/index.scss +9 -0
- package/core/css/pages/admin/verts/index.scss +9 -0
- package/core/css/pages/alternatives/[alternative]/index.scss +0 -0
- package/core/css/pages/blog/[slug].scss +55 -0
- package/core/css/pages/dashboard/account/index.scss +242 -0
- package/core/css/pages/download/index.scss +480 -0
- package/core/css/pages/extension/installed/index.scss +317 -0
- package/core/css/pages/feedback/index.scss +31 -0
- package/core/css/pages/payment/confirmation/index.scss +14 -0
- package/core/css/pages/pricing/index.scss +0 -0
- package/core/css/pages/status/index.scss +162 -0
- package/core/css/pages/test/libraries/layers/index.scss +28 -0
- package/core/css/pages/updates/[update].scss +46 -0
- package/core/css/pages/updates/index.scss +13 -0
- package/core/css/shell/_index.scss +207 -0
- package/core/css/tokens/_index.scss +166 -0
- package/core/icons/flags/ar.svg +1 -0
- package/core/icons/flags/bd.svg +1 -0
- package/core/icons/flags/ca.svg +1 -0
- package/core/icons/flags/ch.svg +1 -0
- package/core/icons/flags/cn.svg +1 -0
- package/core/icons/flags/co.svg +1 -0
- package/core/icons/flags/cw.svg +1 -0
- package/core/icons/flags/cz.svg +1 -0
- package/core/icons/flags/de.svg +1 -0
- package/core/icons/flags/dk.svg +1 -0
- package/core/icons/flags/eg.svg +1 -0
- package/core/icons/flags/es.svg +1 -0
- package/core/icons/flags/fi.svg +1 -0
- package/core/icons/flags/fr.svg +1 -0
- package/core/icons/flags/gb.svg +1 -0
- package/core/icons/flags/gq.svg +1 -0
- package/core/icons/flags/gr.svg +1 -0
- package/core/icons/flags/gt.svg +1 -0
- package/core/icons/flags/hn.svg +1 -0
- package/core/icons/flags/id.svg +1 -0
- package/core/icons/flags/ie.svg +1 -0
- package/core/icons/flags/in.svg +1 -0
- package/core/icons/flags/ir.svg +1 -0
- package/core/icons/flags/it.svg +1 -0
- package/core/icons/flags/jp.svg +1 -0
- package/core/icons/flags/kp.svg +1 -0
- package/core/icons/flags/kr.svg +1 -0
- package/core/icons/flags/ly.svg +1 -0
- package/core/icons/flags/ma.svg +1 -0
- package/core/icons/flags/mu.svg +1 -0
- package/core/icons/flags/mx.svg +1 -0
- package/core/icons/flags/ng.svg +1 -0
- package/core/icons/flags/pa.svg +1 -0
- package/core/icons/flags/ph.svg +1 -0
- package/core/icons/flags/pk.svg +1 -0
- package/core/icons/flags/pt.svg +1 -0
- package/core/icons/flags/ru.svg +1 -0
- package/core/icons/flags/sa.svg +1 -0
- package/core/icons/flags/sd.svg +1 -0
- package/core/icons/flags/se.svg +1 -0
- package/core/icons/flags/th.svg +1 -0
- package/core/icons/flags/tm.svg +1 -0
- package/core/icons/flags/tr.svg +1 -0
- package/core/icons/flags/us.svg +1 -0
- package/core/icons/flags/uy.svg +1 -0
- package/core/icons/flags/vn.svg +1 -0
- package/core/icons/flags/za.svg +1 -0
- package/core/images/exit-popup/subscriber-1.jpg +0 -0
- package/core/images/exit-popup/subscriber-2.jpg +0 -0
- package/core/images/exit-popup/subscriber-3.jpg +0 -0
- package/core/images/exit-popup/subscriber-4.jpg +0 -0
- package/core/images/placeholder/photo-1.jpg +0 -0
- package/core/images/placeholder/photo-2.jpg +0 -0
- package/core/images/placeholder/photo-3.jpg +0 -0
- package/core/js/core/analytics-loader.js +253 -0
- package/core/js/core/app-shell.js +136 -0
- package/core/js/core/appearance.js +195 -0
- package/core/js/core/auth.js +457 -0
- package/core/js/core/complete.js +72 -0
- package/core/js/core/consent.js +384 -0
- package/core/js/core/dev-icon-audit.js +63 -0
- package/core/js/core/dev-palette.js +670 -0
- package/core/js/core/dev-sections.js +53 -0
- package/core/js/core/exit-popup.js +276 -0
- package/core/js/core/initialize.js +8 -0
- package/core/js/core/language-switcher.js +167 -0
- package/core/js/core/lazy-loading.js +479 -0
- package/core/js/core/motion.js +28 -0
- package/core/js/core/query-strings.js +146 -0
- package/core/js/core/service-worker.js +28 -0
- package/core/js/core/social-sharing.js +293 -0
- package/core/js/first-paint.js +35 -0
- package/core/js/layouts/modules/utilities/redirect.js +122 -0
- package/core/js/libs/admin-helpers.js +62 -0
- package/core/js/libs/alert-dismiss.js +35 -0
- package/core/js/libs/analytics.js +368 -0
- package/core/js/libs/auth/email.js +144 -0
- package/core/js/libs/auth/errors.js +119 -0
- package/core/js/libs/auth/forms.js +206 -0
- package/core/js/libs/auth/index.js +181 -0
- package/core/js/libs/auth/oauth.js +394 -0
- package/core/js/libs/auth/orphan.js +110 -0
- package/core/js/libs/auth/password-toggle.js +44 -0
- package/core/js/libs/auth/session-params.js +266 -0
- package/core/js/libs/auth/tracking.js +41 -0
- package/core/js/libs/charts.js +516 -0
- package/core/js/libs/consent-region.js +127 -0
- package/core/js/libs/dev.js +150 -0
- package/core/js/libs/graph.js +201 -0
- package/core/js/libs/initialize-tooltips.js +31 -0
- package/core/js/libs/logger.js +38 -0
- package/core/js/libs/omega-copy.js +89 -0
- package/core/js/libs/path-prefix.js +44 -0
- package/core/js/libs/payment-config.js +76 -0
- package/core/js/libs/recaptcha.js +94 -0
- package/core/js/libs/sale-name.js +295 -0
- package/core/js/libs/showcase-frames.js +37 -0
- package/core/js/libs/srcset.js +69 -0
- package/core/js/libs/tracking-consent.js +161 -0
- package/core/js/main.js +144 -0
- package/core/js/pages/404/index.js +57 -0
- package/core/js/pages/admin/calendar/calendar-core.js +701 -0
- package/core/js/pages/admin/calendar/calendar-events.js +853 -0
- package/core/js/pages/admin/calendar/calendar-renderer.js +807 -0
- package/core/js/pages/admin/calendar/campaign-preview.js +105 -0
- package/core/js/pages/admin/calendar/index.js +40 -0
- package/core/js/pages/admin/firebase/index.js +727 -0
- package/core/js/pages/admin/index.js +531 -0
- package/core/js/pages/admin/posts/editor/index.js +230 -0
- package/core/js/pages/admin/posts/index.js +237 -0
- package/core/js/pages/admin/studio/index.js +538 -0
- package/core/js/pages/admin/users/index.js +669 -0
- package/core/js/pages/admin/users/new/index.js +65 -0
- package/core/js/pages/admin/verts/index.js +407 -0
- package/core/js/pages/alternatives/[alternative]/index.js +17 -0
- package/core/js/pages/app/index.js +135 -0
- package/core/js/pages/blog/[slug].js +132 -0
- package/core/js/pages/blog/_search.mjs +108 -0
- package/core/js/pages/blog/index.js +193 -0
- package/core/js/pages/blog/page/[num]/index.js +7 -0
- package/core/js/pages/connections/callback/index.js +148 -0
- package/core/js/pages/contact/index.js +170 -0
- package/core/js/pages/cookies.js +2 -0
- package/core/js/pages/dashboard/account/index.js +366 -0
- package/core/js/pages/dashboard/account/modules/orders.js +82 -0
- package/core/js/pages/dashboard/account/sections/api-keys.js +149 -0
- package/core/js/pages/dashboard/account/sections/billing.js +1667 -0
- package/core/js/pages/dashboard/account/sections/connections.js +333 -0
- package/core/js/pages/dashboard/account/sections/data-request.js +225 -0
- package/core/js/pages/dashboard/account/sections/delete.js +80 -0
- package/core/js/pages/dashboard/account/sections/notifications.js +167 -0
- package/core/js/pages/dashboard/account/sections/orders.js +151 -0
- package/core/js/pages/dashboard/account/sections/profile.js +200 -0
- package/core/js/pages/dashboard/account/sections/referrals.js +217 -0
- package/core/js/pages/dashboard/account/sections/refund.js +253 -0
- package/core/js/pages/dashboard/account/sections/security.js +726 -0
- package/core/js/pages/dashboard/account/sections/team.js +247 -0
- package/core/js/pages/download/index.js +418 -0
- package/core/js/pages/extension/index.js +164 -0
- package/core/js/pages/extension/installed/index.js +135 -0
- package/core/js/pages/feedback/index.js +193 -0
- package/core/js/pages/legal/_document.js +91 -0
- package/core/js/pages/payment/checkout/index.js +420 -0
- package/core/js/pages/payment/checkout/modules/api.js +139 -0
- package/core/js/pages/payment/checkout/modules/dev-section.js +175 -0
- package/core/js/pages/payment/checkout/modules/discount.js +69 -0
- package/core/js/pages/payment/checkout/modules/pricing.js +73 -0
- package/core/js/pages/payment/checkout/modules/state.js +339 -0
- package/core/js/pages/payment/checkout/modules/tracking.js +54 -0
- package/core/js/pages/payment/confirmation/index.js +91 -0
- package/core/js/pages/payment/confirmation/modules/celebration.js +45 -0
- package/core/js/pages/payment/confirmation/modules/state.js +112 -0
- package/core/js/pages/payment/confirmation/modules/tracking.js +133 -0
- package/core/js/pages/payment/confirmation/modules/verify.js +142 -0
- package/core/js/pages/portal/email-preferences/index.js +183 -0
- package/core/js/pages/pricing/index.js +425 -0
- package/core/js/pages/privacy.js +2 -0
- package/core/js/pages/reset/index.js +12 -0
- package/core/js/pages/signin/index.js +22 -0
- package/core/js/pages/signup/index.js +23 -0
- package/core/js/pages/status/index.js +687 -0
- package/core/js/pages/terms.js +2 -0
- package/core/js/pages/test/components/[category]/[name]/index.js +26 -0
- package/core/js/pages/test/libraries/appearance/index.js +220 -0
- package/core/js/pages/test/libraries/form-manager/index.js +328 -0
- package/core/js/pages/test/libraries/layers/index.js +16 -0
- package/core/js/pages/test/libraries/verts/index.js +68 -0
- package/core/js/pages/test/sections/[category]/[name]/index.js +28 -0
- package/core/js/pages/test/styleguide/index.js +35 -0
- package/core/js/pages/token/index.js +203 -0
- package/core/logos/brandmarks/original/apple-app-store.svg +1 -0
- package/core/logos/brandmarks/original/apple.svg +6 -0
- package/core/logos/brandmarks/original/bitbucket.svg +14 -0
- package/core/logos/brandmarks/original/bitcoin.svg +1 -0
- package/core/logos/brandmarks/original/bluesky.svg +7 -0
- package/core/logos/brandmarks/original/bootstrap.svg +43 -0
- package/core/logos/brandmarks/original/brave.svg +1 -0
- package/core/logos/brandmarks/original/chrome.svg +1 -0
- package/core/logos/brandmarks/original/discord.svg +1 -0
- package/core/logos/brandmarks/original/dropbox.svg +10 -0
- package/core/logos/brandmarks/original/edge.svg +1 -0
- package/core/logos/brandmarks/original/facebook-f.svg +1 -0
- package/core/logos/brandmarks/original/facebook.svg +8 -0
- package/core/logos/brandmarks/original/figma.svg +10 -0
- package/core/logos/brandmarks/original/firefox.svg +1 -0
- package/core/logos/brandmarks/original/github.svg +6 -0
- package/core/logos/brandmarks/original/gitlab.svg +17 -0
- package/core/logos/brandmarks/original/google-a-s.svg +1 -0
- package/core/logos/brandmarks/original/google-chrome-webstore.svg +49 -0
- package/core/logos/brandmarks/original/google-play-store.svg +1 -0
- package/core/logos/brandmarks/original/google-web-search.svg +1 -0
- package/core/logos/brandmarks/original/google.svg +1 -0
- package/core/logos/brandmarks/original/gulp.svg +4 -0
- package/core/logos/brandmarks/original/html-5.svg +1 -0
- package/core/logos/brandmarks/original/instagram.svg +1 -0
- package/core/logos/brandmarks/original/javascript.svg +1 -0
- package/core/logos/brandmarks/original/jira.svg +18 -0
- package/core/logos/brandmarks/original/kick.svg +7 -0
- package/core/logos/brandmarks/original/linkedin.svg +1 -0
- package/core/logos/brandmarks/original/linux.svg +28 -0
- package/core/logos/brandmarks/original/messenger.svg +16 -0
- package/core/logos/brandmarks/original/microsoft.svg +1 -0
- package/core/logos/brandmarks/original/npm.svg +8 -0
- package/core/logos/brandmarks/original/opera.svg +1 -0
- package/core/logos/brandmarks/original/patreon.svg +6 -0
- package/core/logos/brandmarks/original/pinterest.svg +6 -0
- package/core/logos/brandmarks/original/reddit.svg +1 -0
- package/core/logos/brandmarks/original/safari.svg +31 -0
- package/core/logos/brandmarks/original/sass.svg +3 -0
- package/core/logos/brandmarks/original/signal.svg +6 -0
- package/core/logos/brandmarks/original/skype.svg +47 -0
- package/core/logos/brandmarks/original/slack.svg +33 -0
- package/core/logos/brandmarks/original/soundcloud.svg +1 -0
- package/core/logos/brandmarks/original/spotify.svg +5 -0
- package/core/logos/brandmarks/original/steam.svg +6 -0
- package/core/logos/brandmarks/original/telegram.svg +14 -0
- package/core/logos/brandmarks/original/tiktok.svg +1 -0
- package/core/logos/brandmarks/original/trello.svg +16 -0
- package/core/logos/brandmarks/original/tumblr.svg +1 -0
- package/core/logos/brandmarks/original/twitch.svg +6 -0
- package/core/logos/brandmarks/original/twitter.svg +6 -0
- package/core/logos/brandmarks/original/unity.svg +6 -0
- package/core/logos/brandmarks/original/x.svg +7 -0
- package/core/logos/brandmarks/original/yahoo-y.svg +1 -0
- package/core/logos/brandmarks/original/youtube-square.svg +1 -0
- package/core/logos/brandmarks/original/youtube.svg +1 -0
- package/core/logos/combomarks/dark/airbnb.svg +30 -0
- package/core/logos/combomarks/dark/slack.svg +38 -0
- package/core/logos/combomarks/dark/spotify.svg +35 -0
- package/core/logos/combomarks/dark/twilio.svg +26 -0
- package/core/logos/combomarks/dark/weebly.svg +35 -0
- package/core/logos/combomarks/gray/airbnb.svg +33 -0
- package/core/logos/combomarks/gray/alphabet.svg +23 -0
- package/core/logos/combomarks/gray/amazon.svg +38 -0
- package/core/logos/combomarks/gray/boring-company.svg +91 -0
- package/core/logos/combomarks/gray/burberry.svg +239 -0
- package/core/logos/combomarks/gray/embark.svg +21 -0
- package/core/logos/combomarks/gray/fitbit.svg +61 -0
- package/core/logos/combomarks/gray/fred-perry.svg +94 -0
- package/core/logos/combomarks/gray/google.svg +27 -0
- package/core/logos/combomarks/gray/hollister.svg +34 -0
- package/core/logos/combomarks/gray/hubspot.svg +27 -0
- package/core/logos/combomarks/gray/kaplan.svg +33 -0
- package/core/logos/combomarks/gray/lenovo.svg +27 -0
- package/core/logos/combomarks/gray/levis.svg +25 -0
- package/core/logos/combomarks/gray/mapbox.svg +37 -0
- package/core/logos/combomarks/gray/netflix.svg +15 -0
- package/core/logos/combomarks/gray/new-balance.svg +32 -0
- package/core/logos/combomarks/gray/nike.svg +17 -0
- package/core/logos/combomarks/gray/paypal.svg +41 -0
- package/core/logos/combomarks/gray/puma.svg +82 -0
- package/core/logos/combomarks/gray/samsung.svg +26 -0
- package/core/logos/combomarks/gray/shopify.svg +36 -0
- package/core/logos/combomarks/gray/slack.svg +42 -0
- package/core/logos/combomarks/gray/spotify.svg +38 -0
- package/core/logos/combomarks/gray/stripe.svg +24 -0
- package/core/logos/combomarks/gray/tnf.svg +30 -0
- package/core/logos/combomarks/gray/twilio.svg +29 -0
- package/core/logos/combomarks/gray/uber.svg +24 -0
- package/core/logos/combomarks/gray/weebly.svg +38 -0
- package/core/logos/combomarks/original/airbnb.svg +33 -0
- package/core/logos/combomarks/original/alphabet.svg +23 -0
- package/core/logos/combomarks/original/amazon.svg +38 -0
- package/core/logos/combomarks/original/asana.svg +8 -0
- package/core/logos/combomarks/original/boring-company.svg +91 -0
- package/core/logos/combomarks/original/discord.svg +6 -0
- package/core/logos/combomarks/original/embark.svg +24 -0
- package/core/logos/combomarks/original/fitbit.svg +60 -0
- package/core/logos/combomarks/original/github.svg +6 -0
- package/core/logos/combomarks/original/gitlab.svg +20 -0
- package/core/logos/combomarks/original/google.svg +28 -0
- package/core/logos/combomarks/original/hubspot.svg +29 -0
- package/core/logos/combomarks/original/instagram.svg +7 -0
- package/core/logos/combomarks/original/kickstarter.svg +6 -0
- package/core/logos/combomarks/original/lenovo.svg +24 -0
- package/core/logos/combomarks/original/linkedin.svg +6 -0
- package/core/logos/combomarks/original/mapbox.svg +39 -0
- package/core/logos/combomarks/original/mastodon.svg +7 -0
- package/core/logos/combomarks/original/medium.svg +6 -0
- package/core/logos/combomarks/original/meta.svg +22 -0
- package/core/logos/combomarks/original/microsoft.svg +13 -0
- package/core/logos/combomarks/original/monday.svg +12 -0
- package/core/logos/combomarks/original/netflix.svg +15 -0
- package/core/logos/combomarks/original/notion.svg +9 -0
- package/core/logos/combomarks/original/paypal.svg +41 -0
- package/core/logos/combomarks/original/reddit.svg +13 -0
- package/core/logos/combomarks/original/samsung.svg +25 -0
- package/core/logos/combomarks/original/shopify.svg +35 -0
- package/core/logos/combomarks/original/slack.svg +47 -0
- package/core/logos/combomarks/original/spotify.svg +37 -0
- package/core/logos/combomarks/original/stackoverflow.svg +28 -0
- package/core/logos/combomarks/original/stripe.svg +24 -0
- package/core/logos/combomarks/original/tiktok.svg +12 -0
- package/core/logos/combomarks/original/tumblr.svg +6 -0
- package/core/logos/combomarks/original/uber.svg +24 -0
- package/core/logos/combomarks/original/weebly.svg +34 -0
- package/core/logos/combomarks/original/whatsapp.svg +20 -0
- package/core/logos/combomarks/original/youtube.svg +10 -0
- package/core/logos/combomarks/original/zoom.svg +7 -0
- package/core/logos/combomarks/white/airbnb.svg +33 -0
- package/core/logos/combomarks/white/alphabet.svg +23 -0
- package/core/logos/combomarks/white/amazon.svg +38 -0
- package/core/logos/combomarks/white/boring-company.svg +91 -0
- package/core/logos/combomarks/white/embark.svg +21 -0
- package/core/logos/combomarks/white/fitbit.svg +61 -0
- package/core/logos/combomarks/white/google.svg +27 -0
- package/core/logos/combomarks/white/hubspot.svg +27 -0
- package/core/logos/combomarks/white/kaplan.svg +33 -0
- package/core/logos/combomarks/white/lenovo.svg +27 -0
- package/core/logos/combomarks/white/mapbox.svg +37 -0
- package/core/logos/combomarks/white/netflix.svg +15 -0
- package/core/logos/combomarks/white/paypal.svg +40 -0
- package/core/logos/combomarks/white/samsung.svg +26 -0
- package/core/logos/combomarks/white/shopify.svg +36 -0
- package/core/logos/combomarks/white/slack.svg +42 -0
- package/core/logos/combomarks/white/spotify.svg +38 -0
- package/core/logos/combomarks/white/stripe.svg +24 -0
- package/core/logos/combomarks/white/twilio.svg +29 -0
- package/core/logos/combomarks/white/uber.svg +24 -0
- package/core/logos/combomarks/white/weebly.svg +38 -0
- package/defaults/pages/404.md +4 -0
- package/defaults/pages/about.md +4 -0
- package/defaults/pages/admin/calendar/index.html +4 -0
- package/defaults/pages/admin/firebase/index.html +4 -0
- package/defaults/pages/admin/index.md +4 -0
- package/defaults/pages/admin/posts/editor.html +4 -0
- package/defaults/pages/admin/posts/index.html +4 -0
- package/defaults/pages/admin/stackblitz/index.html +4 -0
- package/defaults/pages/admin/studio/index.html +4 -0
- package/defaults/pages/admin/users/index.html +4 -0
- package/defaults/pages/admin/users/new.html +4 -0
- package/defaults/pages/admin/verts/index.html +4 -0
- package/defaults/pages/ads-txt.html +11 -0
- package/defaults/pages/alternatives/index.md +4 -0
- package/defaults/pages/app.md +4 -0
- package/defaults/pages/auth/reset.md +4 -0
- package/defaults/pages/auth/signin.md +4 -0
- package/defaults/pages/auth/signup.md +4 -0
- package/defaults/pages/auth/token.md +4 -0
- package/defaults/pages/blog/categories/category.html +21 -0
- package/defaults/pages/blog/categories.md +4 -0
- package/defaults/pages/blog/index-json.html +27 -0
- package/defaults/pages/blog/tags/tag.html +21 -0
- package/defaults/pages/blog/tags.md +4 -0
- package/defaults/pages/blog.md +22 -0
- package/defaults/pages/careers.md +4 -0
- package/defaults/pages/connections/callback.md +4 -0
- package/defaults/pages/contact.md +4 -0
- package/defaults/pages/dashboard/account.md +4 -0
- package/defaults/pages/dashboard/index.md +12 -0
- package/defaults/pages/download.md +4 -0
- package/defaults/pages/extension/index.html +4 -0
- package/defaults/pages/extension/installed.html +4 -0
- package/defaults/pages/feedback.md +4 -0
- package/defaults/pages/feeds/posts-json.html +40 -0
- package/defaults/pages/feeds/posts-xml.html +38 -0
- package/defaults/pages/humans.html +25 -0
- package/defaults/pages/index.md +4 -0
- package/defaults/pages/legal/cookies.md +4 -0
- package/defaults/pages/legal/privacy.md +4 -0
- package/defaults/pages/legal/terms.md +4 -0
- package/defaults/pages/llms-txt.html +46 -0
- package/defaults/pages/opensearch.html +16 -0
- package/defaults/pages/pages-json.html +42 -0
- package/defaults/pages/payment/checkout.md +4 -0
- package/defaults/pages/payment/confirmation.md +4 -0
- package/defaults/pages/portal/email-preferences.md +4 -0
- package/defaults/pages/pricing.md +4 -0
- package/defaults/pages/redirects/account.md +12 -0
- package/defaults/pages/redirects/admin/dashboard.md +11 -0
- package/defaults/pages/redirects/auth/change-password.md +9 -0
- package/defaults/pages/redirects/auth/forgot.md +9 -0
- package/defaults/pages/redirects/auth/join.md +9 -0
- package/defaults/pages/redirects/auth/login.md +9 -0
- package/defaults/pages/redirects/auth/recover.md +9 -0
- package/defaults/pages/redirects/auth/register.md +9 -0
- package/defaults/pages/redirects/auth/reset-password.md +9 -0
- package/defaults/pages/redirects/billing/cancel.md +9 -0
- package/defaults/pages/redirects/billing/refund.md +9 -0
- package/defaults/pages/robots.html +36 -0
- package/defaults/pages/security-txt.html +11 -0
- package/defaults/pages/sitemap.html +37 -0
- package/defaults/pages/status.md +4 -0
- package/defaults/pages/team/index.md +4 -0
- package/defaults/pages/test/account/dashboard.html +16 -0
- package/defaults/pages/test/index.md +12 -0
- package/defaults/pages/test/libraries/appearance.html +268 -0
- package/defaults/pages/test/libraries/cover.html +50 -0
- package/defaults/pages/test/libraries/error.html +30 -0
- package/defaults/pages/test/libraries/firestore.html +202 -0
- package/defaults/pages/test/libraries/form-manager.html +374 -0
- package/defaults/pages/test/libraries/layers.html +52 -0
- package/defaults/pages/test/libraries/lazy-loading.html +450 -0
- package/defaults/pages/test/libraries/verts.html +149 -0
- package/defaults/pages/test/redirect/external.md +13 -0
- package/defaults/pages/test/redirect/internal.md +13 -0
- package/defaults/pages/test/styleguide/index.html +588 -0
- package/defaults/pages/test/translation/index.md +50 -0
- package/defaults/pages/updates/index.md +4 -0
- package/defaults/sample-posts/2026-01-12-welcome-to-the-blog.md +26 -0
- package/defaults/sample-posts/2026-02-03-a-field-guide-to-good-defaults.md +50 -0
- package/defaults/sample-posts/2026-02-24-shipping-week-notes.md +37 -0
- package/defaults/sample-posts/2026-03-17-the-case-for-one-config.md +36 -0
- package/defaults/sample-posts/2026-04-09-write-release-notes-people-read.md +34 -0
- package/defaults/sample-posts/2026-05-21-dark-mode-done-properly.md +38 -0
- package/defaults/sample-posts/2026-06-02-the-boring-deploy-is-the-good-deploy.md +30 -0
- package/defaults/sample-posts/2026-06-11-naming-things-a-survival-guide.md +30 -0
- package/defaults/sample-posts/2026-06-19-your-pricing-page-is-a-product.md +28 -0
- package/defaults/sample-posts/2026-06-30-email-is-an-api-with-feelings.md +28 -0
- package/defaults/sample-posts/2026-07-08-ship-the-docs-with-the-diff.md +30 -0
- package/defaults/sample-team/avery-quinn.md +22 -0
- package/defaults/sample-team/jordan-reyes.md +25 -0
- package/defaults/sample-team/riley-nakamura.md +22 -0
- package/defaults/sample-team/sam-okafor.md +22 -0
- package/defaults/sample-updates/v1.0.0.md +28 -0
- package/defaults/sample-updates/v1.1.0.md +27 -0
- package/defaults/sample-updates/v1.2.0.md +27 -0
- package/defaults/sample-updates/v1.3.0.md +28 -0
- package/defaults/showcase/components.html +62 -0
- package/defaults/showcase/entry.html +76 -0
- package/defaults/showcase/frame.html +62 -0
- package/defaults/showcase/index.html +59 -0
- package/dist/assets.js +1349 -0
- package/dist/brand-tokens.js +195 -0
- package/dist/build.js +192 -0
- package/dist/cachebreak-html.js +71 -0
- package/dist/cli-run.js +25 -0
- package/dist/cli.js +47 -0
- package/dist/collections.js +183 -0
- package/dist/commands/audit.js +470 -0
- package/dist/commands/build.js +197 -0
- package/dist/commands/clean.js +18 -0
- package/dist/commands/customize.js +125 -0
- package/dist/commands/deploy.js +441 -0
- package/dist/commands/dev.js +1067 -0
- package/dist/commands/install.js +31 -0
- package/dist/commands/lib/deploy-precheck.js +43 -0
- package/dist/commands/lib/ensure-target.js +115 -0
- package/dist/commands/migrate.js +86 -0
- package/dist/commands/purge.js +31 -0
- package/dist/commands/test.js +237 -0
- package/dist/commands/translate.js +57 -0
- package/dist/commands/update.js +22 -0
- package/dist/commands/version.js +11 -0
- package/dist/config-sections.js +245 -0
- package/dist/consumer-scan.js +118 -0
- package/dist/consumer.js +49 -0
- package/dist/customize.js +328 -0
- package/dist/decisions.js +153 -0
- package/dist/dist-audit.js +417 -0
- package/dist/dynamic-pages.js +357 -0
- package/dist/engine.js +1402 -0
- package/dist/firebase-auth-helpers.js +198 -0
- package/dist/font-metrics.js +420 -0
- package/dist/frontmatter-liquid.js +253 -0
- package/dist/github-secrets.js +80 -0
- package/dist/hero-animations.js +170 -0
- package/dist/imagemin.js +261 -0
- package/dist/index.js +41 -0
- package/dist/inline-icons.js +65 -0
- package/dist/language-flags.js +55 -0
- package/dist/layers.js +80 -0
- package/dist/layouts.js +44 -0
- package/dist/limit-collections.js +199 -0
- package/dist/link-resolver.js +303 -0
- package/dist/markdown-images.js +85 -0
- package/dist/merge.js +20 -0
- package/dist/migrate/codemod.js +172 -0
- package/dist/migrate/config-convert.js +448 -0
- package/dist/migrate/consumer-assets.js +130 -0
- package/dist/migrate/index.js +223 -0
- package/dist/migrate/lint.js +108 -0
- package/dist/migrate/rules.js +1929 -0
- package/dist/minify-html.js +148 -0
- package/dist/mode-helpers.js +96 -0
- package/dist/omega-bin.js +7 -0
- package/dist/overrides.js +190 -0
- package/dist/path-prefix.js +167 -0
- package/dist/paths.js +37 -0
- package/dist/pricing.js +301 -0
- package/dist/purge.js +95 -0
- package/dist/redirect-page.js +45 -0
- package/dist/sample-content.js +210 -0
- package/dist/scaffold.js +123 -0
- package/dist/sections.js +810 -0
- package/dist/service-worker.js +200 -0
- package/dist/social-pages.js +142 -0
- package/dist/srcset.js +67 -0
- package/dist/static-assets.js +154 -0
- package/dist/target-shortlinks.js +123 -0
- package/dist/theme-vocabulary.js +76 -0
- package/dist/translate/collect-text-nodes.js +93 -0
- package/dist/translate/default-routes.js +152 -0
- package/dist/translate/generate-defaults.js +297 -0
- package/dist/translate/index.js +568 -0
- package/dist/translate/packaged-defaults.js +127 -0
- package/dist/translate/sitemap.js +108 -0
- package/dist/vendor/account/engine.js +182 -0
- package/dist/vendor/account/features.js +220 -0
- package/dist/vendor/account/index.js +53 -0
- package/dist/vendor/account/schema.js +272 -0
- package/dist/vendor/account/subscription.js +38 -0
- package/dist/vendor/config/company.js +31 -0
- package/dist/vendor/config/defaults.js +173 -0
- package/dist/vendor/config/demo.js +18 -0
- package/dist/vendor/config/desktop-artifacts.js +110 -0
- package/dist/vendor/config/edit.js +769 -0
- package/dist/vendor/config/env-delivery.js +145 -0
- package/dist/vendor/config/env-rules.js +93 -0
- package/dist/vendor/config/env-schema.js +1078 -0
- package/dist/vendor/config/env.js +445 -0
- package/dist/vendor/config/hooks.js +97 -0
- package/dist/vendor/config/index.js +237 -0
- package/dist/vendor/config/instances.js +208 -0
- package/dist/vendor/config/load.js +490 -0
- package/dist/vendor/config/merge.js +68 -0
- package/dist/vendor/config/order.js +139 -0
- package/dist/vendor/config/ports.js +374 -0
- package/dist/vendor/config/providers.js +32 -0
- package/dist/vendor/config/repo.js +142 -0
- package/dist/vendor/config/retired-keys.js +430 -0
- package/dist/vendor/config/schema.js +1610 -0
- package/dist/vendor/config/secrets.js +50 -0
- package/dist/vendor/config/seed.js +34 -0
- package/dist/vendor/config/site-global.js +205 -0
- package/dist/vendor/config/validate.js +554 -0
- package/dist/vendor/config/winback.js +61 -0
- package/dist/vendor/devkit/actions-secrets.js +145 -0
- package/dist/vendor/devkit/attach-log-file.js +262 -0
- package/dist/vendor/devkit/bare-requires.js +153 -0
- package/dist/vendor/devkit/bundle.js +292 -0
- package/dist/vendor/devkit/ci-workflows.js +520 -0
- package/dist/vendor/devkit/cli-router.js +156 -0
- package/dist/vendor/devkit/command-path.js +46 -0
- package/dist/vendor/devkit/defaults-engine.js +372 -0
- package/dist/vendor/devkit/deploy-precheck.js +54 -0
- package/dist/vendor/devkit/deploy-record.js +180 -0
- package/dist/vendor/devkit/deploy.js +286 -0
- package/dist/vendor/devkit/env-watch.js +132 -0
- package/dist/vendor/devkit/framework-deps.js +46 -0
- package/dist/vendor/devkit/icons.js +178 -0
- package/dist/vendor/devkit/license.js +155 -0
- package/dist/vendor/devkit/local-https.js +360 -0
- package/dist/vendor/devkit/local.js +1905 -0
- package/dist/vendor/devkit/logger.js +128 -0
- package/dist/vendor/devkit/merge-line-files.js +296 -0
- package/dist/vendor/devkit/omega-bin.js +345 -0
- package/dist/vendor/devkit/reads.js +274 -0
- package/dist/vendor/devkit/safe-install.js +18 -0
- package/dist/vendor/devkit/scaffold-guard.js +96 -0
- package/dist/vendor/devkit/stop-signals.js +28 -0
- package/dist/vendor/devkit/strip-dev-blocks-plugin.js +29 -0
- package/dist/vendor/devkit/strip-dev-blocks.js +32 -0
- package/dist/vendor/devkit/target-secrets.js +165 -0
- package/dist/vendor/devkit/test/scope.js +162 -0
- package/dist/vendor/devkit/translate/cache.js +84 -0
- package/dist/vendor/devkit/translate/engine.js +243 -0
- package/dist/vendor/devkit/translate/index.js +49 -0
- package/dist/vendor/devkit/translate/languages.js +134 -0
- package/dist/vendor/devkit/translate/providers.js +188 -0
- package/dist/vendor/devkit/update.js +569 -0
- package/dist/vendor/template-kit/data/language-flags.js +54 -0
- package/dist/vendor/template-kit/data/languages.js +195 -0
- package/dist/vendor/template-kit/data/social-urls.js +26 -0
- package/dist/vendor/template-kit/filters.js +227 -0
- package/dist/vendor/template-kit/index.js +28 -0
- package/dist/vendor/template-kit/jekyll-compat.js +268 -0
- package/dist/vendor/template-kit/register-liquid.js +0 -0
- package/dist/vendor/template-kit/tags/collections.js +191 -0
- package/dist/vendor/template-kit/tags/conditionals.js +101 -0
- package/dist/vendor/template-kit/tags/content.js +175 -0
- package/dist/vendor/template-kit/tags/index.js +33 -0
- package/dist/vendor/template-kit/tags/media.js +376 -0
- package/dist/vendor/template-kit/variable-resolver.js +163 -0
- package/docs/ads-system.md +109 -0
- package/docs/classy-v2/DIRECTION.md +92 -0
- package/docs/classy-v2/draft-1.html +1932 -0
- package/docs/classy-v2/draft-2.html +1956 -0
- package/docs/frontmatter.md +153 -0
- package/docs/index.md +254 -0
- package/docs/libs.md +298 -0
- package/docs/omega-sections-spec.md +159 -0
- package/docs/page-contract.md +112 -0
- package/docs/sections.md +845 -0
- package/docs/shared/agent-docs.md +89 -0
- package/docs/shared/analytics.md +612 -0
- package/docs/shared/brands.md +51 -0
- package/docs/shared/breaking-changes.md +497 -0
- package/docs/shared/config.md +1387 -0
- package/docs/shared/deploys.md +215 -0
- package/docs/shared/icons.md +201 -0
- package/docs/shared/local-dev.md +147 -0
- package/docs/shared/logging.md +202 -0
- package/docs/shared/monitoring.md +153 -0
- package/docs/shared/publishing.md +183 -0
- package/docs/shared/rulings.md +34 -0
- package/docs/shared/testing.md +147 -0
- package/docs/shared/theming.md +604 -0
- package/docs/shared/translation.md +291 -0
- package/docs/shared/updates.md +61 -0
- package/docs/template-kit.md +128 -0
- package/docs/test-framework.md +163 -0
- package/package.json +122 -0
- package/runtime/boot.js +149 -0
- package/runtime/icons.js +77 -0
- package/runtime/manager.js +36 -0
- package/scaffold/.github/workflows/build.yml +139 -0
- package/scaffold/.nvmrc +1 -0
- package/scaffold/AGENTS.md +55 -0
- package/scaffold/CLAUDE.md +1 -0
- package/scaffold/_.gitattributes +4 -0
- package/scaffold/_.gitignore +27 -0
- package/scaffold/config/omega.json5 +20 -0
- package/scaffold/src/assets/css/pages/.gitkeep +0 -0
- package/scaffold/src/assets/images/.gitkeep +0 -0
- package/scaffold/src/assets/js/pages/.gitkeep +0 -0
- package/scaffold/src/pages/.gitkeep +0 -0
- package/scaffold/src/pages/example.md.txt +94 -0
- package/scaffold/src/service-worker.js +16 -0
- package/sw/entry.js +19 -0
- package/sw/manager.js +281 -0
- package/themes/_template/README.md +115 -0
- package/themes/_template/_config.scss +60 -0
- package/themes/_template/_theme.js +14 -0
- package/themes/_template/_theme.scss +28 -0
- package/themes/_template/css/base/_root.scss +19 -0
- package/themes/_template/css/components/_components.scss +23 -0
- package/themes/base/README.md +5 -0
- package/themes/base/_components/data/org-chart/component.html +33 -0
- package/themes/base/_components/data/org-chart/component.json5 +33 -0
- package/themes/base/_components/data/org-chart/component.scss +183 -0
- package/themes/base/_components/heading/masthead/component.html +22 -0
- package/themes/base/_components/heading/masthead/component.json5 +30 -0
- package/themes/base/_components/heading/section-head/component.html +18 -0
- package/themes/base/_components/heading/section-head/component.json5 +27 -0
- package/themes/base/_components/pricing/features/component.html +29 -0
- package/themes/base/_components/pricing/features/component.json5 +30 -0
- package/themes/base/_hero/orbit/index.html +30 -0
- package/themes/base/_hero/orbit/script.js +33 -0
- package/themes/base/_hero/orbit/style.scss +133 -0
- package/themes/base/_includes/admin/sections/sidebar.html +3 -0
- package/themes/base/_includes/admin/sections/topbar.html +3 -0
- package/themes/base/_includes/backend/sections/sidebar.html +3 -0
- package/themes/base/_includes/backend/sections/topbar.html +3 -0
- package/themes/base/_includes/frontend/components/connection-card.html +49 -0
- package/themes/base/_includes/frontend/components/post-card.html +54 -0
- package/themes/base/_includes/frontend/sections/account-connections.html +42 -0
- package/themes/base/_includes/frontend/sections/account-section-header.html +11 -0
- package/themes/base/_includes/frontend/sections/footer.html +154 -0
- package/themes/base/_includes/frontend/sections/nav.html +276 -0
- package/themes/base/_includes/global/sections/account.html +72 -0
- package/themes/base/_includes/global/sections/app-sidebar.html +248 -0
- package/themes/base/_includes/global/sections/app-topbar.html +228 -0
- package/themes/base/_includes/global/sections/page-header.html +99 -0
- package/themes/base/_layouts/admin/core/minimal-viewport-locked.html +22 -0
- package/themes/base/_layouts/admin/core/minimal.html +23 -0
- package/themes/base/_layouts/backend/core/base.html +57 -0
- package/themes/base/_layouts/backend/core/minimal-viewport-locked.html +32 -0
- package/themes/base/_layouts/backend/core/minimal.html +32 -0
- package/themes/base/_layouts/backend/pages/dashboard/index.html +230 -0
- package/themes/base/_layouts/frontend/core/base.html +31 -0
- package/themes/base/_layouts/frontend/core/cover.html +40 -0
- package/themes/base/_layouts/frontend/core/minimal.html +47 -0
- package/themes/base/_layouts/frontend/pages/404.html +54 -0
- package/themes/base/_layouts/frontend/pages/about.html +144 -0
- package/themes/base/_layouts/frontend/pages/account/index.html +2104 -0
- package/themes/base/_layouts/frontend/pages/alternatives/alternative.html +319 -0
- package/themes/base/_layouts/frontend/pages/alternatives/index.html +186 -0
- package/themes/base/_layouts/frontend/pages/app.html +142 -0
- package/themes/base/_layouts/frontend/pages/auth/reset.html +53 -0
- package/themes/base/_layouts/frontend/pages/auth/signin.html +90 -0
- package/themes/base/_layouts/frontend/pages/auth/signup.html +141 -0
- package/themes/base/_layouts/frontend/pages/auth/token.html +44 -0
- package/themes/base/_layouts/frontend/pages/blog/categories/category.html +88 -0
- package/themes/base/_layouts/frontend/pages/blog/categories/index.html +88 -0
- package/themes/base/_layouts/frontend/pages/blog/index.html +255 -0
- package/themes/base/_layouts/frontend/pages/blog/post.html +261 -0
- package/themes/base/_layouts/frontend/pages/blog/tags/index.html +84 -0
- package/themes/base/_layouts/frontend/pages/blog/tags/tag.html +88 -0
- package/themes/base/_layouts/frontend/pages/collection/category.html +72 -0
- package/themes/base/_layouts/frontend/pages/collection/document.html +54 -0
- package/themes/base/_layouts/frontend/pages/collection/index.html +84 -0
- package/themes/base/_layouts/frontend/pages/connections/callback.html +55 -0
- package/themes/base/_layouts/frontend/pages/contact.html +214 -0
- package/themes/base/_layouts/frontend/pages/download.html +793 -0
- package/themes/base/_layouts/frontend/pages/extension/index.html +253 -0
- package/themes/base/_layouts/frontend/pages/extension/installed.html +198 -0
- package/themes/base/_layouts/frontend/pages/feedback.html +171 -0
- package/themes/base/_layouts/frontend/pages/index.html +66 -0
- package/themes/base/_layouts/frontend/pages/legal/document.html +78 -0
- package/themes/base/_layouts/frontend/pages/payment/checkout.html +459 -0
- package/themes/base/_layouts/frontend/pages/payment/confirmation.html +131 -0
- package/themes/base/_layouts/frontend/pages/portal/email-preferences.html +104 -0
- package/themes/base/_layouts/frontend/pages/pricing.html +596 -0
- package/themes/base/_layouts/frontend/pages/status.html +361 -0
- package/themes/base/_layouts/frontend/pages/team/index.html +210 -0
- package/themes/base/_layouts/frontend/pages/team/member.html +125 -0
- package/themes/base/_layouts/frontend/pages/updates/index.html +119 -0
- package/themes/base/_layouts/frontend/pages/updates/update.html +118 -0
- package/themes/base/_sections/about/hero/section.html +68 -0
- package/themes/base/_sections/about/hero/section.json5 +58 -0
- package/themes/base/_sections/about/letter/section.html +60 -0
- package/themes/base/_sections/about/letter/section.json5 +56 -0
- package/themes/base/_sections/about/photo-band/section.html +24 -0
- package/themes/base/_sections/about/photo-band/section.json5 +23 -0
- package/themes/base/_sections/about/principles/section.html +17 -0
- package/themes/base/_sections/about/principles/section.json5 +34 -0
- package/themes/base/_sections/about/timeline/section.html +20 -0
- package/themes/base/_sections/about/timeline/section.json5 +34 -0
- package/themes/base/_sections/marketing/bento/section.html +91 -0
- package/themes/base/_sections/marketing/bento/section.json5 +152 -0
- package/themes/base/_sections/marketing/cta/section.html +44 -0
- package/themes/base/_sections/marketing/cta/section.json5 +47 -0
- package/themes/base/_sections/marketing/faq/section.html +52 -0
- package/themes/base/_sections/marketing/faq/section.json5 +68 -0
- package/themes/base/_sections/marketing/hero/section.html +349 -0
- package/themes/base/_sections/marketing/hero/section.json5 +335 -0
- package/themes/base/_sections/marketing/newsletter-cta/section.html +38 -0
- package/themes/base/_sections/marketing/newsletter-cta/section.js +86 -0
- package/themes/base/_sections/marketing/newsletter-cta/section.json5 +29 -0
- package/themes/base/_sections/marketing/pricing-cards/section.html +115 -0
- package/themes/base/_sections/marketing/pricing-cards/section.json5 +72 -0
- package/themes/base/_sections/marketing/product-demo/section.html +62 -0
- package/themes/base/_sections/marketing/product-demo/section.js +44 -0
- package/themes/base/_sections/marketing/product-demo/section.json5 +52 -0
- package/themes/base/_sections/marketing/prose/section.html +22 -0
- package/themes/base/_sections/marketing/prose/section.json5 +41 -0
- package/themes/base/_sections/marketing/showcase/section.html +35 -0
- package/themes/base/_sections/marketing/showcase/section.json5 +52 -0
- package/themes/base/_sections/marketing/stats/section.html +32 -0
- package/themes/base/_sections/marketing/stats/section.json5 +44 -0
- package/themes/base/_sections/marketing/testimonials/section.html +54 -0
- package/themes/base/_sections/marketing/testimonials/section.json5 +70 -0
- package/themes/base/_sections/marketing/trusted-by/section.html +30 -0
- package/themes/base/_sections/marketing/trusted-by/section.json5 +38 -0
- package/themes/base/_sections/verts/unit/section.html +7 -0
- package/themes/base/_sections/verts/unit/section.js +22 -0
- package/themes/base/_sections/verts/unit/section.json5 +24 -0
- package/themes/base/_sections/verts/unit/section.scss +24 -0
- package/themes/base/_theme.js +13 -0
- package/themes/base/_theme.scss +4 -0
- package/themes/bootstrap/_theme.js +16 -0
- package/themes/bootstrap/_theme.scss +11 -0
- package/themes/bootstrap/js/index.esm.js +19 -0
- package/themes/bootstrap/js/index.umd.js +34 -0
- package/themes/bootstrap/js/src/alert.js +87 -0
- package/themes/bootstrap/js/src/base-component.js +86 -0
- package/themes/bootstrap/js/src/button.js +72 -0
- package/themes/bootstrap/js/src/carousel.js +474 -0
- package/themes/bootstrap/js/src/collapse.js +297 -0
- package/themes/bootstrap/js/src/dom/data.js +55 -0
- package/themes/bootstrap/js/src/dom/event-handler.js +317 -0
- package/themes/bootstrap/js/src/dom/manipulator.js +71 -0
- package/themes/bootstrap/js/src/dom/selector-engine.js +126 -0
- package/themes/bootstrap/js/src/dropdown.js +458 -0
- package/themes/bootstrap/js/src/modal.js +378 -0
- package/themes/bootstrap/js/src/offcanvas.js +282 -0
- package/themes/bootstrap/js/src/popover.js +97 -0
- package/themes/bootstrap/js/src/scrollspy.js +296 -0
- package/themes/bootstrap/js/src/tab.js +315 -0
- package/themes/bootstrap/js/src/toast.js +224 -0
- package/themes/bootstrap/js/src/tooltip.js +632 -0
- package/themes/bootstrap/js/src/util/backdrop.js +151 -0
- package/themes/bootstrap/js/src/util/component-functions.js +35 -0
- package/themes/bootstrap/js/src/util/config.js +65 -0
- package/themes/bootstrap/js/src/util/focustrap.js +115 -0
- package/themes/bootstrap/js/src/util/index.js +306 -0
- package/themes/bootstrap/js/src/util/sanitizer.js +117 -0
- package/themes/bootstrap/js/src/util/scrollbar.js +114 -0
- package/themes/bootstrap/js/src/util/swipe.js +146 -0
- package/themes/bootstrap/js/src/util/template-factory.js +160 -0
- package/themes/bootstrap/overrides/_accordion.scss +21 -0
- package/themes/bootstrap/overrides/_avatars.scss +51 -0
- package/themes/bootstrap/overrides/_blog-images.scss +45 -0
- package/themes/bootstrap/overrides/_buttons-adaptive.scss +141 -0
- package/themes/bootstrap/overrides/_color-shades.scss +25 -0
- package/themes/bootstrap/overrides/_cursor-utilities.scss +13 -0
- package/themes/bootstrap/overrides/_index.scss +30 -0
- package/themes/bootstrap/overrides/_links.scss +26 -0
- package/themes/bootstrap/overrides/_soft-colors.scss +99 -0
- package/themes/bootstrap/overrides/_spacing-extended.scss +74 -0
- package/themes/bootstrap/overrides/_spinners.scss +22 -0
- package/themes/bootstrap/overrides/_typography-utilities.scss +40 -0
- package/themes/bootstrap/scss/_accordion.scss +153 -0
- package/themes/bootstrap/scss/_alert.scss +68 -0
- package/themes/bootstrap/scss/_badge.scss +38 -0
- package/themes/bootstrap/scss/_breadcrumb.scss +40 -0
- package/themes/bootstrap/scss/_button-group.scss +147 -0
- package/themes/bootstrap/scss/_buttons.scss +216 -0
- package/themes/bootstrap/scss/_card.scss +238 -0
- package/themes/bootstrap/scss/_carousel.scss +226 -0
- package/themes/bootstrap/scss/_close.scss +66 -0
- package/themes/bootstrap/scss/_containers.scss +41 -0
- package/themes/bootstrap/scss/_dropdown.scss +250 -0
- package/themes/bootstrap/scss/_forms.scss +9 -0
- package/themes/bootstrap/scss/_functions.scss +302 -0
- package/themes/bootstrap/scss/_grid.scss +39 -0
- package/themes/bootstrap/scss/_helpers.scss +12 -0
- package/themes/bootstrap/scss/_images.scss +42 -0
- package/themes/bootstrap/scss/_list-group.scss +199 -0
- package/themes/bootstrap/scss/_maps.scss +174 -0
- package/themes/bootstrap/scss/_mixins.scss +42 -0
- package/themes/bootstrap/scss/_modal.scss +240 -0
- package/themes/bootstrap/scss/_nav.scss +197 -0
- package/themes/bootstrap/scss/_navbar.scss +289 -0
- package/themes/bootstrap/scss/_offcanvas.scss +147 -0
- package/themes/bootstrap/scss/_pagination.scss +109 -0
- package/themes/bootstrap/scss/_placeholders.scss +51 -0
- package/themes/bootstrap/scss/_popover.scss +196 -0
- package/themes/bootstrap/scss/_progress.scss +68 -0
- package/themes/bootstrap/scss/_reboot.scss +611 -0
- package/themes/bootstrap/scss/_root.scss +187 -0
- package/themes/bootstrap/scss/_spinners.scss +85 -0
- package/themes/bootstrap/scss/_tables.scss +171 -0
- package/themes/bootstrap/scss/_toasts.scss +73 -0
- package/themes/bootstrap/scss/_tooltip.scss +119 -0
- package/themes/bootstrap/scss/_transitions.scss +27 -0
- package/themes/bootstrap/scss/_type.scss +106 -0
- package/themes/bootstrap/scss/_utilities.scss +806 -0
- package/themes/bootstrap/scss/_variables-dark.scss +102 -0
- package/themes/bootstrap/scss/_variables.scss +1753 -0
- package/themes/bootstrap/scss/bootstrap-grid.scss +62 -0
- package/themes/bootstrap/scss/bootstrap-reboot.scss +10 -0
- package/themes/bootstrap/scss/bootstrap-utilities.scss +19 -0
- package/themes/bootstrap/scss/bootstrap.scss +52 -0
- package/themes/bootstrap/scss/forms/_floating-labels.scss +97 -0
- package/themes/bootstrap/scss/forms/_form-check.scss +189 -0
- package/themes/bootstrap/scss/forms/_form-control.scss +214 -0
- package/themes/bootstrap/scss/forms/_form-range.scss +91 -0
- package/themes/bootstrap/scss/forms/_form-select.scss +80 -0
- package/themes/bootstrap/scss/forms/_form-text.scss +11 -0
- package/themes/bootstrap/scss/forms/_input-group.scss +132 -0
- package/themes/bootstrap/scss/forms/_labels.scss +36 -0
- package/themes/bootstrap/scss/forms/_validation.scss +12 -0
- package/themes/bootstrap/scss/helpers/_clearfix.scss +3 -0
- package/themes/bootstrap/scss/helpers/_color-bg.scss +7 -0
- package/themes/bootstrap/scss/helpers/_colored-links.scss +30 -0
- package/themes/bootstrap/scss/helpers/_focus-ring.scss +5 -0
- package/themes/bootstrap/scss/helpers/_icon-link.scss +25 -0
- package/themes/bootstrap/scss/helpers/_position.scss +36 -0
- package/themes/bootstrap/scss/helpers/_ratio.scss +26 -0
- package/themes/bootstrap/scss/helpers/_stacks.scss +15 -0
- package/themes/bootstrap/scss/helpers/_stretched-link.scss +15 -0
- package/themes/bootstrap/scss/helpers/_text-truncation.scss +7 -0
- package/themes/bootstrap/scss/helpers/_visually-hidden.scss +8 -0
- package/themes/bootstrap/scss/helpers/_vr.scss +8 -0
- package/themes/bootstrap/scss/mixins/_alert.scss +18 -0
- package/themes/bootstrap/scss/mixins/_backdrop.scss +14 -0
- package/themes/bootstrap/scss/mixins/_banner.scss +7 -0
- package/themes/bootstrap/scss/mixins/_border-radius.scss +78 -0
- package/themes/bootstrap/scss/mixins/_box-shadow.scss +18 -0
- package/themes/bootstrap/scss/mixins/_breakpoints.scss +127 -0
- package/themes/bootstrap/scss/mixins/_buttons.scss +70 -0
- package/themes/bootstrap/scss/mixins/_caret.scss +69 -0
- package/themes/bootstrap/scss/mixins/_clearfix.scss +9 -0
- package/themes/bootstrap/scss/mixins/_color-mode.scss +21 -0
- package/themes/bootstrap/scss/mixins/_color-scheme.scss +7 -0
- package/themes/bootstrap/scss/mixins/_container.scss +11 -0
- package/themes/bootstrap/scss/mixins/_deprecate.scss +10 -0
- package/themes/bootstrap/scss/mixins/_forms.scss +163 -0
- package/themes/bootstrap/scss/mixins/_gradients.scss +47 -0
- package/themes/bootstrap/scss/mixins/_grid.scss +151 -0
- package/themes/bootstrap/scss/mixins/_image.scss +16 -0
- package/themes/bootstrap/scss/mixins/_list-group.scss +26 -0
- package/themes/bootstrap/scss/mixins/_lists.scss +7 -0
- package/themes/bootstrap/scss/mixins/_pagination.scss +10 -0
- package/themes/bootstrap/scss/mixins/_reset-text.scss +17 -0
- package/themes/bootstrap/scss/mixins/_resize.scss +6 -0
- package/themes/bootstrap/scss/mixins/_table-variants.scss +24 -0
- package/themes/bootstrap/scss/mixins/_text-truncate.scss +8 -0
- package/themes/bootstrap/scss/mixins/_transition.scss +26 -0
- package/themes/bootstrap/scss/mixins/_utilities.scss +97 -0
- package/themes/bootstrap/scss/mixins/_visually-hidden.scss +38 -0
- package/themes/bootstrap/scss/tests/jasmine.js +16 -0
- package/themes/bootstrap/scss/tests/mixins/_auto-import-of-variables-dark.test.scss +7 -0
- package/themes/bootstrap/scss/tests/mixins/_color-modes.test.scss +69 -0
- package/themes/bootstrap/scss/tests/mixins/_media-query-color-mode-full.test.scss +8 -0
- package/themes/bootstrap/scss/tests/mixins/_utilities.test.scss +393 -0
- package/themes/bootstrap/scss/tests/sass-true/register.js +14 -0
- package/themes/bootstrap/scss/tests/sass-true/runner.js +17 -0
- package/themes/bootstrap/scss/tests/utilities/_api.test.scss +75 -0
- package/themes/bootstrap/scss/utilities/_api.scss +47 -0
- package/themes/bootstrap/scss/vendor/_rfs.scss +348 -0
- package/themes/classy/README.md +89 -0
- package/themes/classy/_config.scss +245 -0
- package/themes/classy/_theme.js +31 -0
- package/themes/classy/_theme.scss +57 -0
- package/themes/classy/css/app/_panels.scss +183 -0
- package/themes/classy/css/base/_fonts.scss +79 -0
- package/themes/classy/css/base/_no-raise.scss +49 -0
- package/themes/classy/css/base/_root.scss +113 -0
- package/themes/classy/css/base/_typography.scss +154 -0
- package/themes/classy/css/base/_utilities.scss +284 -0
- package/themes/classy/css/components/_alerts.scss +51 -0
- package/themes/classy/css/components/_badges.scss +46 -0
- package/themes/classy/css/components/_buttons.scss +175 -0
- package/themes/classy/css/components/_cards.scss +41 -0
- package/themes/classy/css/components/_dropdowns.scss +48 -0
- package/themes/classy/css/components/_forms.scss +171 -0
- package/themes/classy/css/components/_receipt.scss +45 -0
- package/themes/classy/css/layout/_footer.scss +298 -0
- package/themes/classy/css/layout/_general.scss +209 -0
- package/themes/classy/css/layout/_nav.scss +154 -0
- package/themes/classy/css/layout/_shell.scss +458 -0
- package/themes/classy/css/marketing/_bento.scss +238 -0
- package/themes/classy/css/marketing/_content.scss +1328 -0
- package/themes/classy/css/marketing/_downloads.scss +246 -0
- package/themes/classy/css/marketing/_hero.scss +311 -0
- package/themes/classy/css/marketing/_sections.scss +649 -0
- package/themes/classy/css/pages/404/index.scss +67 -0
- package/themes/classy/css/pages/_auth.scss +235 -0
- package/themes/classy/css/pages/admin/firebase/index.scss +22 -0
- package/themes/classy/css/pages/blog/[slug].scss +56 -0
- package/themes/classy/css/pages/cookies.scss +2 -0
- package/themes/classy/css/pages/dashboard/account/index.scss +31 -0
- package/themes/classy/css/pages/feedback/index.scss +39 -0
- package/themes/classy/css/pages/legal/_document.scss +192 -0
- package/themes/classy/css/pages/payment/checkout/index.scss +585 -0
- package/themes/classy/css/pages/payment/confirmation/index.scss +9 -0
- package/themes/classy/css/pages/privacy.scss +2 -0
- package/themes/classy/css/pages/status/index.scss +164 -0
- package/themes/classy/css/pages/terms.scss +2 -0
- package/themes/classy/css/pages/updates/index.scss +128 -0
- package/themes/classy/fonts/inter-italic-latin-ext.woff2 +0 -0
- package/themes/classy/fonts/inter-italic-latin.woff2 +0 -0
- package/themes/classy/fonts/inter-normal-latin-ext.woff2 +0 -0
- package/themes/classy/fonts/inter-normal-latin.woff2 +0 -0
- package/themes/classy/fonts/newsreader-italic-latin-ext.woff2 +0 -0
- package/themes/classy/fonts/newsreader-italic-latin.woff2 +0 -0
- package/themes/classy/fonts/newsreader-normal-latin-ext.woff2 +0 -0
- package/themes/classy/fonts/newsreader-normal-latin.woff2 +0 -0
- package/themes/classy/js/hero-demo-form.js +56 -0
- package/themes/neobrutalism/README.md +118 -0
- package/themes/neobrutalism/_config.scss +139 -0
- package/themes/neobrutalism/_layouts/frontend/core/base.html +30 -0
- package/themes/neobrutalism/_layouts/frontend/pages/index.html +275 -0
- package/themes/neobrutalism/_theme.js +24 -0
- package/themes/neobrutalism/_theme.scss +61 -0
- package/themes/neobrutalism/css/base/_mixins.scss +46 -0
- package/themes/neobrutalism/css/base/_root.scss +169 -0
- package/themes/neobrutalism/css/base/_typography.scss +61 -0
- package/themes/neobrutalism/css/base/_utilities.scss +89 -0
- package/themes/neobrutalism/css/components/_billing-toggle.scss +74 -0
- package/themes/neobrutalism/css/components/_buttons.scss +148 -0
- package/themes/neobrutalism/css/components/_cards.scss +47 -0
- package/themes/neobrutalism/css/components/_forms.scss +88 -0
- package/themes/neobrutalism/css/components/_marketing.scss +244 -0
- package/themes/neobrutalism/css/layout/_general.scss +213 -0
- package/themes/neobrutalism/css/layout/_navigation.scss +171 -0
- package/themes/neobrutalism/css/pages/index.scss +203 -0
- package/themes/neobrutalism/css/pages/pricing/index.scss +58 -0
- package/themes/neobrutalism/css/pages/test/libraries/layers/index.scss +7 -0
- package/themes/neobrutalism/js/pages/test/libraries/layers/index.js +9 -0
- package/themes/newsflash/README.md +117 -0
- package/themes/newsflash/_components/heading/lede/component.html +14 -0
- package/themes/newsflash/_components/heading/lede/component.json5 +23 -0
- package/themes/newsflash/_components/heading/rule-head/component.html +17 -0
- package/themes/newsflash/_components/heading/rule-head/component.json5 +23 -0
- package/themes/newsflash/_components/news/byline/component.html +12 -0
- package/themes/newsflash/_components/news/byline/component.json5 +18 -0
- package/themes/newsflash/_components/news/story-card/component.html +25 -0
- package/themes/newsflash/_components/news/story-card/component.json5 +21 -0
- package/themes/newsflash/_config.scss +147 -0
- package/themes/newsflash/_layouts/frontend/core/base.html +51 -0
- package/themes/newsflash/_layouts/frontend/pages/blog/index.html +299 -0
- package/themes/newsflash/_layouts/frontend/pages/blog/post.html +235 -0
- package/themes/newsflash/_layouts/frontend/pages/index.html +328 -0
- package/themes/newsflash/_sections/marketing/desks/section.html +39 -0
- package/themes/newsflash/_sections/marketing/desks/section.json5 +49 -0
- package/themes/newsflash/_sections/marketing/newsletter-cta/section.html +103 -0
- package/themes/newsflash/_sections/marketing/newsletter-cta/section.json5 +50 -0
- package/themes/newsflash/_sections/marketing/rundown/section.html +28 -0
- package/themes/newsflash/_sections/marketing/rundown/section.json5 +40 -0
- package/themes/newsflash/_theme.js +24 -0
- package/themes/newsflash/_theme.scss +70 -0
- package/themes/newsflash/css/base/_fonts.scss +80 -0
- package/themes/newsflash/css/base/_mixins.scss +51 -0
- package/themes/newsflash/css/base/_root.scss +257 -0
- package/themes/newsflash/css/base/_typography.scss +49 -0
- package/themes/newsflash/css/base/_utilities.scss +122 -0
- package/themes/newsflash/css/components/_badges.scss +65 -0
- package/themes/newsflash/css/components/_billing-toggle.scss +61 -0
- package/themes/newsflash/css/components/_buttons.scss +139 -0
- package/themes/newsflash/css/components/_cards.scss +47 -0
- package/themes/newsflash/css/components/_editorial.scss +173 -0
- package/themes/newsflash/css/components/_forms.scss +75 -0
- package/themes/newsflash/css/components/_infinite-scroll.scss +102 -0
- package/themes/newsflash/css/components/_panels.scss +156 -0
- package/themes/newsflash/css/components/_ticker.scss +67 -0
- package/themes/newsflash/css/layout/_general.scss +330 -0
- package/themes/newsflash/css/layout/_navigation.scss +168 -0
- package/themes/newsflash/css/pages/404/index.scss +79 -0
- package/themes/newsflash/css/pages/blog/[slug].scss +186 -0
- package/themes/newsflash/css/pages/blog/index.scss +17 -0
- package/themes/newsflash/css/pages/index.scss +157 -0
- package/themes/newsflash/css/pages/pricing/index.scss +55 -0
- package/themes/newsflash/css/pages/test/libraries/layers/index.scss +7 -0
- package/themes/newsflash/fonts/fraunces-italic-latin-ext.woff2 +0 -0
- package/themes/newsflash/fonts/fraunces-italic-latin.woff2 +0 -0
- package/themes/newsflash/fonts/fraunces-normal-latin-ext.woff2 +0 -0
- package/themes/newsflash/fonts/fraunces-normal-latin.woff2 +0 -0
- package/themes/newsflash/fonts/schibsted-grotesk-italic-latin-ext.woff2 +0 -0
- package/themes/newsflash/fonts/schibsted-grotesk-italic-latin.woff2 +0 -0
- package/themes/newsflash/fonts/schibsted-grotesk-normal-latin-ext.woff2 +0 -0
- package/themes/newsflash/fonts/schibsted-grotesk-normal-latin.woff2 +0 -0
- package/themes/newsflash/js/pages/blog/[slug].js +29 -0
- package/themes/newsflash/js/pages/test/libraries/layers/index.js +9 -0
- package/translations/es/pages/404.json +86 -0
- package/translations/es/pages/account.json +36 -0
- package/translations/es/pages/app.json +44 -0
- package/translations/es/pages/cancel.json +36 -0
- package/translations/es/pages/change-password.json +36 -0
- package/translations/es/pages/connections/callback.json +41 -0
- package/translations/es/pages/dashboard/account.json +475 -0
- package/translations/es/pages/dashboard.json +36 -0
- package/translations/es/pages/forgot.json +36 -0
- package/translations/es/pages/join.json +36 -0
- package/translations/es/pages/login.json +36 -0
- package/translations/es/pages/payment/checkout.json +83 -0
- package/translations/es/pages/payment/confirmation.json +62 -0
- package/translations/es/pages/portal/email-preferences.json +46 -0
- package/translations/es/pages/recover.json +36 -0
- package/translations/es/pages/refund.json +36 -0
- package/translations/es/pages/register.json +36 -0
- package/translations/es/pages/reset-password.json +36 -0
- package/translations/es/pages/reset.json +42 -0
- package/translations/es/pages/signin.json +45 -0
- package/translations/es/pages/signup.json +60 -0
- package/translations/es/pages/token.json +39 -0
- package/translations/fa/pages/404.json +86 -0
- package/translations/fa/pages/account.json +36 -0
- package/translations/fa/pages/app.json +44 -0
- package/translations/fa/pages/cancel.json +36 -0
- package/translations/fa/pages/change-password.json +36 -0
- package/translations/fa/pages/connections/callback.json +41 -0
- package/translations/fa/pages/dashboard/account.json +475 -0
- package/translations/fa/pages/dashboard.json +36 -0
- package/translations/fa/pages/forgot.json +36 -0
- package/translations/fa/pages/join.json +36 -0
- package/translations/fa/pages/login.json +36 -0
- package/translations/fa/pages/payment/checkout.json +83 -0
- package/translations/fa/pages/payment/confirmation.json +62 -0
- package/translations/fa/pages/portal/email-preferences.json +46 -0
- package/translations/fa/pages/recover.json +36 -0
- package/translations/fa/pages/refund.json +36 -0
- package/translations/fa/pages/register.json +36 -0
- package/translations/fa/pages/reset-password.json +36 -0
- package/translations/fa/pages/reset.json +42 -0
- package/translations/fa/pages/signin.json +45 -0
- package/translations/fa/pages/signup.json +60 -0
- package/translations/fa/pages/token.json +39 -0
|
@@ -0,0 +1,1667 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing Section JavaScript
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Libraries
|
|
6
|
+
import { FormManager } from '@omega.js/client/modules/form-manager.js';
|
|
7
|
+
import omega from '@omega.js/client';
|
|
8
|
+
import { createLogger } from '__main_assets__/js/libs/logger.js';
|
|
9
|
+
import initializeTooltips from '__main_assets__/js/libs/initialize-tooltips.js';
|
|
10
|
+
import { event } from '__main_assets__/js/libs/analytics.js';
|
|
11
|
+
import { FREQUENCIES, getAvailableFrequencies } from '../../../payment/checkout/modules/state.js';
|
|
12
|
+
import { resolveFeatures } from '@omega.js/client/modules/features.js';
|
|
13
|
+
|
|
14
|
+
const logger = createLogger('account:billing');
|
|
15
|
+
|
|
16
|
+
let paymentConfig = null;
|
|
17
|
+
let cancelFormManager = null;
|
|
18
|
+
let currentAccount = null;
|
|
19
|
+
|
|
20
|
+
// Whether the account's provider can do these at all is the BACKEND's answer
|
|
21
|
+
// (the capability gate): we attempt the route and branch on the code that comes
|
|
22
|
+
// back, never on a provider map here. A refusal retires the button for the
|
|
23
|
+
// session — the billing portal is the path that works for that provider.
|
|
24
|
+
let uncancelSupported = true;
|
|
25
|
+
let planSwitchSupported = true;
|
|
26
|
+
let winbackSupported = true;
|
|
27
|
+
|
|
28
|
+
// The save offer (#268) is pitched per CANCEL ATTEMPT, never once per session
|
|
29
|
+
// ([#324]): every fresh click on "Cancel subscription" re-evaluates
|
|
30
|
+
// eligibility, so a customer who declined, shut the questionnaire and came back
|
|
31
|
+
// to cancel is pitched again — the subscription is still live and the offer is
|
|
32
|
+
// still unclaimed, which is exactly the moment the pitch is for. A one-shot
|
|
33
|
+
// flag made the SECOND attempt the silent one, which is the state Ian's QA
|
|
34
|
+
// found.
|
|
35
|
+
//
|
|
36
|
+
// Only two things end the pitch, and both are facts about the account rather
|
|
37
|
+
// than about the click: the offer has been CLAIMED (the discount is on the
|
|
38
|
+
// subscription, and the backend refuses a second claim), or the backend has
|
|
39
|
+
// refused it in a way no retry fixes (`winbackSupported`).
|
|
40
|
+
let winbackClaimed = false;
|
|
41
|
+
|
|
42
|
+
// Cancellation reasons (will be shuffled on each render)
|
|
43
|
+
const CANCEL_REASONS = [
|
|
44
|
+
'Too expensive',
|
|
45
|
+
'Not using it enough',
|
|
46
|
+
'Missing features I need',
|
|
47
|
+
'Found a better alternative',
|
|
48
|
+
'Technical issues or bugs',
|
|
49
|
+
'Just testing or temporary need',
|
|
50
|
+
'Other',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// Status display configuration (classy status pill: dot + label, never color alone)
|
|
54
|
+
const STATUS_CONFIG = {
|
|
55
|
+
free: { label: 'Free', badgeClass: 'omega-status', dotClass: 'omega-dot omega-dot--muted' },
|
|
56
|
+
active: { label: 'Active', badgeClass: 'omega-status omega-status--ok', dotClass: 'omega-dot omega-dot--ok' },
|
|
57
|
+
trialing: { label: 'Active', badgeClass: 'omega-status omega-status--ok', dotClass: 'omega-dot omega-dot--ok' },
|
|
58
|
+
cancelling: { label: 'Active', badgeClass: 'omega-status omega-status--ok', dotClass: 'omega-dot omega-dot--ok' },
|
|
59
|
+
suspended: { label: 'Suspended', badgeClass: 'omega-status omega-status--danger', dotClass: 'omega-dot omega-dot--danger' },
|
|
60
|
+
cancelled: { label: 'Cancelled', badgeClass: 'omega-status', dotClass: 'omega-dot omega-dot--muted' },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const FREQUENCY_LABELS = { daily: 'day', weekly: 'week', monthly: 'month', annually: 'year' };
|
|
64
|
+
|
|
65
|
+
// The cadence toggle's segment copy — the same wording the pricing page's
|
|
66
|
+
// billing toggle uses, for the same catalog frequencies.
|
|
67
|
+
const CADENCE_LABELS = { daily: 'Daily', weekly: 'Weekly', monthly: 'Monthly', annually: 'Annually' };
|
|
68
|
+
|
|
69
|
+
// The details row's slots always render, so a value the subscription never
|
|
70
|
+
// recorded needs something to SAY: one placeholder word for every slot, drawn
|
|
71
|
+
// in the muted ink so it reads as "not known" rather than as a fact ([#236] QA
|
|
72
|
+
// round 3). Never an empty gap, and never `undefined`/`NaN`.
|
|
73
|
+
//
|
|
74
|
+
// Both class names are spelled out in FULL, never composed: the purge pass
|
|
75
|
+
// reads the built JS as content, and a class it only ever sees assembled at
|
|
76
|
+
// runtime is a class it deletes.
|
|
77
|
+
const UNKNOWN_DETAIL = 'Unknown';
|
|
78
|
+
const DETAIL_CLASS = 'omega-billing-detail';
|
|
79
|
+
const UNKNOWN_DETAIL_CLASS = 'omega-billing-detail omega-billing-detail--unknown';
|
|
80
|
+
|
|
81
|
+
// How many bullets a plan option shows in the switcher. The modal is a billing
|
|
82
|
+
// ACTION, not the pricing page — "Compare plans" carries the full story.
|
|
83
|
+
const HEADLINE_FEATURE_LIMIT = 4;
|
|
84
|
+
|
|
85
|
+
// The cadence the switcher is currently showing prices at. Set from the
|
|
86
|
+
// account's own cadence every time the modal opens, then by the toggle.
|
|
87
|
+
let switcherFrequency = null;
|
|
88
|
+
|
|
89
|
+
// The plan the PRICING page sent us here to switch to (`?product=&frequency=`),
|
|
90
|
+
// held from the moment the URL is read until the billing tab is on screen —
|
|
91
|
+
// a modal cannot open inside a section that is still `d-none`.
|
|
92
|
+
let pendingSwitch = null;
|
|
93
|
+
|
|
94
|
+
// Initialize billing section
|
|
95
|
+
export async function init() {
|
|
96
|
+
setupActionButtons();
|
|
97
|
+
setupTrialCancelWarning();
|
|
98
|
+
setupWinbackOffer();
|
|
99
|
+
setupRetentionWarning();
|
|
100
|
+
setupCancellationForm();
|
|
101
|
+
setupUncancelConfirm();
|
|
102
|
+
setupPlanSwitcher();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Load billing data
|
|
106
|
+
export async function loadData(account, sharedPaymentConfig) {
|
|
107
|
+
if (!account) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
paymentConfig = sharedPaymentConfig;
|
|
112
|
+
currentAccount = account;
|
|
113
|
+
|
|
114
|
+
updateUI(account);
|
|
115
|
+
readPlanSwitchRequest();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Called when section is shown
|
|
119
|
+
export function onShow() {
|
|
120
|
+
openRequestedPlanSwitch();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── UI Update ──────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
// Previewing a billing state is the dev palette's job and only its job (Ian's
|
|
126
|
+
// ruling, [#329]): a seeded persona shows this card a real account, so there is
|
|
127
|
+
// no synthetic preview path beside it to keep honest.
|
|
128
|
+
|
|
129
|
+
function updateUI(account) {
|
|
130
|
+
const state = buildBillingState(account);
|
|
131
|
+
|
|
132
|
+
omega.bindings().update(state);
|
|
133
|
+
syncCancelTriggerToggle(dialogOwed(state));
|
|
134
|
+
updateUsageInfo(account);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Does a dialog stand in front of the questionnaire right now? ALL THREE
|
|
138
|
+
// pre-questionnaire steps ride the same switch: the trial warning (#267), the
|
|
139
|
+
// save offer (#268) and the retention warning ([#341]) each need the
|
|
140
|
+
// declarative collapse toggle off the trigger, and they are mutually exclusive
|
|
141
|
+
// by state.
|
|
142
|
+
//
|
|
143
|
+
// A step only counts if the DIALOG IT OPENS is on the page. The gate works by
|
|
144
|
+
// taking the trigger's toggle away, so a layout that dropped a dialog (a
|
|
145
|
+
// consumer override, a trimmed template) would otherwise be left with a cancel
|
|
146
|
+
// button that opens nothing at all — the one path in this flow that cannot be
|
|
147
|
+
// clicked out of ([#324]).
|
|
148
|
+
function dialogOwed(state) {
|
|
149
|
+
return (state.billing.cancelWarning.show && !!document.getElementById('cancel-trial-warning-modal'))
|
|
150
|
+
|| (state.billing.winbackOffer.show && !!document.getElementById('cancel-winback-modal'))
|
|
151
|
+
|| (state.billing.cancelRetention.show && !!document.getElementById('cancel-retention-modal'));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function buildBillingState(account) {
|
|
155
|
+
const subscription = account?.subscription || {};
|
|
156
|
+
const resolved = omega.auth().resolveSubscription(account);
|
|
157
|
+
const rawStatus = subscription.status;
|
|
158
|
+
const isPaid = subscription.product?.id !== 'basic' && !!subscription.product?.id;
|
|
159
|
+
const displayName = getDisplayName(subscription);
|
|
160
|
+
|
|
161
|
+
// Map raw + resolved state to UI config
|
|
162
|
+
let configKey = 'free';
|
|
163
|
+
if (isPaid) {
|
|
164
|
+
if (rawStatus === 'suspended') configKey = 'suspended';
|
|
165
|
+
else if (rawStatus === 'cancelled') configKey = 'cancelled';
|
|
166
|
+
else if (resolved.active) configKey = 'active';
|
|
167
|
+
}
|
|
168
|
+
const config = STATUS_CONFIG[configKey] || STATUS_CONFIG.free;
|
|
169
|
+
|
|
170
|
+
// Pre-format alert dates
|
|
171
|
+
const cancelTimestamp = subscription.cancellation?.date?.timestampUNIX;
|
|
172
|
+
const cancelDate = formatDate(cancelTimestamp) || 'the end of your billing period';
|
|
173
|
+
const trialEndDate = formatDate(subscription.trial?.expires?.timestampUNIX);
|
|
174
|
+
|
|
175
|
+
// Pre-format billing details. EVERY paid state owes the user its price, its
|
|
176
|
+
// cadence and a date line — the row used to gate on `resolved.active &&
|
|
177
|
+
// hasValidBilling`, so a cancelled or cancelling account, the two states
|
|
178
|
+
// where "when does this end?" IS the question, showed nothing at all
|
|
179
|
+
// ([#236]). Every slot now RENDERS in every paid state: a piece the
|
|
180
|
+
// subscription does not record shows the placeholder (the QA persona whose
|
|
181
|
+
// doc carries neither price nor frequency dropped two of the three slots and
|
|
182
|
+
// left the row looking half-built).
|
|
183
|
+
const nextBillingUnix = subscription.expires?.timestampUNIX;
|
|
184
|
+
const amount = subscription.payment?.price;
|
|
185
|
+
const currency = paymentConfig?.currency || 'USD';
|
|
186
|
+
const frequency = subscription.payment?.frequency;
|
|
187
|
+
const amountText = (typeof amount === 'number' && amount > 0) ? formatCurrency(amount, currency) : '';
|
|
188
|
+
const cadenceText = CADENCE_LABELS[frequency] || frequency || '';
|
|
189
|
+
|
|
190
|
+
// The date line's LABEL is the claim it makes, so only a renewing
|
|
191
|
+
// subscription may say "renews": a scheduled cancellation says how long
|
|
192
|
+
// access lasts, an ended one says when it ended, and a suspended one — where
|
|
193
|
+
// nothing is scheduled while a payment is failing — names the slot without
|
|
194
|
+
// claiming a date for it.
|
|
195
|
+
let dateLabel = 'Next billing';
|
|
196
|
+
let dateValue = '';
|
|
197
|
+
if (configKey === 'cancelled') {
|
|
198
|
+
dateLabel = 'Ended';
|
|
199
|
+
dateValue = formatDate(cancelTimestamp) || formatDate(nextBillingUnix);
|
|
200
|
+
} else if (subscription.cancellation?.pending === true) {
|
|
201
|
+
dateLabel = 'Access until';
|
|
202
|
+
dateValue = formatDate(cancelTimestamp) || formatDate(nextBillingUnix);
|
|
203
|
+
} else if (configKey === 'active') {
|
|
204
|
+
dateLabel = 'Renews';
|
|
205
|
+
dateValue = formatDate(nextBillingUnix);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// One home for "is cancelling on the table at all" — the button and the trial
|
|
209
|
+
// warning that gates it both read it (#267)
|
|
210
|
+
const canCancel = isPaid && rawStatus !== 'cancelled' && !resolved.cancelling;
|
|
211
|
+
|
|
212
|
+
// And one home for "this cancel would end a TRIAL", read by the warning dialog
|
|
213
|
+
// AND by the questionnaire copy behind it (#267). Two rules would eventually
|
|
214
|
+
// disagree, and a form contradicting the dialog that opened it is the bug the
|
|
215
|
+
// dialog exists to fix.
|
|
216
|
+
const trialCancel = canCancel && resolved.trialing;
|
|
217
|
+
|
|
218
|
+
// The save offer (#268): pitched to a PAID cancel, before the questionnaire.
|
|
219
|
+
// A trial is never offered a discount on a cycle it has not paid for — its
|
|
220
|
+
// access ends today (#267), so "off your next month" is the wrong sentence —
|
|
221
|
+
// which is what keeps the two gates from ever wanting the same click.
|
|
222
|
+
//
|
|
223
|
+
// The offer itself is the brand's, resolved at BUILD time from omega.json5
|
|
224
|
+
// (`payment.winback`, @omega.js/config's resolveWinbackOffer): the 50%-off
|
|
225
|
+
// default has one home and the browser never applies one of its own.
|
|
226
|
+
//
|
|
227
|
+
// The offer is only pitchable to a subscription the apply route can REACH:
|
|
228
|
+
// it discounts the live subscription at the provider, so it needs the
|
|
229
|
+
// provider and the resource it holds it under. A paid, active subscription
|
|
230
|
+
// can carry neither — granted by an admin, imported, a backfill that never
|
|
231
|
+
// landed — and pitching that account an offer whose only button can 400 is a
|
|
232
|
+
// dead end in front of the cancel it came to make ([#311]).
|
|
233
|
+
const reachable = !!subscription.payment?.provider && !!subscription.payment?.resourceId;
|
|
234
|
+
|
|
235
|
+
const offer = paymentConfig?.winback;
|
|
236
|
+
const offerable = canCancel
|
|
237
|
+
&& !trialCancel
|
|
238
|
+
&& reachable
|
|
239
|
+
&& offer?.enabled === true
|
|
240
|
+
&& winbackSupported
|
|
241
|
+
&& !winbackClaimed
|
|
242
|
+
// A claim persisted on the account ([#325]): the backend stamps the discount
|
|
243
|
+
// it applied with source 'winback', so a past claimant is not re-pitched in
|
|
244
|
+
// a fresh session. Only that source suppresses the pitch — a checkout code's
|
|
245
|
+
// discount rides the same node with its own source and keeps the offer open.
|
|
246
|
+
&& subscription.discount?.source !== 'winback';
|
|
247
|
+
|
|
248
|
+
// The fallback dialog ([#341]): a cancel the other two steps have nothing to
|
|
249
|
+
// say to still gets a word before the questionnaire. Ian's QA found the hole
|
|
250
|
+
// in three states, and every one of them is a real cancel walking into
|
|
251
|
+
// silence: a brand that turned the offer off, a subscription the apply route
|
|
252
|
+
// cannot reach, a customer who already claimed the discount.
|
|
253
|
+
//
|
|
254
|
+
// It is the LAST gate by construction: it reads the other two rather than
|
|
255
|
+
// restating their rules, so exactly one dialog is ever owed and adding a
|
|
256
|
+
// condition to either of them can never leave the click with two.
|
|
257
|
+
const retentionCancel = canCancel && !offerable && !trialCancel;
|
|
258
|
+
|
|
259
|
+
// The discount riding the subscription right now ([#325]). Accepting the save
|
|
260
|
+
// offer applies a real discount at the provider and the card said nothing
|
|
261
|
+
// about it at all, so the saving a customer had just been given was invisible
|
|
262
|
+
// on the one page that exists to explain their billing.
|
|
263
|
+
//
|
|
264
|
+
// The shape is the one the whole payment stack already speaks — a
|
|
265
|
+
// discount-codes validate result (`{ valid, percent | amount, duration }`),
|
|
266
|
+
// the same object the apply route records and hands back — so nothing here
|
|
267
|
+
// learns a second shape, and an invalid one is no discount at all.
|
|
268
|
+
const discount = subscription.discount?.valid === true ? subscription.discount : null;
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
billing: {
|
|
272
|
+
plan: {
|
|
273
|
+
name: displayName,
|
|
274
|
+
},
|
|
275
|
+
status: {
|
|
276
|
+
label: config.label,
|
|
277
|
+
badgeClass: config.badgeClass,
|
|
278
|
+
dotClass: config.dotClass,
|
|
279
|
+
},
|
|
280
|
+
description: {
|
|
281
|
+
free: !isPaid,
|
|
282
|
+
active: resolved.active,
|
|
283
|
+
suspended: rawStatus === 'suspended',
|
|
284
|
+
cancelled: rawStatus === 'cancelled',
|
|
285
|
+
},
|
|
286
|
+
alerts: {
|
|
287
|
+
suspended: rawStatus === 'suspended',
|
|
288
|
+
cancelling: resolved.cancelling,
|
|
289
|
+
trialing: resolved.trialing,
|
|
290
|
+
cancelDate: cancelDate,
|
|
291
|
+
trialEndDate: trialEndDate,
|
|
292
|
+
trialHasEndDate: !!trialEndDate,
|
|
293
|
+
},
|
|
294
|
+
details: {
|
|
295
|
+
visible: isPaid,
|
|
296
|
+
dateLabel: dateLabel,
|
|
297
|
+
date: dateValue || UNKNOWN_DETAIL,
|
|
298
|
+
dateClass: detailClass(dateValue),
|
|
299
|
+
amount: amountText || UNKNOWN_DETAIL,
|
|
300
|
+
amountClass: detailClass(amountText),
|
|
301
|
+
cadence: cadenceText || UNKNOWN_DETAIL,
|
|
302
|
+
cadenceClass: detailClass(cadenceText),
|
|
303
|
+
},
|
|
304
|
+
// Cancelling a TRIAL ends access immediately (#267), which is the
|
|
305
|
+
// opposite of what the questionnaire's own copy promises — so a trial is
|
|
306
|
+
// told before it answers questions, never after. The gate reads the
|
|
307
|
+
// cancel button's OWN flag rather than restating its rule: a warning that
|
|
308
|
+
// could disagree with the button it guards is the bug twice.
|
|
309
|
+
cancelWarning: {
|
|
310
|
+
show: trialCancel,
|
|
311
|
+
trialEndDate: trialEndDate,
|
|
312
|
+
hasTrialEndDate: !!trialEndDate,
|
|
313
|
+
},
|
|
314
|
+
// What leaving MIGHT cost, for the cancel neither the offer nor the trial
|
|
315
|
+
// warning speaks to ([#341]). The dialog's copy is a retention-policy
|
|
316
|
+
// statement, not a threat, so all this carries is whether it is owed.
|
|
317
|
+
cancelRetention: {
|
|
318
|
+
show: retentionCancel,
|
|
319
|
+
},
|
|
320
|
+
// The questionnaire the warning hands the customer to: its explanation and
|
|
321
|
+
// the checkbox they must tick both state what THIS cancel does, so a trial
|
|
322
|
+
// is never asked to attest to access it will not get.
|
|
323
|
+
cancelForm: {
|
|
324
|
+
trial: trialCancel,
|
|
325
|
+
},
|
|
326
|
+
// The saving already on the subscription (#325): how much comes off, and
|
|
327
|
+
// which bills it comes off. Both are TEXT — a discount announced by a
|
|
328
|
+
// colored pill alone is not announced at all.
|
|
329
|
+
discount: {
|
|
330
|
+
show: !!discount,
|
|
331
|
+
label: discount ? `${discountOffText(discount)} off` : '',
|
|
332
|
+
when: discount ? discountWhenText(discount, subscription.payment?.frequency) : '',
|
|
333
|
+
},
|
|
334
|
+
// The save offer the customer reads before the questionnaire (#268). Its
|
|
335
|
+
// wording is built here, from the brand's own numbers and the cadence the
|
|
336
|
+
// subscription is billed at, so the markup carries no discount at all.
|
|
337
|
+
winbackOffer: {
|
|
338
|
+
show: offerable,
|
|
339
|
+
headline: offerable ? winbackHeadline(offer, subscription.payment?.frequency) : '',
|
|
340
|
+
},
|
|
341
|
+
buttons: {
|
|
342
|
+
upgrade: !isPaid || rawStatus === 'cancelled',
|
|
343
|
+
change: canChangePlan(account),
|
|
344
|
+
manage: isPaid && rawStatus !== 'cancelled',
|
|
345
|
+
cancel: canCancel,
|
|
346
|
+
// Undo reads the SAME raw flag Change does, not `resolved.cancelling`
|
|
347
|
+
// (which is `pending && !trialing`): a TRIALING subscription with a
|
|
348
|
+
// scheduled cancellation is reachable — the provider's own billing
|
|
349
|
+
// portal schedules one — and reading the derived flag left that account
|
|
350
|
+
// with neither button, a dead end on its own billing page.
|
|
351
|
+
uncancel: uncancelSupported && isPaid && rawStatus === 'active' && subscription.cancellation?.pending === true,
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Is a plan switch on the table at all? ONE home for the rule, read by the
|
|
358
|
+
// Change button and by the pricing page's preselect alike — a modal that opens
|
|
359
|
+
// on a plan the button would not have offered is the same bug twice.
|
|
360
|
+
//
|
|
361
|
+
// Change is hidden while a cancellation is scheduled: the providers swap the
|
|
362
|
+
// price, never the schedule, so the backend refuses the switch outright
|
|
363
|
+
// (`cancellation-pending`, [#237]). Undo cancellation is the honest button in
|
|
364
|
+
// that state, and this reads the SAME raw flag the backend guard does so the
|
|
365
|
+
// two can never disagree.
|
|
366
|
+
function canChangePlan(account) {
|
|
367
|
+
return planSwitchSupported
|
|
368
|
+
&& omega.auth().resolveSubscription(account).active
|
|
369
|
+
&& account?.subscription?.cancellation?.pending !== true;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ─── Action Buttons ──────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
function setupActionButtons() {
|
|
375
|
+
const $upgradeBtn = document.getElementById('upgrade-plan-btn');
|
|
376
|
+
const $changeBtn = document.getElementById('change-plan-btn');
|
|
377
|
+
const $manageBtn = document.getElementById('manage-billing-btn');
|
|
378
|
+
|
|
379
|
+
if ($upgradeBtn) {
|
|
380
|
+
$upgradeBtn.addEventListener('click', () => {
|
|
381
|
+
trackBilling('upgrade_click');
|
|
382
|
+
window.location.href = '/pricing';
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// The plan switcher modal itself opens declaratively (data-bs-toggle), the
|
|
387
|
+
// same way the signin link modal does — this only records the intent
|
|
388
|
+
if ($changeBtn) {
|
|
389
|
+
$changeBtn.addEventListener('click', () => {
|
|
390
|
+
trackBilling('change_plan_click');
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if ($manageBtn) {
|
|
395
|
+
$manageBtn.addEventListener('click', () => {
|
|
396
|
+
trackBilling('manage_billing_click');
|
|
397
|
+
openBillingPortal();
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ─── Billing Portal ─────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
async function openBillingPortal() {
|
|
405
|
+
const $manageBtn = document.getElementById('manage-billing-btn');
|
|
406
|
+
const $btnText = $manageBtn?.querySelector('.button-text');
|
|
407
|
+
const originalText = $btnText?.textContent;
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
// Show loading state
|
|
411
|
+
if ($manageBtn) $manageBtn.disabled = true;
|
|
412
|
+
if ($btnText) $btnText.textContent = 'Opening...';
|
|
413
|
+
|
|
414
|
+
const response = await omega.request(`/omega/payments/portal`, {
|
|
415
|
+
method: 'POST',
|
|
416
|
+
timeout: 15000,
|
|
417
|
+
body: {
|
|
418
|
+
returnUrl: window.location.href,
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
if (response.url) {
|
|
423
|
+
window.open(response.url, '_blank');
|
|
424
|
+
} else {
|
|
425
|
+
throw new Error('No portal URL returned');
|
|
426
|
+
}
|
|
427
|
+
} catch (error) {
|
|
428
|
+
console.error('Failed to open billing portal:', error);
|
|
429
|
+
omega.utilities().showNotification(error.message || 'Failed to open billing portal. Please try again later.', 'danger');
|
|
430
|
+
} finally {
|
|
431
|
+
if ($manageBtn) $manageBtn.disabled = false;
|
|
432
|
+
if ($btnText) $btnText.textContent = originalText;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ─── Undo Cancellation ──────────────────────────────────────
|
|
437
|
+
|
|
438
|
+
function setupUncancelConfirm() {
|
|
439
|
+
const $confirmBtn = document.getElementById('uncancel-confirm-btn');
|
|
440
|
+
if (!$confirmBtn) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
$confirmBtn.addEventListener('click', () => uncancelSubscription($confirmBtn));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Withdraw the scheduled cancellation so the subscription just keeps renewing.
|
|
448
|
+
// Nothing is charged today — the provider resumes the existing term.
|
|
449
|
+
async function uncancelSubscription($confirmBtn) {
|
|
450
|
+
const $btnText = $confirmBtn.querySelector('.button-text');
|
|
451
|
+
const originalText = $btnText?.textContent;
|
|
452
|
+
|
|
453
|
+
trackBilling('uncancel_submit');
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
// Show loading state
|
|
457
|
+
$confirmBtn.disabled = true;
|
|
458
|
+
if ($btnText) $btnText.textContent = 'Resuming...';
|
|
459
|
+
|
|
460
|
+
await omega.request(`/omega/payments/uncancel`, {
|
|
461
|
+
method: 'POST',
|
|
462
|
+
timeout: 30000,
|
|
463
|
+
body: {
|
|
464
|
+
confirmed: true,
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
logger.log('Cancellation withdrawn:', { productId: currentAccount?.subscription?.product?.id });
|
|
469
|
+
|
|
470
|
+
// The webhook pipeline writes the real state — patch locally so the card
|
|
471
|
+
// stops saying "cancellation scheduled" the moment the route succeeds
|
|
472
|
+
const currentSub = currentAccount?.subscription;
|
|
473
|
+
if (currentSub) {
|
|
474
|
+
currentSub.cancellation = { pending: false };
|
|
475
|
+
updateUI(currentAccount);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
collapseUncancelConfirm();
|
|
479
|
+
|
|
480
|
+
omega.utilities().showNotification('Your subscription will continue as normal. You were not charged today.', 'success');
|
|
481
|
+
} catch (error) {
|
|
482
|
+
logger.error('Failed to withdraw cancellation:', error);
|
|
483
|
+
|
|
484
|
+
// The provider cannot resume a subscription AT ALL (the backend's
|
|
485
|
+
// capability gate). The button is a dead end for this account, so retire it
|
|
486
|
+
// for the session — the message already points at the billing portal, which
|
|
487
|
+
// is the "Manage billing" button right beside it.
|
|
488
|
+
if (error.properties?.additional?.code === 'not-supported-by-provider') {
|
|
489
|
+
uncancelSupported = false;
|
|
490
|
+
updateUI(currentAccount);
|
|
491
|
+
collapseUncancelConfirm();
|
|
492
|
+
omega.utilities().showNotification(error.message, { type: 'warning', timeout: 8000 });
|
|
493
|
+
} else {
|
|
494
|
+
omega.utilities().showNotification(error.message || 'Failed to resume your subscription. Please try again later.', 'danger');
|
|
495
|
+
}
|
|
496
|
+
} finally {
|
|
497
|
+
$confirmBtn.disabled = false;
|
|
498
|
+
if ($btnText) $btnText.textContent = originalText;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function collapseUncancelConfirm() {
|
|
503
|
+
const $confirm = document.getElementById('uncancel-subscription-confirm');
|
|
504
|
+
if (!$confirm) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const bsCollapse = bootstrap.Collapse.getInstance($confirm);
|
|
509
|
+
if (bsCollapse) bsCollapse.hide();
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ─── Plan Switcher ──────────────────────────────────────────
|
|
513
|
+
|
|
514
|
+
function setupPlanSwitcher() {
|
|
515
|
+
const $modal = document.getElementById('change-plan-modal');
|
|
516
|
+
if (!$modal) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const $cadence = document.getElementById('change-plan-cadence');
|
|
521
|
+
const $options = document.getElementById('change-plan-options');
|
|
522
|
+
const $confirmBtn = document.getElementById('change-plan-confirm-btn');
|
|
523
|
+
|
|
524
|
+
// Rebuild the picker every time it opens — the plan it must exclude, and the
|
|
525
|
+
// cadence it opens on, are whatever the account is on right now
|
|
526
|
+
$modal.addEventListener('show.bs.modal', () => {
|
|
527
|
+
switcherFrequency = defaultCadence();
|
|
528
|
+
renderCadenceToggle($cadence);
|
|
529
|
+
populatePlanOptions($options);
|
|
530
|
+
$confirmBtn.disabled = true;
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// Flipping the cadence re-prices every card, which rebuilds the radios: the
|
|
534
|
+
// pick does not survive it, so neither does the enabled Confirm button
|
|
535
|
+
$cadence.addEventListener('change', (event) => {
|
|
536
|
+
switcherFrequency = event.target.value;
|
|
537
|
+
populatePlanOptions($options);
|
|
538
|
+
$confirmBtn.disabled = true;
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// Nothing to confirm until a plan is picked
|
|
542
|
+
$options.addEventListener('change', () => {
|
|
543
|
+
$confirmBtn.disabled = !getSelectedPlan();
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// The WHOLE card is the target, not just the head: the feature list and the
|
|
547
|
+
// card's padding are most of its area, and they were dead space — worst on
|
|
548
|
+
// touch, where the head is a thin strip. Two things keep their own clicks: a
|
|
549
|
+
// label (the browser already checks the radio for it, and doing it again
|
|
550
|
+
// would fire a second change event) and a feature's hover explanation, whose
|
|
551
|
+
// tooltip a stretched overlay would have swallowed entirely.
|
|
552
|
+
$options.addEventListener('click', (event) => {
|
|
553
|
+
if (event.target.closest('[data-bs-toggle="tooltip"]') || event.target.closest('label')) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const $card = event.target.closest('.omega-plan-card');
|
|
558
|
+
const $radio = $card?.previousElementSibling;
|
|
559
|
+
|
|
560
|
+
if (!$radio || $radio.disabled || $radio.checked) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
$radio.checked = true;
|
|
565
|
+
$radio.dispatchEvent(new Event('change', { bubbles: true }));
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
$confirmBtn.addEventListener('click', () => changePlan($confirmBtn, $modal));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ─── Arriving from the pricing page ─────────────────────────
|
|
572
|
+
|
|
573
|
+
// The pricing page's "Switch to this plan" button lands here naming the plan it
|
|
574
|
+
// meant: `/dashboard/account?product=<id>&frequency=<cadence>#billing`
|
|
575
|
+
// ([#236]). Reading it is one thing and acting on it is another — the modal
|
|
576
|
+
// cannot open while the billing section is still `d-none` — so the request is
|
|
577
|
+
// read the moment the account loads and honoured when the tab is on screen.
|
|
578
|
+
//
|
|
579
|
+
// The params are consumed ONCE and struck from the URL right here: a refresh
|
|
580
|
+
// (or a back to this page) must not reopen a modal the user already closed.
|
|
581
|
+
function readPlanSwitchRequest() {
|
|
582
|
+
const params = new URLSearchParams(window.location.search || '');
|
|
583
|
+
const productId = params.get('product');
|
|
584
|
+
|
|
585
|
+
if (!productId) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
pendingSwitch = { productId: productId, frequency: params.get('frequency') };
|
|
590
|
+
|
|
591
|
+
params.delete('product');
|
|
592
|
+
params.delete('frequency');
|
|
593
|
+
|
|
594
|
+
const query = params.toString();
|
|
595
|
+
window.history.replaceState({}, '', `${window.location.pathname}${query ? `?${query}` : ''}${window.location.hash || ''}`);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Open the switcher on the plan the pricing page named. A state that cannot
|
|
599
|
+
// switch at all lands on the billing tab plainly: while a cancellation is
|
|
600
|
+
// scheduled the Change button is hidden (the backend refuses the switch), and
|
|
601
|
+
// opening the modal anyway would offer a move that cannot happen.
|
|
602
|
+
function openRequestedPlanSwitch() {
|
|
603
|
+
const request = pendingSwitch;
|
|
604
|
+
pendingSwitch = null;
|
|
605
|
+
|
|
606
|
+
const $modal = document.getElementById('change-plan-modal');
|
|
607
|
+
if (!request || !$modal || !canChangePlan(currentAccount)) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Bootstrap's show() fires `show.bs.modal`, which is what renders the toggle
|
|
612
|
+
// and the cards — so the picking below happens over the real thing.
|
|
613
|
+
bootstrap.Modal.getOrCreateInstance($modal).show();
|
|
614
|
+
|
|
615
|
+
// The cadence goes FIRST: flipping it re-prices and rebuilds every card, so
|
|
616
|
+
// a plan picked before it would not survive the rebuild.
|
|
617
|
+
if (request.frequency) {
|
|
618
|
+
pickRadio(document.getElementById('change-plan-cadence'), (radio) => radio.value === request.frequency);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Exactly the mechanism a card click uses — check the radio, announce it —
|
|
622
|
+
// so Confirm arms the way it would have by hand.
|
|
623
|
+
pickRadio(document.getElementById('change-plan-options'), (radio) => radio.value === request.productId && !radio.disabled);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Check one radio inside a container and announce it the way a click does. The
|
|
627
|
+
// match is a PREDICATE, never a selector built from the URL.
|
|
628
|
+
function pickRadio($container, matches) {
|
|
629
|
+
const $radio = [...($container?.querySelectorAll('input[type="radio"]') || [])].find(matches);
|
|
630
|
+
|
|
631
|
+
if (!$radio || $radio.checked) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
$radio.checked = true;
|
|
636
|
+
$radio.dispatchEvent(new Event('change', { bubbles: true }));
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// The plans a live subscription can move to: the brand's subscription
|
|
640
|
+
// products, minus the free tier (leaving a paid plan is CANCELLING, not
|
|
641
|
+
// switching) and minus one-time purchases. The enterprise tier excludes
|
|
642
|
+
// itself — it carries no prices, so it is on sale at no cadence.
|
|
643
|
+
function switchableProducts() {
|
|
644
|
+
return (paymentConfig?.products || [])
|
|
645
|
+
.filter(product => (product.type || 'subscription') === 'subscription' && product.id !== 'basic');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Every cadence the brand actually sells a switchable plan at, shortest first.
|
|
649
|
+
function availableCadences() {
|
|
650
|
+
const sold = new Set(switchableProducts().flatMap(product => getAvailableFrequencies(product)));
|
|
651
|
+
|
|
652
|
+
return FREQUENCIES.filter(frequency => sold.has(frequency));
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// The cadence the modal opens on: the one the account is billed at, whenever
|
|
656
|
+
// the brand still sells it. A subscription with no recorded frequency opens on
|
|
657
|
+
// monthly, and a brand that doesn't sell monthly opens on its shortest cadence.
|
|
658
|
+
function defaultCadence() {
|
|
659
|
+
const cadences = availableCadences();
|
|
660
|
+
const current = currentAccount?.subscription?.payment?.frequency;
|
|
661
|
+
|
|
662
|
+
if (current && cadences.includes(current)) {
|
|
663
|
+
return current;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return cadences.includes('monthly') ? 'monthly' : (cadences[0] || 'monthly');
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// The one cadence control, in the pricing page's own vocabulary
|
|
670
|
+
// (`omega-billing-toggle`): a segmented group of real radios that re-prices
|
|
671
|
+
// every card. A brand selling a single cadence gets no toggle — there is
|
|
672
|
+
// nothing to toggle between.
|
|
673
|
+
function renderCadenceToggle($container) {
|
|
674
|
+
const cadences = availableCadences();
|
|
675
|
+
|
|
676
|
+
// Nothing rendered means nothing reserved either — an empty container still
|
|
677
|
+
// held its bottom margin, so a single-cadence brand opened on a gap.
|
|
678
|
+
if (cadences.length < 2) {
|
|
679
|
+
$container.innerHTML = '';
|
|
680
|
+
$container.classList.add('d-none');
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
$container.classList.remove('d-none');
|
|
685
|
+
|
|
686
|
+
const savings = annualSavingsPercent();
|
|
687
|
+
|
|
688
|
+
const segments = cadences.map((frequency) => {
|
|
689
|
+
const id = `change-plan-cadence-${frequency}`;
|
|
690
|
+
const save = (frequency === 'annually' && savings > 0)
|
|
691
|
+
? ` <span class="omega-chip omega-chip--accent omega-billing-toggle__save ms-1">Save ${savings}%</span>`
|
|
692
|
+
: '';
|
|
693
|
+
|
|
694
|
+
return `
|
|
695
|
+
<input type="radio" class="btn-check" name="change_plan_cadence" id="${id}" value="${omega.utilities().escapeHTML(frequency)}" autocomplete="off"${frequency === switcherFrequency ? ' checked' : ''}>
|
|
696
|
+
<label class="btn" for="${id}">${CADENCE_LABELS[frequency] || frequency}${save}</label>
|
|
697
|
+
`;
|
|
698
|
+
}).join('');
|
|
699
|
+
|
|
700
|
+
// `data-omega-segmented` is the pricing page's own attribute: the motion
|
|
701
|
+
// engine injects the thumb and glides it under the checked segment. The
|
|
702
|
+
// engine's MutationObserver would find this node eventually, but the modal
|
|
703
|
+
// renders and opens in the same tick — scanning the container here is the
|
|
704
|
+
// deterministic path, and adoption is idempotent (`omegaSegmentedReady`
|
|
705
|
+
// makes a second pass a no-op). The thumb measures 0 while the modal is
|
|
706
|
+
// still hidden and the engine's own ResizeObserver re-places it the moment
|
|
707
|
+
// Bootstrap shows the dialog.
|
|
708
|
+
$container.innerHTML = `<div class="omega-billing-toggle" role="group" aria-label="Billing cadence" data-omega-segmented>${segments}</div>`;
|
|
709
|
+
|
|
710
|
+
omega.library?.().motion?.scan($container);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// The honest annual discount: the best saving the catalog really offers across
|
|
714
|
+
// the plans on sale, the same arithmetic the pricing page's badge runs
|
|
715
|
+
// (`packages/web/src/pricing.js`). Annual pricing that doesn't beat twelve
|
|
716
|
+
// months of monthly earns no tag.
|
|
717
|
+
function annualSavingsPercent() {
|
|
718
|
+
let best = 0;
|
|
719
|
+
|
|
720
|
+
for (const product of switchableProducts()) {
|
|
721
|
+
const monthly = resolvePlanPrice(product, 'monthly');
|
|
722
|
+
const annually = resolvePlanPrice(product, 'annually');
|
|
723
|
+
|
|
724
|
+
if (monthly <= 0 || annually <= 0) {
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const percent = Math.round((1 - (annually / (monthly * 12))) * 100);
|
|
729
|
+
if (percent > best) {
|
|
730
|
+
best = percent;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return best;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// ONE card per plan the brand sells at the cadence the toggle is on — the
|
|
738
|
+
// price on the card follows the toggle, so the same plan never occupies two
|
|
739
|
+
// rows ([#236] round 2). The plan the account is already on RENDERS —
|
|
740
|
+
// greyed out, badged "Current plan", not selectable — rather than
|
|
741
|
+
// disappearing: a plan list missing the plan you are on reads as a bug, and
|
|
742
|
+
// dropping it was what let a no-op switch through when the recorded frequency
|
|
743
|
+
// was absent.
|
|
744
|
+
function populatePlanOptions($container) {
|
|
745
|
+
const subscription = currentAccount?.subscription || {};
|
|
746
|
+
const currentProductId = subscription.product?.id;
|
|
747
|
+
const currentFrequency = subscription.payment?.frequency;
|
|
748
|
+
const currency = paymentConfig?.currency || 'USD';
|
|
749
|
+
const frequency = switcherFrequency;
|
|
750
|
+
|
|
751
|
+
const options = switchableProducts()
|
|
752
|
+
.filter(product => getAvailableFrequencies(product).includes(frequency))
|
|
753
|
+
.map(product => ({
|
|
754
|
+
product: product,
|
|
755
|
+
current: isCurrentPlan(product.id, frequency, currentProductId, currentFrequency),
|
|
756
|
+
}));
|
|
757
|
+
|
|
758
|
+
// Bootstrap keeps ONE instance per element and never learns the element is
|
|
759
|
+
// gone: the bullets below are about to be replaced (every open, every
|
|
760
|
+
// cadence flip), so retire their tooltips first or each pass leaks a set.
|
|
761
|
+
disposeTooltips($container);
|
|
762
|
+
|
|
763
|
+
if (options.length === 0) {
|
|
764
|
+
$container.innerHTML = '<div class="text-muted small">There are no other plans to switch to right now.</div>';
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const list = options.map(({ product, current }, i) => {
|
|
769
|
+
// The bullets sit OUTSIDE the label (a list is not phrasing content), so the
|
|
770
|
+
// radio points at them with aria-describedby — otherwise a screen reader
|
|
771
|
+
// hears the plan's name and price and never its features.
|
|
772
|
+
const featuresId = `change-plan-features-${i}`;
|
|
773
|
+
const features = renderPlanFeatures(product, featuresId);
|
|
774
|
+
const id = `change-plan-option-${i}`;
|
|
775
|
+
|
|
776
|
+
return `
|
|
777
|
+
<div class="omega-plan-option">
|
|
778
|
+
<input class="btn-check" type="radio" name="change_plan_option" id="${id}" value="${omega.utilities().escapeHTML(product.id)}" data-frequency="${omega.utilities().escapeHTML(frequency)}" autocomplete="off"${current ? ' disabled' : ''}${features ? ` aria-describedby="${featuresId}"` : ''}>
|
|
779
|
+
<div class="omega-plan-card${current ? ' omega-plan-card--current' : ''}">
|
|
780
|
+
<label class="omega-plan-card__head" for="${id}">
|
|
781
|
+
<span class="omega-plan-card__name">${omega.utilities().escapeHTML(product.name || product.id)}</span>
|
|
782
|
+
${current ? '<span class="omega-chip omega-plan-card__badge">Current plan</span>' : ''}
|
|
783
|
+
<span class="omega-plan-card__price">${omega.utilities().escapeHTML(formatCurrency(resolvePlanPrice(product, frequency), currency))}<span class="omega-plan-card__per"> / ${FREQUENCY_LABELS[frequency] || frequency}</span></span>
|
|
784
|
+
</label>
|
|
785
|
+
${features}
|
|
786
|
+
</div>
|
|
787
|
+
</div>
|
|
788
|
+
`;
|
|
789
|
+
}).join('');
|
|
790
|
+
|
|
791
|
+
// Every card IS the current plan — a brand that sells a single plan. The
|
|
792
|
+
// card still renders (the plan you are on is a fact worth seeing), with the
|
|
793
|
+
// sentence that explains why nothing here is pickable.
|
|
794
|
+
const nothingToPick = options.every(option => option.current)
|
|
795
|
+
? '<div class="text-muted small mt-3">There are no other plans to switch to right now.</div>'
|
|
796
|
+
: '';
|
|
797
|
+
|
|
798
|
+
$container.innerHTML = `${list}${nothingToPick}`;
|
|
799
|
+
|
|
800
|
+
// The bullets are freshly injected, so the page-load pass never saw them
|
|
801
|
+
initializeTooltips($container);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Retire the tooltips of a container whose contents are about to be replaced.
|
|
805
|
+
function disposeTooltips($container) {
|
|
806
|
+
$container.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(($el) => {
|
|
807
|
+
bootstrap.Tooltip.getInstance($el)?.dispose();
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Is this option the plan the account is on right now? When the subscription
|
|
812
|
+
// carries no recorded frequency, the product id alone decides — deliberately
|
|
813
|
+
// conservative, so BOTH cadences disable instead of offering a switch the
|
|
814
|
+
// backend's same-plan guard would refuse anyway ([#237]).
|
|
815
|
+
function isCurrentPlan(productId, frequency, currentProductId, currentFrequency) {
|
|
816
|
+
if (productId !== currentProductId) {
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
return !currentFrequency || frequency === currentFrequency;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// A plan's headline features, from the SAME two halves the pricing page composes
|
|
824
|
+
// its cards from — the FEATURES CATALOG (name, icon, definition, row order) and
|
|
825
|
+
// the product's own values map — and presented the way the pricing card presents
|
|
826
|
+
// them: a green check, the value, and the feature's definition on hover behind
|
|
827
|
+
// the same dotted underline. -1 is the catalog's "unlimited" sentinel, and a
|
|
828
|
+
// value of `false` (or absent) means the tier does not include the feature at
|
|
829
|
+
// all ([#647](https://github.com/Omega-JS-Stack/omega/issues/647)).
|
|
830
|
+
//
|
|
831
|
+
// The definition is ALSO spelled out for assistive tech: a tooltip is
|
|
832
|
+
// hover/focus text on an element that is neither, and these bullets are a
|
|
833
|
+
// radio's description rather than a stop of their own.
|
|
834
|
+
function renderPlanFeatures(product, id) {
|
|
835
|
+
const catalog = featureCatalog();
|
|
836
|
+
const values = product.features || {};
|
|
837
|
+
const features = Object.keys(catalog)
|
|
838
|
+
.filter((featureId) => values[featureId] !== undefined && values[featureId] !== false)
|
|
839
|
+
.slice(0, HEADLINE_FEATURE_LIMIT)
|
|
840
|
+
.map((featureId) => ({ id: featureId, ...catalog[featureId] }));
|
|
841
|
+
|
|
842
|
+
if (features.length === 0) {
|
|
843
|
+
return '';
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const items = features.map((feature) => {
|
|
847
|
+
const value = values[feature.id];
|
|
848
|
+
const definition = feature.definition || null;
|
|
849
|
+
let prefix = '';
|
|
850
|
+
|
|
851
|
+
// A value only prints when it SAYS something. `true` means "included" —
|
|
852
|
+
// which the check already says — and so do `false`, `''` and absent, which
|
|
853
|
+
// used to print themselves literally ("false Priority support"). A number
|
|
854
|
+
// always prints, zero included: "0 Seats" beside a check is the honest
|
|
855
|
+
// reading. Same visible output as the pricing composer's rule.
|
|
856
|
+
if (value === -1) {
|
|
857
|
+
prefix = 'Unlimited ';
|
|
858
|
+
} else if (typeof value === 'number') {
|
|
859
|
+
prefix = `${value.toLocaleString()} `;
|
|
860
|
+
} else if (typeof value === 'string' && value !== '') {
|
|
861
|
+
prefix = `${value} `;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const name = omega.utilities().escapeHTML(feature.name);
|
|
865
|
+
const explained = definition
|
|
866
|
+
? `<span class="text-decoration-underline text-decoration-dotted cursor-help" data-bs-toggle="tooltip" data-bs-title="${omega.utilities().escapeHTML(definition)}">${name}</span><span class="visually-hidden"> — ${omega.utilities().escapeHTML(definition)}</span>`
|
|
867
|
+
: name;
|
|
868
|
+
|
|
869
|
+
return `<li><span class="omega-feature-check text-success"><i class="fa-solid fa-check fa-sm"></i></span><span>${omega.utilities().escapeHTML(prefix)}${explained}</span></li>`;
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
return `<ul class="omega-plan-card__features list-unstyled mb-0" id="${id}">${items.join('')}</ul>`;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// The FEATURES CATALOG — the ONE home of what a feature is called, what icon it
|
|
876
|
+
// wears and what its definition says. A product names only its value, so no copy
|
|
877
|
+
// here can disagree with the pricing page's
|
|
878
|
+
// ([#647](https://github.com/Omega-JS-Stack/omega/issues/647)).
|
|
879
|
+
function featureCatalog() {
|
|
880
|
+
return omega.config?.features || {};
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function getSelectedPlan() {
|
|
884
|
+
const $selected = document.querySelector('input[name="change_plan_option"]:checked');
|
|
885
|
+
if (!$selected) {
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
return { productId: $selected.value, frequency: $selected.dataset.frequency };
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Move the live subscription onto another plan. The provider prorates the
|
|
893
|
+
// difference — no cancel-and-resubscribe, no second checkout.
|
|
894
|
+
async function changePlan($confirmBtn, $modal) {
|
|
895
|
+
const selection = getSelectedPlan();
|
|
896
|
+
if (!selection) {
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const $btnText = $confirmBtn.querySelector('.button-text');
|
|
901
|
+
const originalText = $btnText?.textContent;
|
|
902
|
+
|
|
903
|
+
trackBilling('change_plan_submit');
|
|
904
|
+
|
|
905
|
+
try {
|
|
906
|
+
// Show loading state
|
|
907
|
+
$confirmBtn.disabled = true;
|
|
908
|
+
if ($btnText) $btnText.textContent = 'Changing...';
|
|
909
|
+
|
|
910
|
+
await omega.request(`/omega/payments/plan`, {
|
|
911
|
+
method: 'POST',
|
|
912
|
+
timeout: 30000,
|
|
913
|
+
body: {
|
|
914
|
+
productId: selection.productId,
|
|
915
|
+
frequency: selection.frequency,
|
|
916
|
+
confirmed: true,
|
|
917
|
+
},
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
logger.log('Plan change requested:', selection);
|
|
921
|
+
|
|
922
|
+
// The webhook pipeline writes the real state — patch locally so the card
|
|
923
|
+
// shows the plan they just picked
|
|
924
|
+
const currentSub = currentAccount?.subscription;
|
|
925
|
+
const product = paymentConfig?.products?.find(candidate => candidate.id === selection.productId);
|
|
926
|
+
if (currentSub && product) {
|
|
927
|
+
currentSub.product = { id: product.id, name: product.name };
|
|
928
|
+
currentSub.payment = {
|
|
929
|
+
...currentSub.payment,
|
|
930
|
+
frequency: selection.frequency,
|
|
931
|
+
price: resolvePlanPrice(product, selection.frequency),
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
updateUI(currentAccount);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
hidePlanSwitcher($modal);
|
|
938
|
+
|
|
939
|
+
omega.utilities().showNotification(`You're now on the ${product?.name || selection.productId} plan. Your next invoice reflects the change.`, 'success');
|
|
940
|
+
} catch (error) {
|
|
941
|
+
logger.error('Failed to change plan:', error);
|
|
942
|
+
|
|
943
|
+
// Same capability gate as uncancel: the provider cannot move a live
|
|
944
|
+
// subscription at all, so retire the button for the session and let the
|
|
945
|
+
// message send them to the billing portal
|
|
946
|
+
if (error.properties?.additional?.code === 'not-supported-by-provider') {
|
|
947
|
+
planSwitchSupported = false;
|
|
948
|
+
updateUI(currentAccount);
|
|
949
|
+
hidePlanSwitcher($modal);
|
|
950
|
+
omega.utilities().showNotification(error.message, { type: 'warning', timeout: 8000 });
|
|
951
|
+
} else {
|
|
952
|
+
omega.utilities().showNotification(error.message || 'Failed to change your plan. Please try again later.', 'danger');
|
|
953
|
+
}
|
|
954
|
+
} finally {
|
|
955
|
+
$confirmBtn.disabled = false;
|
|
956
|
+
if ($btnText) $btnText.textContent = originalText;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function hidePlanSwitcher($modal) {
|
|
961
|
+
const bsModal = bootstrap.Modal.getInstance($modal);
|
|
962
|
+
if (bsModal) bsModal.hide();
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// ─── Trial Cancel Warning ───────────────────────────────────
|
|
966
|
+
|
|
967
|
+
// Cancelling a free trial ends access IMMEDIATELY (#267) — the opposite of what
|
|
968
|
+
// the questionnaire's own copy promises — so a trial is told before it answers
|
|
969
|
+
// questions. The "Cancel subscription" link opens the questionnaire
|
|
970
|
+
// declaratively, and that toggle CANNOT be intercepted from the button:
|
|
971
|
+
// Bootstrap's collapse data-api is a delegate on `document`, and its
|
|
972
|
+
// EventHandler passes `isDelegated` straight through as `useCapture`, so it
|
|
973
|
+
// runs on the way DOWN (`document` → button) and has already opened the
|
|
974
|
+
// accordion before any listener on the button — capture or bubble — is
|
|
975
|
+
// reached. Taking `data-bs-toggle` off the button is the only thing that stops
|
|
976
|
+
// it, which is what syncCancelTriggerToggle() does for a cancel that owes a
|
|
977
|
+
// warning; the listener below then owns that button's clicks outright. A paid
|
|
978
|
+
// cancel keeps the attribute, is never intercepted, and behaves exactly as it
|
|
979
|
+
// did.
|
|
980
|
+
function setupTrialCancelWarning() {
|
|
981
|
+
const $trigger = document.getElementById('cancel-subscription-trigger-btn');
|
|
982
|
+
const $modal = document.getElementById('cancel-trial-warning-modal');
|
|
983
|
+
|
|
984
|
+
if (!$trigger || !$modal) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const $accordion = document.getElementById('cancel-subscription-accordion');
|
|
989
|
+
|
|
990
|
+
$trigger.addEventListener('click', (event) => {
|
|
991
|
+
if (!needsTrialCancelWarning()) {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
event.preventDefault();
|
|
996
|
+
event.stopPropagation();
|
|
997
|
+
|
|
998
|
+
// The button is a TOGGLE, and half of a toggle is closing. With the
|
|
999
|
+
// questionnaire already open the warning has been shown and answered, so
|
|
1000
|
+
// this click only shuts it — warning again would leave the accordion with
|
|
1001
|
+
// no way closed at all.
|
|
1002
|
+
if ($accordion?.classList.contains('show')) {
|
|
1003
|
+
bootstrap.Collapse.getOrCreateInstance($accordion, { toggle: false }).hide();
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Open FIRST, count second: counting is the analytics providers' business
|
|
1008
|
+
// and this dialog is the customer's, so a blocked snippet must never be
|
|
1009
|
+
// able to leave this button doing nothing at all.
|
|
1010
|
+
bootstrap.Modal.getOrCreateInstance($modal).show();
|
|
1011
|
+
trackBilling('cancel_trial_warning_shown');
|
|
1012
|
+
}, true);
|
|
1013
|
+
|
|
1014
|
+
// While the gate holds the trigger, Bootstrap does not count it as one of the
|
|
1015
|
+
// collapse's triggers and stops keeping its expanded state honest. Mirror it
|
|
1016
|
+
// off the collapse itself, which covers every way the questionnaire opens or
|
|
1017
|
+
// shuts — this button, "Keep my plan" inside it, and the auto-collapse after
|
|
1018
|
+
// a cancel goes through.
|
|
1019
|
+
$accordion?.addEventListener('shown.bs.collapse', () => setCancelTriggerExpanded($trigger, true));
|
|
1020
|
+
$accordion?.addEventListener('hidden.bs.collapse', () => setCancelTriggerExpanded($trigger, false));
|
|
1021
|
+
|
|
1022
|
+
document.getElementById('cancel-trial-continue-btn')?.addEventListener('click', () => {
|
|
1023
|
+
bootstrap.Modal.getInstance($modal)?.hide();
|
|
1024
|
+
|
|
1025
|
+
if ($accordion) {
|
|
1026
|
+
bootstrap.Collapse.getOrCreateInstance($accordion, { toggle: false }).show();
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
trackBilling('cancel_trial_warning_continue');
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
document.getElementById('cancel-trial-keep-btn')?.addEventListener('click', () => {
|
|
1033
|
+
trackBilling('cancel_trial_warning_keep');
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// The declarative collapse toggle on the "Cancel subscription" button: ON for
|
|
1038
|
+
// every cancel that goes straight to the questionnaire, OFF for one that owes a
|
|
1039
|
+
// warning first (#267). Bootstrap re-reads `[data-bs-toggle="collapse"]` from
|
|
1040
|
+
// the document on every click, so the attribute IS the switch — and it is
|
|
1041
|
+
// driven from the SAME state that decides whether the warning is owed at all,
|
|
1042
|
+
// so the markup and the gate can never disagree.
|
|
1043
|
+
function syncCancelTriggerToggle(warningOwed) {
|
|
1044
|
+
const $trigger = document.getElementById('cancel-subscription-trigger-btn');
|
|
1045
|
+
|
|
1046
|
+
if (!$trigger) {
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (warningOwed) {
|
|
1051
|
+
$trigger.removeAttribute('data-bs-toggle');
|
|
1052
|
+
} else {
|
|
1053
|
+
$trigger.setAttribute('data-bs-toggle', 'collapse');
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// What Bootstrap's own `_addAriaAndCollapsedClass` would do for this trigger,
|
|
1058
|
+
// for the states where it no longer tracks it.
|
|
1059
|
+
function setCancelTriggerExpanded($trigger, isOpen) {
|
|
1060
|
+
$trigger.classList.toggle('collapsed', !isOpen);
|
|
1061
|
+
$trigger.setAttribute('aria-expanded', isOpen);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Does the account on screen owe a warning before the questionnaire? Read off
|
|
1065
|
+
// the SAME state the bindings render, so the gate and the button it guards can
|
|
1066
|
+
// never disagree.
|
|
1067
|
+
function needsTrialCancelWarning() {
|
|
1068
|
+
return buildBillingState(currentAccount).billing.cancelWarning.show === true;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// ─── Winback Offer ──────────────────────────────────────────
|
|
1072
|
+
|
|
1073
|
+
// A customer who starts cancelling a PAID subscription is pitched a discount on
|
|
1074
|
+
// the next cycle before they are asked why they are leaving (#268). The system
|
|
1075
|
+
// already read a returning subscriber as a purchase (the `subscription-winback`
|
|
1076
|
+
// transition); this is the offer made while they are still here to keep.
|
|
1077
|
+
//
|
|
1078
|
+
// The gate is #267's, for the same reason: the "Cancel subscription" link opens
|
|
1079
|
+
// the questionnaire through Bootstrap's collapse data-api, which is a delegate
|
|
1080
|
+
// on `document` running in the CAPTURE phase, so the only thing that stops it is
|
|
1081
|
+
// taking `data-bs-toggle` off the button (syncCancelTriggerToggle) — and this
|
|
1082
|
+
// listener then owns that button's clicks. The two gates never contend: a trial
|
|
1083
|
+
// cancel is warned, a paid cancel is offered, and `offersWinback()` reads the
|
|
1084
|
+
// same state the button does.
|
|
1085
|
+
function setupWinbackOffer() {
|
|
1086
|
+
const $trigger = document.getElementById('cancel-subscription-trigger-btn');
|
|
1087
|
+
const $modal = document.getElementById('cancel-winback-modal');
|
|
1088
|
+
|
|
1089
|
+
if (!$trigger || !$modal) {
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const $accordion = document.getElementById('cancel-subscription-accordion');
|
|
1094
|
+
|
|
1095
|
+
$trigger.addEventListener('click', (event) => {
|
|
1096
|
+
if (!offersWinback()) {
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
event.preventDefault();
|
|
1101
|
+
event.stopPropagation();
|
|
1102
|
+
|
|
1103
|
+
// Half of a toggle is closing: with the questionnaire already open this
|
|
1104
|
+
// click only shuts it, exactly as the trial gate handles its own.
|
|
1105
|
+
if ($accordion?.classList.contains('show')) {
|
|
1106
|
+
bootstrap.Collapse.getOrCreateInstance($accordion, { toggle: false }).hide();
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Open FIRST, count second — a blocked analytics snippet must never be able
|
|
1111
|
+
// to leave this button doing nothing ([#283], [#306]).
|
|
1112
|
+
bootstrap.Modal.getOrCreateInstance($modal).show();
|
|
1113
|
+
trackBilling('winback_offer_shown');
|
|
1114
|
+
}, true);
|
|
1115
|
+
|
|
1116
|
+
// Declining answers THIS attempt: the questionnaire opens behind the dialog
|
|
1117
|
+
// and nothing is retired ([#324]). The trigger keeps its gate, so shutting the
|
|
1118
|
+
// questionnaire and coming back to cancel meets the offer again.
|
|
1119
|
+
document.getElementById('cancel-winback-decline-btn')?.addEventListener('click', () => {
|
|
1120
|
+
bootstrap.Modal.getInstance($modal)?.hide();
|
|
1121
|
+
|
|
1122
|
+
if ($accordion) {
|
|
1123
|
+
bootstrap.Collapse.getOrCreateInstance($accordion, { toggle: false }).show();
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
trackBilling('winback_offer_declined');
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
const $acceptBtn = document.getElementById('cancel-winback-accept-btn');
|
|
1130
|
+
|
|
1131
|
+
$acceptBtn?.addEventListener('click', () => acceptWinbackOffer($acceptBtn, $modal, $accordion));
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Apply the discount to the live subscription and call the cancel off. The
|
|
1135
|
+
// route talks to the provider and the webhook pipeline writes whatever state
|
|
1136
|
+
// changes — the subscription the customer keeps is the one they already had, so
|
|
1137
|
+
// the only thing patched locally is the discount the route hands back, which is
|
|
1138
|
+
// what the billing card's indicator reads ([#325]).
|
|
1139
|
+
async function acceptWinbackOffer($acceptBtn, $modal, $accordion) {
|
|
1140
|
+
const $btnText = $acceptBtn.querySelector('.button-text');
|
|
1141
|
+
const originalText = $btnText?.textContent;
|
|
1142
|
+
|
|
1143
|
+
try {
|
|
1144
|
+
// Show loading state
|
|
1145
|
+
$acceptBtn.disabled = true;
|
|
1146
|
+
if ($btnText) $btnText.textContent = 'Applying...';
|
|
1147
|
+
|
|
1148
|
+
const response = await omega.request(`/omega/payments/winback`, {
|
|
1149
|
+
method: 'POST',
|
|
1150
|
+
timeout: 30000,
|
|
1151
|
+
body: {
|
|
1152
|
+
confirmed: true,
|
|
1153
|
+
},
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
logger.log('Winback offer accepted:', { productId: currentAccount?.subscription?.product?.id });
|
|
1157
|
+
|
|
1158
|
+
claimWinbackOffer(response?.discount);
|
|
1159
|
+
|
|
1160
|
+
bootstrap.Modal.getInstance($modal)?.hide();
|
|
1161
|
+
|
|
1162
|
+
trackBilling('winback_offer_accepted');
|
|
1163
|
+
|
|
1164
|
+
omega.utilities().showNotification('Your discount is applied to your next bill. Nothing else changes, and you can still cancel any time.', 'success');
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
logger.error('Failed to apply the winback offer:', error);
|
|
1167
|
+
|
|
1168
|
+
// Same capability gate uncancel and plan-switch ride: the provider cannot
|
|
1169
|
+
// discount a live subscription at all, the claim has nowhere to be
|
|
1170
|
+
// recorded, or it was already claimed once (the offer's memory lives on the
|
|
1171
|
+
// order doc, which the account this card reads never carries, so a past
|
|
1172
|
+
// claimant IS pitched again) — either way the offer is a dead end for this
|
|
1173
|
+
// account, so retire it and open the questionnaire — a customer who came
|
|
1174
|
+
// here to cancel must never be left in a dialog that cannot answer them.
|
|
1175
|
+
//
|
|
1176
|
+
// The route names every refusal ([#311]), and the rest of them are dead
|
|
1177
|
+
// ends for the same reason: the brand turned the offer off since this page
|
|
1178
|
+
// loaded, the subscription is not the state the offer is for (ended,
|
|
1179
|
+
// suspended, trialing, already scheduled to cancel), or it carries no
|
|
1180
|
+
// provider to discount through. Pressing the button again changes none of
|
|
1181
|
+
// them. The one refusal LEFT OFF is `confirmation-required`: that is a
|
|
1182
|
+
// request that went out without its confirmation, which the same button
|
|
1183
|
+
// sending it again is the fix for.
|
|
1184
|
+
const deadEndCodes = [
|
|
1185
|
+
'not-supported-by-provider',
|
|
1186
|
+
'offer-not-claimable',
|
|
1187
|
+
'offer-already-claimed',
|
|
1188
|
+
'offer-disabled',
|
|
1189
|
+
'no-active-subscription',
|
|
1190
|
+
'trial-not-eligible',
|
|
1191
|
+
'cancellation-pending',
|
|
1192
|
+
'missing-payment-details',
|
|
1193
|
+
'unknown-provider',
|
|
1194
|
+
];
|
|
1195
|
+
if (deadEndCodes.includes(error.properties?.additional?.code)) {
|
|
1196
|
+
winbackSupported = false;
|
|
1197
|
+
updateUI(currentAccount);
|
|
1198
|
+
|
|
1199
|
+
bootstrap.Modal.getInstance($modal)?.hide();
|
|
1200
|
+
|
|
1201
|
+
if ($accordion) {
|
|
1202
|
+
bootstrap.Collapse.getOrCreateInstance($accordion, { toggle: false }).show();
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
omega.utilities().showNotification(error.message, { type: 'warning', timeout: 8000 });
|
|
1206
|
+
} else {
|
|
1207
|
+
// A failure is not a decline: the dialog stays up so the same button can
|
|
1208
|
+
// be pressed again.
|
|
1209
|
+
omega.utilities().showNotification(error.message || 'We could not apply your discount right now. Please try again later.', 'danger');
|
|
1210
|
+
}
|
|
1211
|
+
} finally {
|
|
1212
|
+
$acceptBtn.disabled = false;
|
|
1213
|
+
if ($btnText) $btnText.textContent = originalText;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// The offer has been CLAIMED. The discount is on the live subscription now and
|
|
1218
|
+
// the backend refuses a second claim, so no later cancel attempt is pitched it
|
|
1219
|
+
// again ([#324]). With nothing left to pitch, the trigger keeps its gate: the
|
|
1220
|
+
// next click meets the retention warning ([#341]) instead of the offer.
|
|
1221
|
+
//
|
|
1222
|
+
// The claim's durable home is the backend's (the order doc, and whatever the
|
|
1223
|
+
// webhook pipeline writes onto the account). The discount the route hands back
|
|
1224
|
+
// is patched onto the account this page holds so the card's indicator ([#325])
|
|
1225
|
+
// shows the saving in the same breath the customer earned it, exactly as the
|
|
1226
|
+
// cancel and plan-switch paths patch what they just changed.
|
|
1227
|
+
function claimWinbackOffer(discount) {
|
|
1228
|
+
winbackClaimed = true;
|
|
1229
|
+
|
|
1230
|
+
const currentSub = currentAccount?.subscription;
|
|
1231
|
+
if (currentSub && discount?.valid === true) {
|
|
1232
|
+
// The route's response is the discount shape without its source; the
|
|
1233
|
+
// persisted doc carries source 'winback' (payments/winback/post.js), so the
|
|
1234
|
+
// in-session patch stamps the same ([#325]). The persisted node also
|
|
1235
|
+
// carries a resourceId stamp ([#333]) this patch skips: nothing on the
|
|
1236
|
+
// client reads it, and the next account read carries it.
|
|
1237
|
+
currentSub.discount = { ...discount, source: 'winback' };
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
updateUI(currentAccount);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// Is a save offer owed before the questionnaire? Read off the SAME state the
|
|
1244
|
+
// bindings render, so the gate and the dialog can never disagree.
|
|
1245
|
+
function offersWinback() {
|
|
1246
|
+
return buildBillingState(currentAccount).billing.winbackOffer.show === true;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// ─── Retention Warning ──────────────────────────────────────
|
|
1250
|
+
|
|
1251
|
+
// The cancel flow's LAST word ([#341]): a cancel that gets neither the save
|
|
1252
|
+
// offer (#268) nor the trial warning (#267) used to reach the questionnaire
|
|
1253
|
+
// with nothing said at all. This dialog is what it owes them: what leaving
|
|
1254
|
+
// might cost, stated as the retention policy states it.
|
|
1255
|
+
//
|
|
1256
|
+
// The gate is #267's and #268's, mechanism for mechanism: Bootstrap's collapse
|
|
1257
|
+
// data-api is a CAPTURE-phase delegate on `document`, so the only thing that
|
|
1258
|
+
// stops it is taking `data-bs-toggle` off the trigger (syncCancelTriggerToggle),
|
|
1259
|
+
// and this listener then owns that button's clicks. The three never contend:
|
|
1260
|
+
// `needsRetentionWarning()` reads the same state the other two do, and that
|
|
1261
|
+
// state only owes this dialog when neither of theirs is owed.
|
|
1262
|
+
function setupRetentionWarning() {
|
|
1263
|
+
const $trigger = document.getElementById('cancel-subscription-trigger-btn');
|
|
1264
|
+
const $modal = document.getElementById('cancel-retention-modal');
|
|
1265
|
+
|
|
1266
|
+
if (!$trigger || !$modal) {
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
const $accordion = document.getElementById('cancel-subscription-accordion');
|
|
1271
|
+
|
|
1272
|
+
$trigger.addEventListener('click', (event) => {
|
|
1273
|
+
if (!needsRetentionWarning()) {
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
event.preventDefault();
|
|
1278
|
+
event.stopPropagation();
|
|
1279
|
+
|
|
1280
|
+
// Half of a toggle is closing: with the questionnaire already open this
|
|
1281
|
+
// click only shuts it, exactly as the other two gates handle their own.
|
|
1282
|
+
if ($accordion?.classList.contains('show')) {
|
|
1283
|
+
bootstrap.Collapse.getOrCreateInstance($accordion, { toggle: false }).hide();
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// Open FIRST, count second: a blocked analytics snippet must never be able
|
|
1288
|
+
// to leave this button doing nothing ([#283], [#306]).
|
|
1289
|
+
bootstrap.Modal.getOrCreateInstance($modal).show();
|
|
1290
|
+
trackBilling('cancel_retention_warning_shown');
|
|
1291
|
+
}, true);
|
|
1292
|
+
|
|
1293
|
+
document.getElementById('cancel-retention-continue-btn')?.addEventListener('click', () => {
|
|
1294
|
+
bootstrap.Modal.getInstance($modal)?.hide();
|
|
1295
|
+
|
|
1296
|
+
if ($accordion) {
|
|
1297
|
+
bootstrap.Collapse.getOrCreateInstance($accordion, { toggle: false }).show();
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
trackBilling('cancel_retention_warning_continue');
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
document.getElementById('cancel-retention-keep-btn')?.addEventListener('click', () => {
|
|
1304
|
+
trackBilling('cancel_retention_warning_keep');
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// Is the retention warning owed before the questionnaire? Read off the SAME
|
|
1309
|
+
// state the bindings render, so the gate and the dialog can never disagree.
|
|
1310
|
+
function needsRetentionWarning() {
|
|
1311
|
+
return buildBillingState(currentAccount).billing.cancelRetention.show === true;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// What a discount is WORTH, in the brand's own numbers: a flat amount off in
|
|
1315
|
+
// the brand's currency, or a percentage. ONE home, read by the offer being
|
|
1316
|
+
// pitched (#268) and by the discount already applied (#325) — two spellings of
|
|
1317
|
+
// the same number read as two different discounts.
|
|
1318
|
+
function discountOffText(discount) {
|
|
1319
|
+
return discount.amount > 0
|
|
1320
|
+
? formatCurrency(discount.amount, paymentConfig?.currency || 'USD')
|
|
1321
|
+
: `${discount.percent}%`;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// Which bills an applied discount comes off, in the cadence the subscription is
|
|
1325
|
+
// actually billed at. A permanent cut does not promise a single cycle.
|
|
1326
|
+
function discountWhenText(discount, frequency) {
|
|
1327
|
+
if (discount.duration === 'forever') {
|
|
1328
|
+
return 'Applied to every bill for as long as you stay';
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
return `Applied to your next ${FREQUENCY_LABELS[frequency] || 'bill'}`;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// What the offer SAYS: the brand's own number and the cadence the subscription
|
|
1335
|
+
// is billed at, in the dialog's promotional voice (#323) — short, forward, and
|
|
1336
|
+
// the whole pitch in one line.
|
|
1337
|
+
function winbackHeadline(offer, frequency) {
|
|
1338
|
+
const off = discountOffText(offer);
|
|
1339
|
+
|
|
1340
|
+
if (offer.duration === 'forever') {
|
|
1341
|
+
return `Take ${off} off for as long as you stay`;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
return `Take ${off} off your next ${FREQUENCY_LABELS[frequency] || 'cycle'}`;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// ─── Cancellation Form ──────────────────────────────────────
|
|
1348
|
+
|
|
1349
|
+
function setupCancellationForm() {
|
|
1350
|
+
const $form = document.getElementById('cancel-subscription-form');
|
|
1351
|
+
if (!$form) {
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// Populate randomized reasons
|
|
1356
|
+
populateCancelReasons();
|
|
1357
|
+
|
|
1358
|
+
cancelFormManager = new FormManager('#cancel-subscription-form', {
|
|
1359
|
+
allowResubmit: false,
|
|
1360
|
+
warnOnUnsavedChanges: false,
|
|
1361
|
+
submittingText: 'Cancelling...',
|
|
1362
|
+
submittedText: 'Subscription Cancelled',
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
cancelFormManager.on('submit', async ({ data }) => {
|
|
1366
|
+
// Get selected reason
|
|
1367
|
+
const $selectedReason = document.querySelector('input[name="cancel_reason"]:checked');
|
|
1368
|
+
const reason = $selectedReason?.value || '';
|
|
1369
|
+
|
|
1370
|
+
// Capture state BEFORE the API call — the auth listener may update currentAccount
|
|
1371
|
+
// with Firestore data (cancellation.pending=true) before we reach the post-cancel code
|
|
1372
|
+
const resolvedBeforeCancel = omega.auth().resolveSubscription(currentAccount);
|
|
1373
|
+
const isTrialCancel = resolvedBeforeCancel.trialing;
|
|
1374
|
+
|
|
1375
|
+
logger.log('Cancelling:', { plan: resolvedBeforeCancel.plan, isTrialCancel });
|
|
1376
|
+
|
|
1377
|
+
trackBilling('cancel_submit');
|
|
1378
|
+
|
|
1379
|
+
const response = await omega.request(`/omega/payments/cancel`, {
|
|
1380
|
+
method: 'POST',
|
|
1381
|
+
timeout: 30000,
|
|
1382
|
+
body: {
|
|
1383
|
+
reason: reason,
|
|
1384
|
+
feedback: data.feedback || '',
|
|
1385
|
+
confirmed: true,
|
|
1386
|
+
},
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
if (response.error) {
|
|
1390
|
+
throw new Error(response.message || 'Failed to cancel subscription. Please try again.');
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
logger.log('Cancel complete:', { isTrialCancel, productId: currentAccount?.subscription?.product?.id });
|
|
1394
|
+
|
|
1395
|
+
if (isTrialCancel) {
|
|
1396
|
+
cancelFormManager.showSuccess('Your trial has been cancelled. You\'ve been moved to the free plan. You can subscribe again anytime.');
|
|
1397
|
+
} else {
|
|
1398
|
+
cancelFormManager.showSuccess('Your subscription has been cancelled. You\'ll continue to have access until the end of your current billing period.');
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// Collapse the cancel form after a short delay
|
|
1402
|
+
setTimeout(() => {
|
|
1403
|
+
const $accordion = document.getElementById('cancel-subscription-accordion');
|
|
1404
|
+
if ($accordion) {
|
|
1405
|
+
const bsCollapse = bootstrap.Collapse.getInstance($accordion);
|
|
1406
|
+
if (bsCollapse) bsCollapse.hide();
|
|
1407
|
+
}
|
|
1408
|
+
}, 1000);
|
|
1409
|
+
|
|
1410
|
+
// Update the UI to reflect cancellation
|
|
1411
|
+
// Re-read currentAccount.subscription since the auth listener may have replaced it
|
|
1412
|
+
const currentSub = currentAccount?.subscription;
|
|
1413
|
+
if (currentSub) {
|
|
1414
|
+
if (isTrialCancel) {
|
|
1415
|
+
// Trial cancellations are immediate — downgrade to basic
|
|
1416
|
+
currentSub.status = 'cancelled';
|
|
1417
|
+
currentSub.product = { id: 'basic', name: 'Basic' };
|
|
1418
|
+
currentSub.cancellation = {
|
|
1419
|
+
pending: false,
|
|
1420
|
+
date: {
|
|
1421
|
+
timestamp: new Date().toISOString(),
|
|
1422
|
+
timestampUNIX: Math.floor(Date.now() / 1000),
|
|
1423
|
+
},
|
|
1424
|
+
};
|
|
1425
|
+
} else {
|
|
1426
|
+
// Non-trial cancellations are pending until end of billing period
|
|
1427
|
+
const expiresUnix = currentSub.expires?.timestampUNIX || 0;
|
|
1428
|
+
currentSub.cancellation = {
|
|
1429
|
+
pending: true,
|
|
1430
|
+
date: {
|
|
1431
|
+
timestamp: new Date(expiresUnix * 1000).toISOString(),
|
|
1432
|
+
timestampUNIX: expiresUnix,
|
|
1433
|
+
},
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
logger.log('UI update after cancel:', { status: currentSub.status, productId: currentSub.product?.id, cancellationPending: currentSub.cancellation?.pending });
|
|
1438
|
+
|
|
1439
|
+
updateUI(currentAccount);
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function populateCancelReasons() {
|
|
1445
|
+
const $container = document.getElementById('cancel-reasons-container');
|
|
1446
|
+
if (!$container) {
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
const reasons = [...CANCEL_REASONS];
|
|
1451
|
+
const other = reasons.pop(); // Remove 'Other' from the end
|
|
1452
|
+
const shuffled = [...shuffleArray(reasons), other]; // Shuffle rest, append 'Other' last
|
|
1453
|
+
|
|
1454
|
+
$container.innerHTML = shuffled.map((reason, i) => `
|
|
1455
|
+
<div class="form-check mb-2">
|
|
1456
|
+
<input class="form-check-input" type="radio" name="cancel_reason" id="cancel-reason-${i}" value="${omega.utilities().escapeHTML(reason)}">
|
|
1457
|
+
<label class="form-check-label" for="cancel-reason-${i}">${omega.utilities().escapeHTML(reason)}</label>
|
|
1458
|
+
</div>
|
|
1459
|
+
`).join('');
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// ─── Usage Metrics ───────────────────────────────────────────
|
|
1463
|
+
|
|
1464
|
+
// A bar per COUNTED feature the catalog defines: what is left today, what is
|
|
1465
|
+
// left this month, and the override credits included in the month's number when
|
|
1466
|
+
// an admin granted any ([#647](https://github.com/Omega-JS-Stack/omega/issues/647)).
|
|
1467
|
+
//
|
|
1468
|
+
// Both numbers are shown because either one can be the thing standing in the
|
|
1469
|
+
// user's way: a paced feature refuses on the day's share long before the month
|
|
1470
|
+
// runs out, and a bar that only drew the month would leave "why did that fail?"
|
|
1471
|
+
// unanswered. The arithmetic is @omega.js/account's, the same module the
|
|
1472
|
+
// backend's gate reads, so a bar can never draw a limit the gate would not
|
|
1473
|
+
// enforce. Names, icons and definitions come from the catalog.
|
|
1474
|
+
function updateUsageInfo(account) {
|
|
1475
|
+
const $container = document.getElementById('usage-metrics-container');
|
|
1476
|
+
|
|
1477
|
+
if (!$container) {
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// The EFFECTIVE plan (basic if cancelled/suspended) is the one whose numbers apply
|
|
1482
|
+
const resolved = omega.auth().resolveSubscription(account);
|
|
1483
|
+
const product = paymentConfig?.products?.find(p => p.id === resolved.plan);
|
|
1484
|
+
const catalog = featureCatalog();
|
|
1485
|
+
|
|
1486
|
+
const features = resolveFeatures({ catalog, product: product || {}, account })
|
|
1487
|
+
.filter((feature) => feature.counted && feature.limit !== 0);
|
|
1488
|
+
|
|
1489
|
+
// Clear container
|
|
1490
|
+
$container.innerHTML = '';
|
|
1491
|
+
|
|
1492
|
+
if (features.length === 0) {
|
|
1493
|
+
$container.innerHTML = '<div class="text-muted">Usage tracking not available for this plan.</div>';
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
$container.innerHTML = features.map(renderUsageFeature).join('');
|
|
1498
|
+
|
|
1499
|
+
initializeTooltips($container);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// One feature's block: its name (with the catalog's definition on hover), the
|
|
1503
|
+
// day bar when the feature is paced, and the month bar always.
|
|
1504
|
+
function renderUsageFeature(feature) {
|
|
1505
|
+
const escape = omega.utilities().escapeHTML;
|
|
1506
|
+
const icon = feature.icon
|
|
1507
|
+
? `<i class="fa-solid fa-${escape(feature.icon)} fa-sm me-1 text-muted"></i>`
|
|
1508
|
+
: '';
|
|
1509
|
+
const name = feature.definition
|
|
1510
|
+
? `<span class="text-decoration-underline text-decoration-dotted cursor-help" data-bs-toggle="tooltip" data-bs-title="${escape(feature.definition)}">${escape(feature.name)}</span>`
|
|
1511
|
+
+ `<span class="visually-hidden"> — ${escape(feature.definition)}</span>`
|
|
1512
|
+
: escape(feature.name);
|
|
1513
|
+
|
|
1514
|
+
// The override is EXTRA credits an admin granted ON TOP of the plan — say so,
|
|
1515
|
+
// because the number beside it is not the number the pricing page quotes.
|
|
1516
|
+
// Only when it is genuinely extra: an override can also dial a plan DOWN for
|
|
1517
|
+
// one account, and subtracting unconditionally announced that as "+-50
|
|
1518
|
+
// granted". A lower override is still the limit the bar draws; it just has no
|
|
1519
|
+
// gift to claim.
|
|
1520
|
+
const extra = feature.override !== null ? feature.override - feature.planLimit : 0;
|
|
1521
|
+
const granted = extra > 0
|
|
1522
|
+
? `<span class="badge text-bg-success-subtle ms-2">+${escape(formatMetricValue(feature.id, extra))} granted</span>`
|
|
1523
|
+
: '';
|
|
1524
|
+
|
|
1525
|
+
const bars = [
|
|
1526
|
+
feature.day.limit >= 0 ? renderUsageBar(feature, feature.day.limit, feature.day.used, feature.day.left, 'today') : '',
|
|
1527
|
+
renderUsageBar(feature, feature.limit, feature.used, feature.left, 'this month'),
|
|
1528
|
+
].join('');
|
|
1529
|
+
|
|
1530
|
+
return `
|
|
1531
|
+
<div class="mb-3">
|
|
1532
|
+
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
1533
|
+
<small class="text-muted fw-semibold">${icon}${name}${granted}</small>
|
|
1534
|
+
</div>
|
|
1535
|
+
${bars}
|
|
1536
|
+
</div>
|
|
1537
|
+
`;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// One bar. `limit` of -1 is unlimited: no percentage can be honest about a
|
|
1541
|
+
// number with no ceiling, so the bar stays empty and the label says so.
|
|
1542
|
+
function renderUsageBar(feature, limit, used, left, period) {
|
|
1543
|
+
const escape = omega.utilities().escapeHTML;
|
|
1544
|
+
const isUnlimited = limit < 0;
|
|
1545
|
+
const usagePercent = isUnlimited ? 0 : Math.min(100, Math.round((used / limit) * 100));
|
|
1546
|
+
|
|
1547
|
+
let progressClass = 'bg-success';
|
|
1548
|
+
if (!isUnlimited && usagePercent >= 80) {
|
|
1549
|
+
progressClass = 'bg-danger';
|
|
1550
|
+
} else if (!isUnlimited && usagePercent >= 50) {
|
|
1551
|
+
progressClass = 'bg-warning';
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
const label = isUnlimited
|
|
1555
|
+
? `Unlimited ${period}`
|
|
1556
|
+
: `${formatMetricValue(feature.id, left)} of ${formatMetricValue(feature.id, limit)} left ${period}`;
|
|
1557
|
+
|
|
1558
|
+
return `
|
|
1559
|
+
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
1560
|
+
<small class="text-muted">${escape(period)}</small>
|
|
1561
|
+
<small class="text-muted">${escape(label)}</small>
|
|
1562
|
+
</div>
|
|
1563
|
+
<div class="progress mb-2" style="height: 20px;">
|
|
1564
|
+
<div class="progress-bar ${progressClass}" role="progressbar"
|
|
1565
|
+
style="width: ${usagePercent}%"
|
|
1566
|
+
aria-valuenow="${usagePercent}"
|
|
1567
|
+
aria-valuemin="0"
|
|
1568
|
+
aria-valuemax="100"
|
|
1569
|
+
aria-label="${escape(`${feature.name} used ${period}`)}">
|
|
1570
|
+
${usagePercent}%
|
|
1571
|
+
</div>
|
|
1572
|
+
</div>
|
|
1573
|
+
`;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// ─── Helpers ─────────────────────────────────────────────────
|
|
1577
|
+
|
|
1578
|
+
function getDisplayName(subscription) {
|
|
1579
|
+
// Use backend-provided product name first
|
|
1580
|
+
if (subscription.product?.name && subscription.product.name !== 'Basic') {
|
|
1581
|
+
return subscription.product.name;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// Fall back to config product name
|
|
1585
|
+
const productId = subscription.product?.id || 'basic';
|
|
1586
|
+
const product = paymentConfig?.products?.find(p => p.id === productId);
|
|
1587
|
+
return product?.name || 'Free';
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// A plan's price at one frequency (a price entry is either `{ amount: N }` or
|
|
1591
|
+
// a plain `N`, the same two shapes checkout resolves)
|
|
1592
|
+
function resolvePlanPrice(product, frequency) {
|
|
1593
|
+
const entry = product.prices?.[frequency];
|
|
1594
|
+
if (entry == null) {
|
|
1595
|
+
return 0;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
return typeof entry === 'object' ? (entry.amount || 0) : Number(entry) || 0;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// How one details slot is drawn: a known value at full ink, an unknown one in
|
|
1602
|
+
// the muted placeholder's own class.
|
|
1603
|
+
function detailClass(value) {
|
|
1604
|
+
return value ? DETAIL_CLASS : UNKNOWN_DETAIL_CLASS;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// A UNIX timestamp as the local short date, or '' when there is no date to
|
|
1608
|
+
// show — the caller decides what an absent date means for its own line.
|
|
1609
|
+
function formatDate(timestampUNIX) {
|
|
1610
|
+
return (timestampUNIX && timestampUNIX > 0)
|
|
1611
|
+
? new Date(timestampUNIX * 1000).toLocaleDateString()
|
|
1612
|
+
: '';
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
function formatCurrency(amount, currency) {
|
|
1616
|
+
return new Intl.NumberFormat('en-US', {
|
|
1617
|
+
style: 'currency',
|
|
1618
|
+
currency: (currency || 'USD').toUpperCase(),
|
|
1619
|
+
}).format(amount); // amount is already in display dollars
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// A feature's value in the units it is counted in. The NAME is never derived
|
|
1623
|
+
// here any more — the catalog holds it (#647), so a brand that renames a
|
|
1624
|
+
// feature renames it in one place.
|
|
1625
|
+
function formatMetricValue(metricId, value) {
|
|
1626
|
+
if (metricId === 'storage' || metricId === 'bandwidth') {
|
|
1627
|
+
return formatBytes(value);
|
|
1628
|
+
}
|
|
1629
|
+
return value.toLocaleString();
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
function formatBytes(bytes, decimals = 2) {
|
|
1633
|
+
if (bytes === 0) {
|
|
1634
|
+
return '0 Bytes';
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
const k = 1024;
|
|
1638
|
+
const dm = decimals < 0 ? 0 : decimals;
|
|
1639
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
1640
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1641
|
+
|
|
1642
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function shuffleArray(arr) {
|
|
1646
|
+
for (let i = arr.length - 1; i > 0; i--) {
|
|
1647
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
1648
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
1649
|
+
}
|
|
1650
|
+
return arr;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// ─── Tracking ────────────────────────────────────────────────
|
|
1654
|
+
|
|
1655
|
+
// Counting a billing action may never COST the customer that action ([#283]).
|
|
1656
|
+
// An ad blocker does not stub these snippets, it stops them loading, so the
|
|
1657
|
+
// names are simply never defined — and every caller here counts before it acts,
|
|
1658
|
+
// so one ReferenceError turned "Undo cancellation" into a dead button. The
|
|
1659
|
+
// guard this card was given first is now the ONE canonical call ([#328]): the
|
|
1660
|
+
// catalog decides who hears `user_billing_action`, and the transport under it asks
|
|
1661
|
+
// for each provider on its own — blockers work per list, so a page with Google
|
|
1662
|
+
// allowed and Meta blocked still counts what it can.
|
|
1663
|
+
function trackBilling(action) {
|
|
1664
|
+
event('user_billing_action', {
|
|
1665
|
+
action: action,
|
|
1666
|
+
});
|
|
1667
|
+
}
|