Skip to content

Commit 53fd518

Browse files
committed
UI for proposed account migration
If an attempt to link a GitHub identity requires a merge, display a modal explaining the situation and asking the user whether they wish to proceed. A couple of interesting pieces here: * We now make a request directly to the GitHub API to retrieve the profile of the GitHub account in question, so that we can display its avatar and username in the modal. * As a corollary, the data model for a migration now contains an entire (synthetic) `UserAccount` record, including the credential in question, rather than just the credential itself. * There is now a general-purpose `<Modal>` component that displays its contents in a modal dialog, which is rendered using a portal. This cargo-cults styles from a couple of unmerged PRs, but the implementation is new here.
1 parent f2c274b commit 53fd518

16 files changed

Lines changed: 319 additions & 22 deletions

File tree

locales/en/translation.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,19 @@
7575
}
7676
}
7777
},
78+
"account-migration": {
79+
"header": "Combine these accounts?",
80+
"message": [
81+
"Your GitHub login is linked to a different Popcode account. Do you want to combine the account you’re using now with that other account?",
82+
"All of the saved projects from the GitHub-linked account will be transferred into the account you’re using now."
83+
],
84+
"your-account": "Your account",
85+
"account-to-merge": "Account to merge",
86+
"buttons": {
87+
"migrate": "Yes, combine these accounts",
88+
"cancel": "No, keep them separate"
89+
}
90+
},
7891
"utility": {
7992
"or": " or "
8093
},

src/actions/user.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const linkIdentityFailed = createAction(
1919

2020
export const accountMigrationNeeded = createAction(
2121
'ACCOUNT_MIGRATION_NEEDED',
22-
credentialToMerge => ({credentialToMerge}),
22+
(profile, credential) => ({profile, credential}),
2323
);
2424

2525
export const logOut = createAction('LOG_OUT');

src/clients/github.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ function normalizeTitle(title) {
2626
return titleWithoutPunctuationAndWhitespace;
2727
}
2828

29+
export async function getProfileForAuthenticatedUser(accessToken) {
30+
const github = await createClient(accessToken);
31+
return github.getUser().getProfile();
32+
}
33+
2934
export async function createOrUpdateRepoFromProject(project, accessToken) {
3035
const repoAlreadyExists = Boolean(project.externalLocations.githubRepoName);
3136
if (repoAlreadyExists) {
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import classnames from 'classnames';
2+
import isNull from 'lodash-es/isNull';
3+
import map from 'lodash-es/map';
4+
import PropTypes from 'prop-types';
5+
import React from 'react';
6+
import {t} from 'i18next';
7+
8+
import {
9+
AccountMigration as AccountMigrationRecord,
10+
UserAccount as UserAccountRecord,
11+
} from '../records';
12+
13+
import Modal from './Modal';
14+
15+
export default function AccountMigration({currentUserAccount, migration}) {
16+
if (isNull(currentUserAccount) || isNull(migration)) {
17+
return null;
18+
}
19+
20+
return (
21+
<Modal>
22+
<div className="account-migration">
23+
<h1 className="account-migration__header">
24+
{t('account-migration.header')}
25+
</h1>
26+
<div className="account-migration__accounts">
27+
<div className="account-migration__account">
28+
<p className="account-migration__account-label">
29+
{t('account-migration.your-account')}
30+
</p>
31+
<img
32+
className="account-migration__avatar"
33+
src={currentUserAccount.avatarUrl}
34+
/>
35+
<div className="account-migration__user-name">
36+
{currentUserAccount.displayName}
37+
</div>
38+
</div>
39+
<div
40+
className="account-migration__merge-icon u__icon u__icon_disabled"
41+
>
42+
&#xf0ec;
43+
</div>
44+
<div className="account-migration__account">
45+
<p className="account-migration__account-label">
46+
{t('account-migration.account-to-merge')}
47+
</p>
48+
<img
49+
className="account-migration__avatar"
50+
src={migration.userAccountToMerge.avatarUrl}
51+
/>
52+
<div className="account-migration__user-name">
53+
{migration.userAccountToMerge.displayName}
54+
</div>
55+
</div>
56+
</div>
57+
{
58+
map(
59+
t('account-migration.message', {returnObjects: true}),
60+
paragraph => (
61+
<p key={paragraph}>{paragraph}</p>
62+
),
63+
)
64+
}
65+
<div className="account-migration__buttons">
66+
<button
67+
className={classnames(
68+
'account-migration__button',
69+
'account-migration__button_confirm',
70+
)}
71+
>
72+
{t('account-migration.buttons.migrate')}
73+
</button>
74+
<button
75+
className={classnames(
76+
'account-migration__button',
77+
'account-migration__button_cancel',
78+
)}
79+
>
80+
{t('account-migration.buttons.cancel')}
81+
</button>
82+
</div>
83+
</div>
84+
</Modal>
85+
);
86+
}
87+
88+
AccountMigration.propTypes = {
89+
currentUserAccount: PropTypes.instanceOf(UserAccountRecord),
90+
migration: PropTypes.instanceOf(AccountMigrationRecord),
91+
};
92+
93+
AccountMigration.defaultProps = {
94+
currentUserAccount: null,
95+
migration: null,
96+
};

src/components/Modal.jsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from 'react';
2+
import {createPortal} from 'react-dom';
3+
import PropTypes from 'prop-types';
4+
5+
export default function Modal({children, isOpen}) {
6+
if (!isOpen) {
7+
return null;
8+
}
9+
10+
return createPortal(
11+
(
12+
<div className="modal">
13+
<div className="modal__contents">
14+
{children}
15+
</div>
16+
</div>
17+
),
18+
document.getElementById('modals'),
19+
);
20+
}
21+
22+
Modal.propTypes = {
23+
children: PropTypes.node.isRequired,
24+
isOpen: PropTypes.bool,
25+
};
26+
27+
Modal.defaultProps = {
28+
isOpen: true,
29+
};

src/components/Workspace.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {dehydrateProject, rehydrateProject} from '../clients/localStorage';
1414

1515
import {isPristineProject} from '../util/projectUtils';
1616

17+
import AccountMigration from '../containers/AccountMigration';
1718
import TopBar from '../containers/TopBar';
1819
import Instructions from '../containers/Instructions';
1920
import NotificationList from '../containers/NotificationList';
@@ -151,6 +152,7 @@ export default class Workspace extends React.Component {
151152
{this._renderEnvironment()}
152153
</div>
153154
</main>
155+
<AccountMigration />
154156
</div>
155157
);
156158
}

src/containers/AccountMigration.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {connect} from 'react-redux';
2+
3+
import AccountMigration from '../components/AccountMigration';
4+
import {getCurrentAccountMigration, getCurrentUser} from '../selectors';
5+
6+
function mapStateToProps(state) {
7+
return {
8+
currentUserAccount: getCurrentUser(state),
9+
migration: getCurrentAccountMigration(state),
10+
};
11+
}
12+
13+
export default connect(
14+
mapStateToProps,
15+
)(AccountMigration);

src/css/application.css

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,10 @@
7575
--color-gray: #666;
7676
--color-dark-gray: #444;
7777
--color-green: #00ff85;
78+
--color-low-contrast-green: #a2ffd2;
7879
--color-yellow: #ffd100;
7980
--color-red: #ff5e4f;
81+
--color-low-contrast-red: #ffc4bf;
8082
--color-purple: #f0f;
8183
--color-low-contrast-blue: #baecff;
8284
--color-blue: #00b8ff;
@@ -103,6 +105,7 @@ body {
103105
position: relative;
104106
display: flex;
105107
flex-direction: column;
108+
z-index: 0;
106109
}
107110

108111
.layout__columns {
@@ -778,6 +781,71 @@ body {
778781
margin: 0;
779782
}
780783

784+
/** @define account-migration */
785+
786+
.account-migration__header {
787+
text-align: center;
788+
}
789+
790+
.account-migration__accounts {
791+
align-items: center;
792+
display: flex;
793+
justify-content: space-around;
794+
margin: 1em 0;
795+
}
796+
797+
.account-migration__merge-icon {
798+
font-size: 2em;
799+
color: var(--color-gray);
800+
}
801+
802+
.account-migration__account {
803+
border: 1px solid var(--color-light-gray);
804+
margin: 0 1em;
805+
padding: 0.5em 1em;
806+
}
807+
808+
.account-migration__account-label {
809+
color: var(--color-gray);
810+
font-size: 0.8em;
811+
margin: 0 0 0.5em;
812+
text-align: center;
813+
}
814+
815+
.account-migration__avatar {
816+
display: block;
817+
height: 128px;
818+
}
819+
820+
.account-migration__user-name {
821+
font-weight: bold;
822+
margin: 0.5em 0 0;
823+
text-align: center;
824+
}
825+
826+
.account-migration__buttons {
827+
display: flex;
828+
justify-content: space-around;
829+
}
830+
831+
.account-migration__button {
832+
border: none;
833+
border-radius: 5px;
834+
cursor: pointer;
835+
margin: 0.5em;
836+
padding: 1em;
837+
width: 20em;
838+
}
839+
840+
.account-migration__button_confirm {
841+
background-color: var(--color-low-contrast-green);
842+
font-weight: bold;
843+
}
844+
845+
.account-migration__button_cancel {
846+
background-color: var(--color-low-contrast-red);
847+
}
848+
781849
/** @define notification-list */
782850

783851
.notification-list__notification {
@@ -855,6 +923,26 @@ body {
855923
fill: var(--color-low-contrast-gray) !important;
856924
}
857925

926+
/** @define modal */
927+
928+
.modal {
929+
background: rgba(0, 0, 0, 0.48);
930+
height: 100vh;
931+
left: 0;
932+
position: fixed;
933+
top: 0;
934+
width: 100vw;
935+
z-index: 1;
936+
}
937+
938+
.modal__contents {
939+
background: #fafafa;
940+
box-shadow: 0 2px 8px #888;
941+
margin: 10% auto 0;
942+
padding: 1em 2em 2em;
943+
width: 40vw;
944+
}
945+
858946
/** @define label */
859947

860948
.label {

src/html/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@
1818
<div class="pop-throbber__image"></div>
1919
</div>
2020
</div>
21+
<div id="modals"></div>
2122
</body>
2223
</html>

src/records/AccountMigration.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ import {AccountMigrationState} from '../enums';
44

55
export default Record({
66
state: AccountMigrationState.PROPOSED,
7-
credentialToMerge: null,
7+
userAccountToMerge: null,
88
}, 'AccountMigration');

0 commit comments

Comments
 (0)