> ## Documentation Index
> Fetch the complete documentation index at: https://docs.threadi.au/llms.txt
> Use this file to discover all available pages before exploring further.

# Pricing

> Explore our app subscription options and choose a plan that suits your needs

export default function PricingSwitcher() {
  const normalizeAppParam = value => {
    if (!value) {
      return null;
    }
    const normalized = value.toLowerCase().trim();
    if (normalized === 'stopwatch') {
      return 'stopwatch';
    }
    if (normalized === 'utilities') {
      return 'utilities';
    }
    return null;
  };
  const CURRENCIES = [{
    code: 'AUD',
    label: '$ AUD'
  }, {
    code: 'USD',
    label: '$ USD'
  }, {
    code: 'EUR',
    label: '€ EUR'
  }, {
    code: 'GBP',
    label: '£ GBP'
  }, {
    code: 'CAD',
    label: '$ CAD'
  }, {
    code: 'NZD',
    label: '$ NZD'
  }];
  const BILLING_PERIODS = ['monthly', 'yearly'];
  const STRIPE_PUBLISHABLE_KEY = 'pk_live_51K9rkcAOwnRQWt03SQa6t9Wbx6lRuRz0nNGbH8C9pXUw4xOwWbY9F6gDaK3ZQ8QyikGmDbHPBlRjERweByAiXX8X00QVqYuETx';
  const STRIPE_TABLE_IDS = {
    stopwatch: {
      AUD: {
        monthly: 'prctbl_1RAKmAAOwnRQWt037p3smYRE',
        yearly: 'prctbl_1RAKpaAOwnRQWt03GET720e4'
      },
      NZD: {
        monthly: 'prctbl_1RAKuvAOwnRQWt039IqDXlIO',
        yearly: 'prctbl_1RAKy1AOwnRQWt03DuAoBefx'
      },
      GBP: {
        monthly: 'prctbl_1RAL03AOwnRQWt03mPbNpEap',
        yearly: 'prctbl_1RAL1uAOwnRQWt034vUANoyr'
      },
      EUR: {
        monthly: 'prctbl_1RAL3yAOwnRQWt03Bs7wDW4D',
        yearly: 'prctbl_1RAL64AOwnRQWt03oIbo3pmm'
      },
      USD: {
        monthly: 'prctbl_1RAL7lAOwnRQWt032qPzHdTc',
        yearly: 'prctbl_1RAL9aAOwnRQWt03ZW1TunfE'
      },
      CAD: {
        monthly: 'prctbl_1RALBhAOwnRQWt03C4FEAk3b',
        yearly: 'prctbl_1RALDYAOwnRQWt03Q6eLl2Hp'
      }
    },
    utilities: {
      AUD: {
        monthly: 'prctbl_1RCberAOwnRQWt03Zg3hSiQz',
        yearly: 'prctbl_1RCbgqAOwnRQWt03KRC0noc4'
      },
      NZD: {
        monthly: 'prctbl_1RCbigAOwnRQWt03KghhsH8L',
        yearly: 'prctbl_1RCbjrAOwnRQWt036HoARdWD'
      },
      GBP: {
        monthly: 'prctbl_1RCbl4AOwnRQWt03uJjBGUrK',
        yearly: 'prctbl_1RCbmPAOwnRQWt03KwvaIYYb'
      },
      EUR: {
        monthly: 'prctbl_1RCbnUAOwnRQWt03QxohuhNq',
        yearly: 'prctbl_1RCboPAOwnRQWt03mWjHfNf7'
      },
      USD: {
        monthly: 'prctbl_1RCbpQAOwnRQWt03Mbvv6oeA',
        yearly: 'prctbl_1RCbqIAOwnRQWt03Joisfjz6'
      },
      CAD: {
        monthly: 'prctbl_1RCbrHAOwnRQWt03iYhUpsJZ',
        yearly: 'prctbl_1RCbsDAOwnRQWt03lr4OMVKi'
      }
    }
  };
  const APP_LABELS = {
    stopwatch: 'StopWatch',
    utilities: 'Utilities'
  };
  const [selectedApp, setSelectedApp] = useState('stopwatch');
  const [selectedCurrency, setSelectedCurrency] = useState('AUD');
  const [selectedBilling, setSelectedBilling] = useState('monthly');
  const [isDark, setIsDark] = useState(false);
  const stripeContainerRef = useRef(null);
  const appLabel = APP_LABELS[selectedApp];
  const activeTableId = STRIPE_TABLE_IDS[selectedApp][selectedCurrency][selectedBilling];
  useEffect(() => {
    const appFromQuery = normalizeAppParam(new URLSearchParams(window.location.search).get('app'));
    if (appFromQuery) {
      setSelectedApp(appFromQuery);
    }
  }, []);
  useEffect(() => {
    const src = 'https://js.stripe.com/v3/pricing-table.js';
    if (!document.querySelector(`script[src="${src}"]`)) {
      const script = document.createElement('script');
      script.src = src;
      script.async = true;
      document.body.appendChild(script);
    }
  }, []);
  useEffect(() => {
    const root = document.documentElement;
    const updateTheme = () => {
      const hasDarkClass = root.classList.contains('dark');
      const dataTheme = root.getAttribute('data-theme');
      setIsDark(hasDarkClass || dataTheme === 'dark');
    };
    updateTheme();
    const observer = new MutationObserver(updateTheme);
    observer.observe(root, {
      attributes: true,
      attributeFilter: ['class', 'data-theme']
    });
    return () => observer.disconnect();
  }, []);
  useEffect(() => {
    if (!stripeContainerRef.current) {
      return;
    }
    stripeContainerRef.current.innerHTML = '';
    const stripeTable = document.createElement('stripe-pricing-table');
    stripeTable.setAttribute('pricing-table-id', activeTableId);
    stripeTable.setAttribute('publishable-key', STRIPE_PUBLISHABLE_KEY);
    stripeContainerRef.current.appendChild(stripeTable);
  }, [activeTableId, STRIPE_PUBLISHABLE_KEY]);
  const neutralBorder = isDark ? '#4B5563' : '#D0D5DD';
  const panelBg = isDark ? '#1F2937' : '#FFFFFF';
  const controlBg = isDark ? '#111827' : '#FFFFFF';
  const controlText = isDark ? '#E5E7EB' : '#111827';
  return <>
      <Columns cols={2}>
        <Callout icon="dove" iconType="duotone" color={isDark ? '#D1D5DB' : '#4D4D4D'}>
          We are proud to contribute 1% of our revenue to <strong>BirdLife</strong>, leaders of bird conservation efforts in Australia. Read more about their impact <a href="https://birdlife.org.au/our-impact/" target="_blank" rel="noreferrer">here</a>.
        </Callout>
        <Callout icon="trees" iconType="duotone" color={isDark ? '#D1D5DB' : '#4D4D4D'}>
         Another 1% of our revenue is contributed to removing CO₂ from the atmosphere. Read more about <strong>Stripe's</strong> climate mission <a href="https://stripe.com/au/climate" target="_blank" rel="noreferrer">here</a>.
        </Callout>
      </Columns>
      <div style={{
    border: `1px solid ${neutralBorder}`,
    borderRadius: '12px',
    padding: '16px',
    background: panelBg,
    marginBottom: '40px',
    marginTop: '15px'
  }}>
        <h3 style={{
    marginTop: '0',
    marginBottom: '10px'
  }}>Choose App</h3>
        <div style={{
    display: 'flex',
    gap: '8px',
    flexWrap: 'wrap',
    marginBottom: '16px'
  }}>
          <button type="button" onClick={() => setSelectedApp('stopwatch')} style={{
    padding: '10px 14px',
    borderRadius: '10px',
    border: selectedApp === 'stopwatch' ? '2px solid #CFA860' : `1px solid ${neutralBorder}`,
    background: selectedApp === 'stopwatch' ? '#F0CB8E' : controlBg,
    color: selectedApp === 'stopwatch' ? '#1F2937' : controlText,
    fontWeight: selectedApp === 'stopwatch' ? 700 : 500,
    cursor: 'pointer'
  }}>
            StopWatch
          </button>
          <button type="button" onClick={() => setSelectedApp('utilities')} style={{
    padding: '10px 14px',
    borderRadius: '10px',
    border: selectedApp === 'utilities' ? '2px solid #5D7E62' : `1px solid ${neutralBorder}`,
    background: selectedApp === 'utilities' ? '#7A9F7F' : controlBg,
    color: selectedApp === 'utilities' ? '#FFFFFF' : controlText,
    fontWeight: selectedApp === 'utilities' ? 700 : 500,
    cursor: 'pointer'
  }}>
            Utilities
          </button>
        </div>

        <h3 style={{
    marginTop: '10px',
    marginBottom: '10px'
  }}>Choose Currency</h3>
        <div style={{
    display: 'flex',
    gap: '8px',
    flexWrap: 'wrap',
    marginBottom: '16px'
  }}>
          {CURRENCIES.map(currency => <button key={currency.code} type="button" onClick={() => setSelectedCurrency(currency.code)} style={{
    padding: '8px 12px',
    borderRadius: '10px',
    border: selectedCurrency === currency.code ? '2px solid #84CC16' : `1px solid ${neutralBorder}`,
    background: selectedCurrency === currency.code ? '#ECFCCB' : controlBg,
    color: selectedCurrency === currency.code ? '#1F2937' : controlText,
    fontWeight: selectedCurrency === currency.code ? 700 : 500,
    cursor: 'pointer'
  }}>
              {currency.label}
            </button>)}
        </div>

        <h3 style={{
    marginTop: '10px',
    marginBottom: '10px'
  }}>Choose Billing Frequency</h3>
        <div style={{
    display: 'flex',
    gap: '8px',
    flexWrap: 'wrap'
  }}>
          {BILLING_PERIODS.map(period => <button key={period} type="button" onClick={() => setSelectedBilling(period)} style={{
    padding: '8px 12px',
    borderRadius: '10px',
    border: selectedBilling === period ? '2px solid #4473C3' : `1px solid ${neutralBorder}`,
    background: selectedBilling === period ? '#E9F0FF' : controlBg,
    color: selectedBilling === period ? '#1E3A8A' : controlText,
    fontWeight: selectedBilling === period ? 700 : 500,
    textTransform: 'capitalize',
    cursor: 'pointer'
  }}>
              {period}
            </button>)}
        </div>
      </div>

      <h3 style={{
    marginTop: '12px',
    marginBottom: '10px'
  }}>Review Options & Create Subscription</h3>
      <Note>
        <strong>{appLabel}</strong> subscription options in <strong>{selectedCurrency}</strong> on a{' '}
        <strong style={{
    textTransform: 'capitalize'
  }}>{selectedBilling}</strong> plan
      </Note>
      <Callout icon="hubspot">
        All our <strong>subscription prices are applied per HubSpot Portal</strong>. Enjoy an unlimited number of users at no extra cost.
      </Callout>

      <div key={`${selectedApp}-${selectedCurrency}-${selectedBilling}`} ref={stripeContainerRef} />
    </>;
}

<PricingSwitcher />
