diff --git a/electron/js/lib/installNativeMessagingHost.js b/electron/js/lib/installNativeMessagingHost.js index 22de950fc5..e86566093c 100644 --- a/electron/js/lib/installNativeMessagingHost.js +++ b/electron/js/lib/installNativeMessagingHost.js @@ -17,7 +17,7 @@ const { fixPathForAsarUnpack, is } = require('electron-util'); const APP_NAME = 'com.anytype.desktop'; const MANIFEST_FILENAME = `${APP_NAME}.json`; -const EXTENSION_IDS = [ 'jbnammhjiplhpjfncnlejjjejghimdkf', 'jkmhmgghdjjbafmkgjmplhemjjnkligf' ]; +const EXTENSION_IDS = [ 'jbnammhjiplhpjfncnlejjjejghimdkf', 'jkmhmgghdjjbafmkgjmplhemjjnkligf', 'lcamkcmpcofgmbmloefimnelnjpcdpfn' ]; const USER_PATH = app.getPath('userData'); const EXE_PATH = app.getPath('exe'); @@ -191,4 +191,4 @@ const writeManifest = (dst, data) => { }; -module.exports = { installNativeMessagingHost }; \ No newline at end of file +module.exports = { installNativeMessagingHost }; diff --git a/src/json/extension.ts b/src/json/extension.ts index 8173cc8a8b..6b19e30efa 100644 --- a/src/json/extension.ts +++ b/src/json/extension.ts @@ -1,8 +1,8 @@ export default { clipper: { - ids: [ 'jbnammhjiplhpjfncnlejjjejghimdkf', 'jkmhmgghdjjbafmkgjmplhemjjnkligf' ], + ids: [ 'jbnammhjiplhpjfncnlejjjejghimdkf', 'jkmhmgghdjjbafmkgjmplhemjjnkligf', 'lcamkcmpcofgmbmloefimnelnjpcdpfn' ], name: 'Anytype Webclipper', prefix: 'anytypeWebclipper', emojiUrl: 'https://anytype-static.fra1.cdn.digitaloceanspaces.com/emojies/' } -}; \ No newline at end of file +}; diff --git a/src/json/text.json b/src/json/text.json index cc8e0d9939..f56a5364ca 100644 --- a/src/json/text.json +++ b/src/json/text.json @@ -170,6 +170,7 @@ "commonProgress": "Progress", "commonMainChat": "Chat", "commonMenu": "Menu", + "commonSignUp": "Sign Up", "pluralDay": "day|days", "pluralObject": "Object|Objects", @@ -1849,6 +1850,18 @@ "onboardingObjectCreationStart21": "Choose here from the most popular object types, such as Page, Task, or Collection. You can also select an object from the Type menu.", "onboardingObjectCreationStart2Button": "I got it!", + "emailCollectionStep0Title": "Want to stay in touch?", + "emailCollectionStep0Description": "Enter your email to receive tips and updates. We do not link your account with your email, ever. Cancel anytime.", + "emailCollectionStep1Title": "Just a moment", + "emailCollectionStep1Description": "Enter the code we’ve sent you to your email", + "emailCollectionStep2Title": "You’re Subscribed!", + "emailCollectionStep2Description": "You are now set to receive the latest updates and perks. Enjoy exploring!", + + "emailCollectionCheckboxTipsLabel": "Insider tips & tutorials on using Anytype", + "emailCollectionCheckboxNewsLabel": "Product updates & company news", + "emailCollectionEnterEmail": "Enter email...", + "emailCollectionGreat": "Great!", + "onboardingObjectCreationFinish": "Creating Objects", "onboardingObjectCreationFinish11": "For the Object you created, you can adjust it using the top menu. Change the cover, layout, or set up a relations to build the graph.", "onboardingObjectCreationFinish1Button": "Ok! I like it", diff --git a/src/scss/component/common.scss b/src/scss/component/common.scss index ab60c1f748..4503327dac 100644 --- a/src/scss/component/common.scss +++ b/src/scss/component/common.scss @@ -30,4 +30,6 @@ @import "./share"; @import "./preview/common"; -@import "./media/common"; \ No newline at end of file +@import "./media/common"; + +@import "./emailCollectionForm"; diff --git a/src/scss/component/emailCollectionForm.scss b/src/scss/component/emailCollectionForm.scss new file mode 100644 index 0000000000..ee77dcbc08 --- /dev/null +++ b/src/scss/component/emailCollectionForm.scss @@ -0,0 +1,38 @@ +@import "~scss/_mixins"; + +.emailCollectionForm { display: flex; flex-direction: column; } +.emailCollectionForm { + .statusBar { @include text-small; min-height: 18px; margin: 0px 0px 2px; padding-top: 4px; color: var(--color-text-secondary); } + .statusBar.error { color: var(--color-red); } + + .buttonWrapper { padding-top: 8px; } + .buttonWrapper { + .button { width: 100%; } + } + + .step0 { padding-top: 16px; } + .step0 { + .check { @include text-small; } + .inputWrapper { padding-top: 16px; } + .inputWrapper { + .input { border-radius: 3px; height: 36px; } + } + } + + .step1 { padding-top: 16px; text-align: center; } + .step1 { + .pin { margin-bottom: 8px; } + .pin { + .input { @include text-header1; width: 35px; height: 47px; border-radius: 6px; border-color: var(--color-shape-primary); } + } + + .resend { font-weight: 500; color: var(--color-text-secondary); @include text-small; } + .resend.countdown { color: var(--color-text-primary); } + .resend:not(.countdown):hover { color: var(--color-control-active); } + } + + .step2 { padding-top: 8px; } + .step2 { + .icon { width: 100%; height: 138px; background: url('~img/icon/payment/green.svg') 50% 50% no-repeat; background-size: 80px; } + } +} diff --git a/src/scss/menu/onboarding.scss b/src/scss/menu/onboarding.scss index a65d3c8dbc..ae0ea5ccbc 100644 --- a/src/scss/menu/onboarding.scss +++ b/src/scss/menu/onboarding.scss @@ -74,6 +74,8 @@ } } + .menu.menuOnboarding.invertedColor { box-shadow: inset 0px 0px 0px 1px var(--color-control-inactive), 0px 4px 12px 0px rgba(37, 37, 37, 0.20); background-color: var(--color-bg-primary); color: var(--color-text-primary); } + .menu.menuOnboarding.isWizard { min-height: 458px; } .menu.menuOnboarding.isWizard { .bottom { position: absolute; bottom: 16px; width: calc(100% - 32px); margin: 0px; } diff --git a/src/ts/app.tsx b/src/ts/app.tsx index ea0709d831..fb87781966 100644 --- a/src/ts/app.tsx +++ b/src/ts/app.tsx @@ -220,6 +220,10 @@ class App extends React.Component { componentDidMount () { this.init(); + + window.setTimeout(() => { + Onboarding.start('emailCollection', false, true); + }, 1500); }; init () { diff --git a/src/ts/component/index.tsx b/src/ts/component/index.tsx index 5935ea0fcc..ff919c2825 100644 --- a/src/ts/component/index.tsx +++ b/src/ts/component/index.tsx @@ -94,6 +94,8 @@ import ShareTooltip from './util/share/tooltip'; import ShareBanner from './util/share/banner'; import FooterAuthDisclaimer from './footer/auth/disclaimer'; +import EmailCollectionForm from './util/emailCollectionForm'; + export { Page, EditorPage, @@ -184,5 +186,7 @@ export { ProgressBar, ShareTooltip, ShareBanner, - FooterAuthDisclaimer + FooterAuthDisclaimer, + + EmailCollectionForm, }; diff --git a/src/ts/component/menu/onboarding.tsx b/src/ts/component/menu/onboarding.tsx index ee03c5e1c0..84dfd67cdd 100644 --- a/src/ts/component/menu/onboarding.tsx +++ b/src/ts/component/menu/onboarding.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import $ from 'jquery'; import raf from 'raf'; import { observer } from 'mobx-react'; -import { Button, Icon, Label } from 'Component'; +import { Button, Icon, Label, EmailCollectionForm } from 'Component'; import { I, C, S, U, J, Onboarding, analytics, keyboard, translate } from 'Lib'; import ReactCanvasConfetti from 'react-canvas-confetti'; @@ -37,6 +37,7 @@ const MenuOnboarding = observer(class MenuSelect extends React.Component 1; + const withEmailForm = key == 'emailCollection'; let buttons = []; let category = ''; @@ -88,6 +89,9 @@ const MenuOnboarding = observer(class MenuSelect extends React.Component ) : ''} + {withEmailForm ? ( + this.props.close()} /> + ) : ''}
{withSteps ? ( diff --git a/src/ts/component/popup/page/membership/current.tsx b/src/ts/component/popup/page/membership/current.tsx index c63ac3376b..037bc0fa59 100644 --- a/src/ts/component/popup/page/membership/current.tsx +++ b/src/ts/component/popup/page/membership/current.tsx @@ -188,7 +188,7 @@ const PopupMembershipPageCurrent = observer(class PopupMembershipPageCurrent ext this.refButton.setLoading(true); - C.MembershipGetVerificationEmail(this.refEmail.getValue(), true, (message) => { + C.MembershipGetVerificationEmail(this.refEmail.getValue(), true, false, false, (message) => { this.refButton.setLoading(false); if (message.error.code) { diff --git a/src/ts/component/popup/page/membership/free.tsx b/src/ts/component/popup/page/membership/free.tsx index 70a520dbe2..ac566b67a5 100644 --- a/src/ts/component/popup/page/membership/free.tsx +++ b/src/ts/component/popup/page/membership/free.tsx @@ -138,7 +138,7 @@ const PopupMembershipPageFree = observer(class PopupMembershipPageFree extends R this.refButton.setLoading(true); - C.MembershipGetVerificationEmail(this.refEmail.getValue(), this.refCheckbox?.getValue(), (message) => { + C.MembershipGetVerificationEmail(this.refEmail.getValue(), this.refCheckbox?.getValue(), false, false, (message) => { this.refButton.setLoading(false); if (message.error.code) { diff --git a/src/ts/component/util/emailCollectionForm.tsx b/src/ts/component/util/emailCollectionForm.tsx new file mode 100644 index 0000000000..9ffeda44fb --- /dev/null +++ b/src/ts/component/util/emailCollectionForm.tsx @@ -0,0 +1,249 @@ +import * as React from 'react'; +import { Label, Checkbox, Input, Button, Icon, Pin } from 'Component'; +import { analytics, C, I, J, S, translate, U } from 'Lib'; + +interface Props { + onStepChange: () => void; + onComplete: () => void; +}; + +interface State { + countdown: number; + status: string; + statusText: string; + email: string, + subscribeNews: boolean, + subscribeTips: boolean, +}; + +class EmailCollectionForm extends React.Component { + + state = { + status: '', + statusText: '', + countdown: 60, + email: '', + subscribeNews: false, + subscribeTips: false, + }; + + step = 0; + node: any = null; + refCheckboxTips: any = null; + refCheckboxNews: any = null; + refEmail: any = null; + refButton: any = null; + refCode: any = null; + + interval = null; + timeout = null; + + constructor (props: Props) { + super(props); + + this.onCheck = this.onCheck.bind(this); + this.onSubmitEmail = this.onSubmitEmail.bind(this); + this.verifyEmail = this.verifyEmail.bind(this); + this.onConfirmEmailCode = this.onConfirmEmailCode.bind(this); + this.onResend = this.onResend.bind(this); + this.validateEmail = this.validateEmail.bind(this); + }; + + render () { + const { status, statusText, countdown } = this.state; + + let content = null; + + switch (this.step) { + case 0: { + content = ( +
+
+
this.onCheck(this.refCheckboxTips)}> + this.refCheckboxTips = ref} value={false} /> {translate('emailCollectionCheckboxTipsLabel')} +
+
this.onCheck(this.refCheckboxNews)}> + this.refCheckboxNews = ref} value={false} /> {translate('emailCollectionCheckboxNewsLabel')} +
+ +
+ this.refEmail = ref} onKeyUp={this.validateEmail} placeholder={translate(`emailCollectionEnterEmail`)} /> +
+ + {status ?
{statusText}
: ''} + +
+
+
+
+ ); + break; + }; + + case 1: { + content = ( +
+ this.refCode = ref} + pinLength={4} + isVisible={true} + onSuccess={this.onConfirmEmailCode} + /> + + {status ?
{statusText}
: ''} + +
+ {translate('popupMembershipResend')} + {countdown ? U.Common.sprintf(translate('popupMembershipCountdown'), countdown) : ''} +
+
+ ); + break; + }; + + case 2: { + content = ( +
+ + +
+
+
+ ); + break; + }; + }; + + return ( +
+
+ ); + }; + + componentDidMount () { + this.refButton?.setDisabled(true); + }; + + onCheck (ref) { + ref.toggle(); + }; + + setStatus (status: string, statusText: string) { + this.setState({ status, statusText }); + + window.clearTimeout(this.timeout); + this.timeout = window.setTimeout(() => this.clearStatus(), 4000); + }; + + clearStatus () { + this.setState({ status: '', statusText: '' }); + }; + + validateEmail () { + this.clearStatus(); + + window.clearTimeout(this.timeout); + this.timeout = window.setTimeout(() => { + const value = this.refEmail?.getValue(); + const isValid = U.Common.checkEmail(value); + + if (value && !isValid) { + this.setStatus('error', translate('errorIncorrectEmail')); + }; + + this.refButton?.setDisabled(!isValid); + }, J.Constant.delay.keyboard); + }; + + onSubmitEmail (e: any) { + if (!this.refButton || !this.refEmail) { + return; + }; + + if (this.refButton.isDisabled()) { + return; + }; + + this.setState({ + email: this.refEmail.getValue(), + subscribeNews: this.refCheckboxNews?.getValue(), + subscribeTips: this.refCheckboxTips?.getValue(), + }, () => { + this.refButton.setLoading(true); + this.verifyEmail(e) + }); + }; + + verifyEmail (e: any) { + e.preventDefault(); + + const { email, subscribeNews, subscribeTips } = this.state; + + C.MembershipGetVerificationEmail(email, subscribeNews, subscribeTips, true, (message) => { + this.refButton?.setLoading(false); + + if (message.error.code) { + this.setStatus('error', message.error.description); + return; + }; + + this.step = 1; + this.startCountdown(60); + this.forceUpdate(); + this.props.onStepChange(); + }); + }; + + onConfirmEmailCode () { + const code = this.refCode.getValue(); + + C.MembershipVerifyEmailCode(code, (message) => { + if (message.error.code) { + this.setStatus('error', message.error.description); + this.refCode.reset(); + return; + }; + + this.step = 2; + this.forceUpdate(); + this.props.onStepChange(); + }); + }; + + onResend (e: any) { + if (!this.state.countdown) { + this.verifyEmail(e); + }; + }; + + startCountdown (seconds) { + const { emailConfirmationTime } = S.Common; + + if (!emailConfirmationTime) { + S.Common.emailConfirmationTimeSet(U.Date.now()); + }; + + this.setState({ countdown: seconds }); + this.interval = window.setInterval(() => { + let { countdown } = this.state; + + countdown--; + this.setState({ countdown }); + + if (!countdown) { + S.Common.emailConfirmationTimeSet(0); + window.clearInterval(this.interval); + this.interval = null; + }; + }, 1000); + }; + +}; + +export default EmailCollectionForm; diff --git a/src/ts/docs/help/onboarding.ts b/src/ts/docs/help/onboarding.ts index 18dfe8ef0d..4cce54a52b 100644 --- a/src/ts/docs/help/onboarding.ts +++ b/src/ts/docs/help/onboarding.ts @@ -24,6 +24,25 @@ export default { }, }), + emailCollection: () => ({ + items: [ + { + noButton: true + }, + ], + param: { + element: '#page.isFull #footer #button-help', + classNameWrap: 'fixed', + className: 'invertedColor', + vertical: I.MenuDirection.Top, + horizontal: I.MenuDirection.Right, + noArrow: true, + noClose: true, + passThrough: true, + offsetY: -4, + }, + }), + objectCreationStart: () => ({ category: translate('onboardingObjectCreationStart'), items: [ diff --git a/src/ts/lib/api/command.ts b/src/ts/lib/api/command.ts index d67b73e220..5bafdb5c9d 100644 --- a/src/ts/lib/api/command.ts +++ b/src/ts/lib/api/command.ts @@ -1998,11 +1998,13 @@ export const MembershipGetPortalLinkUrl = (callBack?: (message: any) => void) => dispatcher.request(MembershipGetPortalLinkUrl.name, request, callBack); }; -export const MembershipGetVerificationEmail = (email: string, isSubscribed: boolean, callBack?: (message: any) => void) => { +export const MembershipGetVerificationEmail = (email: string, subscribeNews: boolean, subscribeTips: boolean, isOnboardingList: boolean, callBack?: (message: any) => void) => { const request = new Rpc.Membership.GetVerificationEmail.Request(); request.setEmail(email); - request.setSubscribetonewsletter(isSubscribed); + request.setSubscribetonewsletter(subscribeNews); + request.setInsidertipsandtutorials(subscribeTips); + request.setIsonboardinglist(isOnboardingList); dispatcher.request(MembershipGetVerificationEmail.name, request, callBack); };