• 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

95.74
/frontend/src/js/components/subscription/SubscriptionDrawer.tsx
1
// Copyright 2025 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

2✔
17
import { CheckCircleOutlined as CheckCircleOutlinedIcon, ErrorOutline as ErrorOutlineIcon } from '@mui/icons-material';
2✔
18
import { Alert, Button, CircularProgress, Divider, Drawer, Typography, buttonClasses } from '@mui/material';
2✔
19
import { makeStyles } from 'tss-react/mui';
2✔
20

2✔
21
import { DrawerTitle } from '@northern.tech/common-ui/DrawerTitle';
2✔
22
import Loader from '@northern.tech/common-ui/Loader';
2✔
23
import { SupportLink } from '@northern.tech/common-ui/SupportLink';
2✔
24
import Form from '@northern.tech/common-ui/forms/Form';
2✔
25
import { Address } from '@northern.tech/store/api/types';
2✔
26
import { AvailableAddon, PLANS, Plan } from '@northern.tech/store/constants';
2✔
27
import { Organization } from '@northern.tech/store/organizationSlice/types';
2✔
28
import { getBillingProfile, getCard, getCurrentUser, getSubscription } from '@northern.tech/store/selectors';
2✔
29
import { useAppDispatch } from '@northern.tech/store/store';
2✔
30
import {
2✔
31
  confirmCardUpdate,
2✔
32
  createBillingProfile,
2✔
33
  editBillingProfile,
2✔
34
  getBillingPreview,
2✔
35
  requestPlanUpgrade,
2✔
36
  startCardUpdate
2✔
37
} from '@northern.tech/store/thunks';
2✔
38

2✔
39
import CardSection from '../settings/CardSection';
2✔
40
import { PlanExpandedForm } from '../settings/PlanExpandedForm';
2✔
41
import { CardDetails } from '../settings/organization/Billing';
2✔
42
import { BillingDetails } from '../settings/organization/BillingDetails';
2✔
43
import OrganizationPaymentSettings from '../settings/organization/OrganizationPaymentSettings';
2✔
44
import { SubscriptionConfirmation } from './SubscriptionConfirmation';
2✔
45
import { PreviewPrice } from './SubscriptionPage';
2✔
46
import { SubscriptionSummary } from './SubscriptionSummary';
2✔
47
import { formatPrice } from './utils';
2✔
48

2✔
49
interface SubscriptionDrawerProps {
2✔
50
  addons: AvailableAddon[];
2✔
51
  currentPlanId?: string;
2✔
52
  isTrial?: boolean;
2✔
53
  onClose: () => void;
2✔
54
  order?: any;
2✔
55
  organization: Organization;
2✔
56
  plan: Plan;
2✔
57
  previewPrice?: PreviewPrice;
2✔
58
}
2✔
59

2✔
60
const useStyles = makeStyles()(theme => ({
8✔
61
  formWrapper: {
2✔
62
    display: 'flex',
2✔
63
    flexDirection: 'column',
2✔
64
    gap: theme.spacing(2),
2✔
65
    maxWidth: 600,
2✔
66
    '.required .relative': { marginLeft: theme.spacing(10) }
2✔
67
  },
2✔
68
  buttonWrapper: {
2✔
69
    '&.button-wrapper': { justifyContent: 'initial' },
2✔
70
    [`.${buttonClasses.root}`]: { lineHeight: 'initial' }
2✔
71
  }
2✔
72
}));
2✔
73

2✔
74
const emptyAddress: Address = { city: '', country: '', line1: '', postal_code: '', state: '' };
8✔
75

2✔
76
export const SubscriptionDrawer = (props: SubscriptionDrawerProps) => {
8✔
77
  const { onClose, previewPrice, order, isTrial, plan: selectedPlan, organization, currentPlanId } = props;
11✔
78
  const { email } = useSelector(getCurrentUser);
11✔
79
  const card = useSelector(getCard);
11✔
80
  const billing = useSelector(getBillingProfile);
11✔
81
  const currentSubscription = useSelector(getSubscription);
11✔
82
  const initialValues = { email, name: organization?.name || '', line1: '', state: '', city: '', postal_code: '', country: '' };
11!
83
  const [formInitialValues, setFormInitialValues] = useState(initialValues);
11✔
84
  const [isValid, setIsValid] = useState(false);
11✔
85
  const [isEdit, setIsEdit] = useState<boolean>(false);
11✔
86
  const [successConfirmationShown, setSuccessConfirmationShown] = useState(false);
11✔
87

2✔
88
  const [nextPayment, setNextPayment] = useState(0);
11✔
89
  const [updatingCard, setUpdatingCard] = useState(false);
11✔
90
  const dispatch = useAppDispatch();
11✔
91
  const [error, setError] = useState(false);
11✔
92
  const [loading, setLoading] = useState(false);
11✔
93
  const [billingSaved, setBillingSaved] = useState(false);
11✔
94

2✔
95
  const { classes } = useStyles();
11✔
96

2✔
97
  const onInitEditProfile = () => {
11✔
98
    setIsEdit(true);
2✔
99
    setFormInitialValues({ ...(billing.address || emptyAddress), name: billing.name, email: billing.email });
2!
100
  };
2✔
101
  useEffect(() => {
11✔
102
    if (!isTrial) {
5✔
103
      dispatch(getBillingPreview({ ...order, preview_mode: 'next' }))
3✔
104
        .unwrap()
2✔
105
        .then(next => {
2✔
106
          setNextPayment(next.total);
3✔
107
        });
2✔
108
    }
2✔
109
  }, [dispatch, isTrial, order]);
2✔
110
  const handleBillingProfileEdit = async values => {
11✔
111
    const { email, name, state, city, line1, postal_code } = values;
3✔
112
    const code: string = values.country.code ? values.country.code : values.country;
3!
113
    const billing_profile = { email, name, address: { country: code, state, city, line1, postal_code } };
3✔
114
    if (isEdit) {
3!
115
      await dispatch(editBillingProfile({ billingProfile: billing_profile }));
2✔
116
    } else if (isTrial) {
3!
117
      await dispatch(createBillingProfile({ billingProfile: billing_profile }))
3✔
118
        .unwrap()
2✔
119
        .then(() => setBillingSaved(true));
3✔
120
    }
2✔
121
    setIsEdit(false);
3✔
122
  };
2✔
123
  const upgradePlanSubmit = async () => {
11✔
124
    try {
3✔
125
      setLoading(true);
3✔
126
      setError(false);
3✔
127
      await dispatch(requestPlanUpgrade(order));
3✔
UNCOV
128
      setSuccessConfirmationShown(true);
2✔
129
    } catch (e) {
2✔
130
      console.error(e);
2✔
131
      setError(true);
2✔
132
    } finally {
2✔
UNCOV
133
      setLoading(false);
2✔
134
    }
2✔
135
  };
2✔
136
  const onCardConfirmed = async () => {
11✔
137
    await dispatch(confirmCardUpdate()).unwrap();
2✔
138
    await upgradePlanSubmit();
2✔
139
  };
2✔
140

2✔
141
  const summary = previewPrice && order && (
11!
142
    <div style={{ maxWidth: '250px' }} className="margin-top-large">
2✔
143
      <SubscriptionSummary
2✔
144
        previewPrice={previewPrice}
2✔
145
        plan={props.plan}
2✔
146
        title="Your new subscription"
2✔
147
        isNew={false}
2✔
148
        addons={props.addons}
2✔
149
        deviceLimit={order.products[0].quantity}
2✔
150
        readOnly
2✔
151
      />
2✔
152
    </div>
2✔
153
  );
2✔
154
  const cardDetailsDisabled = isTrial && !billing;
11✔
155
  return (
11✔
156
    <Drawer anchor="right" open={true} PaperProps={{ style: { minWidth: '50vw' } }}>
2✔
157
      <DrawerTitle title={currentSubscription ? `Upgrade your subscription` : `Subscribe to Mender ${selectedPlan.name}`} onClose={onClose} />
2✔
158
      <Divider className="margin-bottom-small" />
2✔
159
      {selectedPlan && (
2✔
160
        <div className="margin-bottom-large">
2✔
161
          Complete checkout to subscribe to Mender <b>{selectedPlan.name}</b> at <b> {previewPrice ? formatPrice(previewPrice.total) : ''}</b>
2!
162
        </div>
2✔
163
      )}
2✔
164

2✔
165
      {isEdit || (isTrial && !billing) ? (
2✔
166
        <Form
2✔
167
          classes={classes}
2✔
168
          onSubmit={handleBillingProfileEdit}
2✔
UNCOV
169
          handleCancel={billing && (() => setIsEdit(false))}
2!
170
          defaultValues={formInitialValues}
2✔
171
          submitLabel="Save Billing details"
2✔
172
          showButtons
2✔
173
          autocomplete="off"
2✔
174
          validationMode="onSubmit"
2✔
175
        >
2✔
176
          <PlanExpandedForm className={classes.formWrapper} setIsValid={setIsValid} />
2✔
177
        </Form>
2✔
178
      ) : (
2✔
179
        <>
2✔
180
          <Typography className="margin-top-small margin-bottom-x-small" variant="subtitle1">
2✔
181
            Your billing details
2✔
182
          </Typography>
2✔
183
          <BillingDetails setChangeBilling={onInitEditProfile} hideCard editDisabled={updatingCard} />
2✔
184

2✔
185
          {billingSaved && (
2✔
186
            <Alert className="margin-top-large" icon={<CheckCircleOutlinedIcon />}>
2✔
187
              Billing details saved
2✔
188
            </Alert>
2✔
189
          )}
2✔
190
        </>
2✔
191
      )}
2✔
192
      {isTrial ? (
2✔
193
        <div style={{ maxWidth: 600 }}>
2✔
194
          <Typography variant="subtitle1" color={cardDetailsDisabled ? 'textDisabled' : 'textPrimary'} className="margin-top-large margin-bottom-none">
2✔
195
            Card details
2✔
196
          </Typography>
2✔
197
          <CardSection
2✔
198
            organization={organization}
2✔
199
            onCardConfirmed={onCardConfirmed}
2✔
UNCOV
200
            onSubmit={() => dispatch(startCardUpdate()).unwrap()}
2✔
201
            summary={summary}
2✔
202
            isSignUp
2✔
203
            isValid={!isEdit}
2✔
204
            disabled={cardDetailsDisabled}
2✔
205
          />
2✔
206
        </div>
2✔
207
      ) : (
2✔
208
        <div>
2✔
209
          <Typography className="margin-top-large margin-bottom-x-small" variant="subtitle1">
2✔
210
            Card details
2✔
211
          </Typography>
2✔
212
          {updatingCard ? (
2!
213
            <OrganizationPaymentSettings
2✔
214
              className={classes.formWrapper}
2✔
215
              updatingCard={updatingCard}
2✔
216
              setUpdatingCard={setUpdatingCard}
2✔
217
              isValid={isValid}
2✔
218
              omitHeader
2✔
219
            />
2✔
220
          ) : (
2✔
221
            <>
2✔
222
              <CardDetails card={card} />
2✔
UNCOV
223
              <Button disabled={isEdit} variant="outlined" className="margin-top-x-small" onClick={() => setUpdatingCard(true)} size="medium">
2✔
224
                Change card
2✔
225
              </Button>
2✔
226
            </>
2✔
227
          )}
2✔
228
        </div>
2✔
229
      )}
2✔
230
      {!isTrial && summary}
2✔
231
      {nextPayment > 0 && currentSubscription && currentPlanId ? (
2✔
232
        <div className={classes.formWrapper}>
2✔
233
          <Typography variant="body2">
2✔
234
            You’re currently subscribed to {PLANS[currentPlanId].name} at {formatPrice(currentSubscription.total)}/month. On your next payment, you&#39;ll be
2✔
235
            charged for any days used under your current rate, and the rest will be billed at your new subscription rate. The total amount for your next payment
2✔
236
            will be {formatPrice(nextPayment)}.
2✔
237
          </Typography>
2✔
238
          {error && (
2!
239
            <Alert icon={<ErrorOutlineIcon />} severity="error">
2✔
240
              There was an issue while processing your order. Please try again, or contact <SupportLink variant="email" />.
2✔
241
            </Alert>
2✔
242
          )}
2✔
243
          <div className="margin-top flexbox">
2✔
244
            <Button className="margin-right-small" onClick={onClose}>
2✔
245
              Cancel
2✔
246
            </Button>
2✔
247
            <Button
2✔
248
              className="margin-right-small"
2✔
249
              onClick={() => upgradePlanSubmit()}
3✔
250
              color="secondary"
2✔
251
              variant="contained"
2✔
252
              disabled={isEdit || loading || updatingCard}
2✔
253
            >
2✔
254
              Confirm Subscription
2✔
255
            </Button>
2✔
256
            {loading && <CircularProgress />}
2✔
257
          </div>
2✔
258
        </div>
2✔
259
      ) : (
2✔
260
        currentSubscription && <Loader show />
2✔
261
      )}
2✔
262
      {successConfirmationShown && previewPrice && (
2!
263
        <SubscriptionConfirmation
2✔
264
          devices={order.products[0].quantity}
2✔
265
          plan={selectedPlan}
2✔
266
          price={previewPrice?.total}
2✔
267
          orderedAddons={order.products[0].addons}
2✔
268
        />
2✔
269
      )}
2✔
270
    </Drawer>
2✔
271
  );
2✔
272
};
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