(core) Add Support Grist page and nudge

Summary:
Adds a new Support Grist page (accessible only in grist-core), containing
options to opt in to telemetry and sponsor Grist Labs on GitHub.

A nudge is also shown in the doc menu, which can be collapsed or permanently
dismissed.

Test Plan: Browser and server tests.

Reviewers: paulfitz, dsagal

Reviewed By: paulfitz

Subscribers: jarek, dsagal

Differential Revision: https://phab.getgrist.com/D3926
This commit is contained in:
George Gevoian
2023-07-04 17:21:34 -04:00
parent 051c6d52fe
commit 35237a5835
47 changed files with 1743 additions and 365 deletions

View File

@@ -9,6 +9,7 @@ import {urlState} from 'app/client/models/gristUrlState';
import {Notifier} from 'app/client/models/NotifyModel';
import {getFlavor, ProductFlavor} from 'app/client/ui/CustomThemes';
import {buildNewSiteModal, buildUpgradeModal} from 'app/client/ui/ProductUpgrades';
import {SupportGristNudge} from 'app/client/ui/SupportGristNudge';
import {attachCssThemeVars, prefersDarkModeObs} from 'app/client/ui2018/cssVars';
import {OrgUsageSummary} from 'app/common/DocUsage';
import {Features, isLegacyPlan, Product} from 'app/common/Features';
@@ -31,7 +32,15 @@ const t = makeT('AppModel');
// Reexported for convenience.
export {reportError} from 'app/client/models/errors';
export type PageType = "doc" | "home" | "billing" | "welcome";
export type PageType =
| "doc"
| "home"
| "billing"
| "welcome"
| "account"
| "support-grist"
| "activation";
const G = getBrowserGlobals('document', 'window');
// TopAppModel is the part of the app model that persists across org and user switches.
@@ -107,6 +116,8 @@ export interface AppModel {
behavioralPromptsManager: BehavioralPromptsManager;
supportGristNudge: SupportGristNudge;
refreshOrgUsage(): Promise<void>;
showUpgradeModal(): void;
showNewSiteModal(): void;
@@ -253,7 +264,23 @@ export class AppModelImpl extends Disposable implements AppModel {
// Get the current PageType from the URL.
public readonly pageType: Observable<PageType> = Computed.create(this, urlState().state,
(use, state) => (state.doc ? "doc" : (state.billing ? "billing" : (state.welcome ? "welcome" : "home"))));
(_use, state) => {
if (state.doc) {
return 'doc';
} else if (state.billing) {
return 'billing';
} else if (state.welcome) {
return 'welcome';
} else if (state.account) {
return 'account';
} else if (state.supportGrist) {
return 'support-grist';
} else if (state.activation) {
return 'activation';
} else {
return 'home';
}
});
public readonly needsOrg: Observable<boolean> = Computed.create(
this, urlState().state, (use, state) => {
@@ -265,6 +292,8 @@ export class AppModelImpl extends Disposable implements AppModel {
public readonly behavioralPromptsManager: BehavioralPromptsManager =
BehavioralPromptsManager.create(this, this);
public readonly supportGristNudge: SupportGristNudge = SupportGristNudge.create(this, this);
constructor(
public readonly topAppModel: TopAppModel,
public readonly currentUser: FullUser|null,

View File

@@ -0,0 +1,32 @@
import {AppModel, getHomeUrl} from 'app/client/models/AppModel';
import {TelemetryPrefs} from 'app/common/Install';
import {InstallAPI, InstallAPIImpl, TelemetryPrefsWithSources} from 'app/common/InstallAPI';
import {bundleChanges, Disposable, Observable} from 'grainjs';
export interface TelemetryModel {
/** Telemetry preferences (e.g. the current telemetry level). */
readonly prefs: Observable<TelemetryPrefsWithSources | null>;
fetchTelemetryPrefs(): Promise<void>;
updateTelemetryPrefs(prefs: Partial<TelemetryPrefs>): Promise<void>;
}
export class TelemetryModelImpl extends Disposable implements TelemetryModel {
public readonly prefs: Observable<TelemetryPrefsWithSources | null> = Observable.create(this, null);
private readonly _installAPI: InstallAPI = new InstallAPIImpl(getHomeUrl());
constructor(_appModel: AppModel) {
super();
}
public async fetchTelemetryPrefs(): Promise<void> {
const prefs = await this._installAPI.getInstallPrefs();
bundleChanges(() => {
this.prefs.set(prefs.telemetry);
});
}
public async updateTelemetryPrefs(prefs: Partial<TelemetryPrefs>): Promise<void> {
await this._installAPI.updateInstallPrefs({telemetry: prefs});
await this.fetchTelemetryPrefs();
}
}

View File

@@ -156,7 +156,8 @@ export class UrlStateImpl {
*/
public updateState(prevState: IGristUrlState, newState: IGristUrlState): IGristUrlState {
const keepState = (newState.org || newState.ws || newState.homePage || newState.doc || isEmpty(newState) ||
newState.account || newState.billing || newState.activation || newState.welcome) ?
newState.account || newState.billing || newState.activation || newState.welcome ||
newState.supportGrist) ?
(prevState.org ? {org: prevState.org} : {}) :
prevState;
return {...keepState, ...newState};
@@ -186,8 +187,11 @@ export class UrlStateImpl {
// Reload when moving to/from the Grist sign-up page.
const signupReload = [prevState.login, newState.login].includes('signup')
&& prevState.login !== newState.login;
return Boolean(orgReload || accountReload || billingReload || activationReload
|| gristConfig.errPage || docReload || welcomeReload || linkKeysReload || signupReload);
// Reload when moving to/from the support Grist page.
const supportGristReload = Boolean(prevState.supportGrist) !== Boolean(newState.supportGrist);
return Boolean(orgReload || accountReload || billingReload || activationReload ||
gristConfig.errPage || docReload || welcomeReload || linkKeysReload || signupReload ||
supportGristReload);
}
/**