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

mendersoftware / mender-server / 1590815032

16 Dec 2024 01:53PM UTC coverage: 73.522% (+0.7%) from 72.839%
1590815032

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 6186 branches covered (68.82%)

Branch coverage included in aggregate %.

57 of 89 new or added lines in 11 files covered. (64.04%)

1 existing line in 1 file now uncovered.

40090 of 54132 relevant lines covered (74.06%)

22.98 hits per line

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

60.91
/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
  emailTooltip: {
49
    paddingRight: '10px'
50
  },
51
  cardDetails: {
52
    marginLeft: theme.spacing(12.5)
53
  }
54
}));
55

56
dayjs.extend(relativeTime);
8✔
57

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

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

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

99
export const CancelSubscriptionButton = ({ handleCancelSubscription, isTrial }) => (
8✔
100
  <p className="margin-left-small margin-right-small" style={{ maxWidth }}>
5✔
101
    <a href="" onClick={handleCancelSubscription}>
102
      {isTrial ? 'End trial' : 'Cancel subscription'} and deactivate account
5✔
103
    </a>
104
  </p>
105
);
106
const Address = props => {
8✔
107
  const {
108
    //TODO: remove default country when backend is fixed!
109
    address: { city, country, line1, postal_code },
110
    name,
111
    email
NEW
112
  } = props;
×
113

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

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

155
  const planName = PLANS[currentPlan].name;
4✔
156

157
  useEffect(() => {
4✔
158
    dispatch(getCurrentCard());
2✔
159
  }, [dispatch]);
160

161
  const enabledAddOns =
162
    organization.addons?.reduce((accu: string[], addon) => {
4!
163
      if (addon.enabled) {
4!
164
        const { title } = ADDONS[addon.name];
4✔
165
        let addonPrice = '';
4✔
166
        if (!organization.trial && !isEnterprise) {
4!
167
          const planAddon = ADDONS[addon.name][currentPlan] ? ADDONS[addon.name][currentPlan] : ADDONS[addon.name].os;
×
168
          addonPrice = ` - ${planAddon.price}`;
×
169
        }
170
        accu.push(`${title}${addonPrice}`);
4✔
171
      }
172
      return accu;
4✔
173
    }, []) || [];
174

175
  const cancelSubscriptionSubmit = async reason =>
4✔
176
    dispatch(cancelRequest(reason)).then(() => {
×
177
      setCancelSubscription(false);
×
178
      setCancelSubscriptionConfirmation(true);
×
179
    });
180

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

271
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