зеркало из https://github.com/mozilla/fxa.git
feat(settings): display alert messages from login in new settings
Some actions in fxa-content-server end up displaying an alert message on the settings page. We want to be able to display those messages across the "app" boundary between the content-server front-end and new settings. To do this we store the message in localStorage and whichever app gets to it first will display it and remove it from storage.
This commit is contained in:
Родитель
226c677577
Коммит
4de585d4d4
|
@ -55,6 +55,7 @@ const PERSISTENT = {
|
|||
sessionTokenContext: undefined,
|
||||
uid: undefined,
|
||||
verified: undefined,
|
||||
alertText: undefined,
|
||||
};
|
||||
|
||||
const DEFAULTS = _.extend(
|
||||
|
|
|
@ -435,6 +435,10 @@ var BaseView = Backbone.View.extend({
|
|||
if (success) {
|
||||
this.displaySuccess(success);
|
||||
this.model.unset('success');
|
||||
const account = this.model.get('account');
|
||||
if (account) {
|
||||
account.unset('alertText');
|
||||
}
|
||||
}
|
||||
|
||||
var unsafeSuccess = this.model.get('unsafeSuccess');
|
||||
|
|
|
@ -10,6 +10,9 @@ import _ from 'underscore';
|
|||
|
||||
const NavigationBehavior = function (endpoint, options = {}) {
|
||||
const behavior = function (view, account) {
|
||||
if (account && options.success) {
|
||||
account.set('alertText', options.success);
|
||||
}
|
||||
const navigateOptions = _.assign({}, options, { account });
|
||||
view.navigate(endpoint, navigateOptions);
|
||||
|
||||
|
|
|
@ -1243,4 +1243,14 @@ describe('views/base', function () {
|
|||
assert.isTrue(view._onRequiredClick.calledOnce);
|
||||
});
|
||||
});
|
||||
|
||||
describe('displayStatusMessages', () => {
|
||||
it('clears the alertText after displaying success', () => {
|
||||
const account = { unset: sinon.spy() };
|
||||
model.set('account', account);
|
||||
model.set('success', 'ok');
|
||||
view.displayStatusMessages();
|
||||
assert.isTrue(account.unset.calledOnceWithExactly('alertText'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,7 +18,7 @@ describe('views/behaviors/navigate', function () {
|
|||
navigate: sinon.spy(),
|
||||
};
|
||||
|
||||
const accountMock = {};
|
||||
const accountMock = { set: sinon.spy() };
|
||||
|
||||
const promise = navigateBehavior(viewMock, accountMock);
|
||||
// navigateBehavior returns a promise that never resolves,
|
||||
|
@ -28,6 +28,9 @@ describe('views/behaviors/navigate', function () {
|
|||
const endpoint = viewMock.navigate.args[0][0];
|
||||
const navigateOptions = viewMock.navigate.args[0][1];
|
||||
|
||||
assert.isTrue(
|
||||
accountMock.set.calledOnceWithExactly('alertText', options.success)
|
||||
);
|
||||
assert.equal(endpoint, 'settings');
|
||||
assert.equal(navigateOptions.success, 'success');
|
||||
assert.equal(navigateOptions.error, 'error');
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import React, { useRef, ReactNode } from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import AlertExternal from './index';
|
||||
import { AlertBarRootAndContextProvider } from '../../lib/AlertBarContext';
|
||||
import { MockedCache } from '../../models/_mocks';
|
||||
|
||||
describe('AlertExternal', () => {
|
||||
it('renders as expected', () => {
|
||||
const { rerender } = render(
|
||||
<MockedCache account={{ alertTextExternal: 'ok' }}>
|
||||
<AlertBarRootAndContextProvider />
|
||||
</MockedCache>
|
||||
);
|
||||
rerender(
|
||||
<MockedCache account={{ alertTextExternal: 'ok' }}>
|
||||
<AlertBarRootAndContextProvider>
|
||||
<AlertExternal />
|
||||
</AlertBarRootAndContextProvider>
|
||||
</MockedCache>
|
||||
);
|
||||
expect(screen.getByTestId('alert-bar-root')).toContainElement(
|
||||
screen.getByTestId('alert-bar')
|
||||
);
|
||||
expect(screen.queryByTestId('alert-external-text')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render with no alertTextExternal text', () => {
|
||||
const { rerender } = render(
|
||||
<MockedCache>
|
||||
<AlertBarRootAndContextProvider />
|
||||
</MockedCache>
|
||||
);
|
||||
rerender(
|
||||
<MockedCache>
|
||||
<AlertBarRootAndContextProvider>
|
||||
<AlertExternal />
|
||||
</AlertBarRootAndContextProvider>
|
||||
</MockedCache>
|
||||
);
|
||||
expect(screen.queryByTestId('alert-external-text')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
import { AlertBar } from '../AlertBar';
|
||||
import { useAccount } from '../../models';
|
||||
import { alertTextExternal } from '../../lib/cache';
|
||||
|
||||
export const AlertExternal = () => {
|
||||
const account = useAccount();
|
||||
|
||||
return account.alertTextExternal ? (
|
||||
<AlertBar
|
||||
onDismiss={() => {
|
||||
alertTextExternal(null);
|
||||
}}
|
||||
>
|
||||
<p data-testid="alert-external-text">{account.alertTextExternal}</p>
|
||||
</AlertBar>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default AlertExternal;
|
|
@ -43,6 +43,7 @@ export const GET_INITIAL_STATE = gql`
|
|||
created
|
||||
productName
|
||||
}
|
||||
alertTextExternal @client
|
||||
}
|
||||
session {
|
||||
verified
|
||||
|
@ -72,12 +73,9 @@ export const App = ({ queryParams }: AppProps) => {
|
|||
<AppLayout>
|
||||
<Router basepath="/beta/settings">
|
||||
<Settings path="/" />
|
||||
<FlowContainer path="/avatar/change" title="Profile picture"/>
|
||||
<FlowContainer path="/display_name" title="Display name"/>
|
||||
<FlowContainer
|
||||
path="/change_password"
|
||||
title="Change password"
|
||||
/>
|
||||
<FlowContainer path="/avatar/change" title="Profile picture" />
|
||||
<FlowContainer path="/display_name" title="Display name" />
|
||||
<FlowContainer path="/change_password" title="Change password" />
|
||||
</Router>
|
||||
</AppLayout>
|
||||
);
|
||||
|
|
|
@ -7,8 +7,8 @@ import UnitRow from '../UnitRow';
|
|||
import UnitRowWithAvatar from '../UnitRowWithAvatar';
|
||||
import Security from '../Security';
|
||||
import UnitRowSecondaryEmail from '../UnitRowSecondaryEmail';
|
||||
import { useLocation, RouteComponentProps } from '@reach/router';
|
||||
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
import AlertExternal from '../AlertExternal';
|
||||
import { useAccount } from '../../models';
|
||||
|
||||
export const Settings = (_: RouteComponentProps) => {
|
||||
|
@ -30,6 +30,7 @@ export const Settings = (_: RouteComponentProps) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<AlertExternal />
|
||||
<section className="mt-11" id="profile" data-testid="settings-profile">
|
||||
<h2 className="font-header font-bold ml-4 mb-4">Profile</h2>
|
||||
|
||||
|
@ -38,7 +39,11 @@ export const Settings = (_: RouteComponentProps) => {
|
|||
|
||||
<hr className="unit-row-hr" />
|
||||
|
||||
<UnitRow header="Display name" headerValue={displayName} route="/beta/settings/display_name" />
|
||||
<UnitRow
|
||||
header="Display name"
|
||||
headerValue={displayName}
|
||||
route="/beta/settings/display_name"
|
||||
/>
|
||||
|
||||
<hr className="unit-row-hr" />
|
||||
|
||||
|
@ -46,7 +51,7 @@ export const Settings = (_: RouteComponentProps) => {
|
|||
header="Password"
|
||||
headerValueClassName="tracking-wider"
|
||||
headerValue="••••••••••••••••••"
|
||||
route="/beta/settings/change_password"
|
||||
route="/beta/settings/change_password"
|
||||
>
|
||||
<p className="text-grey-400 text-xs mobileLandscape:mt-3">
|
||||
Created {pwdDateText}
|
||||
|
|
|
@ -1,27 +1,67 @@
|
|||
import { InMemoryCache, gql } from '@apollo/client';
|
||||
import { InMemoryCache, gql, makeVar } from '@apollo/client';
|
||||
import Storage from './storage';
|
||||
import { Email } from '../models';
|
||||
|
||||
const storage = Storage.factory('localStorage');
|
||||
|
||||
export function sessionToken(newToken?: string) {
|
||||
export interface OldSettingsData {
|
||||
uid: hexstring;
|
||||
sessionToken: hexstring;
|
||||
alertText?: string;
|
||||
}
|
||||
|
||||
type LocalAccount = OldSettingsData | undefined;
|
||||
type LocalAccounts = Record<hexstring, LocalAccount> | undefined;
|
||||
|
||||
function accounts(accounts?: LocalAccounts) {
|
||||
if (accounts) {
|
||||
storage.set('accounts', accounts);
|
||||
return accounts;
|
||||
}
|
||||
return storage.get('accounts') as LocalAccounts;
|
||||
}
|
||||
|
||||
function currentAccount(account?: OldSettingsData) {
|
||||
const all = accounts() || {};
|
||||
const uid = storage.get('currentAccountUid') as hexstring;
|
||||
if (account) {
|
||||
all[account.uid] = account;
|
||||
accounts(all);
|
||||
return account;
|
||||
}
|
||||
return all[uid];
|
||||
}
|
||||
|
||||
export function sessionToken(newToken?: hexstring) {
|
||||
try {
|
||||
const storedAccounts = storage.get('accounts');
|
||||
const currentAccountUid = storage.get('currentAccountUid');
|
||||
const account = currentAccount();
|
||||
if (newToken) {
|
||||
storedAccounts[currentAccountUid].sessionToken = newToken;
|
||||
storage.set('accounts', storedAccounts);
|
||||
account!.sessionToken = newToken;
|
||||
currentAccount(account);
|
||||
}
|
||||
return storedAccounts[currentAccountUid].sessionToken as string;
|
||||
return account!.sessionToken;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function consumeAlertTextExternal() {
|
||||
const account = currentAccount();
|
||||
const text = account?.alertText || null;
|
||||
if (text) {
|
||||
account!.alertText = undefined;
|
||||
currentAccount(account);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
export const alertTextExternal = makeVar(consumeAlertTextExternal());
|
||||
|
||||
// sessionToken is added as a local field as an example.
|
||||
export const typeDefs = gql`
|
||||
extend type Account {
|
||||
primaryEmail: Email!
|
||||
alertTextExternal: String
|
||||
}
|
||||
extend type Session {
|
||||
token: String!
|
||||
|
@ -38,6 +78,11 @@ export const cache = new InMemoryCache({
|
|||
return emails?.find((email) => email.isPrimary);
|
||||
},
|
||||
},
|
||||
alertTextExternal: {
|
||||
read() {
|
||||
return alertTextExternal();
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Session: {
|
||||
|
|
|
@ -7,7 +7,7 @@ export interface Email {
|
|||
}
|
||||
|
||||
export interface Account {
|
||||
uid: string;
|
||||
uid: hexstring;
|
||||
displayName: string | null;
|
||||
avatarUrl: string | null;
|
||||
accountCreated: number;
|
||||
|
@ -30,6 +30,7 @@ export interface Account {
|
|||
created: number;
|
||||
productName: string;
|
||||
}[];
|
||||
alertTextExternal: string | null;
|
||||
}
|
||||
|
||||
export const GET_ACCOUNT = gql`
|
||||
|
@ -62,6 +63,7 @@ export const GET_ACCOUNT = gql`
|
|||
created
|
||||
productName
|
||||
}
|
||||
alertTextExternal @client
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -39,6 +39,7 @@ const MOCK_ACCOUNT: Account = {
|
|||
exists: true,
|
||||
verified: true,
|
||||
},
|
||||
alertTextExternal: null,
|
||||
};
|
||||
|
||||
export interface MockedProps {
|
||||
|
|
Загрузка…
Ссылка в новой задаче