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

mendersoftware / mender-server / 1593959582

18 Dec 2024 10:51AM UTC coverage: 73.514% (+0.7%) from 72.829%
1593959582

Pull #253

gitlab-ci

mineralsfree
feat: updated billing section in My Organization settings

Ticket: MEN-7466
Changelog: None

Signed-off-by: Mikita Pilinka <mikita.pilinka@northern.tech>
Pull Request #253: MEN-7466-feat: updated billing section in My Organization settings

4257 of 6185 branches covered (68.83%)

Branch coverage included in aggregate %.

53 of 87 new or added lines in 11 files covered. (60.92%)

43 existing lines in 11 files now uncovered.

40083 of 54130 relevant lines covered (74.05%)

22.98 hits per line

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

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

18
// material ui
19
import { Error as ErrorIcon, OpenInNew as OpenInNewIcon } from '@mui/icons-material';
20
import { Button, List } from '@mui/material';
21
import { makeStyles } from 'tss-react/mui';
22

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

33
import { PlanExpanded } from '../PlanExpanded';
34
import CancelRequestDialog from '../dialogs/cancelrequest';
35
import OrganizationSettingsItem, { maxWidth } from './organizationsettingsitem';
36

37
const useStyles = makeStyles()(theme => ({
8✔
38
  wrapper: {
39
    marginTop: theme.spacing(4),
40
    padding: theme.spacing(2),
41
    paddingBottom: theme.spacing(6),
42
    '&>h5': { marginTop: 0, marginBottom: 0 }
43
  },
44
  billingSection: {
45
    backgroundColor: theme.palette.background.lightgrey,
46
    padding: theme.spacing(2)
47
  }
48
}));
49

50
dayjs.extend(relativeTime);
8✔
51

52
export const TrialExpirationNote = ({ trial_expiration }) => (
8✔
53
  <div className="flexbox centered muted">
5✔
54
    <ErrorIcon fontSize="small" />
55
    <span className="margin-left-small">
56
      Your trial expires in {dayjs().from(dayjs(trial_expiration), true)}. <Link to="/settings/upgrade">Upgrade to a paid plan</Link>
57
    </span>
58
  </div>
59
);
60

61
export const DeviceLimitExpansionNotification = ({ isTrial }) => (
8✔
62
  <div className="flexbox centered">
1✔
63
    <ErrorIcon className="muted margin-right-small" fontSize="small" />
64
    <div className="muted" style={{ marginRight: 4 }}>
65
      To increase your device limit,{' '}
66
    </div>
67
    {isTrial ? (
1!
68
      <Link to="/settings/upgrade">upgrade to a paid plan</Link>
69
    ) : (
70
      <a href="mailto:support@mender.io" target="_blank" rel="noopener noreferrer">
71
        contact our sales team
72
      </a>
73
    )}
74
    <div className="muted">.</div>
75
  </div>
76
);
77

78
export const CancelSubscriptionAlert = () => (
8✔
79
  <Alert className="margin-top-large" severity="error" style={{ maxWidth }}>
1✔
80
    <p>We&#39;ve started the process to cancel your plan and deactivate your account.</p>
81
    <p>
82
      We&#39;ll send you an email confirming your deactivation. If you have any question at all, contact us at our{' '}
83
      <strong>
84
        <a href="https://support.northern.tech" target="_blank" rel="noopener noreferrer">
85
          support portal
86
        </a>
87
        .
88
      </strong>
89
    </p>
90
  </Alert>
91
);
92

93
export const CancelSubscriptionButton = ({ handleCancelSubscription, isTrial }) => (
8✔
94
  <p className="margin-left-small margin-right-small" style={{ maxWidth }}>
5✔
95
    <a href="" onClick={handleCancelSubscription}>
96
      {isTrial ? 'End trial' : 'Cancel subscription'} and deactivate account
5✔
97
    </a>
98
  </p>
99
);
100
const Address = props => {
8✔
101
  const {
102
    address: { city, country, line1, postal_code },
103
    name,
104
    email
NEW
105
  } = props;
×
106

NEW
107
  const displayNames = new Intl.DisplayNames('en', { type: 'region' });
×
NEW
108
  return (
×
109
    <div>
110
      <div>
111
        <b>{name}</b>
112
      </div>
113
      <div>{line1}</div>
114
      <div>
115
        {postal_code}, {city}
116
      </div>
117
      {country && <div>{displayNames.of(country) || ''}</div>}
×
118
      <div>{email}</div>
119
    </div>
120
  );
121
};
122
export const CardDetails = props => {
8✔
123
  const { card, containerClass } = props;
1✔
124
  return (
1✔
125
    <div className={containerClass || ''}>
2✔
126
      <div>Payment card ending: **** {card.last4}</div>
127
      <div>
128
        Expires {String(card.expiration.month).padStart(2, '0')}/{card.expiration.year}
129
      </div>
130
    </div>
131
  );
132
};
133

134
export const Billing = () => {
8✔
135
  const [cancelSubscription, setCancelSubscription] = useState(false);
4✔
136
  const [changeBilling, setChangeBilling] = useState<boolean>(false);
4✔
137
  const [cancelSubscriptionConfirmation, setCancelSubscriptionConfirmation] = useState(false);
4✔
138
  const { isAdmin } = useSelector(getUserRoles);
4✔
139
  const isEnterprise = useSelector(getIsEnterprise);
4✔
140
  const organization = useSelector(getOrganization);
4✔
141
  const card = useSelector(getCard);
4✔
142
  const billing = useSelector(getBillingProfile);
4✔
143
  const { plan: currentPlan = PLANS.os.id } = organization;
4!
144
  const dispatch = useAppDispatch();
4✔
145
  const navigate = useNavigate();
4✔
146
  const { classes } = useStyles();
4✔
147

148
  const planName = PLANS[currentPlan].name;
4✔
149

150
  useEffect(() => {
4✔
151
    dispatch(getCurrentCard());
2✔
152
  }, [dispatch]);
153

154
  const enabledAddOns =
155
    organization.addons?.reduce((accu: string[], addon) => {
4!
156
      if (addon.enabled) {
4!
157
        const { title } = ADDONS[addon.name];
4✔
158
        let addonPrice = '';
4✔
159
        if (!organization.trial && !isEnterprise) {
4!
160
          const planAddon = ADDONS[addon.name][currentPlan] ? ADDONS[addon.name][currentPlan] : ADDONS[addon.name].os;
×
161
          addonPrice = ` - ${planAddon.price}`;
×
162
        }
163
        accu.push(`${title}${addonPrice}`);
4✔
164
      }
165
      return accu;
4✔
166
    }, []) || [];
167

168
  const cancelSubscriptionSubmit = async reason =>
4✔
169
    dispatch(cancelRequest(reason)).then(() => {
×
170
      setCancelSubscription(false);
×
171
      setCancelSubscriptionConfirmation(true);
×
172
    });
173

174
  const handleCancelSubscription = e => {
4✔
175
    if (e !== undefined) {
×
176
      e.preventDefault();
×
177
    }
178
    setCancelSubscription(toggle);
×
179
  };
180

181
  return (
4✔
182
    <div className={classes.wrapper}>
183
      <h5>Billing</h5>
184
      <List>
185
        <OrganizationSettingsItem
186
          title="Current plan"
187
          content={{
188
            action: { title: 'Compare product plans', internal: false, target: 'https://mender.io/plans/pricing' },
189
            description: organization.trial ? 'Trial' : planName
4✔
190
          }}
191
          notification={organization.trial ? <TrialExpirationNote trial_expiration={organization.trial_expiration} /> : null}
4✔
192
        />
193
        <OrganizationSettingsItem
194
          title="Current add-ons"
195
          content={{
196
            action: { title: 'Purchase an add-on', internal: true, action: () => navigate('/settings/upgrade') },
×
197
            description: enabledAddOns.length ? enabledAddOns.join(', ') : `You currently don't have any add-ons`
4✔
198
          }}
199
          notification={organization.trial && <TrialExpirationNote trial_expiration={organization.trial_expiration} />}
6✔
200
          sideBarContent={
201
            <div className="margin-left-small margin-bottom">
202
              {/* eslint-disable-next-line react/jsx-no-target-blank */}
203
              <a className="flexbox center-aligned" href="https://mender.io/plans/pricing" target="_blank" rel="noopener">
204
                <div style={{ maxWidth: 200 }}>Compare plans and add-ons at mender.io</div>
205
                <OpenInNewIcon fontSize="small" />
206
              </a>
207
            </div>
208
          }
209
        />
NEW
210
        {billing && changeBilling && <PlanExpanded isEdit onCloseClick={() => setChangeBilling(false)} currentBillingProfile={billing} card={card} />}
×
211
        <div className={classes.billingSection}>
212
          <div className="flexbox center-aligned">
213
            <div className="padding-right-x-small">
214
              <b>Billing details</b>
215
            </div>
216
            {!isEnterprise && billing && billing.address && (
4!
NEW
217
              <Button className="margin-left" onClick={() => setChangeBilling(true)}>
×
218
                Edit
219
              </Button>
220
            )}
221
          </div>
222
          {isEnterprise ? (
4!
223
            <InfoText>
224
              Enterprise plan payments are invoiced periodically to your organization. If you have any questions about your billing, <br /> please contact{' '}
225
              <a href="mailto:support@mender.io" target="_blank" rel="noopener noreferrer">
226
                support@mender.io
227
              </a>
228
            </InfoText>
229
          ) : billing && billing.address ? (
×
230
            <div className="flexbox">
231
              <Address address={billing.address} email={billing.email} name={billing.name} />
232
              {card && <CardDetails card={card} containerClass="margin-left-x-large" />}
×
233
            </div>
234
          ) : (
235
            <InfoText>
236
              Your account is not set up for automatic billing. If you believe this is a mistake, please contact{' '}
237
              <a href="mailto:support@mender.io" target="_blank" rel="noopener noreferrer">
238
                support@mender.io
239
              </a>
240
            </InfoText>
241
          )}
242
        </div>
243
      </List>
244
      {cancelSubscriptionConfirmation && <CancelSubscriptionAlert />}
4!
245
      {isAdmin && !cancelSubscriptionConfirmation && (
12✔
246
        <CancelSubscriptionButton handleCancelSubscription={handleCancelSubscription} isTrial={organization.trial} />
247
      )}
248
      {cancelSubscription && <CancelRequestDialog onCancel={() => setCancelSubscription(false)} onSubmit={cancelSubscriptionSubmit} />}
×
249
    </div>
250
  );
251
};
252

253
export default Billing;
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