feat: Purchase "Create account" & "Sign in" flows and Floating label input (#672)
This commit is contained in:
@@ -64,6 +64,7 @@ import { IconDirective } from './components/Icon';
|
||||
import { NoteTagsContainerDirective } from './components/NoteTagsContainer';
|
||||
import { PreferencesDirective } from './preferences';
|
||||
import { AppVersion, IsWebPlatform } from '@/version';
|
||||
import { PurchaseFlowDirective } from './purchaseFlow';
|
||||
import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu';
|
||||
|
||||
function reloadHiddenFirefoxTab(): boolean {
|
||||
@@ -165,7 +166,8 @@ const startApplication: StartApplication = async function startApplication(
|
||||
.directive('notesOptionsPanel', NotesOptionsPanelDirective)
|
||||
.directive('icon', IconDirective)
|
||||
.directive('noteTagsContainer', NoteTagsContainerDirective)
|
||||
.directive('preferences', PreferencesDirective);
|
||||
.directive('preferences', PreferencesDirective)
|
||||
.directive('purchaseFlow', PurchaseFlowDirective);
|
||||
|
||||
// Filters
|
||||
angular.module('app').filter('trusted', ['$sce', trusted]);
|
||||
|
||||
75
app/assets/javascripts/components/FloatingLabelInput.tsx
Normal file
75
app/assets/javascripts/components/FloatingLabelInput.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { FunctionComponent, Ref } from 'preact';
|
||||
import { JSXInternal } from 'preact/src/jsx';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
import { useState } from 'preact/hooks';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
type: 'text' | 'email' | 'password'; // Have no use cases for other types so far
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: JSXInternal.GenericEventHandler<HTMLInputElement>;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
inputClassName?: string;
|
||||
isInvalid?: boolean;
|
||||
};
|
||||
|
||||
export const FloatingLabelInput: FunctionComponent<Props> = forwardRef(
|
||||
(
|
||||
{
|
||||
id,
|
||||
type,
|
||||
label,
|
||||
disabled,
|
||||
value,
|
||||
isInvalid,
|
||||
onChange,
|
||||
className = '',
|
||||
labelClassName = '',
|
||||
inputClassName = '',
|
||||
},
|
||||
ref: Ref<HTMLInputElement>
|
||||
) => {
|
||||
const [focused, setFocused] = useState(false);
|
||||
|
||||
const BASE_CLASSNAME = `relative bg-default`;
|
||||
|
||||
const LABEL_CLASSNAME = `hidden absolute ${
|
||||
!focused ? 'color-neutral' : 'color-info'
|
||||
} ${focused || value ? 'flex top-0 left-2 pt-1.5 px-1' : ''} ${
|
||||
isInvalid ? 'color-dark-red' : ''
|
||||
} ${labelClassName}`;
|
||||
|
||||
const INPUT_CLASSNAME = `w-full h-full ${
|
||||
focused || value ? 'pt-6 pb-2' : 'py-2.5'
|
||||
} px-3 text-input border-1 border-solid border-gray-300 rounded placeholder-medium text-input focus:ring-info ${
|
||||
isInvalid ? 'border-dark-red placeholder-dark-red' : ''
|
||||
} ${inputClassName}`;
|
||||
|
||||
const handleFocus = () => setFocused(true);
|
||||
|
||||
const handleBlur = () => setFocused(false);
|
||||
|
||||
return (
|
||||
<div className={`${BASE_CLASSNAME} ${className}`}>
|
||||
<label htmlFor={id} className={LABEL_CLASSNAME}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
id={id}
|
||||
className={INPUT_CLASSNAME}
|
||||
placeholder={!focused ? label : ''}
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
ref={ref}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
68
app/assets/javascripts/purchaseFlow/PurchaseFlowView.tsx
Normal file
68
app/assets/javascripts/purchaseFlow/PurchaseFlowView.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { PurchaseFlowPane } from '@/ui_models/app_state/purchase_flow_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { CreateAccount } from './panes/CreateAccount';
|
||||
import { SignIn } from './panes/SignIn';
|
||||
import SNLogoFull from '../../svg/ic-sn-logo-full.svg';
|
||||
|
||||
type PaneSelectorProps = {
|
||||
currentPane: PurchaseFlowPane;
|
||||
} & PurchaseFlowViewProps;
|
||||
|
||||
type PurchaseFlowViewProps = {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
const PurchaseFlowPaneSelector: FunctionComponent<PaneSelectorProps> = ({
|
||||
currentPane,
|
||||
appState,
|
||||
application,
|
||||
}) => {
|
||||
switch (currentPane) {
|
||||
case PurchaseFlowPane.CreateAccount:
|
||||
return <CreateAccount appState={appState} application={application} />;
|
||||
case PurchaseFlowPane.SignIn:
|
||||
return <SignIn appState={appState} application={application} />;
|
||||
}
|
||||
};
|
||||
|
||||
export const PurchaseFlowView: FunctionComponent<PurchaseFlowViewProps> =
|
||||
observer(({ appState, application }) => {
|
||||
const { currentPane } = appState.purchaseFlow;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full w-full absolute top-left-0 z-index-purchase-flow bg-grey-2">
|
||||
<div className="relative fit-content">
|
||||
<div className="relative p-12 mb-4 bg-default border-1 border-solid border-gray-300 rounded">
|
||||
<SNLogoFull className="mb-5" />
|
||||
<PurchaseFlowPaneSelector
|
||||
currentPane={currentPane}
|
||||
appState={appState}
|
||||
application={application}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<a
|
||||
className="mr-3 font-medium color-grey-1"
|
||||
href="https://standardnotes.com/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Privacy
|
||||
</a>
|
||||
<a
|
||||
className="font-medium color-grey-1"
|
||||
href="https://standardnotes.com/help"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Help
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
33
app/assets/javascripts/purchaseFlow/PurchaseFlowWrapper.tsx
Normal file
33
app/assets/javascripts/purchaseFlow/PurchaseFlowWrapper.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { isDesktopApplication } from '@/utils';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { PurchaseFlowView } from './PurchaseFlowView';
|
||||
|
||||
export type PurchaseFlowWrapperProps = {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
export const loadPurchaseFlowUrl = async (
|
||||
application: WebApplication
|
||||
): Promise<void> => {
|
||||
const url = await application.getPurchaseFlowUrl();
|
||||
if (url) {
|
||||
const currentUrl = window.location.href.split('/?')[0];
|
||||
const successUrl = isDesktopApplication()
|
||||
? `standardnotes://${currentUrl}`
|
||||
: currentUrl;
|
||||
window.location.assign(`${url}&success_url=${successUrl}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const PurchaseFlowWrapper: FunctionComponent<PurchaseFlowWrapperProps> =
|
||||
observer(({ appState, application }) => {
|
||||
if (!appState.purchaseFlow.isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <PurchaseFlowView appState={appState} application={application} />;
|
||||
});
|
||||
8
app/assets/javascripts/purchaseFlow/index.ts
Normal file
8
app/assets/javascripts/purchaseFlow/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { toDirective } from '@/components/utils';
|
||||
import {
|
||||
PurchaseFlowWrapper,
|
||||
PurchaseFlowWrapperProps,
|
||||
} from './PurchaseFlowWrapper';
|
||||
|
||||
export const PurchaseFlowDirective =
|
||||
toDirective<PurchaseFlowWrapperProps>(PurchaseFlowWrapper);
|
||||
200
app/assets/javascripts/purchaseFlow/panes/CreateAccount.tsx
Normal file
200
app/assets/javascripts/purchaseFlow/panes/CreateAccount.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import { Button } from '@/components/Button';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { PurchaseFlowPane } from '@/ui_models/app_state/purchase_flow_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import Illustration from '../../../svg/create-account-illustration.svg';
|
||||
import Circle from '../../../svg/circle-55.svg';
|
||||
import BlueDot from '../../../svg/blue-dot.svg';
|
||||
import Diamond from '../../../svg/diamond-with-horizontal-lines.svg';
|
||||
import { FloatingLabelInput } from '@/components/FloatingLabelInput';
|
||||
import { isEmailValid } from '@/utils';
|
||||
import { loadPurchaseFlowUrl } from '../PurchaseFlowWrapper';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
export const CreateAccount: FunctionComponent<Props> = observer(
|
||||
({ appState, application }) => {
|
||||
const { setCurrentPane } = appState.purchaseFlow;
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [isCreatingAccount, setIsCreatingAccount] = useState(false);
|
||||
const [isEmailInvalid, setIsEmailInvalid] = useState(false);
|
||||
const [isPasswordNotMatching, setIsPasswordNotMatching] = useState(false);
|
||||
|
||||
const emailInputRef = useRef<HTMLInputElement>();
|
||||
const passwordInputRef = useRef<HTMLInputElement>();
|
||||
const confirmPasswordInputRef = useRef<HTMLInputElement>();
|
||||
|
||||
useEffect(() => {
|
||||
if (emailInputRef.current) emailInputRef.current.focus();
|
||||
}, []);
|
||||
|
||||
const handleEmailChange = (e: Event) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setEmail(e.target.value);
|
||||
setIsEmailInvalid(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordChange = (e: Event) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setPassword(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmPasswordChange = (e: Event) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setConfirmPassword(e.target.value);
|
||||
setIsPasswordNotMatching(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignInInstead = () => {
|
||||
setCurrentPane(PurchaseFlowPane.SignIn);
|
||||
};
|
||||
|
||||
const handleCreateAccount = async () => {
|
||||
if (!email) {
|
||||
emailInputRef?.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isEmailValid(email)) {
|
||||
setIsEmailInvalid(true);
|
||||
emailInputRef?.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
passwordInputRef?.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirmPassword) {
|
||||
confirmPasswordInputRef?.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setConfirmPassword('');
|
||||
setIsPasswordNotMatching(true);
|
||||
confirmPasswordInputRef?.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCreatingAccount(true);
|
||||
|
||||
try {
|
||||
const response = await application.register(email, password);
|
||||
if (response.error || response.data?.error) {
|
||||
throw new Error(
|
||||
response.error?.message || response.data?.error?.message
|
||||
);
|
||||
} else {
|
||||
loadPurchaseFlowUrl(application).catch((err) => {
|
||||
console.error(err);
|
||||
application.alertService.alert(err);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
application.alertService.alert(err as string);
|
||||
} finally {
|
||||
setIsCreatingAccount(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Circle className="absolute w-8 h-8 top-40% -left-28" />
|
||||
<BlueDot className="absolute w-4 h-4 top-35% -left-10" />
|
||||
<Diamond className="absolute w-26 h-26 -bottom-5 left-0 -translate-x-1/2 -z-index-1" />
|
||||
|
||||
<Circle className="absolute w-8 h-8 bottom-35% -right-20" />
|
||||
<BlueDot className="absolute w-4 h-4 bottom-25% -right-10" />
|
||||
<Diamond className="absolute w-18 h-18 top-0 -right-2 translate-x-1/2 -z-index-1" />
|
||||
|
||||
<div className="mr-12">
|
||||
<h1 className="mt-0 mb-2 text-2xl">Create your free account</h1>
|
||||
<div className="mb-4 font-medium text-sm">
|
||||
to continue to Standard Notes.
|
||||
</div>
|
||||
<form onSubmit={handleCreateAccount}>
|
||||
<div className="flex flex-col">
|
||||
<FloatingLabelInput
|
||||
className={`min-w-90 ${isEmailInvalid ? 'mb-2' : 'mb-4'}`}
|
||||
id="purchase-sign-in-email"
|
||||
type="email"
|
||||
label="Email"
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
ref={emailInputRef}
|
||||
disabled={isCreatingAccount}
|
||||
isInvalid={isEmailInvalid}
|
||||
/>
|
||||
{isEmailInvalid ? (
|
||||
<div className="color-dark-red mb-4">
|
||||
Please provide a valid email.
|
||||
</div>
|
||||
) : null}
|
||||
<FloatingLabelInput
|
||||
className="min-w-90 mb-4"
|
||||
id="purchase-create-account-password"
|
||||
type="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
ref={passwordInputRef}
|
||||
disabled={isCreatingAccount}
|
||||
/>
|
||||
<FloatingLabelInput
|
||||
className={`min-w-90 ${
|
||||
isPasswordNotMatching ? 'mb-2' : 'mb-4'
|
||||
}`}
|
||||
id="create-account-confirm"
|
||||
type="password"
|
||||
label="Repeat password"
|
||||
value={confirmPassword}
|
||||
onChange={handleConfirmPasswordChange}
|
||||
ref={confirmPasswordInputRef}
|
||||
disabled={isCreatingAccount}
|
||||
isInvalid={isPasswordNotMatching}
|
||||
/>
|
||||
{isPasswordNotMatching ? (
|
||||
<div className="color-dark-red mb-4">
|
||||
Passwords don't match. Please try again.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={handleSignInInstead}
|
||||
disabled={isCreatingAccount}
|
||||
className="p-0 bg-default border-0 font-medium color-info cursor-pointer hover:underline"
|
||||
>
|
||||
Sign in instead
|
||||
</button>
|
||||
<Button
|
||||
className="py-2.5"
|
||||
type="primary"
|
||||
label={
|
||||
isCreatingAccount ? 'Creating account...' : 'Create account'
|
||||
}
|
||||
onClick={handleCreateAccount}
|
||||
disabled={isCreatingAccount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Illustration />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
175
app/assets/javascripts/purchaseFlow/panes/SignIn.tsx
Normal file
175
app/assets/javascripts/purchaseFlow/panes/SignIn.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import { Button } from '@/components/Button';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { PurchaseFlowPane } from '@/ui_models/app_state/purchase_flow_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FunctionComponent } from 'preact';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import Circle from '../../../svg/circle-55.svg';
|
||||
import BlueDot from '../../../svg/blue-dot.svg';
|
||||
import Diamond from '../../../svg/diamond-with-horizontal-lines.svg';
|
||||
import { FloatingLabelInput } from '@/components/FloatingLabelInput';
|
||||
import { isEmailValid } from '@/utils';
|
||||
import { loadPurchaseFlowUrl } from '../PurchaseFlowWrapper';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
export const SignIn: FunctionComponent<Props> = observer(
|
||||
({ appState, application }) => {
|
||||
const { setCurrentPane } = appState.purchaseFlow;
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isSigningIn, setIsSigningIn] = useState(false);
|
||||
const [isEmailInvalid, setIsEmailInvalid] = useState(false);
|
||||
const [isPasswordInvalid, setIsPasswordInvalid] = useState(false);
|
||||
const [otherErrorMessage, setOtherErrorMessage] = useState('');
|
||||
|
||||
const emailInputRef = useRef<HTMLInputElement>();
|
||||
const passwordInputRef = useRef<HTMLInputElement>();
|
||||
|
||||
useEffect(() => {
|
||||
if (emailInputRef.current) emailInputRef.current.focus();
|
||||
}, []);
|
||||
|
||||
const handleEmailChange = (e: Event) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setEmail(e.target.value);
|
||||
setIsEmailInvalid(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordChange = (e: Event) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
setPassword(e.target.value);
|
||||
setIsPasswordInvalid(false);
|
||||
setOtherErrorMessage('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateAccountInstead = () => {
|
||||
if (isSigningIn) return;
|
||||
setCurrentPane(PurchaseFlowPane.CreateAccount);
|
||||
};
|
||||
|
||||
const handleSignIn = async () => {
|
||||
if (!email) {
|
||||
emailInputRef?.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isEmailValid(email)) {
|
||||
setIsEmailInvalid(true);
|
||||
emailInputRef?.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
passwordInputRef?.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSigningIn(true);
|
||||
|
||||
try {
|
||||
const response = await application.signIn(email, password);
|
||||
if (response.error || response.data?.error) {
|
||||
throw new Error(
|
||||
response.error?.message || response.data?.error?.message
|
||||
);
|
||||
} else {
|
||||
loadPurchaseFlowUrl(application).catch((err) => {
|
||||
console.error(err);
|
||||
application.alertService.alert(err);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if ((err as Error).toString().includes('Invalid email or password')) {
|
||||
setIsSigningIn(false);
|
||||
setIsEmailInvalid(true);
|
||||
setIsPasswordInvalid(true);
|
||||
setOtherErrorMessage('Invalid email or password.');
|
||||
setPassword('');
|
||||
} else {
|
||||
application.alertService.alert(err as string);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Circle className="absolute w-8 h-8 top-35% -left-56" />
|
||||
<BlueDot className="absolute w-4 h-4 top-30% -left-40" />
|
||||
<Diamond className="absolute w-26 h-26 -bottom-5 left-0 -translate-x-1/2 -z-index-1" />
|
||||
|
||||
<Circle className="absolute w-8 h-8 bottom-30% -right-56" />
|
||||
<BlueDot className="absolute w-4 h-4 bottom-20% -right-44" />
|
||||
<Diamond className="absolute w-18 h-18 top-0 -right-2 translate-x-1/2 -z-index-1" />
|
||||
|
||||
<div>
|
||||
<h1 className="mt-0 mb-2 text-2xl">Sign in</h1>
|
||||
<div className="mb-4 font-medium text-sm">
|
||||
to continue to Standard Notes.
|
||||
</div>
|
||||
<form onSubmit={handleSignIn}>
|
||||
<div className="flex flex-col">
|
||||
<FloatingLabelInput
|
||||
className={`min-w-90 ${
|
||||
isEmailInvalid && !otherErrorMessage ? 'mb-2' : 'mb-4'
|
||||
}`}
|
||||
id="purchase-sign-in-email"
|
||||
type="email"
|
||||
label="Email"
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
ref={emailInputRef}
|
||||
disabled={isSigningIn}
|
||||
isInvalid={isEmailInvalid}
|
||||
/>
|
||||
{isEmailInvalid && !otherErrorMessage ? (
|
||||
<div className="color-dark-red mb-4">
|
||||
Please provide a valid email.
|
||||
</div>
|
||||
) : null}
|
||||
<FloatingLabelInput
|
||||
className={`min-w-90 ${otherErrorMessage ? 'mb-2' : 'mb-4'}`}
|
||||
id="purchase-sign-in-password"
|
||||
type="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
ref={passwordInputRef}
|
||||
disabled={isSigningIn}
|
||||
isInvalid={isPasswordInvalid}
|
||||
/>
|
||||
{otherErrorMessage ? (
|
||||
<div className="color-dark-red mb-4">{otherErrorMessage}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Button
|
||||
className={`${isSigningIn ? 'min-w-30' : 'min-w-24'} py-2.5 mb-5`}
|
||||
type="primary"
|
||||
label={isSigningIn ? 'Signing in...' : 'Sign in'}
|
||||
onClick={handleSignIn}
|
||||
disabled={isSigningIn}
|
||||
/>
|
||||
</form>
|
||||
<div className="text-sm font-medium color-grey-1">
|
||||
Don’t have an account yet?{' '}
|
||||
<a
|
||||
className={`color-info ${
|
||||
isSigningIn ? 'cursor-not-allowed' : 'cursor-pointer '
|
||||
}`}
|
||||
onClick={handleCreateAccountInstead}
|
||||
>
|
||||
Create account
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -23,6 +23,7 @@ import { NotesState } from './notes_state';
|
||||
import { TagsState } from './tags_state';
|
||||
import { AccountMenuState } from '@/ui_models/app_state/account_menu_state';
|
||||
import { PreferencesState } from './preferences_state';
|
||||
import { PurchaseFlowState } from './purchase_flow_state';
|
||||
import { QuickSettingsState } from './quick_settings_state';
|
||||
|
||||
export enum AppStateEvent {
|
||||
@@ -67,6 +68,7 @@ export class AppState {
|
||||
readonly accountMenu: AccountMenuState;
|
||||
readonly actionsMenu = new ActionsMenuState();
|
||||
readonly preferences = new PreferencesState();
|
||||
readonly purchaseFlow: PurchaseFlowState;
|
||||
readonly noAccountWarning: NoAccountWarningState;
|
||||
readonly noteTags: NoteTagsState;
|
||||
readonly sync = new SyncState();
|
||||
@@ -113,6 +115,7 @@ export class AppState {
|
||||
application,
|
||||
this.appEventObserverRemovers
|
||||
);
|
||||
this.purchaseFlow = new PurchaseFlowState(application);
|
||||
this.addAppEventObserver();
|
||||
this.streamNotesAndTags();
|
||||
this.onVisibilityChange = () => {
|
||||
@@ -284,6 +287,8 @@ export class AppState {
|
||||
break;
|
||||
case ApplicationEvent.Launched:
|
||||
this.locked = false;
|
||||
if (window.location.search.includes('purchase=true'))
|
||||
this.purchaseFlow.openPurchaseFlow();
|
||||
break;
|
||||
case ApplicationEvent.SyncStatusChanged:
|
||||
this.sync.update(this.application.getSyncStatus());
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { action, makeObservable, observable } from 'mobx';
|
||||
import { WebApplication } from '../application';
|
||||
|
||||
export enum PurchaseFlowPane {
|
||||
SignIn,
|
||||
CreateAccount,
|
||||
}
|
||||
|
||||
export class PurchaseFlowState {
|
||||
isOpen = false;
|
||||
currentPane = PurchaseFlowPane.CreateAccount;
|
||||
|
||||
constructor(private application: WebApplication) {
|
||||
makeObservable(this, {
|
||||
isOpen: observable,
|
||||
currentPane: observable,
|
||||
|
||||
setCurrentPane: action,
|
||||
openPurchaseFlow: action,
|
||||
closePurchaseFlow: action,
|
||||
});
|
||||
}
|
||||
|
||||
setCurrentPane = (currentPane: PurchaseFlowPane): void => {
|
||||
this.currentPane = currentPane;
|
||||
};
|
||||
|
||||
openPurchaseFlow = (): void => {
|
||||
const user = this.application.getUser();
|
||||
if (!user) {
|
||||
this.isOpen = true;
|
||||
}
|
||||
};
|
||||
|
||||
closePurchaseFlow = (): void => {
|
||||
this.isOpen = false;
|
||||
};
|
||||
}
|
||||
@@ -41,4 +41,7 @@
|
||||
application='self.application'
|
||||
app-state='self.appState'
|
||||
)
|
||||
|
||||
purchase-flow(
|
||||
application='self.application'
|
||||
app-state='self.appState'
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user