Skip to content

Commit

Permalink
MOBILE-192: Add new app to connect mobile app to web wallet (#592)
Browse files Browse the repository at this point in the history
* MOBILE-192: Add new app to connect mobile app to web wallet

* MOBILE-192: Remove qrcode blob from state and creating code from the frontend instead of fetching it

* MOBILE-192: Update scan code page with PR feedback
  • Loading branch information
escobarjonatan authored Apr 20, 2024
1 parent 3efb7ec commit f6105f2
Show file tree
Hide file tree
Showing 58 changed files with 2,024 additions and 284 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ The following commands will then run each app:

- NEWM Studio: `nx serve studio` (runs at `http://localhost:3000`)
- NEWM Marketplace: `nx serve marketplace` (runs at `http://localhost:4200`)
- NEWM Mobile Wallet Connector: `nx serve mobile-wallet-connector` (runs at `http://localhost:4300`)

## Nx

Expand Down
3 changes: 2 additions & 1 deletion apps/marketplace/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"configurations": {
"development": {
"buildTarget": "marketplace:build:development",
"dev": true
"dev": true,
"port": 4200
},
"production": {
"buildTarget": "marketplace:build:production",
Expand Down
3 changes: 2 additions & 1 deletion apps/marketplace/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { Stack, ThemeProvider } from "@mui/material";
import { AppRouterCacheProvider } from "@mui/material-nextjs/v13-appRouter";
import { FunctionComponent, ReactNode } from "react";
import theme from "@newm-web/theme";
import { StyledComponentsRegistry } from "@newm-web/components";
import { Provider } from "react-redux";
import { Footer, Header, StyledComponentsRegistry } from "../components";
import { Footer, Header } from "../components";
import store from "../store";

interface RootLayoutProps {
Expand Down
1 change: 0 additions & 1 deletion apps/marketplace/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export { default as Footer } from "./footer/Footer";
export { default as Header } from "./header/Header";
export { default as ItemSkeleton } from "./skeletons/ItemSkeleton";
export { default as MoreSongs } from "./MoreSongs";
export { default as StyledComponentsRegistry } from "./StyledComponentsRegistry";
export { default as SimilarArtists } from "./SimilarArtists";
export { default as SimilarSongs } from "./SimilarSongs";
export { default as Songs } from "./Songs";
34 changes: 34 additions & 0 deletions apps/mobile-wallet-connector/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"extends": [
"plugin:@nx/react-typescript",
"next",
"next/core-web-vitals",
"../../.eslintrc.json"
],
"ignorePatterns": ["!**/*", ".next/**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
"@next/next/no-html-link-for-pages": [
"error",
"apps/mobile-wallet-connector/pages"
]
}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"],
"env": {
"jest": true
}
}
]
}
6 changes: 6 additions & 0 deletions apps/mobile-wallet-connector/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
declare module "*.svg" {
const content: any;
export const ReactComponent: any;
export default content;
}
11 changes: 11 additions & 0 deletions apps/mobile-wallet-connector/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* eslint-disable */
export default {
displayName: "mobile-wallet-connector",
preset: "../../jest.preset.js",
transform: {
"^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "@nx/react/plugins/jest",
"^.+\\.[tj]sx?$": ["babel-jest", { presets: ["@nx/next/babel"] }],
},
moduleFileExtensions: ["ts", "tsx", "js", "jsx"],
coverageDirectory: "../../coverage/apps/mobile-wallet-connector",
};
5 changes: 5 additions & 0 deletions apps/mobile-wallet-connector/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
27 changes: 27 additions & 0 deletions apps/mobile-wallet-connector/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//@ts-check

// eslint-disable-next-line @typescript-eslint/no-var-requires
const { composePlugins, withNx } = require("@nx/next");

/**
* @type {import('@nx/next/plugins/with-nx').WithNxOptions}
**/
const nextConfig = {
nx: {
// Set this to true if you would like to to use SVGR
// See: https://github.com/gregberge/svgr
svgr: false,
},

compiler: {
// For other options, see https://styled-components.com/docs/tooling#babel-plugin
styledComponents: true,
},
};

const plugins = [
// Add more Next.js plugins to this list if needed.
withNx,
];

module.exports = composePlugins(...plugins)(nextConfig);
58 changes: 58 additions & 0 deletions apps/mobile-wallet-connector/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"name": "mobile-wallet-connector",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/mobile-wallet-connector",
"projectType": "application",
"targets": {
"build": {
"executor": "@nx/next:build",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"outputPath": "dist/apps/mobile-wallet-connector"
},
"configurations": {
"development": {
"outputPath": "apps/mobile-wallet-connector"
},
"production": {}
}
},
"serve": {
"executor": "@nx/next:server",
"defaultConfiguration": "development",
"options": {
"buildTarget": "mobile-wallet-connector:build",
"dev": true
},
"configurations": {
"development": {
"buildTarget": "mobile-wallet-connector:build:development",
"dev": true,
"port": 4300
},
"production": {
"buildTarget": "mobile-wallet-connector:build:production",
"dev": false
}
}
},
"export": {
"executor": "@nx/next:export",
"options": {
"buildTarget": "mobile-wallet-connector:build:production"
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/mobile-wallet-connector/jest.config.ts"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
},
"tags": []
}
Empty file.
Binary file added apps/mobile-wallet-connector/public/favicon.ico
Binary file not shown.
9 changes: 9 additions & 0 deletions apps/mobile-wallet-connector/src/api/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Maps RTKQuery API endpoint names with name that
* back-end expects for recaptcha action argument.
*/
export const recaptchaEndpointActionMap: Record<string, string> = {
answerChallenge: "answer_challenge",
generateChallenge: "generate_challenge",
getQRCode: "qrcode",
};
1 change: 1 addition & 0 deletions apps/mobile-wallet-connector/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { newmApi } from "./newm/api";
14 changes: 14 additions & 0 deletions apps/mobile-wallet-connector/src/api/newm/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createApi } from "@reduxjs/toolkit/query/react";
import { baseUrls } from "../../buildParams";
import { axiosBaseQuery, prepareHeaders } from "../utils";

export const baseQuery = axiosBaseQuery({
baseUrl: baseUrls.newm,
prepareHeaders,
});

export const newmApi = createApi({
baseQuery,
endpoints: () => ({}),
reducerPath: "newmApi",
});
89 changes: 89 additions & 0 deletions apps/mobile-wallet-connector/src/api/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { BaseQueryApi } from "@reduxjs/toolkit/dist/query/baseQueryTypes";
import axios, { AxiosError, AxiosRequestConfig } from "axios";
import { executeRecaptcha } from "@newm-web/utils";
import { AxiosBaseQueryParams, BaseQuery } from "@newm-web/types";
import { recaptchaEndpointActionMap } from "./constants";

/**
* Returns recaptcha headers for unauthenticated requests.
*/
export const getRecaptchaHeaders = async (api: BaseQueryApi) => {
const { endpoint } = api;
const action = recaptchaEndpointActionMap[endpoint] || endpoint;

return {
"g-recaptcha-platform": "Web",
"g-recaptcha-token": await executeRecaptcha(action),
};
};

/**
* Adds necessary authentication headers to requests.
*/
export const prepareHeaders = async (
api: BaseQueryApi,
headers: AxiosRequestConfig["headers"]
) => {
const recaptchaHeaders = await getRecaptchaHeaders(api);

return {
...recaptchaHeaders,
...headers,
};
};

/**
* Sets up base query using axios request library (allows for tracking
* upload progress, which the native fetch library does not).
*/
export const axiosBaseQuery = (
{ baseUrl, prepareHeaders }: AxiosBaseQueryParams = { baseUrl: "" }
): BaseQuery => {
return async (
{ url, method, body, params, headers = {}, onUploadProgress },
api
) => {
try {
const axiosInstance = axios.create({
headers: prepareHeaders ? await prepareHeaders(api, headers) : headers,

// convert array params to comma separated strings
paramsSerializer: (params) => {
const searchParams = new URLSearchParams();
for (const key of Object.keys(params)) {
const param = params[key];
if (Array.isArray(param)) {
for (const p of param) {
searchParams.append(key, p);
}
} else {
searchParams.append(key, param);
}
}

return searchParams.toString();
},
});

const result = await axiosInstance({
data: body,
headers,
method,
onUploadProgress,
params,
url: baseUrl + url,
});

return { data: result.data };
} catch (axiosError) {
const err = axiosError as AxiosError;

return {
error: {
data: err.response?.data || err.message,
status: err.response?.status,
},
};
}
};
};
88 changes: 88 additions & 0 deletions apps/mobile-wallet-connector/src/app/connect-wallet/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"use client";
import { FunctionComponent, useEffect, useState } from "react";
import { Stack } from "@mui/material";
import { useRouter } from "next/navigation";
import { Form, Formik, FormikValues } from "formik";
import { useConnectWallet } from "@newm.io/cardano-dapp-wallet-connector";
import { Button, SwitchInputField } from "@newm-web/elements";
import { setToastMessage } from "../../modules/ui";
import { selectWallet, useConnectFromMobileThunk } from "../../modules/wallet";
import { useAppDispatch, useAppSelector } from "../../common";

const Page: FunctionComponent = () => {
const router = useRouter();
const dispatch = useAppDispatch();
const { isConnected, wallet } = useConnectWallet();
const { connectionData } = useAppSelector(selectWallet);

const [connectFromMobile, { isLoading: isConnectFromMobileLoading }] =
useConnectFromMobileThunk();

const [isFormSubmitDisabled, setIsFormSubmitDisabled] = useState(true);

const initialValues = {
isHardwareWallet: false,
};

const handleWalletConnect = async ({ isHardwareWallet }: FormikValues) => {
if (wallet) {
await connectFromMobile({
isHardwareWallet,
wallet,
});
} else {
dispatch(
setToastMessage({
message:
"There is a problem with your wallet. Please try disconnecting and reconnecting it.",
severity: "error",
})
);
}
};

useEffect(() => {
if (isConnected) {
setIsFormSubmitDisabled(false);
} else {
router.replace("/");
}
}, [isConnected, router]);

useEffect(() => {
if (connectionData.connectionId) {
router.push("/connect-wallet/scan-code");
}
}, [connectionData, router]);

return (
<>
<Formik initialValues={ initialValues } onSubmit={ handleWalletConnect }>
{ ({ isSubmitting }) => (
<Form>
<Stack alignItems="center" px={ [2, 2, 0] } rowGap={ 3.5 }>
<Button
disabled={ isFormSubmitDisabled || isSubmitting }
isLoading={ isConnectFromMobileLoading }
type="submit"
width="compact"
>
Connect from Mobile app
</Button>
<SwitchInputField
description={
"If you are trying to connect a hardware wallet, please select this option."
}
disabled={ isSubmitting }
name="isHardwareWallet"
title="HARDWARE WALLET"
/>
</Stack>
</Form>
) }
</Formik>
</>
);
};

export default Page;
Loading

0 comments on commit f6105f2

Please sign in to comment.