Back to Journal

Surfacing PRO status on salary estimates

Listen to this articleAI narration
0:00 / 0:00

Two tasks today. The first was cleanup — removing leftover Stripe references from translation files and adding a "read more" link to the subscription pricing page. Fifteen minutes. The second consumed the rest of the day: showing a PRO badge instead of a fee amount on invoice salary estimates, with an explanation that appears on hover.

The request seemed straightforward. PRO subscribers pay a different commission structure. On salary estimates (before the invoice is paid), don't show the fee amount — show a "PRO" badge instead. On actual salary summaries (after payment), show the real amount. The badge should explain itself when the user interacts with it.

The Data Problem

The fee formatting logic in invoice-helper.jsx determines what to display. When proEligibleAtSend is true, it sets a percentageLabel: 'PRO' flag instead of a percentage value:

worknode_fee: {
  label: t('invoice.numbers.worknodeFee'),
  value: data?.commissionAmount,
  percentage: data?.proEligibleAtSend ? undefined : (worknodePercentage || undefined),
  percentageLabel: data?.proEligibleAtSend ? 'PRO' : undefined,
}

The badge never appeared. The proEligibleAtSend boolean was missing from the salary estimation response entirely.

proEligibleAtSend: Where It Exists vs Where It's NeededInvoice EntityGroups: Invoice:CollectionproEligibleAtSend ✓InvoiceAmounts DTOGroups: SalaryproEligibleAtSend ✗Frontend HelperformatSummaryOrSalary()data.proEligibleAtSend → undefinedFix: Dual approachBackend: Add field to DTO + serialization group | Frontend: Enrich response from invoice entity

The invoice entity had proEligibleAtSend serialized under Invoice:Collection and Invoice:Item groups. The salary estimation endpoint returns an InvoiceAmounts DTO serialized under the Salary group — a completely different serialization path. The boolean simply wasn't included.

Two fixes. Backend: add the field to InvoiceAmounts and pass it through from SalaryEstimationProcessor. Frontend: enrich the estimation response with the invoice's own proEligibleAtSend as an immediate fallback:

const enrichedResponse = {...response, proEligibleAtSend: invoice?.proEligibleAtSend}
const _details = {...formatSummaryOrSalary(enrichedResponse), raw: response}

Belt and suspenders. The backend fix is the correct solution. The frontend enrichment is insurance.

Four UI Iterations

With the data flowing, the badge needed to render. What followed was four attempts to get the visual right.

Attempt 1: Reuse the existing PercentageBadge component. Rejected — that component shows percentage values, not labels. Wrong semantic fit.

Attempt 2: Use the Badge component with style="pro" plus a paragraph of explanation text below. The badge looked correct — dark blue pill, white text — but the explanation paragraph floated outside the panel, disconnected from the badge. Not acceptable.

Attempt 3: Use the existing Hint component — a Headless UI Popover with an info icon that reveals text on hover. Functionally correct, but the default Hint renders as a plain info icon with text in the accent color. It looked nothing like a PRO badge.

Attempt 4: Extend Hint to accept a style prop. When style="pro", the PopoverButton renders with Badge-like styling instead of the default info-icon look.

UI Iteration Path1. PercentageBadgeWrong component entirely✗ rejected2. Badge + <p> textExplanation floats outside✗ layout broken3. Default HintInfo icon + plain text~ functional, wrong style4. Hint style="pro"Badge visual + popover✓ shippedFinal Hint Component (style="pro")PopoverButton: bg-blue-400 text-grey-100 rounded-full px-2 + info iconPopoverPanel: bg-secondary-accent text-base-negative (explanation on hover)

The final Hint component conditionally switches its button styling based on the style prop:

const buttonClass = style === 'pro'
  ? 'inline-flex items-center gap-1 font-[580] tx-xs leading-none rounded-full px-2 min-h-5.25 bg-blue-400 text-grey-100 cursor-pointer'
  : 'flex items-center gap-2 text-sm text-primary-accent-text font-[580] cursor-pointer'

When style="pro", the button renders as a dark blue pill with a small info icon — visually identical to the Badge component's PRO style, but tappable with a popover explanation. The default style remains unchanged for existing Hint usage elsewhere.

Where It Shows

The badge appears in two estimation contexts: the invoice creation sidebar (Summary.jsx) and the sent invoice details panel (Row.jsxSentDetails). Both conditionally render:

{percentageLabel === 'PRO'
  ? <Hint description={t('invoice.numbers.worknodeFeeProNote')} label="PRO" style="pro"/>
  : <FormattedCurrency value={value} fromCents decimals style="small" currency="SEK"/>}

The paid salary views (PaidDetails and Salaries/Row.jsx) always show the actual amount — no badge, no conditional. PRO status doesn't matter once the fee is calculated and paid.

The Cleanup

The first task — removing Stripe references — touched the translation files in both locales. The pro.plans.description key had Stripe-specific payment text that no longer applied. The paymentInfo keys under pro.settings were dead code. Both removed. A pro.plans.readMore key was added, linking to the external PRO information page from the pricing cards.

Files Changed

Eight files across two repositories. Frontend: Hint.jsx (extended with style prop), Summary.jsx and Invoice/Row.jsx (PRO badge rendering), Salaries/Row.jsx (removed unused import), invoice-collection.jsx (response enrichment), sv.json and en.json (translation cleanup + new keys), Pricing.jsx (read more link). Backend: InvoiceAmounts.php (added field to DTO), SalaryEstimationProcessor.php (passes boolean through).

The iterative nature of the UI work was the real lesson. The data fix was systematic — trace the serialization path, find the gap, bridge it. The UI required four attempts because the right answer was a component that didn't exist yet: a Badge that explains itself.