From 012a7f8d89227015c9b3be97510924adca628dc8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 9 Feb 2022 19:47:13 -0600 Subject: [PATCH 1/4] Mitra: support Ethereum login --- app/soapbox/actions/auth.js | 28 +++++++++++++++ .../auth_login/components/login_form.js | 11 ++---- .../auth_login/components/login_page.js | 35 +++++++++++++++---- app/soapbox/utils/features.js | 2 ++ 4 files changed, 61 insertions(+), 15 deletions(-) diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 5274a98cf..dde684f77 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -213,6 +213,34 @@ export function logIn(intl, username, password) { }; } +export function ethereumLogin() { + return (dispatch, getState) => { + const { ethereum } = window; + const loginMessage = getState().getIn(['instance', 'login_message']); + + return ethereum.request({ method: 'eth_requestAccounts' }).then(walletAddresses => { + const [walletAddress] = walletAddresses; + + return ethereum.request({ method: 'personal_sign', params: [loginMessage, walletAddress] }).then(signature => { + const params = { + grant_type: 'ethereum', + wallet_address: walletAddress.toLowerCase(), + password: signature, + redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', + scope: getScopes(getState()), + }; + + // Note: skips app creation + // TODO: add to quirks.js for Mitra + return dispatch(obtainOAuthToken(params)).then(token => { + dispatch(authLoggedIn(token)); + return dispatch(verifyCredentials(token.access_token)); + }); + }); + }); + }; +} + export function logOut(intl) { return (dispatch, getState) => { const state = getState(); diff --git a/app/soapbox/features/auth_login/components/login_form.js b/app/soapbox/features/auth_login/components/login_form.js index 62b80f0af..549ebb338 100644 --- a/app/soapbox/features/auth_login/components/login_form.js +++ b/app/soapbox/features/auth_login/components/login_form.js @@ -15,11 +15,10 @@ const messages = defineMessages({ const mapStateToProps = state => { const instance = state.get('instance'); - const features = getFeatures(instance); return { baseURL: getBaseURL(state), - hasResetPasswordAPI: features.resetPasswordAPI, + features: getFeatures(instance), }; }; @@ -28,7 +27,7 @@ export default @connect(mapStateToProps) class LoginForm extends ImmutablePureComponent { render() { - const { intl, isLoading, handleSubmit, baseURL, hasResetPasswordAPI } = this.props; + const { intl, isLoading, handleSubmit, baseURL, features } = this.props; return (
@@ -57,12 +56,8 @@ class LoginForm extends ImmutablePureComponent { autoCapitalize='off' required /> - {/*
- -
*/}

- {hasResetPasswordAPI ? ( + {features.resetPasswordAPI ? ( diff --git a/app/soapbox/features/auth_login/components/login_page.js b/app/soapbox/features/auth_login/components/login_page.js index ca16c2a81..d6de2ca3d 100644 --- a/app/soapbox/features/auth_login/components/login_page.js +++ b/app/soapbox/features/auth_login/components/login_page.js @@ -4,18 +4,25 @@ import { injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { Redirect } from 'react-router-dom'; +import { ethereumLogin } from 'soapbox/actions/auth'; import { logIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth'; import { fetchInstance } from 'soapbox/actions/instance'; +import { getFeatures } from 'soapbox/utils/features'; import { isStandalone } from 'soapbox/utils/state'; import LoginForm from './login_form'; import OtpAuthForm from './otp_auth_form'; -const mapStateToProps = state => ({ - me: state.get('me'), - isLoading: false, - standalone: isStandalone(state), -}); +const mapStateToProps = state => { + const instance = state.get('instance'); + + return { + me: state.get('me'), + isLoading: false, + standalone: isStandalone(state), + features: getFeatures(instance), + }; +}; export default @connect(mapStateToProps) @injectIntl @@ -62,8 +69,18 @@ class LoginPage extends ImmutablePureComponent { event.preventDefault(); } + handleEthereumLogin = e => { + const { dispatch } = this.props; + + dispatch(ethereumLogin()) + .then(() => this.setState({ shouldRedirect: true })) + .catch(console.error); + + e.preventDefault(); + }; + render() { - const { standalone } = this.props; + const { standalone, features } = this.props; const { isLoading, mfa_auth_needed, mfa_token, shouldRedirect } = this.state; if (standalone) return ; @@ -72,7 +89,11 @@ class LoginPage extends ImmutablePureComponent { if (mfa_auth_needed) return ; - return ; + if (features.ethereumLogin) { + return ; + } else { + return ; + } } } diff --git a/app/soapbox/utils/features.js b/app/soapbox/utils/features.js index e55ca455c..2df8e1c7a 100644 --- a/app/soapbox/utils/features.js +++ b/app/soapbox/utils/features.js @@ -9,6 +9,7 @@ const any = arr => arr.some(Boolean); // For uglification export const MASTODON = 'Mastodon'; export const PLEROMA = 'Pleroma'; +export const MITRA = 'Mitra'; export const getFeatures = createSelector([ instance => parseVersion(instance.get('version')), @@ -83,6 +84,7 @@ export const getFeatures = createSelector([ accountEndorsements: v.software === PLEROMA && gte(v.version, '2.4.50'), quotePosts: v.software === PLEROMA && gte(v.version, '2.4.50'), birthdays: v.software === PLEROMA && gte(v.version, '2.4.50'), + ethereumLogin: v.software === MITRA, }; }); From f051b70b102cf8d141c7ea3e9dbd056c17a4f073 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 10 Feb 2022 16:33:28 -0600 Subject: [PATCH 2/4] Support Ethereum external login --- app/soapbox/actions/auth.js | 21 +++-- app/soapbox/actions/external_auth.js | 82 +++++++++++++------ .../auth_login/components/login_page.js | 35 ++------ .../components/external_login_form.js | 4 +- app/soapbox/utils/quirks.js | 15 ++-- 5 files changed, 89 insertions(+), 68 deletions(-) diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index dde684f77..65c1dc0f5 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -18,6 +18,7 @@ import KVStore from 'soapbox/storage/kv_store'; import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth'; import sourceCode from 'soapbox/utils/code'; import { getFeatures } from 'soapbox/utils/features'; +import { getQuirks } from 'soapbox/utils/quirks'; import { isStandalone } from 'soapbox/utils/state'; import api, { baseClient } from '../api'; @@ -62,6 +63,10 @@ function createAppAndToken() { function createAuthApp() { return (dispatch, getState) => { + // Mitra: skip creating the app + const quirks = getQuirks(getState().get('instance')); + if (quirks.skipsAppCreation) return dispatch(noOp()); + const params = { client_name: sourceCode.displayName, redirect_uris: 'urn:ietf:wg:oauth:2.0:oob', @@ -213,10 +218,13 @@ export function logIn(intl, username, password) { }; } -export function ethereumLogin() { +export function ethereumLogin(instance, baseURL) { return (dispatch, getState) => { + instance = (instance || getState().get('instance')); + const { ethereum } = window; - const loginMessage = getState().getIn(['instance', 'login_message']); + const { scopes } = getFeatures(instance); + const loginMessage = instance.get('login_message'); return ethereum.request({ method: 'eth_requestAccounts' }).then(walletAddresses => { const [walletAddress] = walletAddresses; @@ -227,15 +235,14 @@ export function ethereumLogin() { wallet_address: walletAddress.toLowerCase(), password: signature, redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', - scope: getScopes(getState()), + scope: scopes, }; // Note: skips app creation // TODO: add to quirks.js for Mitra - return dispatch(obtainOAuthToken(params)).then(token => { - dispatch(authLoggedIn(token)); - return dispatch(verifyCredentials(token.access_token)); - }); + return dispatch(obtainOAuthToken(params, baseURL)) + .then(token => dispatch(authLoggedIn(token))) + .then(({ access_token }) => dispatch(verifyCredentials(access_token, baseURL))); }); }); }; diff --git a/app/soapbox/actions/external_auth.js b/app/soapbox/actions/external_auth.js index b3258ab70..35af6699e 100644 --- a/app/soapbox/actions/external_auth.js +++ b/app/soapbox/actions/external_auth.js @@ -9,11 +9,12 @@ import { Map as ImmutableMap, fromJS } from 'immutable'; import { createApp } from 'soapbox/actions/apps'; -import { authLoggedIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth'; +import { authLoggedIn, verifyCredentials, switchAccount, ethereumLogin } from 'soapbox/actions/auth'; import { obtainOAuthToken } from 'soapbox/actions/oauth'; import { parseBaseURL } from 'soapbox/utils/auth'; import sourceCode from 'soapbox/utils/code'; import { getFeatures } from 'soapbox/utils/features'; +import { getQuirks } from 'soapbox/utils/quirks'; import { baseClient } from '../api'; @@ -32,36 +33,65 @@ const fetchExternalInstance = baseURL => { }); }; -export function createAppAndRedirect(host) { +function createExternalApp(instance, baseURL) { + return (dispatch, getState) => { + const { scopes } = getFeatures(instance); + + const params = { + client_name: sourceCode.displayName, + redirect_uris: `${window.location.origin}/auth/external`, + website: sourceCode.homepage, + scopes, + }; + + return dispatch(createApp(params, baseURL)); + }; +} + +function externalAuthorize(instance, baseURL) { + return (dispatch, getState) => { + const { scopes } = getFeatures(instance); + + return dispatch(createExternalApp(instance, baseURL)).then(app => { + const { client_id, redirect_uri } = app; + + const query = new URLSearchParams({ + client_id, + redirect_uri, + response_type: 'code', + scope: scopes, + }); + + localStorage.setItem('soapbox:external:app', JSON.stringify(app)); + localStorage.setItem('soapbox:external:baseurl', baseURL); + localStorage.setItem('soapbox:external:scopes', scopes); + + window.location.href = `${baseURL}/oauth/authorize?${query.toString()}`; + }); + }; +} + +function externalEthereumLogin(instance, baseURL) { + return (dispatch, getState) => { + return dispatch(ethereumLogin(instance, baseURL)) + .then(account => dispatch(switchAccount(account.id))) + .then(() => window.location.href = '/'); + }; +} + +export function externalLogin(host) { return (dispatch, getState) => { const baseURL = parseBaseURL(host) || parseBaseURL(`https://${host}`); return fetchExternalInstance(baseURL).then(instance => { - const { scopes } = getFeatures(instance); + const features = getFeatures(instance); + const quirks = getQuirks(instance); - const params = { - client_name: sourceCode.displayName, - redirect_uris: `${window.location.origin}/auth/external`, - website: sourceCode.homepage, - scopes, - }; - - return dispatch(createApp(params, baseURL)).then(app => { - const { client_id, redirect_uri } = app; - - const query = new URLSearchParams({ - client_id, - redirect_uri, - response_type: 'code', - scope: scopes, - }); - - localStorage.setItem('soapbox:external:app', JSON.stringify(app)); - localStorage.setItem('soapbox:external:baseurl', baseURL); - localStorage.setItem('soapbox:external:scopes', scopes); - - window.location.href = `${baseURL}/oauth/authorize?${query.toString()}`; - }); + if (features.ethereumLogin && quirks.ethereumLoginOnly) { + return dispatch(externalEthereumLogin(instance, baseURL)); + } else { + return dispatch(externalAuthorize(instance, baseURL)); + } }); }; } diff --git a/app/soapbox/features/auth_login/components/login_page.js b/app/soapbox/features/auth_login/components/login_page.js index d6de2ca3d..ca16c2a81 100644 --- a/app/soapbox/features/auth_login/components/login_page.js +++ b/app/soapbox/features/auth_login/components/login_page.js @@ -4,25 +4,18 @@ import { injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { Redirect } from 'react-router-dom'; -import { ethereumLogin } from 'soapbox/actions/auth'; import { logIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth'; import { fetchInstance } from 'soapbox/actions/instance'; -import { getFeatures } from 'soapbox/utils/features'; import { isStandalone } from 'soapbox/utils/state'; import LoginForm from './login_form'; import OtpAuthForm from './otp_auth_form'; -const mapStateToProps = state => { - const instance = state.get('instance'); - - return { - me: state.get('me'), - isLoading: false, - standalone: isStandalone(state), - features: getFeatures(instance), - }; -}; +const mapStateToProps = state => ({ + me: state.get('me'), + isLoading: false, + standalone: isStandalone(state), +}); export default @connect(mapStateToProps) @injectIntl @@ -69,18 +62,8 @@ class LoginPage extends ImmutablePureComponent { event.preventDefault(); } - handleEthereumLogin = e => { - const { dispatch } = this.props; - - dispatch(ethereumLogin()) - .then(() => this.setState({ shouldRedirect: true })) - .catch(console.error); - - e.preventDefault(); - }; - render() { - const { standalone, features } = this.props; + const { standalone } = this.props; const { isLoading, mfa_auth_needed, mfa_token, shouldRedirect } = this.state; if (standalone) return ; @@ -89,11 +72,7 @@ class LoginPage extends ImmutablePureComponent { if (mfa_auth_needed) return ; - if (features.ethereumLogin) { - return ; - } else { - return ; - } + return ; } } diff --git a/app/soapbox/features/external_login/components/external_login_form.js b/app/soapbox/features/external_login/components/external_login_form.js index c5da61efb..83a9ccf87 100644 --- a/app/soapbox/features/external_login/components/external_login_form.js +++ b/app/soapbox/features/external_login/components/external_login_form.js @@ -3,7 +3,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; import { connect } from 'react-redux'; -import { createAppAndRedirect, loginWithCode } from 'soapbox/actions/external_auth'; +import { externalLogin, loginWithCode } from 'soapbox/actions/external_auth'; import LoadingIndicator from 'soapbox/components/loading_indicator'; import { SimpleForm, FieldsGroup, TextInput } from 'soapbox/features/forms'; @@ -31,7 +31,7 @@ class ExternalLoginForm extends ImmutablePureComponent { this.setState({ isLoading: true }); - dispatch(createAppAndRedirect(host)) + dispatch(externalLogin(host)) .then(() => this.setState({ isLoading: false })) .catch(() => this.setState({ isLoading: false })); } diff --git a/app/soapbox/utils/quirks.js b/app/soapbox/utils/quirks.js index 764b10e26..e3ec5e651 100644 --- a/app/soapbox/utils/quirks.js +++ b/app/soapbox/utils/quirks.js @@ -1,12 +1,17 @@ -import { parseVersion } from './features'; +import { createSelector } from 'reselect'; + +import { parseVersion, PLEROMA, MITRA } from './features'; // For solving bugs between API implementations -export const getQuirks = instance => { - const v = parseVersion(instance.get('version')); +export const getQuirks = createSelector([ + instance => parseVersion(instance.get('version')), +], (v) => { return { - invertedPagination: v.software === 'Pleroma', + invertedPagination: v.software === PLEROMA, + skipsAppCreation: v.software === MITRA, + ethereumLoginOnly: v.software === MITRA, }; -}; +}); export const getNextLinkName = getState => getQuirks(getState().get('instance')).invertedPagination ? 'prev' : 'next'; From 01dd53328beff3ff44c8c269ab8d8aa41e08acf4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 10 Feb 2022 16:36:36 -0600 Subject: [PATCH 3/4] Cleanup unused code --- app/soapbox/actions/auth.js | 5 ----- app/soapbox/utils/quirks.js | 1 - 2 files changed, 6 deletions(-) diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 65c1dc0f5..5da8d579e 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -18,7 +18,6 @@ import KVStore from 'soapbox/storage/kv_store'; import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth'; import sourceCode from 'soapbox/utils/code'; import { getFeatures } from 'soapbox/utils/features'; -import { getQuirks } from 'soapbox/utils/quirks'; import { isStandalone } from 'soapbox/utils/state'; import api, { baseClient } from '../api'; @@ -63,10 +62,6 @@ function createAppAndToken() { function createAuthApp() { return (dispatch, getState) => { - // Mitra: skip creating the app - const quirks = getQuirks(getState().get('instance')); - if (quirks.skipsAppCreation) return dispatch(noOp()); - const params = { client_name: sourceCode.displayName, redirect_uris: 'urn:ietf:wg:oauth:2.0:oob', diff --git a/app/soapbox/utils/quirks.js b/app/soapbox/utils/quirks.js index e3ec5e651..864f1effd 100644 --- a/app/soapbox/utils/quirks.js +++ b/app/soapbox/utils/quirks.js @@ -8,7 +8,6 @@ export const getQuirks = createSelector([ ], (v) => { return { invertedPagination: v.software === PLEROMA, - skipsAppCreation: v.software === MITRA, ethereumLoginOnly: v.software === MITRA, }; }); From abcd55752da73fe8cd3d3f9d18cad23dee973fb6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 10 Feb 2022 19:34:23 -0600 Subject: [PATCH 4/4] Support only external Ethereum auth for now --- app/soapbox/actions/auth.js | 30 ------------------------ app/soapbox/actions/external_auth.js | 34 +++++++++++++++++++++++----- app/soapbox/reducers/instance.js | 4 ++-- app/soapbox/utils/ethereum.js | 26 +++++++++++++++++++++ app/soapbox/utils/quirks.js | 3 ++- 5 files changed, 58 insertions(+), 39 deletions(-) create mode 100644 app/soapbox/utils/ethereum.js diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 5da8d579e..5274a98cf 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -213,36 +213,6 @@ export function logIn(intl, username, password) { }; } -export function ethereumLogin(instance, baseURL) { - return (dispatch, getState) => { - instance = (instance || getState().get('instance')); - - const { ethereum } = window; - const { scopes } = getFeatures(instance); - const loginMessage = instance.get('login_message'); - - return ethereum.request({ method: 'eth_requestAccounts' }).then(walletAddresses => { - const [walletAddress] = walletAddresses; - - return ethereum.request({ method: 'personal_sign', params: [loginMessage, walletAddress] }).then(signature => { - const params = { - grant_type: 'ethereum', - wallet_address: walletAddress.toLowerCase(), - password: signature, - redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', - scope: scopes, - }; - - // Note: skips app creation - // TODO: add to quirks.js for Mitra - return dispatch(obtainOAuthToken(params, baseURL)) - .then(token => dispatch(authLoggedIn(token))) - .then(({ access_token }) => dispatch(verifyCredentials(access_token, baseURL))); - }); - }); - }; -} - export function logOut(intl) { return (dispatch, getState) => { const state = getState(); diff --git a/app/soapbox/actions/external_auth.js b/app/soapbox/actions/external_auth.js index 35af6699e..c6fc5d340 100644 --- a/app/soapbox/actions/external_auth.js +++ b/app/soapbox/actions/external_auth.js @@ -9,10 +9,11 @@ import { Map as ImmutableMap, fromJS } from 'immutable'; import { createApp } from 'soapbox/actions/apps'; -import { authLoggedIn, verifyCredentials, switchAccount, ethereumLogin } from 'soapbox/actions/auth'; +import { authLoggedIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth'; import { obtainOAuthToken } from 'soapbox/actions/oauth'; import { parseBaseURL } from 'soapbox/utils/auth'; import sourceCode from 'soapbox/utils/code'; +import { getWalletAndSign } from 'soapbox/utils/ethereum'; import { getFeatures } from 'soapbox/utils/features'; import { getQuirks } from 'soapbox/utils/quirks'; @@ -35,6 +36,9 @@ const fetchExternalInstance = baseURL => { function createExternalApp(instance, baseURL) { return (dispatch, getState) => { + // Mitra: skip creating the auth app + if (getQuirks(instance).noApps) return new Promise(f => f({})); + const { scopes } = getFeatures(instance); const params = { @@ -71,11 +75,29 @@ function externalAuthorize(instance, baseURL) { }; } -function externalEthereumLogin(instance, baseURL) { +export function externalEthereumLogin(instance, baseURL) { return (dispatch, getState) => { - return dispatch(ethereumLogin(instance, baseURL)) - .then(account => dispatch(switchAccount(account.id))) - .then(() => window.location.href = '/'); + const loginMessage = instance.get('login_message'); + + return getWalletAndSign(loginMessage).then(({ wallet, signature }) => { + return dispatch(createExternalApp(instance, baseURL)).then(app => { + const params = { + grant_type: 'ethereum', + wallet_address: wallet.toLowerCase(), + client_id: app.client_id, + client_secret: app.client_secret, + password: signature, + redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', + scope: getFeatures(instance).scopes, + }; + + return dispatch(obtainOAuthToken(params, baseURL)) + .then(token => dispatch(authLoggedIn(token))) + .then(({ access_token }) => dispatch(verifyCredentials(access_token, baseURL))) + .then(account => dispatch(switchAccount(account.id))) + .then(() => window.location.href = '/'); + }); + }); }; } @@ -87,7 +109,7 @@ export function externalLogin(host) { const features = getFeatures(instance); const quirks = getQuirks(instance); - if (features.ethereumLogin && quirks.ethereumLoginOnly) { + if (features.ethereumLogin && quirks.noOAuthForm) { return dispatch(externalEthereumLogin(instance, baseURL)); } else { return dispatch(externalAuthorize(instance, baseURL)); diff --git a/app/soapbox/reducers/instance.js b/app/soapbox/reducers/instance.js index 25a04562e..b0dd8594f 100644 --- a/app/soapbox/reducers/instance.js +++ b/app/soapbox/reducers/instance.js @@ -50,7 +50,7 @@ const initialState = ImmutableMap({ // Build Mastodon configuration from Pleroma instance const pleromaToMastodonConfig = instance => { - return { + return ImmutableMap({ statuses: ImmutableMap({ max_characters: instance.get('max_toot_chars'), }), @@ -60,7 +60,7 @@ const pleromaToMastodonConfig = instance => { min_expiration: instance.getIn(['poll_limits', 'min_expiration']), max_expiration: instance.getIn(['poll_limits', 'max_expiration']), }), - }; + }); }; // Use new value only if old value is undefined diff --git a/app/soapbox/utils/ethereum.js b/app/soapbox/utils/ethereum.js new file mode 100644 index 000000000..3bd8db7b1 --- /dev/null +++ b/app/soapbox/utils/ethereum.js @@ -0,0 +1,26 @@ +export const ethereum = () => window.ethereum; + +export const hasEthereum = () => Boolean(ethereum()); + +// Requests an Ethereum wallet from the browser +// Returns a Promise containing the Ethereum wallet address (string). +export const getWallet = () => { + return ethereum().request({ method: 'eth_requestAccounts' }) + .then(wallets => wallets[0]); +}; + +// Asks the browser to sign a message with Ethereum. +// Returns a Promise containing the signature (string). +export const signMessage = (wallet, message) => { + return ethereum().request({ method: 'personal_sign', params: [message, wallet] }); +}; + +// Combines the above functions. +// Returns an object with the `wallet` and `signature` +export const getWalletAndSign = message => { + return getWallet().then(wallet => { + return signMessage(wallet, message).then(signature => { + return { wallet, signature }; + }); + }); +}; diff --git a/app/soapbox/utils/quirks.js b/app/soapbox/utils/quirks.js index 864f1effd..c64631078 100644 --- a/app/soapbox/utils/quirks.js +++ b/app/soapbox/utils/quirks.js @@ -8,7 +8,8 @@ export const getQuirks = createSelector([ ], (v) => { return { invertedPagination: v.software === PLEROMA, - ethereumLoginOnly: v.software === MITRA, + noApps: v.software === MITRA, + noOAuthForm: v.software === MITRA, }; });