Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New account management #2104

Open
wants to merge 47 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
77076b1
scaffold the ui
beganovich Oct 7, 2024
aae17cf
@stripe/stripe-js
beganovich Oct 14, 2024
d8dd667
VITE_HOSTED_STRIPE_PK
beganovich Oct 14, 2024
29ee6e5
wait helper
beganovich Oct 14, 2024
01f9323
configuring new card
beganovich Oct 14, 2024
1685f66
managing the payment methods
beganovich Oct 15, 2024
a55c51b
upgrading accounts
beganovich Oct 16, 2024
bb5f9d9
clean up
beganovich Oct 16, 2024
beed37a
Merge remote-tracking branch 'origin/develop' into 1573-account-manag…
beganovich Nov 25, 2024
95e7184
Fixes for dynamic plan box
beganovich Nov 26, 2024
39ab091
Conditionally show big add credit card button
beganovich Nov 26, 2024
6a12877
Dynamically drive pricing on main view
beganovich Nov 26, 2024
377a957
Dynamically calculate/populate pricing on compare popup
beganovich Nov 26, 2024
cc3824f
Update passing correct context to upgrade
beganovich Nov 26, 2024
56cb4bd
Fixes for close button not working
beganovich Nov 27, 2024
5bbd169
Update logic for premium business plus plan
beganovich Nov 27, 2024
462b8fc
Import features from the API
beganovich Nov 27, 2024
911c7f3
Marking methods default
beganovich Nov 27, 2024
8a06545
Fixes for pro plan resolving
beganovich Nov 28, 2024
ae9de52
Directly invoking payment with Stripe
beganovich Nov 28, 2024
383c117
Reorganise components into files
beganovich Nov 29, 2024
0daf90e
Refactor change plan page
beganovich Nov 29, 2024
d8a558a
Fixes for imports
beganovich Nov 29, 2024
bfb0ce5
Fixes for build
beganovich Nov 29, 2024
d0cfa89
Update endpoints to include client
beganovich Dec 2, 2024
ffb991b
Update response types
beganovich Dec 2, 2024
6330cf0
Remove sending card when upgrading
beganovich Dec 2, 2024
1eb88f7
Improve loading states
beganovich Dec 2, 2024
589b83e
Adjust visibility of plans during upgrade
beganovich Dec 2, 2024
c92f655
Fixes for plan showing
beganovich Dec 3, 2024
9ccf4fe
Expose plans query
beganovich Dec 3, 2024
e3e82ee
Calculate enterprise price hook
beganovich Dec 3, 2024
0692c06
Add label for users
beganovich Dec 3, 2024
ff86943
Update Plan component
beganovich Dec 3, 2024
4ac0951
Update payment label
beganovich Dec 3, 2024
c10a324
Fixes for plan refs after upgrade
beganovich Dec 4, 2024
a68bb18
Fixes for cycle refs
beganovich Dec 4, 2024
d13bdf4
Fix the type
beganovich Dec 4, 2024
feea166
Fixes for plans interface
beganovich Dec 5, 2024
067bb17
Enterprise pricing utils
beganovich Dec 5, 2024
1bb0f4e
Fixes for labels
beganovich Dec 5, 2024
e0fa19c
Show original price instead of pro rata
beganovich Dec 5, 2024
03884b7
Downgrading to free
beganovich Dec 5, 2024
32c673f
Simplify popup logic
beganovich Dec 5, 2024
685c7c1
Fixes for undefined value access
beganovich Dec 6, 2024
3718e3c
Hide cancel button when adding new credit card
beganovich Dec 6, 2024
c399a03
Fixes for enterprise handling and same plan upgrades
beganovich Dec 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ VITE_ENABLE_DISCARD_CHANGES_TRACKING=false
VITE_PUSHER_APP_KEY=

VITE_RETURN_NUMBER_FIELD=false

VITE_HOSTED_STRIPE_PK=
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@reduxjs/toolkit": "^1.9.1",
"@sentry/react": "^7.77.0",
"@sentry/tracing": "^7.77.0",
"@stripe/stripe-js": "^4.8.0",
"@tinymce/tinymce-react": "^5.0.1",
"@tippyjs/react": "^4.2.6",
"antd": "^5.6.1",
Expand Down
40 changes: 40 additions & 0 deletions src/common/helpers/wait.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/

export function wait(...selectors: string[]) {
return new Promise((resolve) => {
if (!selectors.length) {
resolve([]);
return;
}

const elements = selectors
.map((selector) => document.querySelector(selector))
.filter(Boolean);

if (elements.length === selectors.length) {
resolve(elements);
return;
}

const observer = new MutationObserver(() => {
const foundElements = selectors
.map((selector) => document.querySelector(selector))
.filter(Boolean);

if (foundElements.length === selectors.length) {
observer.disconnect();
resolve(foundElements);
}
});

observer.observe(document.body, { childList: true, subtree: true });
});
}
8 changes: 8 additions & 0 deletions src/common/interfaces/company-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ export interface CompanyGateway {
token_billing: string;
test_mode: boolean;
always_show_required_fields: boolean;
is_default: boolean,
meta: {
exp_month: string;
exp_year: string;
brand: string;
last4: string;
type: number;
};
}

export interface FeesAndLimitsEntry {
Expand Down
35 changes: 35 additions & 0 deletions src/common/queries/plans.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/

import { useQuery } from 'react-query';
import { request } from '../helpers/request';
import { endpoint } from '../helpers';
import { AxiosResponse } from 'axios';

interface Plans {
features: {
enterprise_plan: string[];
free: string[];
premium_business_plan: string[];
pro_plan: string[];
};
plans: Record<string, number>;
}

export function usePlansQuery() {
return useQuery({
queryKey: ['plans'],
queryFn: () =>
request('GET', endpoint('/api/client/account_management/plans')).then(
(response: AxiosResponse<Plans>) => response.data
),
staleTime: Infinity,
});
}
2 changes: 1 addition & 1 deletion src/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export function Modal(props: Props) {
colorScheme: colors.$0,
}}
>
<div className="flex w-full justify-between">
<div className="flex w-full justify-between isolate">
<Dialog.Title
as="h3"
className="text-lg leading-6 font-medium"
Expand Down
1 change: 1 addition & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface ImportMetaEnv extends Readonly<Record<string, string>> {
readonly VITE_IS_PRODUCTION: string;
readonly VITE_WHITELABEL_INVOICE_URL: string;
readonly VITE_PUSHER_APP_KEY: string;
readonly VITE_HOSTED_STRIPE_PK: string;
readonly VITE_ENABLE_PEPPOL_STANDARD: string;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/

import { useCurrentAccount } from '$app/common/hooks/useCurrentAccount';
import { usePlansQuery } from '$app/common/queries/plans';
import { get } from 'lodash';
import { Plan } from '../../component/plan/Popup';
import { Account } from '$app/common/interfaces/account';

interface Rules {
[key: string]: {
term: 'month' | 'year';
condition: (account: Account) => boolean;
}[];
}

const rules: Rules = {
enterprise_plan: [
{
term: 'month',
condition: (account) => account.num_users < 2,
},
{
term: 'year',
condition: (account) => account.num_users < 2,
},
{
term: 'year',
condition: (account) =>
account.plan === 'enterprise' &&
account.num_users === 2 &&
account.plan_term === 'month',
},
],
enterprise_plan_5: [
{
term: 'month',
condition: (account) => account.num_users < 5,
},
{
term: 'year',
condition: (account) => account.num_users < 5,
},
{
term: 'year',
condition: (account) =>
account.plan === 'enterprise' &&
account.num_users === 5 &&
account.plan_term === 'month',
},
],
enterprise_plan_10: [
{
term: 'month',
condition: (account) => account.num_users < 10,
},
{
term: 'year',
condition: (account) => account.num_users < 10,
},
{
term: 'year',
condition: (account) =>
account.plan === 'enterprise' &&
account.num_users === 10 &&
account.plan_term === 'month',
},
],
enterprise_plan_20: [
{
term: 'month',
condition: (account) => account.num_users < 20,
},
{
term: 'year',
condition: (account) => account.num_users < 20,
},
{
term: 'year',
condition: (account) =>
account.plan === 'enterprise' &&
account.num_users === 20 &&
account.plan_term === 'month',
},
],
enterprise_plan_50: [
{
term: 'month',
condition: (account) => account.num_users < 50,
},
{
term: 'year',
condition: (account) => account.num_users < 50,
},
{
term: 'year',
condition: (account) =>
account.plan === 'enterprise' &&
account.num_users === 50 &&
account.plan_term === 'month',
},
],
};

export function useEnterpriseUtils() {
const account = useCurrentAccount();
const { data: plans } = usePlansQuery();

function isEnterprisePlanVisible(plan: Plan, term: 'month' | 'year') {
if (account.plan === '' || account.plan === 'pro') {
return true;
}

const planRules = rules[plan];

if (planRules && account) {
for (const rule of planRules) {
if (rule.term === term && rule.condition(account)) {
return true;
}
}
}

return false;
}

function getFirstAvailableEnterprise(term: 'month' | 'year') {
const available: Plan[] = [
'enterprise_plan',
'enterprise_plan_5',
'enterprise_plan_10',
'enterprise_plan_20',
'enterprise_plan_30',
'enterprise_plan_50',
];

for (const plan of available) {
const planRules = rules[plan];

if (planRules && account) {
for (const rule of planRules) {
if (rule.term === term && rule.condition(account)) {
return plan;
}
}
}
}

return null;
}

function calculatePrice() {
const term = account.plan_term;

if (!plans?.plans) {
return 0;
}

if (account.num_users <= 2) {
return term === 'month'
? get(plans, 'plans.enterprise_plan')
: get(plans, 'plans.enterprise_plan_annual');
}

if (account.num_users <= 5) {
return term === 'month'
? get(plans, 'plans.enterprise_plan_5')
: get(plans, 'plans.enterprise_plan_annual_5');
}

if (account.num_users <= 10) {
return term === 'month'
? get(plans, 'plans.enterprise_plan_10')
: get(plans, 'plans.enterprise_plan_annual_10');
}

if (account.num_users <= 20) {
return term === 'month'
? get(plans, 'plans.enterprise_plan_20')
: get(plans, 'plans.enterprise_plan_annual_20');
}

if (account.num_users <= 30) {
return term === 'month'
? get(plans, 'plans.enterprise_plan_30')
: get(plans, 'plans.enterprise_plan_annual_30');
}

if (account.num_users <= 50) {
return term === 'month'
? get(plans, 'plans.enterprise_plan_50')
: get(plans, 'plans.enterprise_plan_annual_50');
}

return term === 'month'
? get(plans, 'plans.enterprise_plan')
: get(plans, 'plans.enterprise_plan_annual');
}

return {
isEnterprisePlanVisible,
getFirstAvailableEnterprise,
calculatePrice,
};
}
Loading
Loading