• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / mender-server / 10423

11 Nov 2025 04:53PM UTC coverage: 74.435% (-0.1%) from 74.562%
10423

push

gitlab-ci

web-flow
Merge pull request #1071 from mendersoftware/dependabot/npm_and_yarn/frontend/main/development-dependencies-92732187be

3868 of 5393 branches covered (71.72%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 2 files covered. (100.0%)

176 existing lines in 95 files now uncovered.

64605 of 86597 relevant lines covered (74.6%)

7.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.19
/frontend/src/js/components/settings/organization/Billing.tsx
1
// Copyright 2024 Northern.tech AS
2✔
2
//
2✔
3
//    Licensed under the Apache License, Version 2.0 (the "License");
2✔
4
//    you may not use this file except in compliance with the License.
2✔
5
//    You may obtain a copy of the License at
2✔
6
//
2✔
7
//        http://www.apache.org/licenses/LICENSE-2.0
2✔
8
//
2✔
9
//    Unless required by applicable law or agreed to in writing, software
2✔
10
//    distributed under the License is distributed on an "AS IS" BASIS,
2✔
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2✔
12
//    See the License for the specific language governing permissions and
2✔
13
//    limitations under the License.
2✔
14
import { useEffect, useState } from 'react';
2✔
15
import { useSelector } from 'react-redux';
2✔
16
import { Link } from 'react-router-dom';
2✔
17

2✔
18
// material ui
2✔
19
import { Error as ErrorIcon } from '@mui/icons-material';
2✔
20
import { Alert, Button, Typography } from '@mui/material';
2✔
21
import { makeStyles } from 'tss-react/mui';
2✔
22

2✔
23
import { SupportLink } from '@northern.tech/common-ui/SupportLink';
2✔
24
import { ADDONS, PLANS } from '@northern.tech/store/constants';
2✔
25
import { getBillingProfile, getCard, getDeviceLimit, getIsEnterprise, getOrganization, getUserRoles } from '@northern.tech/store/selectors';
2✔
26
import { useAppDispatch } from '@northern.tech/store/store';
2✔
27
import { cancelRequest, getCurrentCard, getUserBilling } from '@northern.tech/store/thunks';
2✔
28
import { toggle } from '@northern.tech/utils/helpers';
2✔
29
import dayjs from 'dayjs';
2✔
30
import relativeTime from 'dayjs/plugin/relativeTime.js';
2✔
31
import pluralize from 'pluralize';
2✔
32

2✔
33
import { PlanExpanded } from '../PlanExpanded';
2✔
34
import CancelRequestDialog from '../dialogs/CancelRequest';
2✔
35
import { BillingDetails } from './BillingDetails';
2✔
36
import OrganizationSettingsItem from './OrganizationSettingsItem';
2✔
37

2✔
38
dayjs.extend(relativeTime);
11✔
39

2✔
40
const useStyles = makeStyles()(theme => ({
11✔
41
  fullWidthUpgrade: {
2✔
42
    '&.settings-item-main-content': {
2✔
43
      gridTemplateColumns: '1fr'
2✔
44
    }
2✔
45
  },
2✔
46
  upgradeSection: {
2✔
47
    backgroundColor: theme.palette.grey[100],
2✔
48
    borderRadius: theme.spacing(0.5),
2✔
49
    padding: theme.spacing(2),
2✔
50
    paddingTop: 0
2✔
51
  },
2✔
52
  wrapper: { gap: theme.spacing(2) }
2✔
53
}));
2✔
54

2✔
55
const AddOnDescriptor = ({ addOns = [], isTrial }: { addOns: string[]; isTrial: boolean }) => {
11✔
56
  if (!addOns.length) {
9!
57
    return <>You currently don&apos;t have any add-ons</>;
9✔
58
  }
2✔
59
  return (
2✔
60
    <>
2✔
61
      You currently have the{' '}
2✔
62
      <b>
2✔
63
        {addOns.join(', ')} {pluralize('Add-on', addOns.length)}
2✔
64
      </b>
2✔
65
      {isTrial ? ' included in the trial plan' : ''}.
2!
66
    </>
2✔
67
  );
2✔
68
};
2✔
69

2✔
70
export const PlanDescriptor = ({
11✔
71
  plan,
2✔
72
  isTrial,
2✔
73
  trialExpiration,
2✔
74
  deviceLimit
2✔
75
}: {
2✔
76
  deviceLimit: number;
2✔
77
  isTrial: boolean;
2✔
78
  plan: string;
2✔
79
  trialExpiration: string;
2✔
80
}) => {
2✔
81
  const deviceLimitNote = (
2✔
82
    <>
10✔
83
      Your device limit is <b>{deviceLimit}</b> {pluralize('device', deviceLimit)}
2✔
84
    </>
2✔
85
  );
2✔
86
  if (isTrial) {
10✔
87
    return (
3✔
88
      <>
2✔
89
        You&apos;re currently on the <b>Trial plan</b>, with your trial expiring in {dayjs().from(dayjs(trialExpiration), true)}.
2✔
90
        <br />
2✔
91
        {deviceLimitNote}
2✔
92
      </>
2✔
93
    );
2✔
94
  }
2✔
95
  return (
9✔
96
    <>
2✔
97
      You&apos;re currently on the <b>{plan} plan</b>.
2✔
98
      <br />
2✔
99
      {deviceLimitNote}
2✔
100
    </>
2✔
101
  );
2✔
102
};
2✔
103

2✔
104
export const DeviceLimitExpansionNotification = ({ isTrial }: { isTrial: boolean }) => (
11✔
105
  <div className="flexbox centered">
3✔
106
    <ErrorIcon className="muted margin-right-small" fontSize="small" />
2✔
107
    <div className="muted" style={{ marginRight: 4 }}>
2✔
108
      To increase your device limit,{' '}
2✔
109
    </div>
2✔
110
    {isTrial ? <Link to="/subscription">upgrade to a paid plan</Link> : <SupportLink variant="salesTeam" />}
2!
111
    <div className="muted">.</div>
2✔
112
  </div>
2✔
113
);
2✔
114

2✔
115
export const CancelSubscriptionAlert = () => (
11✔
116
  <Alert className="margin-top-large" severity="error">
3✔
117
    <p>We&#39;ve started the process to cancel your plan and deactivate your account.</p>
2✔
118
    <p>
2✔
119
      We&#39;ll send you an email confirming your deactivation. If you have any question at all, contact us at our{' '}
2✔
120
      <strong>
2✔
121
        <a href="https://support.northern.tech" target="_blank" rel="noopener noreferrer">
2✔
122
          support portal
2✔
123
        </a>
2✔
124
        .
2✔
125
      </strong>
2✔
126
    </p>
2✔
127
  </Alert>
2✔
128
);
2✔
129

2✔
130
export const CancelSubscription = ({ handleCancelSubscription, isTrial }) => (
11✔
131
  <div className="margin-top-large flexbox column" style={{ gap: 8 }}>
10✔
132
    <Typography variant="h6" color="error">
2✔
133
      Delete account
2✔
134
    </Typography>
2✔
135
    <Typography variant="body2">Once you delete your account, it cannot be undone. Please be certain.</Typography>
2✔
136
    <div>
2✔
137
      <Button variant="outlined" onClick={handleCancelSubscription} color="error">
2✔
138
        Cancel {isTrial ? 'trial' : 'subscription'} and deactivate account
2✔
139
      </Button>
2✔
140
    </div>
2✔
141
  </div>
2✔
142
);
2✔
143

2✔
144
export const CardDetails = props => {
11✔
145
  const { card, containerClass } = props;
18✔
146
  return (
18✔
147
    <div className={containerClass || ''}>
2✔
148
      <div>Payment card ending: ****{card.last4}</div>
2✔
149
      <div>
2✔
150
        Expires {String(card.expiration.month).padStart(2, '0')}/{String(card.expiration.year).slice(-2)}
2✔
151
      </div>
2✔
152
    </div>
2✔
153
  );
2✔
154
};
2✔
155

2✔
156
const UpgradeNote = ({ isTrial }) => {
11✔
157
  const { classes } = useStyles();
9✔
158
  return (
9✔
159
    <div className={classes.upgradeSection}>
2✔
160
      <OrganizationSettingsItem
2✔
161
        classes={{ main: classes.fullWidthUpgrade }}
2✔
162
        title="Upgrade now"
2✔
163
        secondary={
2✔
164
          isTrial
2!
165
            ? 'Upgrade to a paid plan to keep your access going, connect more devices, and get reliable support from our team.'
2✔
166
            : 'Upgrade to access more features, increase your device limit, and enhance your subscription with Add-ons.'
2✔
167
        }
2✔
168
      />
2✔
169
      <div className={`flexbox center-aligned margin-top-x-small ${classes.wrapper}`}>
2✔
170
        <Button component={Link} to="https://mender.io/pricing/plans" target="_blank" rel="noopener noreferrer" size="small">
2✔
171
          Compare all plans
2✔
172
        </Button>
2✔
173
        <Button color="primary" component={Link} to="/subscription" size="small" variant="contained">
2✔
174
          Upgrade
2✔
175
        </Button>
2✔
176
      </div>
2✔
177
    </div>
2✔
178
  );
2✔
179
};
2✔
180

2✔
181
export const Billing = () => {
11✔
182
  const [cancelSubscription, setCancelSubscription] = useState(false);
9✔
183
  const [changeBilling, setChangeBilling] = useState<boolean>(false);
9✔
184
  const [cancelSubscriptionConfirmation, setCancelSubscriptionConfirmation] = useState(false);
9✔
185
  const { isAdmin } = useSelector(getUserRoles);
9✔
186
  const isEnterprise = useSelector(getIsEnterprise);
9✔
187
  const organization = useSelector(getOrganization);
9✔
188
  const card = useSelector(getCard);
9✔
189
  const deviceLimit = useSelector(getDeviceLimit);
9✔
190
  const billing = useSelector(getBillingProfile);
9✔
191
  const { addons = [], plan: currentPlan = PLANS.os.id, trial: isTrial, trial_expiration } = organization;
9✔
192
  const dispatch = useAppDispatch();
9✔
193
  const { classes } = useStyles();
9✔
194

2✔
195
  const planName = PLANS[currentPlan].name;
9✔
196

2✔
197
  useEffect(() => {
9✔
198
    if (isTrial) {
4!
199
      return;
2✔
200
    }
2✔
201
    dispatch(getCurrentCard());
4✔
202
    dispatch(getUserBilling());
4✔
203
  }, [dispatch, isTrial]);
2✔
204

2✔
205
  const enabledAddOns = addons.filter(({ enabled }) => enabled).map(({ name }) => ADDONS[name].title);
9✔
206

2✔
207
  const cancelSubscriptionSubmit = async reason =>
9✔
208
    dispatch(cancelRequest(reason)).then(() => {
2✔
209
      setCancelSubscription(false);
2✔
210
      setCancelSubscriptionConfirmation(true);
2✔
211
    });
2✔
212

2✔
213
  const handleCancelSubscription = e => {
9✔
214
    if (e !== undefined) {
2!
215
      e.preventDefault();
2✔
216
    }
2✔
217
    setCancelSubscription(toggle);
2✔
218
  };
2✔
219

2✔
220
  return (
9✔
221
    <div style={{ maxWidth: 750 }}>
2✔
222
      <Typography variant="h6">Billing</Typography>
2✔
223
      <div className={`flexbox column ${classes.wrapper}`}>
2✔
224
        <OrganizationSettingsItem
2✔
225
          title="Current plan"
2✔
226
          secondary={<PlanDescriptor plan={planName} isTrial={isTrial} trialExpiration={trial_expiration} deviceLimit={deviceLimit} />}
2✔
227
        />
2✔
228
        <OrganizationSettingsItem title="Current Add-ons" secondary={<AddOnDescriptor addOns={enabledAddOns} isTrial={isTrial} />} />
2✔
229
        {!isEnterprise && <UpgradeNote isTrial={isTrial} />}
2✔
230
        <Typography className="margin-top-small" variant="subtitle1">
2✔
231
          Billing details
2✔
232
        </Typography>
2✔
233
        {isEnterprise ? (
2!
234
          <Typography variant="body2">
2✔
235
            Enterprise plan payments are invoiced periodically to your organization. If you&apos;d like to make any changes to your plan, Add-ons, or billing
2✔
236
            details, please contact{' '}
2✔
237
            <a href="mailto:support@mender.io" target="_blank" rel="noopener noreferrer">
2✔
238
              support@mender.io
2✔
239
            </a>
2✔
240
            .
2✔
241
          </Typography>
2✔
242
        ) : (
2✔
243
          <BillingDetails setChangeBilling={setChangeBilling} />
2✔
244
        )}
2✔
245
      </div>
2✔
246
      {billing && changeBilling && <PlanExpanded isEdit onCloseClick={() => setChangeBilling(false)} currentBillingProfile={billing} card={card} />}
3✔
247
      {isAdmin && !cancelSubscriptionConfirmation && <CancelSubscription handleCancelSubscription={handleCancelSubscription} isTrial={isTrial} />}
2✔
248
      {cancelSubscriptionConfirmation && <CancelSubscriptionAlert />}
2!
UNCOV
249
      {cancelSubscription && <CancelRequestDialog onCancel={() => setCancelSubscription(false)} onSubmit={cancelSubscriptionSubmit} />}
2!
250
    </div>
2✔
251
  );
2✔
252
};
2✔
253

2✔
254
export default Billing;
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc