diff --git a/packages/desktop-client/src/components/Modals.js b/packages/desktop-client/src/components/Modals.js
index 8b8d1686035b38917328c5e1979109d9a7426255..361de99e0f7e786ed3bdaf764575e02f9146c0fd 100644
--- a/packages/desktop-client/src/components/Modals.js
+++ b/packages/desktop-client/src/components/Modals.js
@@ -27,6 +27,7 @@ import LoadBackup from './modals/LoadBackup';
 import ManageRulesModal from './modals/ManageRulesModal';
 import MergeUnusedPayees from './modals/MergeUnusedPayees';
 import NordigenExternalMsg from './modals/NordigenExternalMsg';
+import NordigenInitialise from './modals/NordigenInitialise';
 import PlaidExternalMsg from './modals/PlaidExternalMsg';
 import SelectLinkedAccounts from './modals/SelectLinkedAccounts';
 
@@ -219,6 +220,17 @@ function Modals({
             );
           }}
         />
+        <Route
+          path="/nordigen-init"
+          render={() => {
+            return (
+              <NordigenInitialise
+                modalProps={modalProps}
+                onSuccess={options.onSuccess}
+              />
+            );
+          }}
+        />
         <Route
           path="/nordigen-external-msg"
           render={() => {
diff --git a/packages/desktop-client/src/components/alerts.js b/packages/desktop-client/src/components/alerts.tsx
similarity index 66%
rename from packages/desktop-client/src/components/alerts.js
rename to packages/desktop-client/src/components/alerts.tsx
index 5856f1c007e3f961d02e9f15c425a035e8a7c8a4..0fcd81af30856c64de3d322f8314e5c793b7c049 100644
--- a/packages/desktop-client/src/components/alerts.js
+++ b/packages/desktop-client/src/components/alerts.tsx
@@ -1,12 +1,29 @@
 import React from 'react';
 
+import type { CSSProperties } from 'glamor';
+
 import ExclamationOutline from '../icons/v1/ExclamationOutline';
 import InformationOutline from '../icons/v1/InformationOutline';
 import { styles, colors } from '../style';
 
-import { View, Text } from './common';
+import Text from './common/Text';
+import View from './common/View';
+
+interface AlertProps {
+  icon?: React.FC<{ width?: number; style?: CSSProperties }>;
+  color?: string;
+  backgroundColor?: string;
+  style?: CSSProperties;
+  children?: React.ReactNode;
+}
 
-export function Alert({ icon: Icon, color, backgroundColor, style, children }) {
+export const Alert: React.FC<AlertProps> = ({
+  icon: Icon,
+  color,
+  backgroundColor,
+  style,
+  children,
+}) => {
   return (
     <View
       style={[
@@ -39,9 +56,17 @@ export function Alert({ icon: Icon, color, backgroundColor, style, children }) {
       <Text style={{ zIndex: 1, lineHeight: 1.5 }}>{children}</Text>
     </View>
   );
+};
+
+interface ScopedAlertProps {
+  style?: CSSProperties;
+  children?: React.ReactNode;
 }
 
-export function Information({ style, children }) {
+export const Information: React.FC<ScopedAlertProps> = ({
+  style,
+  children,
+}) => {
   return (
     <Alert
       icon={InformationOutline}
@@ -52,9 +77,9 @@ export function Information({ style, children }) {
       {children}
     </Alert>
   );
-}
+};
 
-export function Warning({ style, children }) {
+export const Warning: React.FC<ScopedAlertProps> = ({ style, children }) => {
   return (
     <Alert
       icon={ExclamationOutline}
@@ -65,9 +90,9 @@ export function Warning({ style, children }) {
       {children}
     </Alert>
   );
-}
+};
 
-export function Error({ style, children }) {
+export const Error: React.FC<ScopedAlertProps> = ({ style, children }) => {
   return (
     <Alert
       icon={ExclamationOutline}
@@ -78,4 +103,4 @@ export function Error({ style, children }) {
       {children}
     </Alert>
   );
-}
+};
diff --git a/packages/desktop-client/src/components/common.js b/packages/desktop-client/src/components/common.js
index 899df70335d31a960752dc163d974690e6461ebd..a8129fc7b040409c611685b86ac52d0913e168b4 100644
--- a/packages/desktop-client/src/components/common.js
+++ b/packages/desktop-client/src/components/common.js
@@ -5,7 +5,6 @@ import React, {
   useState,
   useCallback,
 } from 'react';
-import ReactModal from 'react-modal';
 import { Route, NavLink, withRouter, useRouteMatch } from 'react-router-dom';
 
 import {
@@ -16,22 +15,19 @@ import {
   ListboxOption,
 } from '@reach/listbox';
 import { css } from 'glamor';
-import hotkeys from 'hotkeys-js';
 
 import { integerToCurrency } from 'loot-core/src/shared/util';
 
-import Loading from '../icons/AnimatedLoading';
-import Delete from '../icons/v0/Delete';
 import ExpandArrow from '../icons/v0/ExpandArrow';
 import { styles, colors } from '../style';
-import tokens from '../tokens';
 
 import Button from './common/Button';
 import Input, { defaultInputStyle } from './common/Input';
 import Text from './common/Text';
 import View from './common/View';
 
-export { default as Button } from './common/Button';
+export { default as Modal, ModalButtons } from './common/Modal';
+export { default as Button, ButtonWithLoading } from './common/Button';
 export { default as Input } from './common/Input';
 export { default as View } from './common/View';
 export { default as Text } from './common/Text';
@@ -200,44 +196,6 @@ function ButtonLink_({
 
 export const ButtonLink = withRouter(ButtonLink_);
 
-export const ButtonWithLoading = React.forwardRef((props, ref) => {
-  let { loading, children, ...buttonProps } = props;
-  return (
-    <Button
-      {...buttonProps}
-      style={[{ position: 'relative' }, buttonProps.style]}
-    >
-      {loading && (
-        <View
-          style={{
-            position: 'absolute',
-            top: 0,
-            left: 0,
-            right: 0,
-            bottom: 0,
-            alignItems: 'center',
-            justifyContent: 'center',
-          }}
-        >
-          <Loading
-            color="currentColor"
-            style={{ width: 20, height: 20, color: 'currentColor' }}
-          />
-        </View>
-      )}
-      <View
-        style={{
-          opacity: loading ? 0 : 1,
-          flexDirection: 'row',
-          alignItems: 'center',
-        }}
-      >
-        {children}
-      </View>
-    </Button>
-  );
-});
-
 export function InputWithContent({
   leftContent,
   rightContent,
@@ -628,300 +586,6 @@ export function Strong({ style, children, ...props }) {
   );
 }
 
-function ModalContent({
-  style,
-  size,
-  noAnimation,
-  isCurrent,
-  stackIndex,
-  children,
-}) {
-  let contentRef = useRef(null);
-  let mounted = useRef(false);
-  let rotateFactor = useRef(Math.random() * 10 - 5);
-
-  useLayoutEffect(() => {
-    if (contentRef.current == null) {
-      return;
-    }
-
-    function setProps() {
-      if (isCurrent) {
-        contentRef.current.style.transform = 'translateY(0px) scale(1)';
-        contentRef.current.style.pointerEvents = 'auto';
-      } else {
-        contentRef.current.style.transform = `translateY(-40px) scale(.95) rotate(${rotateFactor.current}deg)`;
-        contentRef.current.style.pointerEvents = 'none';
-      }
-    }
-
-    if (!mounted.current) {
-      if (noAnimation) {
-        contentRef.current.style.opacity = 1;
-        contentRef.current.style.transform = 'translateY(0px) scale(1)';
-
-        setTimeout(() => {
-          if (contentRef.current) {
-            contentRef.current.style.transition =
-              'opacity .1s, transform .1s cubic-bezier(.42, 0, .58, 1)';
-          }
-        }, 0);
-      } else {
-        contentRef.current.style.opacity = 0;
-        contentRef.current.style.transform = 'translateY(10px) scale(1)';
-
-        setTimeout(() => {
-          if (contentRef.current) {
-            mounted.current = true;
-            contentRef.current.style.transition =
-              'opacity .1s, transform .1s cubic-bezier(.42, 0, .58, 1)';
-            contentRef.current.style.opacity = 1;
-            setProps();
-          }
-        }, 0);
-      }
-    } else {
-      setProps();
-    }
-  }, [noAnimation, isCurrent, stackIndex]);
-
-  return (
-    <View
-      innerRef={contentRef}
-      style={[
-        style,
-        size && { width: size.width, height: size.height },
-        noAnimation && !isCurrent && { display: 'none' },
-      ]}
-    >
-      {children}
-    </View>
-  );
-}
-
-export function Modal({
-  title,
-  isCurrent,
-  isHidden,
-  size,
-  padding = 20,
-  showHeader = true,
-  showTitle = true,
-  showClose = true,
-  showOverlay = true,
-  loading = false,
-  noAnimation = false,
-  focusAfterClose = true,
-  stackIndex,
-  parent,
-  style,
-  contentStyle,
-  overlayStyle,
-  children,
-  onClose,
-}) {
-  useEffect(() => {
-    // This deactivates any key handlers in the "app" scope. Ideally
-    // each modal would have a name so they could each have their own
-    // key handlers, but we'll do that later
-    let prevScope = hotkeys.getScope();
-    hotkeys.setScope('modal');
-    return () => hotkeys.setScope(prevScope);
-  }, []);
-
-  return (
-    <ReactModal
-      isOpen={true}
-      onRequestClose={onClose}
-      shouldCloseOnOverlayClick={false}
-      shouldFocusAfterRender={!global.IS_DESIGN_MODE}
-      shouldReturnFocusAfterClose={focusAfterClose}
-      appElement={document.querySelector('#root')}
-      parentSelector={parent && (() => parent)}
-      style={{
-        content: {
-          top: 0,
-          left: 0,
-          right: 0,
-          bottom: 0,
-          display: 'flex',
-          justifyContent: 'center',
-          alignItems: 'center',
-          overflow: 'visible',
-          border: 0,
-          fontSize: 14,
-          backgroundColor: 'transparent',
-          padding: 0,
-          pointerEvents: 'auto',
-          ...contentStyle,
-        },
-        overlay: {
-          zIndex: 3000,
-          backgroundColor:
-            showOverlay && stackIndex === 0 ? 'rgba(0, 0, 0, .1)' : 'none',
-          pointerEvents: showOverlay ? 'auto' : 'none',
-          ...overlayStyle,
-          ...(parent
-            ? {
-                position: 'absolute',
-                top: 0,
-                left: 0,
-                right: 0,
-                bottom: 0,
-              }
-            : {}),
-        },
-      }}
-    >
-      <ModalContent
-        noAnimation={noAnimation}
-        isCurrent={isCurrent}
-        size={size}
-        style={[
-          {
-            willChange: 'opacity, transform',
-            minWidth: '100%',
-            minHeight: 0,
-            borderRadius: 4,
-            backgroundColor: 'white',
-            opacity: isHidden ? 0 : 1,
-            [`@media (min-width: ${tokens.breakpoint_narrow})`]: {
-              minWidth: tokens.breakpoint_narrow,
-            },
-          },
-          styles.shadowLarge,
-          style,
-          styles.lightScrollbar,
-        ]}
-      >
-        {showHeader && (
-          <View
-            style={{
-              padding: 20,
-              position: 'relative',
-              flexShrink: 0,
-            }}
-          >
-            {showTitle && (
-              <View
-                style={{
-                  color: colors.n2,
-                  flex: 1,
-                  alignSelf: 'center',
-                  textAlign: 'center',
-                  // We need to force a width for the text-overflow
-                  // ellipses to work because we are aligning center.
-                  // This effectively gives it a padding of 20px
-                  width: 'calc(100% - 40px)',
-                }}
-              >
-                <Text
-                  style={{
-                    fontSize: 25,
-                    fontWeight: 700,
-                    whiteSpace: 'nowrap',
-                    overflow: 'hidden',
-                    textOverflow: 'ellipsis',
-                  }}
-                >
-                  {title}
-                </Text>
-              </View>
-            )}
-
-            <View
-              style={{
-                position: 'absolute',
-                right: 0,
-                top: 0,
-                bottom: 0,
-                justifyContent: 'center',
-                alignItems: 'center',
-              }}
-            >
-              <View
-                style={{
-                  flexDirection: 'row',
-                  marginRight: 15,
-                }}
-              >
-                {showClose && (
-                  <Button
-                    bare
-                    onClick={e => onClose()}
-                    style={{ padding: '10px 10px' }}
-                    aria-label="Close"
-                  >
-                    <Delete width={10} />
-                  </Button>
-                )}
-              </View>
-            </View>
-          </View>
-        )}
-        <View style={{ padding, paddingTop: 0, flex: 1 }}>
-          {typeof children === 'function' ? children() : children}
-        </View>
-        {loading && (
-          <View
-            style={{
-              position: 'absolute',
-              top: 0,
-              left: 0,
-              right: 0,
-              bottom: 0,
-              backgroundColor: 'rgba(255, 255, 255, .6)',
-              alignItems: 'center',
-              justifyContent: 'center',
-              zIndex: 1000,
-            }}
-          >
-            <Loading style={{ width: 20, height: 20 }} color={colors.n1} />
-          </View>
-        )}
-      </ModalContent>
-    </ReactModal>
-  );
-}
-
-export function ModalButtons({
-  style,
-  leftContent,
-  focusButton = false,
-  children,
-}) {
-  let containerRef = useRef(null);
-
-  useEffect(() => {
-    if (focusButton && containerRef.current) {
-      let button = containerRef.current.querySelector(
-        'button:not([data-hidden])',
-      );
-
-      if (button) {
-        button.focus();
-      }
-    }
-  }, [focusButton]);
-
-  return (
-    <View
-      innerRef={containerRef}
-      style={[
-        {
-          flexDirection: 'row',
-          marginTop: 30,
-        },
-        style,
-      ]}
-    >
-      {leftContent}
-      <View style={{ flex: 1 }} />
-      {children}
-    </View>
-  );
-}
-
 export function InlineField({ label, labelWidth, children, width, style }) {
   return (
     <label
diff --git a/packages/desktop-client/src/components/common/Button.tsx b/packages/desktop-client/src/components/common/Button.tsx
index a13fe8b23f73a0276ab30d034d5d4de9b74db460..23f5653d95a2d5e8c333b27ff21757297a8ff139 100644
--- a/packages/desktop-client/src/components/common/Button.tsx
+++ b/packages/desktop-client/src/components/common/Button.tsx
@@ -3,14 +3,19 @@ import React from 'react';
 import { css } from 'glamor';
 import type { CSSProperties } from 'glamor';
 
+import Loading from '../../icons/AnimatedLoading';
 import { styles, colors } from '../../style';
 
-interface ButtonProps extends React.HTMLProps<HTMLButtonElement> {
+import View from './View';
+
+interface ButtonProps
+  extends Omit<React.HTMLProps<HTMLButtonElement>, 'style'> {
   pressed?: boolean;
   primary?: boolean;
   hover?: boolean;
   bare?: boolean;
   disabled?: boolean;
+  style?: CSSProperties;
   hoveredStyle?: CSSProperties;
   activeStyle?: CSSProperties;
   bounce?: boolean;
@@ -110,4 +115,47 @@ const Button: React.FC<ButtonProps> = React.forwardRef<
   },
 );
 
+interface ButtonWithLoadingProps extends ButtonProps {
+  loading?: boolean;
+}
+
+export const ButtonWithLoading: React.FC<ButtonWithLoadingProps> =
+  React.forwardRef((props, ref) => {
+    let { loading, children, ...buttonProps } = props;
+    return (
+      <Button
+        {...buttonProps}
+        style={[{ position: 'relative' }, buttonProps.style]}
+      >
+        {loading && (
+          <View
+            style={{
+              position: 'absolute',
+              top: 0,
+              left: 0,
+              right: 0,
+              bottom: 0,
+              alignItems: 'center',
+              justifyContent: 'center',
+            }}
+          >
+            <Loading
+              color="currentColor"
+              style={{ width: 20, height: 20, color: 'currentColor' }}
+            />
+          </View>
+        )}
+        <View
+          style={{
+            opacity: loading ? 0 : 1,
+            flexDirection: 'row',
+            alignItems: 'center',
+          }}
+        >
+          {children}
+        </View>
+      </Button>
+    );
+  });
+
 export default Button;
diff --git a/packages/desktop-client/src/components/common/Modal.tsx b/packages/desktop-client/src/components/common/Modal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f82449088e4bef5559814001194007bc11fb7fbe
--- /dev/null
+++ b/packages/desktop-client/src/components/common/Modal.tsx
@@ -0,0 +1,348 @@
+import React, { useEffect, useRef, useLayoutEffect } from 'react';
+import ReactModal from 'react-modal';
+
+import type { CSSProperties } from 'glamor';
+import hotkeys from 'hotkeys-js';
+
+import Loading from '../../icons/AnimatedLoading';
+import Delete from '../../icons/v0/Delete';
+import { styles, colors } from '../../style';
+import tokens from '../../tokens';
+
+import Button from './Button';
+import Text from './Text';
+import View from './View';
+
+export interface ModalProps {
+  title: string;
+  isCurrent?: boolean;
+  isHidden?: boolean;
+  children: React.ReactNode;
+  size?: { width?: number; height?: number };
+  padding?: number;
+  showHeader?: boolean;
+  showTitle?: boolean;
+  showClose?: boolean;
+  showOverlay?: boolean;
+  loading?: boolean;
+  noAnimation?: boolean;
+  focusAfterClose?: boolean;
+  stackIndex?: number;
+  parent?: unknown;
+  style?: CSSProperties;
+  contentStyle?: CSSProperties;
+  overlayStyle?: CSSProperties;
+  onClose?: () => void;
+}
+
+const Modal: React.FC<ModalProps> = ({
+  title,
+  isCurrent,
+  isHidden,
+  size,
+  padding = 20,
+  showHeader = true,
+  showTitle = true,
+  showClose = true,
+  showOverlay = true,
+  loading = false,
+  noAnimation = false,
+  focusAfterClose = true,
+  stackIndex,
+  parent,
+  style,
+  contentStyle,
+  overlayStyle,
+  children,
+  onClose,
+}) => {
+  useEffect(() => {
+    // This deactivates any key handlers in the "app" scope. Ideally
+    // each modal would have a name so they could each have their own
+    // key handlers, but we'll do that later
+    let prevScope = hotkeys.getScope();
+    hotkeys.setScope('modal');
+    return () => hotkeys.setScope(prevScope);
+  }, []);
+
+  return (
+    <ReactModal
+      isOpen={true}
+      onRequestClose={onClose}
+      shouldCloseOnOverlayClick={false}
+      shouldFocusAfterRender={!global.IS_DESIGN_MODE}
+      shouldReturnFocusAfterClose={focusAfterClose}
+      appElement={document.querySelector('#root')}
+      parentSelector={parent && (() => parent)}
+      style={{
+        content: {
+          top: 0,
+          left: 0,
+          right: 0,
+          bottom: 0,
+          display: 'flex',
+          justifyContent: 'center',
+          alignItems: 'center',
+          overflow: 'visible',
+          border: 0,
+          fontSize: 14,
+          backgroundColor: 'transparent',
+          padding: 0,
+          pointerEvents: 'auto',
+          ...contentStyle,
+        },
+        overlay: {
+          zIndex: 3000,
+          backgroundColor:
+            showOverlay && stackIndex === 0 ? 'rgba(0, 0, 0, .1)' : 'none',
+          pointerEvents: showOverlay ? 'auto' : 'none',
+          ...overlayStyle,
+          ...(parent
+            ? {
+                position: 'absolute',
+                top: 0,
+                left: 0,
+                right: 0,
+                bottom: 0,
+              }
+            : {}),
+        },
+      }}
+    >
+      <ModalContent
+        noAnimation={noAnimation}
+        isCurrent={isCurrent}
+        size={size}
+        style={[
+          {
+            willChange: 'opacity, transform',
+            minWidth: '100%',
+            minHeight: 0,
+            borderRadius: 4,
+            backgroundColor: 'white',
+            opacity: isHidden ? 0 : 1,
+            [`@media (min-width: ${tokens.breakpoint_narrow})`]: {
+              minWidth: tokens.breakpoint_narrow,
+            },
+          },
+          styles.shadowLarge,
+          style,
+          styles.lightScrollbar,
+        ]}
+      >
+        {showHeader && (
+          <View
+            style={{
+              padding: 20,
+              position: 'relative',
+              flexShrink: 0,
+            }}
+          >
+            {showTitle && (
+              <View
+                style={{
+                  color: colors.n2,
+                  flex: 1,
+                  alignSelf: 'center',
+                  textAlign: 'center',
+                  // We need to force a width for the text-overflow
+                  // ellipses to work because we are aligning center.
+                  // This effectively gives it a padding of 20px
+                  width: 'calc(100% - 40px)',
+                }}
+              >
+                <Text
+                  style={{
+                    fontSize: 25,
+                    fontWeight: 700,
+                    whiteSpace: 'nowrap',
+                    overflow: 'hidden',
+                    textOverflow: 'ellipsis',
+                  }}
+                >
+                  {title}
+                </Text>
+              </View>
+            )}
+
+            <View
+              style={{
+                position: 'absolute',
+                right: 0,
+                top: 0,
+                bottom: 0,
+                justifyContent: 'center',
+                alignItems: 'center',
+              }}
+            >
+              <View
+                style={{
+                  flexDirection: 'row',
+                  marginRight: 15,
+                }}
+              >
+                {showClose && (
+                  <Button
+                    bare
+                    onClick={onClose}
+                    style={{ padding: '10px 10px' }}
+                    aria-label="Close"
+                  >
+                    <Delete width={10} />
+                  </Button>
+                )}
+              </View>
+            </View>
+          </View>
+        )}
+        <View style={{ padding, paddingTop: 0, flex: 1 }}>
+          {typeof children === 'function' ? children() : children}
+        </View>
+        {loading && (
+          <View
+            style={{
+              position: 'absolute',
+              top: 0,
+              left: 0,
+              right: 0,
+              bottom: 0,
+              backgroundColor: 'rgba(255, 255, 255, .6)',
+              alignItems: 'center',
+              justifyContent: 'center',
+              zIndex: 1000,
+            }}
+          >
+            <Loading style={{ width: 20, height: 20 }} color={colors.n1} />
+          </View>
+        )}
+      </ModalContent>
+    </ReactModal>
+  );
+};
+
+interface ModalContentProps {
+  style?: CSSProperties;
+  size?: ModalProps['size'];
+  noAnimation?: boolean;
+  isCurrent?: boolean;
+  stackIndex?: number;
+  children: React.ReactNode;
+}
+
+const ModalContent: React.FC<ModalContentProps> = ({
+  style,
+  size,
+  noAnimation,
+  isCurrent,
+  stackIndex,
+  children,
+}) => {
+  let contentRef = useRef(null);
+  let mounted = useRef(false);
+  let rotateFactor = useRef(Math.random() * 10 - 5);
+
+  useLayoutEffect(() => {
+    if (contentRef.current == null) {
+      return;
+    }
+
+    function setProps() {
+      if (isCurrent) {
+        contentRef.current.style.transform = 'translateY(0px) scale(1)';
+        contentRef.current.style.pointerEvents = 'auto';
+      } else {
+        contentRef.current.style.transform = `translateY(-40px) scale(.95) rotate(${rotateFactor.current}deg)`;
+        contentRef.current.style.pointerEvents = 'none';
+      }
+    }
+
+    if (!mounted.current) {
+      if (noAnimation) {
+        contentRef.current.style.opacity = 1;
+        contentRef.current.style.transform = 'translateY(0px) scale(1)';
+
+        setTimeout(() => {
+          if (contentRef.current) {
+            contentRef.current.style.transition =
+              'opacity .1s, transform .1s cubic-bezier(.42, 0, .58, 1)';
+          }
+        }, 0);
+      } else {
+        contentRef.current.style.opacity = 0;
+        contentRef.current.style.transform = 'translateY(10px) scale(1)';
+
+        setTimeout(() => {
+          if (contentRef.current) {
+            mounted.current = true;
+            contentRef.current.style.transition =
+              'opacity .1s, transform .1s cubic-bezier(.42, 0, .58, 1)';
+            contentRef.current.style.opacity = 1;
+            setProps();
+          }
+        }, 0);
+      }
+    } else {
+      setProps();
+    }
+  }, [noAnimation, isCurrent, stackIndex]);
+
+  return (
+    <View
+      innerRef={contentRef}
+      style={[
+        style,
+        size && { width: size.width, height: size.height },
+        noAnimation && !isCurrent && { display: 'none' },
+      ]}
+    >
+      {children}
+    </View>
+  );
+};
+
+interface ModalButtonsProps {
+  style?: CSSProperties;
+  leftContent?: React.ReactNode;
+  focusButton?: boolean;
+  children: React.ReactNode;
+}
+
+export const ModalButtons: React.FC<ModalButtonsProps> = ({
+  style,
+  leftContent,
+  focusButton = false,
+  children,
+}) => {
+  let containerRef = useRef(null);
+
+  useEffect(() => {
+    if (focusButton && containerRef.current) {
+      let button = containerRef.current.querySelector(
+        'button:not([data-hidden])',
+      );
+
+      if (button) {
+        button.focus();
+      }
+    }
+  }, [focusButton]);
+
+  return (
+    <View
+      innerRef={containerRef}
+      style={[
+        {
+          flexDirection: 'row',
+          marginTop: 30,
+        },
+        style,
+      ]}
+    >
+      {leftContent}
+      <View style={{ flex: 1 }} />
+      {children}
+    </View>
+  );
+};
+
+export default Modal;
diff --git a/packages/desktop-client/src/components/modals/CreateAccount.js b/packages/desktop-client/src/components/modals/CreateAccount.js
index 8fb8610e252e3c2013df31884a32445113d0749d..a6e616427333dd284e566a01a36f21d83a696bdb 100644
--- a/packages/desktop-client/src/components/modals/CreateAccount.js
+++ b/packages/desktop-client/src/components/modals/CreateAccount.js
@@ -1,23 +1,43 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
 import { useDispatch } from 'react-redux';
 
 import { pushModal } from 'loot-core/src/client/actions/modals';
 
+import useNordigenStatus from '../../hooks/useNordigenStatus';
 import { authorizeBank } from '../../nordigen';
 import { colors } from '../../style';
 import { View, Text, Modal, P, Button, ButtonWithLoading } from '../common';
 
 export default function CreateAccount({ modalProps, syncServerStatus }) {
   const dispatch = useDispatch();
+  const [isNordigenSetupComplete, setIsNordigenSetupComplete] = useState(null);
 
   const onConnect = () => {
+    if (!isNordigenSetupComplete) {
+      onNordigenInit();
+      return;
+    }
+
     authorizeBank((modal, params) => dispatch(pushModal(modal, params)));
   };
 
+  const onNordigenInit = () => {
+    dispatch(
+      pushModal('nordigen-init', {
+        onSuccess: () => setIsNordigenSetupComplete(true),
+      }),
+    );
+  };
+
   const onCreateLocalAccount = () => {
     dispatch(pushModal('add-local-account'));
   };
 
+  const { configured } = useNordigenStatus();
+  useEffect(() => {
+    setIsNordigenSetupComplete(configured);
+  }, [configured]);
+
   return (
     <Modal title="Add Account" {...modalProps}>
       {() => (
@@ -39,8 +59,15 @@ export default function CreateAccount({ modalProps, syncServerStatus }) {
             }}
             onClick={onConnect}
           >
-            Link bank account
+            {isNordigenSetupComplete
+              ? 'Link bank account'
+              : 'Set-up Nordigen for bank-sync'}
           </ButtonWithLoading>
+          {isNordigenSetupComplete && (
+            <Button bare onClick={onNordigenInit}>
+              set new API secrets
+            </Button>
+          )}
 
           {syncServerStatus !== 'online' && (
             <P style={{ color: colors.r5, marginTop: 5 }}>
diff --git a/packages/desktop-client/src/components/modals/NordigenExternalMsg.js b/packages/desktop-client/src/components/modals/NordigenExternalMsg.js
index 1537bd92a6efaaf98e7a8e3a9b61b9d1186d7171..5c052b1947ec9b602f824466ffbd1d4fa71e7890 100644
--- a/packages/desktop-client/src/components/modals/NordigenExternalMsg.js
+++ b/packages/desktop-client/src/components/modals/NordigenExternalMsg.js
@@ -1,7 +1,8 @@
 import React, { useEffect, useState, useRef } from 'react';
 
-import { send, sendCatch } from 'loot-core/src/platform/client/fetch';
+import { sendCatch } from 'loot-core/src/platform/client/fetch';
 
+import useNordigenStatus from '../../hooks/useNordigenStatus';
 import AnimatedLoading from '../../icons/AnimatedLoading';
 import { colors } from '../../style';
 import { Error, Warning } from '../alerts';
@@ -50,29 +51,6 @@ function useAvailableBanks(country) {
   };
 }
 
-function useNordigenStatus() {
-  const [configured, setConfigured] = useState(false);
-  const [isLoading, setIsLoading] = useState(false);
-
-  useEffect(() => {
-    async function fetch() {
-      setIsLoading(true);
-
-      const results = await send('nordigen-status');
-
-      setConfigured(results.configured || false);
-      setIsLoading(false);
-    }
-
-    fetch();
-  }, [setConfigured, setIsLoading]);
-
-  return {
-    configured,
-    isLoading,
-  };
-}
-
 function renderError(error) {
   return (
     <Error style={{ alignSelf: 'center' }}>
diff --git a/packages/desktop-client/src/components/modals/NordigenInitialise.tsx b/packages/desktop-client/src/components/modals/NordigenInitialise.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4184695cc438168873a2abc28a0de88fa236a6e3
--- /dev/null
+++ b/packages/desktop-client/src/components/modals/NordigenInitialise.tsx
@@ -0,0 +1,107 @@
+import React, { useState } from 'react';
+
+import { send } from 'loot-core/src/platform/client/fetch';
+
+import { Error } from '../alerts';
+import { ButtonWithLoading } from '../common/Button';
+import Input from '../common/Input';
+import Modal, { ModalButtons } from '../common/Modal';
+import type { ModalProps } from '../common/Modal';
+import Text from '../common/Text';
+import View from '../common/View';
+import { FormField, FormLabel } from '../forms';
+
+interface NordigenInitialiseProps {
+  modalProps?: Partial<ModalProps>;
+  onSuccess: () => void;
+}
+
+const NordigenInitialise: React.FC<NordigenInitialiseProps> = ({
+  modalProps,
+  onSuccess,
+}) => {
+  const [secretId, setSecretId] = useState('');
+  const [secretKey, setSecretKey] = useState('');
+  const [isValid, setIsValid] = useState(true);
+  const [isLoading, setIsLoading] = useState(false);
+
+  const onSubmit = async () => {
+    if (!secretId || !secretKey) {
+      setIsValid(false);
+      return;
+    }
+
+    setIsLoading(true);
+
+    await Promise.all([
+      send('secret-set', {
+        name: 'nordigen_secretId',
+        value: secretId,
+      }),
+      send('secret-set', {
+        name: 'nordigen_secretKey',
+        value: secretKey,
+      }),
+    ]);
+
+    onSuccess();
+    modalProps.onClose();
+    setIsLoading(false);
+  };
+
+  return (
+    <Modal title="Set-up Nordigen" size={{ width: 300 }} {...modalProps}>
+      <View style={{ display: 'flex', gap: 10 }}>
+        <Text>
+          In order to enable bank-sync via Nordigen (only for EU banks) you will
+          need to create access credentials. This can be done by creating an
+          account with{' '}
+          <a
+            href="https://nordigen.com/"
+            target="_blank"
+            rel="noopener noreferrer"
+          >
+            Nordigen
+          </a>
+          .
+        </Text>
+
+        <FormField>
+          <FormLabel title="Secret ID:" htmlFor="secret-id-field" />
+          <Input
+            id="secret-id-field"
+            type="password"
+            value={secretId}
+            onUpdate={setSecretId}
+            onChange={() => setIsValid(true)}
+          />
+        </FormField>
+
+        <FormField>
+          <FormLabel title="Secret Key:" htmlFor="secret-key-field" />
+          <Input
+            id="secret-key-field"
+            type="password"
+            value={secretKey}
+            onUpdate={setSecretKey}
+            onChange={() => setIsValid(true)}
+          />
+        </FormField>
+
+        {!isValid && (
+          <Error>
+            It is required to provide both the secret id and secret key.
+          </Error>
+        )}
+      </View>
+
+      <ModalButtons>
+        <ButtonWithLoading loading={isLoading} primary onClick={onSubmit}>
+          Save and continue
+        </ButtonWithLoading>
+      </ModalButtons>
+    </Modal>
+  );
+};
+
+export default NordigenInitialise;
diff --git a/packages/desktop-client/src/hooks/useNordigenStatus.ts b/packages/desktop-client/src/hooks/useNordigenStatus.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7915a6c8bf4cc46f8f490aa642d68973a88e17a6
--- /dev/null
+++ b/packages/desktop-client/src/hooks/useNordigenStatus.ts
@@ -0,0 +1,26 @@
+import { useEffect, useState } from 'react';
+
+import { send } from 'loot-core/src/platform/client/fetch';
+
+export default function useNordigenStatus() {
+  const [configured, setConfigured] = useState<boolean | null>(null);
+  const [isLoading, setIsLoading] = useState(false);
+
+  useEffect(() => {
+    async function fetch() {
+      setIsLoading(true);
+
+      const results = await send('nordigen-status');
+
+      setConfigured(results.configured || false);
+      setIsLoading(false);
+    }
+
+    fetch();
+  }, [setConfigured, setIsLoading]);
+
+  return {
+    configured,
+    isLoading,
+  };
+}
diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts
index 17a0f752cd96f0504927ce45e0ed515dc81464ff..a07047f9e7317e86ed9c2c9d92f7dfdd527c803b 100644
--- a/packages/loot-core/src/server/main.ts
+++ b/packages/loot-core/src/server/main.ts
@@ -1162,6 +1162,45 @@ handlers['accounts-sync'] = async function ({ id }) {
   return { errors, newTransactions, matchedTransactions, updatedAccounts };
 };
 
+handlers['secret-set'] = async function ({ name, value }) {
+  let userToken = await asyncStorage.getItem('user-token');
+
+  if (userToken) {
+    try {
+      return await post(
+        getServer().BASE_SERVER + '/secret',
+        {
+          name,
+          value,
+        },
+        {
+          'X-ACTUAL-TOKEN': userToken,
+        },
+      );
+    } catch (error) {
+      console.error(error);
+      return { error: 'failed' };
+    }
+  }
+  return { error: 'unauthorized' };
+};
+
+handlers['secret-check'] = async function (name) {
+  let userToken = await asyncStorage.getItem('user-token');
+
+  if (userToken) {
+    try {
+      return await get(getServer().BASE_SERVER + '/secret/' + name, {
+        'X-ACTUAL-TOKEN': userToken,
+      });
+    } catch (error) {
+      console.error(error);
+      return { error: 'failed' };
+    }
+  }
+  return { error: 'unauthorized' };
+};
+
 handlers['nordigen-poll-web-token'] = async function ({
   upgradingAccountId,
   requisitionId,
diff --git a/packages/loot-core/src/types/main.handlers.d.ts b/packages/loot-core/src/types/main.handlers.d.ts
index 0145d703ad63e39c603fca9aa21a6f4f1c752e01..49d3c00053ec221c70f03030650541a35c033ea4 100644
--- a/packages/loot-core/src/types/main.handlers.d.ts
+++ b/packages/loot-core/src/types/main.handlers.d.ts
@@ -190,12 +190,15 @@ export interface MainHandlers {
     updatedAccounts: unknown;
   }>;
 
+  'secret-set': (arg: { name: string; value: string }) => Promise<null>;
+  'secret-check': (arg: string) => Promise<null>;
+
   'nordigen-poll-web-token': (arg: {
     upgradingAccountId;
     requisitionId;
   }) => Promise<null>;
 
-  'nordigen-status': () => Promise<unknown>;
+  'nordigen-status': () => Promise<{ configured: boolean }>;
 
   'nordigen-get-banks': (country) => Promise<unknown>;
 
diff --git a/upcoming-release-notes/968.md b/upcoming-release-notes/968.md
new file mode 100644
index 0000000000000000000000000000000000000000..96187e7e5c8f93aef5648aa954dd5c9fffcefdba
--- /dev/null
+++ b/upcoming-release-notes/968.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [MatissJanis]
+---
+
+Nordigen: ability to configure credentials via the UI