Skip to content

Commit

Permalink
Add signup form; and add flow to connect QBO account (#94)
Browse files Browse the repository at this point in the history
Co-authored-by: russell-pollari <pollarir@mgail.com>
  • Loading branch information
Russell-Pollari and russell-pollari committed Oct 2, 2023
1 parent d06aed8 commit 0b9b925
Show file tree
Hide file tree
Showing 17 changed files with 354 additions and 61 deletions.
4 changes: 2 additions & 2 deletions client/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useMediaQuery } from 'react-responsive';

import { useGetCurrentUserQuery } from './services/api';
import LoadingSpinner from './components/LoadingSpinner';
import Login from './components/Login';
import { LoginContainer } from './components/Login';
import Header from './components/Header';

const App = () => {
Expand All @@ -28,7 +28,7 @@ const App = () => {
</div>
)}

{!isLoggedIn && !isLoading && <Login />}
{!isLoggedIn && !isLoading && <LoginContainer />}

{isLoggedIn && (
<div className="px-6 flex w-full h-full pt-16">
Expand Down
6 changes: 3 additions & 3 deletions client/components/AccountMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ import {
ArrowRightOnRectangleIcon,
} from '@heroicons/react/24/solid';

import type { QBOCompanyInfo } from '../types';
import type { User } from '../types';

const logout = () => {
document.cookie = 'token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
location.reload();
};

const AccountMenu = ({ companyInfo }: { companyInfo: QBOCompanyInfo }) => {
const AccountMenu = ({ user }: { user: User }) => {
return (
<Menu as="div" className="relative text-left z-50">
<Menu.Button className="inline-flex w-full justify-center rounded-md bg-black bg-opacity-20 px-4 py-2 text-sm font-medium text-white hover:bg-opacity-30 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
{companyInfo.CompanyName} ({companyInfo.Country})
{user.email}
<ChevronDownIcon
className="ml-2 -mr-1 h-5 w-5 text-violet-200 hover:text-violet-100"
aria-hidden="true"
Expand Down
16 changes: 5 additions & 11 deletions client/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,22 @@
import * as React from 'react';
import { Link } from 'react-router-dom';

import {
useGetCurrentUserQuery,
useGetCompanyInfoQuery,
} from '../services/api';
import { useGetCurrentUserQuery } from '../services/api';
import AccountMenu from './AccountMenu';
import NavMenu from './NavMenu';

const Header = ({ staticMenu }: { staticMenu: boolean }) => {
const { data: isLoggedIn } = useGetCurrentUserQuery();
const { data: companyInfo } = useGetCompanyInfoQuery('', {
skip: !isLoggedIn,
});
const { data: user } = useGetCurrentUserQuery();

return (
<div className="fixed z-50 w-full flex justify-start items-center bg-green-300 py-2 px-4 h-16 border-b border-solid border-gray-500">
{isLoggedIn && <NavMenu staticMode={staticMenu} />}
{!!user && <NavMenu staticMode={staticMenu} />}
<h1 className="font-semibold">
<Link to="/">Stripe2QBO</Link>
</h1>
{companyInfo && (
{user && (
<div className="absolute top-0 right-0 mt-4 mr-4">
<AccountMenu companyInfo={companyInfo} />
<AccountMenu user={user} />
</div>
)}
</div>
Expand Down
127 changes: 104 additions & 23 deletions client/components/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,150 @@
import * as React from 'react';
import { Formik, Form, Field } from 'formik';
import type { FieldProps } from 'formik';
import { object, string } from 'yup';

import SubmitButton from './SubmitButton';
import { useLoginMutation, useSignupMutation } from '../services/api';
import { Link } from 'react-router-dom';

import { useLoginMutation } from '../services/api';
const schema = object().shape({
email: string().email().required(),
password: string().required(),
});

const Login = () => {
const [login] = useLoginMutation();
const Input = ({
field,
placeholder,
type,
}: {
field: FieldProps;
placeholder: string;
type: string;
}) => {
return (
<input
className="w-full p-2 border border-solid border-green-500 rounded-lg mb-4"
type={type}
placeholder={placeholder}
{...field}
/>
);
};

const Login = ({
handleSubmit,
isSignupForm,
}: {
handleSubmit: (values: {
email: string;
password: string;
}) => Promise<void>;
isSignupForm: boolean;
}) => {
return (
<div className="absolute flex h-full w-full justify-center top-16">
<div className="p-8 my-16 rounded-lg bg-green-100 border border-solid border-green-300 shadow-lg h-max text-center relative">
<a href="https://github.com/Russell-Pollari/stripe2qbo">
<img
decoding="async"
width="149"
height="149"
src="https://github.blog/wp-content/uploads/2008/12/forkme_right_gray_6d6d6d.png?resize=149%2C149"
alt="Fork me on GitHub"
loading="lazy"
className="absolute top-0 right-0 border-0"
data-recalc-dims="1"
/>
</a>
{isSignupForm && (
<a href="https://github.com/Russell-Pollari/stripe2qbo">
<img
decoding="async"
width="149"
height="149"
src="https://github.blog/wp-content/uploads/2008/12/forkme_right_gray_6d6d6d.png?resize=149%2C149"
alt="Fork me on GitHub"
loading="lazy"
className="absolute top-0 right-0 border-0"
data-recalc-dims="1"
/>
</a>
)}
<h1 className="text-2xl text-green-800 font-semibold mb-8 mt-16">
Stripe2QBO
</h1>
<p className="mb-8 w-64 text-left inline-block">
<span className="text-green-700">Stripe2QBO</span> is an
open source tool to help you import your Stripe transactions
into QuickBooks Online.
</p>
{isSignupForm && (
<p className="mb-8 w-64 text-left inline-block">
<span className="text-green-700">Stripe2QBO</span> is an
open source tool to help you import your Stripe
transactions into QuickBooks Online.
</p>
)}
<div>
<Formik
initialValues={{ email: '', password: '' }}
validationSchema={schema}
onSubmit={async (values, { setSubmitting }) => {
await login(values);
await handleSubmit(values);
setSubmitting(false);
}}
>
{({ isSubmitting }) => (
<Form>
<div>
<Field
component={Input}
type="email"
name="email"
placeholder="email"
/>
</div>
<div className="my-4">
<Field
component={Input}
type="password"
name="password"
placeholder="password"
/>
</div>
<SubmitButton isSubmitting={isSubmitting}>
Login
{isSignupForm ? 'Signup' : 'Login'}
</SubmitButton>
</Form>
)}
</Formik>
</div>
<div>
<p>
{isSignupForm
? 'Already have an account?'
: 'No account?'}
</p>
{isSignupForm ? (
<Link to="/" className="text-green-700 text-sm">
Login
</Link>
) : (
<Link to="/signup" className="text-green-700 text-sm">
Sign up
</Link>
)}
</div>
</div>
</div>
);
};

export default Login;
export const LoginContainer = () => {
const [login] = useLoginMutation();

return (
<Login
handleSubmit={async (values) => {
await login(values);
}}
isSignupForm={false}
/>
);
};

export const SignupContainer = () => {
const [signup] = useSignupMutation();

return (
<Login
handleSubmit={async (values) => {
await signup(values);
}}
isSignupForm={true}
/>
);
};
51 changes: 51 additions & 0 deletions client/components/QBOConnection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as React from 'react';

import ConnectionCard from './ConnectionCard';
import { useGetCompanyInfoQuery } from '../services/api';

const connect = () => {
fetch('/api/qbo/oauth2')
.then((response) => response.json())
.then((data: string) => {
location.href = data;
})
.catch((error) => {
console.error(error);
});
};

const QBOConnection = () => {
const { data: qboInfo, isLoading, error } = useGetCompanyInfoQuery();
// const [disconnect] = useDisconnectStripeMutation();
return (
<ConnectionCard
isConnected={!!qboInfo}
title="QBO account"
isLoading={isLoading}
disconnect={() => {
// disconnect()
// .unwrap()
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
// .catch((e: any) => {
// // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
// alert(e?.data?.detail);
// });
}}
>
{qboInfo && !error ? (
<span>
{qboInfo.CompanyName} ({qboInfo.Country})
</span>
) : (
<button
className="inline-block hover:bg-slate-100 text-gray-500 font-bold p-2 rounded-full text-sm"
onClick={connect}
>
Connect a QBO account
</button>
)}
</ConnectionCard>
);
};

export default QBOConnection;
5 changes: 5 additions & 0 deletions client/components/SubmitButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as React from 'react';

import LoadingSpinner from './LoadingSpinner';

const SubmitButton = ({
isSubmitting,
children,
Expand All @@ -20,6 +22,9 @@ const SubmitButton = ({
`}
disabled={isSubmitting}
>
{isSubmitting && (
<LoadingSpinner className="inline-block w-4 h-4 mr-2" />
)}
{children}
</button>
);
Expand Down
10 changes: 10 additions & 0 deletions client/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { store } from './store/store';
import App from './App';
import SettingsPage from './pages/SettingsPage';
import SignupPage from './pages/SignupPage';
import TransactionsPage from './pages/TransactionsPage';
import QBOCallback from './pages/QBOCallback';

const router = createBrowserRouter([
{
Expand All @@ -23,6 +25,14 @@ const router = createBrowserRouter([
},
],
},
{
path: '/signup',
element: <SignupPage />,
},
{
path: '/qbo/oauth2/callback',
element: <QBOCallback />,
},
]);

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
Expand Down
40 changes: 40 additions & 0 deletions client/pages/QBOCallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as React from 'react';
import { useEffect, useState } from 'react';
import { useLocation } from 'react-router';
import { useSetQBOTokenMutation } from '../services/api';

const QBOCallback = () => {
const { search } = useLocation();
const params = new URLSearchParams(search);
const code = params.get('code');
const realmId = params.get('realmId');

const [setQBOCode] = useSetQBOTokenMutation();
const [error, setError] = useState<string | null>(null);

useEffect(() => {
if (code && realmId) {
setQBOCode({ code, realmId })
.unwrap()
.then(() => {
location.href = '/';
})
.catch((e) => {
// eslint-disable-next-line
setError(e?.data?.detail);
console.error(e);
});
}
}, [code, realmId]);

return (
<div className="grid h-screen place-items-center">
<div className="text-2xl font-semibold">
Connecting to QuickBooks...
{error && <div className="text-red-500">{error}</div>}
</div>
</div>
);
};

export default QBOCallback;
Loading

0 comments on commit 0b9b925

Please sign in to comment.