mirror of
https://github.com/dnote/dnote
synced 2026-03-14 22:45:50 +01:00
Display version in settings (#293)
* Display version in settings * Fix placeholder * Fix button size * Fix time format * Add support link * Add license
This commit is contained in:
parent
8e7d24d409
commit
478b412169
29 changed files with 522 additions and 269 deletions
|
|
@ -19,6 +19,10 @@ The following log documentes the history of the server project.
|
|||
- `WebURL`: the URL to your Dnote server, without the trailing slash. (e.g. `https://my-server.com`) (Please see #290)
|
||||
- `SmtpPort`: the SMTP port. (e.g. `465`) optional - required *if you want to configure email*
|
||||
|
||||
#### Added
|
||||
|
||||
- Display version number in the settings (#293)
|
||||
|
||||
#### Fixed
|
||||
|
||||
- Allow to customize the app URL in the emails (#290)
|
||||
|
|
|
|||
7
Makefile
7
Makefile
|
|
@ -87,13 +87,16 @@ endif
|
|||
# development
|
||||
dev-server:
|
||||
@echo "==> running dev environment"
|
||||
@(cd ${GOPATH}/src/github.com/dnote/dnote/web && ./scripts/dev.sh)
|
||||
@(cd ${GOPATH}/src/github.com/dnote/dnote/web && VERSION=master ./scripts/dev.sh)
|
||||
.PHONY: dev-server
|
||||
|
||||
## build
|
||||
build-web:
|
||||
ifndef version
|
||||
$(error version is required. Usage: make version=0.1.0 build-web)
|
||||
endif
|
||||
@echo "==> building web"
|
||||
@(cd ${GOPATH}/src/github.com/dnote/dnote/web && ./scripts/build-prod.sh)
|
||||
@(cd ${GOPATH}/src/github.com/dnote/dnote/web && VERSION=$(version) ./scripts/build-prod.sh)
|
||||
.PHONY: build-web
|
||||
|
||||
build-server: build-web
|
||||
|
|
|
|||
1
web/declrations.d.ts
vendored
1
web/declrations.d.ts
vendored
|
|
@ -23,3 +23,4 @@ declare module '*.scss';
|
|||
declare const __STRIPE_PUBLIC_KEY__: string;
|
||||
declare const __ROOT_URL__: string;
|
||||
declare const __CDN_URL__: string;
|
||||
declare const __VERSION__: string;
|
||||
|
|
|
|||
|
|
@ -15,4 +15,5 @@ ASSET_BASE_URL="$assetBaseUrl" \
|
|||
ROOT_URL="$rootUrl" \
|
||||
PUBLIC_PATH="$publicPath" \
|
||||
COMPILED_PATH="$compiledPath" \
|
||||
VERSION="$VERSION" \
|
||||
"$basePath"/web/scripts/build.sh
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ pushd "$basePath/web"
|
|||
|
||||
OUTPUT_PATH="$COMPILED_PATH" \
|
||||
ROOT_URL="$ROOT_URL" \
|
||||
VERSION="$VERSION" \
|
||||
"$basePath"/web/node_modules/.bin/webpack\
|
||||
--colors\
|
||||
--display-error-details\
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ set +a
|
|||
PUBLIC_PATH="$appPath"/public \
|
||||
COMPILED_PATH="$basePath/web/compiled" \
|
||||
IS_TEST=true \
|
||||
VERSION="$VERSION" \
|
||||
"$appPath"/scripts/webpack-dev.sh
|
||||
) &
|
||||
devServerPID=$!
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ appPath="$basePath"/web
|
|||
node "$appPath"/scripts/placeholder.js &&
|
||||
|
||||
ROOT_URL=$ROOT_URL \
|
||||
VERSION="$VERSION" \
|
||||
"$appPath"/node_modules/.bin/webpack-dev-server\
|
||||
--env.isTest="$IS_TEST"\
|
||||
--config "$appPath"/webpack/dev.config.js
|
||||
|
|
|
|||
62
web/src/components/Settings/About/index.tsx
Normal file
62
web/src/components/Settings/About/index.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/* Copyright (C) 2019 Monomax Software Pty Ltd
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import SettingRow from '../SettingRow';
|
||||
import styles from '../Settings.scss';
|
||||
import { useSelector } from '../../../store';
|
||||
import config from '../../../libs/config';
|
||||
|
||||
interface Props {}
|
||||
|
||||
const About: React.FunctionComponent<Props> = () => {
|
||||
const { user } = useSelector(state => {
|
||||
return {
|
||||
user: state.auth.user.data
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>About</title>
|
||||
</Helmet>
|
||||
|
||||
<h1 className="sr-only">About</h1>
|
||||
|
||||
<div className={styles.wrapper}>
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles['section-heading']}>Software</h2>
|
||||
|
||||
<SettingRow name="Version" value={config.version} />
|
||||
{user.pro && (
|
||||
<SettingRow
|
||||
name="Support"
|
||||
value={<a href="mailto:sung@getdnote.com">sung@getdnote.com</a>}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
|
|
@ -83,7 +83,7 @@ const CancelPlanModal: React.FunctionComponent<Props> = ({
|
|||
<Button
|
||||
type="button"
|
||||
kind="first"
|
||||
size="small"
|
||||
size="normal"
|
||||
isBusy={inProgress}
|
||||
onClick={onDismiss}
|
||||
>
|
||||
|
|
@ -93,7 +93,7 @@ const CancelPlanModal: React.FunctionComponent<Props> = ({
|
|||
<Button
|
||||
type="submit"
|
||||
kind="second"
|
||||
size="small"
|
||||
size="normal"
|
||||
isBusy={inProgress}
|
||||
>
|
||||
Cancel my plan.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
/* Copyright (C) 2019 Monomax Software Pty Ltd
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/* Copyright (C) 2019 Monomax Software Pty Ltd
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import SettingRow from '../../SettingRow';
|
||||
import { SourceData } from '../../../../store/auth/type';
|
||||
import styles from '../../Settings.scss';
|
||||
|
||||
interface Props {
|
||||
stripeLoaded: boolean;
|
||||
source: SourceData;
|
||||
setIsPaymentMethodModalOpen: (bool) => void;
|
||||
}
|
||||
|
||||
const PaymentMethodRow: React.FunctionComponent<Props> = ({
|
||||
stripeLoaded,
|
||||
source,
|
||||
setIsPaymentMethodModalOpen
|
||||
}) => {
|
||||
let value;
|
||||
if (source.brand) {
|
||||
value = `${source.brand} ending in ${source.last4}. expiry ${source.exp_month}/${source.exp_year}`;
|
||||
} else {
|
||||
value = 'No payment method';
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
id="T-payment-method-row"
|
||||
name="Payment method"
|
||||
value={value}
|
||||
actionContent={
|
||||
<button
|
||||
id="T-update-payment-method-button"
|
||||
className={classnames('button-no-ui', styles.edit)}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsPaymentMethodModalOpen(true);
|
||||
}}
|
||||
disabled={!stripeLoaded}
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentMethodRow;
|
||||
|
|
@ -16,82 +16,34 @@
|
|||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@import '../../App/rem';
|
||||
@import '../../App/font';
|
||||
@import '../../App/theme';
|
||||
@import '../../../App/rem';
|
||||
@import '../../../App/font';
|
||||
@import '../../../App/theme';
|
||||
|
||||
.content1 {
|
||||
position: relative;
|
||||
padding: rem(48px) rem(12px);
|
||||
}
|
||||
|
||||
.content1-line1 {
|
||||
width: rem(140px);
|
||||
height: rem(16px);
|
||||
}
|
||||
.content1-line2 {
|
||||
width: rem(240px);
|
||||
height: rem(16px);
|
||||
margin-top: rem(8px);
|
||||
}
|
||||
.content1-line3 {
|
||||
width: rem(80px);
|
||||
height: rem(16px);
|
||||
margin-top: rem(8px);
|
||||
}
|
||||
|
||||
.content2 {
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
padding: rem(8px) rem(12px);
|
||||
padding: rem(16px) rem(12px);
|
||||
|
||||
@include breakpoint(md) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
.content2-line1 {
|
||||
.content-line1 {
|
||||
width: rem(120px);
|
||||
height: rem(16px);
|
||||
margin-top: rem(8px);
|
||||
}
|
||||
|
||||
.content2-line2 {
|
||||
.content-line2 {
|
||||
width: rem(180px);
|
||||
height: rem(16px);
|
||||
margin-top: rem(8px);
|
||||
}
|
||||
|
||||
.content2-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content3 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
padding: rem(8px) rem(12px);
|
||||
|
||||
@include breakpoint(md) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
.content3-line1 {
|
||||
width: rem(120px);
|
||||
height: rem(16px);
|
||||
margin-top: rem(8px);
|
||||
}
|
||||
|
||||
.content3-line2 {
|
||||
width: rem(180px);
|
||||
height: rem(16px);
|
||||
margin-top: rem(8px);
|
||||
}
|
||||
|
||||
.content3-right {
|
||||
.content-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/* Copyright (C) 2019 Monomax Software Pty Ltd
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import settingsStyles from '../../Settings.scss';
|
||||
import styles from './Placeholder.scss';
|
||||
|
||||
const Placeholder: React.FunctionComponent = () => {
|
||||
return (
|
||||
<div className="container-wide">
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-12 col-lg-10">
|
||||
<section className={settingsStyles.section}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles['content-left']}>
|
||||
<div
|
||||
className={classnames('holder', styles['content-line1'])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles['content-right']}>
|
||||
<div
|
||||
className={classnames('holder', styles['content-line2'])}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Placeholder;
|
||||
56
web/src/components/Settings/Billing/PaymentSection/index.tsx
Normal file
56
web/src/components/Settings/Billing/PaymentSection/index.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/* Copyright (C) 2019 Monomax Software Pty Ltd
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import PaymentMethodRow from './PaymentMethodRow';
|
||||
import settingsStyles from '../../Settings.scss';
|
||||
import { SourceData } from '../../../../store/auth/type';
|
||||
import Placeholder from './Placeholder';
|
||||
import styles from './Placeholder.scss';
|
||||
|
||||
interface Props {
|
||||
source: SourceData;
|
||||
setIsPaymentMethodModalOpen: (boolean) => void;
|
||||
stripeLoaded: boolean;
|
||||
isFetched: boolean;
|
||||
}
|
||||
|
||||
const PaymentSection: React.FunctionComponent<Props> = ({
|
||||
source,
|
||||
setIsPaymentMethodModalOpen,
|
||||
stripeLoaded,
|
||||
isFetched
|
||||
}) => {
|
||||
if (!isFetched) {
|
||||
return <Placeholder />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PaymentMethodRow
|
||||
source={source}
|
||||
setIsPaymentMethodModalOpen={setIsPaymentMethodModalOpen}
|
||||
stripeLoaded={stripeLoaded}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentSection;
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/* Copyright (C) 2019 Monomax Software Pty Ltd
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import SettingRow from '../../SettingRow';
|
||||
import styles from '../../Settings.scss';
|
||||
|
||||
interface Props {
|
||||
setIsPlanModalOpen: (bool) => void;
|
||||
}
|
||||
|
||||
const CancelRow: React.FunctionComponent<Props> = ({ setIsPlanModalOpen }) => {
|
||||
return (
|
||||
<SettingRow
|
||||
name="Cancel current plan"
|
||||
desc="If you cancel, the plan will expire at the end of current billing period."
|
||||
actionContent={
|
||||
<button
|
||||
className={classnames('button-no-ui', styles.edit)}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsPlanModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Cancel plan
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CancelRow;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/* Copyright (C) 2019 Monomax Software Pty Ltd
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@import '../../../App/rem';
|
||||
@import '../../../App/font';
|
||||
@import '../../../App/theme';
|
||||
|
||||
.content1 {
|
||||
position: relative;
|
||||
padding: rem(48px) rem(12px);
|
||||
}
|
||||
|
||||
.content1-line1 {
|
||||
width: rem(140px);
|
||||
height: rem(16px);
|
||||
}
|
||||
.content1-line2 {
|
||||
width: rem(240px);
|
||||
height: rem(16px);
|
||||
margin-top: rem(8px);
|
||||
}
|
||||
.content1-line3 {
|
||||
width: rem(80px);
|
||||
height: rem(16px);
|
||||
margin-top: rem(8px);
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import settingsStyles from '../Settings.scss';
|
||||
import settingsStyles from '../../Settings.scss';
|
||||
import styles from './Placeholder.scss';
|
||||
|
||||
const Placeholder: React.FunctionComponent = () => {
|
||||
|
|
@ -28,10 +28,6 @@ const Placeholder: React.FunctionComponent = () => {
|
|||
<div className="row">
|
||||
<div className="col-12 col-md-12 col-lg-10">
|
||||
<section className={settingsStyles.section}>
|
||||
<div className={settingsStyles['section-heading']}>
|
||||
<span> </span>
|
||||
</div>
|
||||
|
||||
<div className={styles.content1}>
|
||||
<div className={classnames('holder', styles['content1-line1'])} />
|
||||
<div className={classnames('holder', styles['content1-line2'])} />
|
||||
|
|
@ -55,26 +51,6 @@ const Placeholder: React.FunctionComponent = () => {
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={settingsStyles.section}>
|
||||
<div className={settingsStyles['section-heading']}>
|
||||
<span> </span>
|
||||
</div>
|
||||
|
||||
<div className={styles.content3}>
|
||||
<div className={styles['content3-left']}>
|
||||
<div
|
||||
className={classnames('holder', styles['content3-line1'])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles['content3-right']}>
|
||||
<div
|
||||
className={classnames('holder', styles['content3-line2'])}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -16,9 +16,9 @@
|
|||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@import '../../App/rem';
|
||||
@import '../../App/font';
|
||||
@import '../../App/theme';
|
||||
@import '../../../App/rem';
|
||||
@import '../../../App/font';
|
||||
@import '../../../App/theme';
|
||||
|
||||
.wrapper {
|
||||
padding-top: rem(48px);
|
||||
|
|
@ -21,11 +21,11 @@ import { Link } from 'react-router-dom';
|
|||
import classnames from 'classnames';
|
||||
|
||||
import { getPlanLabel } from 'web/libs/subscription';
|
||||
import LogoIcon from '../../Icons/Logo';
|
||||
import { nanosecToMillisec } from 'web/helpers/time';
|
||||
import LogoIcon from '../../../Icons/Logo';
|
||||
import { SECOND } from 'web/helpers/time';
|
||||
import formatDate from 'web/helpers/time/format';
|
||||
import styles from './PlanRow.scss';
|
||||
import settingRowStyles from '../SettingRow.scss';
|
||||
import settingRowStyles from '../../SettingRow.scss';
|
||||
|
||||
function getPlanPeriodMessage(subscription: any): string {
|
||||
if (!subscription.id) {
|
||||
|
|
@ -34,12 +34,12 @@ function getPlanPeriodMessage(subscription: any): string {
|
|||
|
||||
const label = getPlanLabel(subscription);
|
||||
|
||||
const endDate = new Date(nanosecToMillisec(subscription.current_period_end));
|
||||
const endDate = new Date(subscription.current_period_end * SECOND);
|
||||
|
||||
if (subscription.cancel_at_period_end) {
|
||||
return `Your ${label} plan will end on ${formatDate(
|
||||
endDate,
|
||||
'YYYY MMM Do'
|
||||
'%YYYY %MMM %Do'
|
||||
)} and will not renew.`;
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ function getPlanPeriodMessage(subscription: any): string {
|
|||
renewDate.setDate(endDate.getDate() + 1);
|
||||
return `Your ${label} plan will renew on ${formatDate(
|
||||
renewDate,
|
||||
'YYYY MMM Do'
|
||||
'%YYYY %MMM %Do'
|
||||
)}.`;
|
||||
}
|
||||
|
||||
|
|
@ -20,10 +20,10 @@ import React, { useState } from 'react';
|
|||
import classnames from 'classnames';
|
||||
|
||||
import services from 'web/libs/services';
|
||||
import { useDispatch } from '../../../store';
|
||||
import { getSubscription } from '../../../store/auth';
|
||||
import SettingRow from '../SettingRow';
|
||||
import styles from '../Settings.scss';
|
||||
import { useDispatch } from '../../../../store';
|
||||
import { getSubscription } from '../../../../store/auth';
|
||||
import SettingRow from '../../SettingRow';
|
||||
import styles from '../../Settings.scss';
|
||||
|
||||
interface Props {
|
||||
subscriptionId: string;
|
||||
67
web/src/components/Settings/Billing/PlanSection/index.tsx
Normal file
67
web/src/components/Settings/Billing/PlanSection/index.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/* Copyright (C) 2019 Monomax Software Pty Ltd
|
||||
*
|
||||
* This file is part of Dnote.
|
||||
*
|
||||
* Dnote is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Dnote is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import PlanRow from './PlanRow';
|
||||
import CancelRow from './CancelRow';
|
||||
import ReactivateRow from './ReactivateRow';
|
||||
import settingsStyles from '../../Settings.scss';
|
||||
import { SubscriptionData } from '../../../../store/auth/type';
|
||||
import Placeholder from './Placeholder';
|
||||
import styles from './Placeholder.scss';
|
||||
|
||||
interface Props {
|
||||
subscription: SubscriptionData;
|
||||
setIsPlanModalOpen: (boolean) => void;
|
||||
setSuccessMsg: (string) => void;
|
||||
setFailureMsg: (string) => void;
|
||||
isFetched: boolean;
|
||||
}
|
||||
|
||||
const PlanSection: React.FunctionComponent<Props> = ({
|
||||
subscription,
|
||||
isFetched,
|
||||
setIsPlanModalOpen,
|
||||
setSuccessMsg,
|
||||
setFailureMsg
|
||||
}) => {
|
||||
if (!isFetched) {
|
||||
return <Placeholder />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PlanRow subscription={subscription} />
|
||||
|
||||
{subscription.id && !subscription.cancel_at_period_end && (
|
||||
<CancelRow setIsPlanModalOpen={setIsPlanModalOpen} />
|
||||
)}
|
||||
{subscription.id && subscription.cancel_at_period_end && (
|
||||
<ReactivateRow
|
||||
subscriptionId={subscription.id}
|
||||
setSuccessMsg={setSuccessMsg}
|
||||
setFailureMsg={setFailureMsg}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlanSection;
|
||||
|
|
@ -32,143 +32,10 @@ import {
|
|||
clearSource
|
||||
} from '../../../store/auth';
|
||||
import SettingRow from '../SettingRow';
|
||||
import ReactivateRow from './ReactivateRow';
|
||||
import PlanRow from './PlanRow';
|
||||
import Placeholder from './Placeholder';
|
||||
import PlanSection from './PlanSection';
|
||||
import PaymentSection from './PaymentSection';
|
||||
import styles from '../Settings.scss';
|
||||
|
||||
function CancelRow({ setIsPlanModalOpen }) {
|
||||
return (
|
||||
<SettingRow
|
||||
name="Cancel current plan"
|
||||
desc="If you cancel, the plan will expire at the end of current billing period."
|
||||
actionContent={
|
||||
<button
|
||||
className={classnames('button-no-ui', styles.edit)}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsPlanModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Cancel plan
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PaymentMethodRow({
|
||||
stripeLoaded,
|
||||
source,
|
||||
setIsPaymentMethodModalOpen
|
||||
}) {
|
||||
let value;
|
||||
if (source.brand) {
|
||||
value = `${source.brand} ending in ${source.last4}. expiry ${source.exp_month}/${source.exp_year}`;
|
||||
} else {
|
||||
value = 'No payment method';
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
id="T-payment-method-row"
|
||||
name="Payment method"
|
||||
value={value}
|
||||
actionContent={
|
||||
<button
|
||||
id="T-update-payment-method-button"
|
||||
className={classnames('button-no-ui', styles.edit)}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsPaymentMethodModalOpen(true);
|
||||
}}
|
||||
disabled={!stripeLoaded}
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface ContentProps {
|
||||
subscription: any;
|
||||
source: any;
|
||||
setIsPlanModalOpen: (boolean) => void;
|
||||
setIsPaymentMethodModalOpen: (boolean) => void;
|
||||
successMsg: string;
|
||||
failureMsg: string;
|
||||
setSuccessMsg: (string) => void;
|
||||
setFailureMsg: (string) => void;
|
||||
stripeLoaded: boolean;
|
||||
}
|
||||
|
||||
const Content: React.FunctionComponent<ContentProps> = ({
|
||||
subscription,
|
||||
source,
|
||||
setIsPlanModalOpen,
|
||||
setIsPaymentMethodModalOpen,
|
||||
successMsg,
|
||||
failureMsg,
|
||||
setSuccessMsg,
|
||||
setFailureMsg,
|
||||
stripeLoaded
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<Flash
|
||||
when={successMsg !== ''}
|
||||
kind="success"
|
||||
wrapperClassName={styles.flash}
|
||||
onDismiss={() => {
|
||||
setSuccessMsg('');
|
||||
}}
|
||||
>
|
||||
{successMsg}
|
||||
</Flash>
|
||||
<Flash
|
||||
when={failureMsg !== ''}
|
||||
kind="danger"
|
||||
wrapperClassName={styles.flash}
|
||||
onDismiss={() => {
|
||||
setFailureMsg('');
|
||||
}}
|
||||
>
|
||||
{failureMsg}
|
||||
</Flash>
|
||||
|
||||
<div className={styles.wrapper}>
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles['section-heading']}>Plan</h2>
|
||||
|
||||
<PlanRow subscription={subscription} />
|
||||
|
||||
{subscription.id && !subscription.cancel_at_period_end && (
|
||||
<CancelRow setIsPlanModalOpen={setIsPlanModalOpen} />
|
||||
)}
|
||||
{subscription.id && subscription.cancel_at_period_end && (
|
||||
<ReactivateRow
|
||||
subscriptionId={subscription.id}
|
||||
setSuccessMsg={setSuccessMsg}
|
||||
setFailureMsg={setFailureMsg}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles['section-heading']}>Payment</h2>
|
||||
|
||||
<PaymentMethodRow
|
||||
source={source}
|
||||
setIsPaymentMethodModalOpen={setIsPaymentMethodModalOpen}
|
||||
stripeLoaded={stripeLoaded}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Billing: React.FunctionComponent = () => {
|
||||
const [isPlanModalOpen, setIsPlanModalOpen] = useState(false);
|
||||
const [isPaymentMethodModalOpen, setIsPaymentMethodModalOpen] = useState(
|
||||
|
|
@ -239,21 +106,53 @@ const Billing: React.FunctionComponent = () => {
|
|||
{stripeLoadError}
|
||||
</Flash>
|
||||
|
||||
{!subscriptionData.isFetched || !sourceData.isFetched ? (
|
||||
<Placeholder />
|
||||
) : (
|
||||
<Content
|
||||
subscription={subscription}
|
||||
source={source}
|
||||
setIsPlanModalOpen={setIsPlanModalOpen}
|
||||
successMsg={successMsg}
|
||||
failureMsg={failureMsg}
|
||||
setSuccessMsg={setSuccessMsg}
|
||||
setFailureMsg={setFailureMsg}
|
||||
setIsPaymentMethodModalOpen={setIsPaymentMethodModalOpen}
|
||||
stripeLoaded={stripeLoaded}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<Flash
|
||||
when={successMsg !== ''}
|
||||
kind="success"
|
||||
wrapperClassName={styles.flash}
|
||||
onDismiss={() => {
|
||||
setSuccessMsg('');
|
||||
}}
|
||||
>
|
||||
{successMsg}
|
||||
</Flash>
|
||||
<Flash
|
||||
when={failureMsg !== ''}
|
||||
kind="danger"
|
||||
wrapperClassName={styles.flash}
|
||||
onDismiss={() => {
|
||||
setFailureMsg('');
|
||||
}}
|
||||
>
|
||||
{failureMsg}
|
||||
</Flash>
|
||||
|
||||
<div className={styles.wrapper}>
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles['section-heading']}>Plan</h2>
|
||||
|
||||
<PlanSection
|
||||
subscription={subscriptionData.data}
|
||||
setIsPlanModalOpen={setIsPlanModalOpen}
|
||||
setSuccessMsg={setSuccessMsg}
|
||||
setFailureMsg={setFailureMsg}
|
||||
isFetched={subscriptionData.isFetched}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles['section-heading']}>Payment</h2>
|
||||
|
||||
<PaymentSection
|
||||
source={sourceData.data}
|
||||
setIsPaymentMethodModalOpen={setIsPaymentMethodModalOpen}
|
||||
stripeLoaded={stripeLoaded}
|
||||
isFetched={sourceData.isFetched}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CancelPlanModal
|
||||
isOpen={isPlanModalOpen}
|
||||
|
|
@ -277,18 +176,4 @@ const Billing: React.FunctionComponent = () => {
|
|||
);
|
||||
};
|
||||
|
||||
// function mapStateToProps(state) {
|
||||
// return {
|
||||
// subscriptionData: state.auth.subscription,
|
||||
// sourceData: state.auth.source
|
||||
// };
|
||||
// }
|
||||
|
||||
// const mapDispatchToProps = {
|
||||
// doGetSubscription: getSubscription,
|
||||
// doClearSubscription: clearSubscription,
|
||||
// doGetSource: getSource,
|
||||
// doClearSource: clearSource
|
||||
// };
|
||||
|
||||
export default Billing;
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@
|
|||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
word-break: break-all;
|
||||
|
||||
@include breakpoint(md) {
|
||||
flex-direction: row;
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ import styles from './SettingRow.scss';
|
|||
|
||||
interface Props {
|
||||
name: string;
|
||||
actionContent: React.ReactNode;
|
||||
actionContent?: React.ReactNode;
|
||||
id?: string;
|
||||
desc?: string;
|
||||
value?: string;
|
||||
value?: React.ReactNode;
|
||||
}
|
||||
|
||||
const SettingRow: React.FunctionComponent<Props> = ({
|
||||
|
|
@ -45,7 +45,8 @@ const SettingRow: React.FunctionComponent<Props> = ({
|
|||
|
||||
<div className={styles.right}>
|
||||
{value}
|
||||
<div className={styles.action}>{actionContent}</div>
|
||||
|
||||
{actionContent && <div className={styles.action}>{actionContent}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -47,6 +47,15 @@ const Sidebar: React.FunctionComponent<Props> = () => {
|
|||
Billing
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavLink
|
||||
className={styles.item}
|
||||
activeClassName={styles.active}
|
||||
to={getSettingsPath(SettingSections.about)}
|
||||
>
|
||||
About
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import classnames from 'classnames';
|
|||
import { SettingSections } from 'web/libs/paths';
|
||||
import Account from './Account';
|
||||
import Sidebar from './Sidebar';
|
||||
import About from './About';
|
||||
import Billing from './Billing';
|
||||
import styles from './Settings.scss';
|
||||
|
||||
|
|
@ -34,6 +35,9 @@ function renderContent(section: string): React.ReactNode {
|
|||
if (section === SettingSections.billing) {
|
||||
return <Billing />;
|
||||
}
|
||||
if (section === SettingSections.about) {
|
||||
return <About />;
|
||||
}
|
||||
|
||||
return <div>Not found</div>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,5 +17,6 @@
|
|||
*/
|
||||
|
||||
export default {
|
||||
cdnUrl: __CDN_URL__
|
||||
cdnUrl: __CDN_URL__,
|
||||
version: __VERSION__
|
||||
};
|
||||
|
|
|
|||
|
|
@ -210,8 +210,8 @@ export function getPasswordResetConfirmPath(searchObj = {}): Location {
|
|||
|
||||
export enum SettingSections {
|
||||
account = 'account',
|
||||
spacedRepeition = 'spaced-repetition',
|
||||
billing = 'billing'
|
||||
billing = 'billing',
|
||||
about = 'about'
|
||||
}
|
||||
|
||||
export function getSettingsPath(section: SettingSections) {
|
||||
|
|
|
|||
|
|
@ -31,11 +31,13 @@ module.exports = ({ production = false, test = false } = {}) => {
|
|||
}
|
||||
|
||||
const cdnUrl = 'https://cdn.getdnote.com';
|
||||
const version = process.env.VERSION;
|
||||
|
||||
const compileTimeConstantForMinification = {
|
||||
__ROOT_URL__: JSON.stringify(rootUrl),
|
||||
__STRIPE_PUBLIC_KEY__: JSON.stringify(stripePublicKey),
|
||||
__CDN_URL__: JSON.stringify(cdnUrl)
|
||||
__CDN_URL__: JSON.stringify(cdnUrl),
|
||||
__VERSION__: JSON.stringify(version)
|
||||
};
|
||||
|
||||
if (!production) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue