From 01967b80c33425f26a22799819ecc1e1b66d91ce Mon Sep 17 00:00:00 2001 From: Gizmotronn Date: Fri, 20 May 2022 15:46:32 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=92=F0=9F=93=A1=20->=20Adding=20#1=20p?= =?UTF-8?q?rofile=20section=20to=20magic.link=20for=20prev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 6148 -> 8196 bytes src/.DS_Store | Bin 0 -> 6148 bytes src/app.js | 2 +- src/components/layout.js | 23 +++ src/components/loading.js | 7 + src/components/login-form.js | 53 ++++++ src/components/login.js | 67 ++++++++ src/components/payment-form.js | 271 ++++++++++++++++++++++++++++++ src/components/payment.js | 41 +++++ src/components/premium-content.js | 105 ++++++++++++ src/components/profile.js | 45 +++++ src/components/signup-form.js | 53 ++++++ src/components/signup.js | 72 ++++++++ src/package.json | 44 +++++ src/server.js | 94 +++++++++++ 15 files changed, 876 insertions(+), 1 deletion(-) create mode 100644 src/.DS_Store create mode 100644 src/components/layout.js create mode 100644 src/components/loading.js create mode 100644 src/components/login-form.js create mode 100644 src/components/login.js create mode 100644 src/components/payment-form.js create mode 100644 src/components/payment.js create mode 100644 src/components/premium-content.js create mode 100644 src/components/profile.js create mode 100644 src/components/signup-form.js create mode 100644 src/components/signup.js create mode 100644 src/package.json create mode 100644 src/server.js diff --git a/.DS_Store b/.DS_Store index d2e004d5e30250c52620fdcec7185edeb14eb12b..c7a5f361636ba2d7b72ced9b0d50833e966ec3ce 100644 GIT binary patch literal 8196 zcmeHMO=}ZD7=EWs+f7wkLA0X6UcA-P#8~M;h%rSf2x3GJD(!AICTll4VY4x4jO6as zlY)Q2pWwxVc=P1JU*J#Bi$3#VOg5PVTV{6Q7>?xE~_*B^g(^RHY45sK9 zw}m+{ud%04(TOQKF+DTW848oLgJ(-~VtIw8H3}F7Mimgb`y7?YL9Gqt@8XAa8FlTu zERuQ@HV#GPNsMU?EKVUkAfKwZdPvQIF95>Bn-LEUIoBTADplwl1@KU&J;fH@hCQ$| z2@i*9*n^yP4DWe@HxFKmT+HWFIWfoAgl(E`(iV1Kr0c*ns*^X&n5iZh3)I9eBKU4l zJfeco@%4a?v#^emtB_iav2xqZhW@7V!EFS7GhSc+#AaqwbMp(9wP-CKS-fAW@pdV8 z;#RZZM0ds24JWR-x_^uFpx<}g+kU0JvwZ9d4`asbzLgJGV}MD_*^J>YY$h^XlCD&1j>xpVvpFC=7Tg zPEf2ah4ZT5JmTRKmT@O1ffJv=p0YE}W^&n8 zd*edxug`eGzQ>&X1jb-Yy#M$)_g2NkMAolZ`E%Bo7@9CoBDN3&rWggLRDoHwt`(90 zZ|(p6e@a(xmSq$$3j7NNSgKem7U1dUA8SdcBGP%4%D9-FgF}!RXb2Dpa03ZfkhL2NzcWwfmvIES3uG}1h-QK~3@o-eo@Wj-0Kk|e AI{*Lx diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..415dac8173514c354e051c58d105b3873478738b GIT binary patch literal 6148 zcmeHKJ5B>J6ddORA*4u2xhIIk4VEY%QBiOK@>75$EZXo%xuWF`w3KiVZb3_hXlQ_W zwuQVKQqZ6Xy|=Q*YrpZc$69+0z_c$1+dvaQjY+UlXZC|hyQBpx`I-jN*gj4%+n*%; z-X!TQcx#0!pbD&-0`l#yp^I}qE#$vnZVKj^?XRESyzdM%3~`bB)@1{EU}`y6mVi3? zNHAs33C_qkTjZ2(=yxCVzm5)eaKN$j8716JWHh(*4kI+Z_a0KtVagcSjFn=%g#Z2v zKDEF+*v%NfG%D~LKK=>kHemF*)3C_njDWj9nJSJs&V=JE%+P8Wde%_ryD*|EdwvS^ zD`P@NFYjd%dOZeS4X#O-3XhqOIBvW0vPyYgX-M8%z}v7ZpW=#~)UommypZt`F!-vr zq6+ZL7OQPK)La!%1yq4=1!RASm;_^wnM1pEFtb+xVujV#Se9P_;Y1!|kC{WZ(2S!J z9o6_NhH-S(N8T^?m^pNG7#o@W_{hfJP>hYv`pCM uA1yq5&0%iBwl=FXg`Tajn z(l=E=75G;Qm`dlc)8>@?*}5<}Icp=PBPKEN%N$w?Gk+Z0f*i$LOxD;INr4!9%p9_Y PrauB!2F+A~KULrh2R6Zg literal 0 HcmV?d00001 diff --git a/src/app.js b/src/app.js index 2f3557831..60ad8cd80 100644 --- a/src/app.js +++ b/src/app.js @@ -18,7 +18,7 @@ import Layout from "./components/layout"; // Magic/stripe import { magic } from "./lib/magic"; import { loadStripe } from "@stripe/stripe-js"; -import { ELements } from "@stripe/react-stripe-js"; +import { Elements } from "@stripe/react-stripe-js"; const promise = loadStripe(process.env.REACT_APP_STRIPE_PK_KEY); diff --git a/src/components/layout.js b/src/components/layout.js new file mode 100644 index 000000000..e854b9bb2 --- /dev/null +++ b/src/components/layout.js @@ -0,0 +1,23 @@ +import Header from "./header"; + +const Layout = ({ children }) => ( + <> +
+
+
{children}
+
+ + +); + +export default Layout; \ No newline at end of file diff --git a/src/components/loading.js b/src/components/loading.js new file mode 100644 index 000000000..f90ce846b --- /dev/null +++ b/src/components/loading.js @@ -0,0 +1,7 @@ +const Loading = () => ( +
+ Loading +
+ ); + + export default Loading; \ No newline at end of file diff --git a/src/components/login-form.js b/src/components/login-form.js new file mode 100644 index 000000000..359811893 --- /dev/null +++ b/src/components/login-form.js @@ -0,0 +1,53 @@ +// Note - update this with a highly modern, minimal form +import { useState } from 'react'; +import { Input, Icon, MonochromeIcons, CallToAction } from '@chakra-ui/react'; + +const LoginForm = ({ onEmailSubmit, disabled }) => { + const [email, setEmail] = useState(""); + + const handleSubmit = async (e) => { + e.preventDefault(); + onEmailSubmit(email); + }; + + return ( + <> +
+

Log in

+
+ setEmail(e.target.value)} + prefix={} + /> +
+
+ + Login + +
+
+ + + ) +} + +export default LoginForm; \ No newline at end of file diff --git a/src/components/login.js b/src/components/login.js new file mode 100644 index 000000000..edfa72eda --- /dev/null +++ b/src/components/login.js @@ -0,0 +1,67 @@ +import { useState, useEffect, useContext } from "react"; +import { useHistory } from "react-router-dom"; +import { magic } from "../lib/magic"; +import { UserContext } from "../lib/UserContext"; +import LoginForm from "./login-form"; + +const Login = () => { + const history = useHistory(); + const [disabled, setDisabled] = useState(false); + const [user, setUser] = useContext(UserContext); + + // If user is already logged in: redirect to profile page + useEffect(() => { + user && user.issuer && history.push("/profile"); + }, [user, history]); + + async function handleLoginWithEmail(email) { + try { + setDisabled(true); // Prevent multiple emails from being triggered by disabling the login button + + // Trigger Magic.Link email to user + let didToken = await magic.auth.loginWithMagicLink({ + email, + }); + + // Validate didToken with server + const res = await fetch(`${process.env.REACT_APP_SERVER_URL}/login`, { + method: "POST", + headers: { + 'Content-Type': 'application/json', + Authorization: "Bearer " + didToken, + }, + }); + + if (res.status === 200) { + let userMetadata = await magic.user.getMetadata(); // save information for the logged in user + await setUser(userMetadata); // UserContext is set to the logged in user + history.push('/profile'); + } + } catch (error) { + setDisabled(false); // Reenable login button (if the user requests to edit their email) + console.log(error); + } + } + + return ( + <> +
+ +
+ + + ); +}; + +export default Login; \ No newline at end of file diff --git a/src/components/payment-form.js b/src/components/payment-form.js new file mode 100644 index 000000000..88f107e92 --- /dev/null +++ b/src/components/payment-form.js @@ -0,0 +1,271 @@ +import React, { useState, useEffect, useContext } from 'react'; +import { CardElement, useStripe, useElements } from "@stripe/react-stripe-js"; +import { useHistory } from "react-router-dom"; +import { LifetimeContext } from "../lib/LifetimeContext"; + +export default function PaymentForm({ email }) { + const [succeeded, setSucceeded] = useState(false); + const [error, setError] = useState(null); + const [processing, setProcessing] = useState(""); + const [disabled, setDisabled] = useState(true); + const [clientSecret, setClientSecret] = useState(""); + + const [customerId, setCustomerId] = useState(""); + const [, setLifetimeAccess] = useContext(LifetimeContext); + + const stripe = useStripe(); + const elements = useElements(); + const history = useHistory(); + + useEffect(() => { + // Create a paymentintent as soon as the page loads + fetch(`{process.env.REACT_APP_SERVER_URL}/create-payment-intent`, { + method: "POST", + headers: { + 'Content-Type': "application/json", + }, + body: JSON.stringify({ email }) + }) + .then((res) => { + return res.json(); + }) + .then((data) => { + setClientSecret(data.clientSecret); + setCustomerId(data.customer); + setLifetimeAccess(true); + }); + }, [email]); + + const cardStyle = { + style: { + base: { + color: "#32325d", + fontFamily: "Arial, sans-serif", + fontSmoothing: "antialiased", + fontSize: "16px", + "::placeholder": { + color: "#32325d", + }, + }, + invalid: { + color: "#fa755a", + iconColor: "#fa755a", + }, + }, + }; + + const handleChange = async (event) => { + // Listen for changes in the card element. Display errors as the user types card details + setDisabled(event.empty); + setError(event.error ? event.error.message : ""); + }; + + const handleSubmit = async (ev) => { + ev.preventDefault(); + setProcessing(true); + const payload = await stripe.confirmCardPayment(clientSecret, { + payment_method: { + card: elements.getElement(CardElement), + }, + }); + + if (payload.error) { + setError(`Payment failed ${payload.error.message}`); + setProcessing(false); + } else { + setError(null); + setProcessing(false); + setSucceeded(true); + + // Update Stripe customer info to include metadata (determine if lifetimeAccess = true) + fetch(`${process.env.REACT_APP_SERVER_URL}/update-customer`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ customerId }), + }) + .then((res) => { + return res.json(); + }) + .then((Data) => { + console.log("Updated Stripe customer object: ", data); + history.push("/premium-content"); + }); + } + }; + + return ( + <> +
+ + + {/* Show any error that happens when processing the payment */} + {error && ( +
+ {error}confirmCardPayment +
+ )} + + + + ); +} \ No newline at end of file diff --git a/src/components/payment.js b/src/components/payment.js new file mode 100644 index 000000000..571519d6a --- /dev/null +++ b/src/components/payment.js @@ -0,0 +1,41 @@ +import { useContext, useEffect } from "react"; +import { useHistory } from "react-router"; +import { UserContext } from "../lib/UserContext"; +import Loading from "./loading"; + +export default function Payment({ Elements, PaymentForm, promise }) { + const [user] = useContext(UserContext); + const history = useHistory(); + + // If not loading & no user found, redirect to /login page + useEffect(() => { + user && !user.loading && !user.issuer && history.push("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/login"); + }, [user, history]); + + return ( + <> +

+ Purchase Star Sailors to join the club +

+

+ Hi {user?.loading ? : user?.email}! You've signed up with your email account - now enter your card information. Currently, payment is conducted through our custom ERC20 token, or in Australian Dollars using the Stripe secure payment system +

+ {user?.loading ? ( + + ) : ( + + + + )} + + + ); +} \ No newline at end of file diff --git a/src/components/premium-content.js b/src/components/premium-content.js new file mode 100644 index 000000000..872b4c615 --- /dev/null +++ b/src/components/premium-content.js @@ -0,0 +1,105 @@ +import React, { useContext, useEffect } from "react"; +import { useHistory } from "react-router-dom"; +import { UserContext } from "../lib/UserContext"; +import { LifetimeContext } from "../lib/LifetimeContext"; +import { LifetimeAccessRequestStatusContext } from "../lib/LifetimeAccessRequestStatusContext"; +import { CallToAction } from "@magiclabs/ui"; + +const PremiumContent = () => { + const [user] = useContext(UserContext); + const [lifetimeAccess, setLifetimeAccess] = useContext(LifetimeContext); + const [ + lifetimeAccessRequestStatus, + setLifetimeAccessRequestStatus + ] = useContext(LifetimeAccessRequestStatusContext); + const history = useHistory(); + + // Check if the user has paid (and thus has the membership) + useEffect(() => { + if (user && !user.loading && user.issuer && !lifetimeAccessRequestStatus) { + setLifetimeAccessRequestStatus("Pending"); + window + .fetch(`${process.env.REACT_APP_SERVER_URL}/validate-customer`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email: user.email }), + }) + .then((res) => { + return res.json(); + }) + .then((data) => { + setLifetimeAccessRequestStatus("Complete"); + if ( + data.customer.length && + data.customer[0].metadata.lifetimeAccess === "true" + ) { + setLifetimeAccess(true); + } + }); + } + }); + + return ( + <> + { + // Display Premium Content if the user's logged in & have already paid for lifetime access + user && lifetimeAccess ? ( + <> +

Here's all of our content only available for our members!

+
♡ PREMIUM .
+
♡ PREMIUM .
+
♡ PREMIUM .
+
♡ PREMIUM .
+ + ) : ( + <> +

So you want our PREMIUM content? 😎

+
🧠 Our premium content includes some AWESOME stuff.
+ +
+ 🪄 To access the awesomeness, purchase a Lifetime Access Pass + below. +
+ +
💸 The Awesome Lifetime Access Pass is only $500!
+ { + // If the user is logged in, go straight to payment + user && user.issuer ? ( + history.push("/payment")} + > + Count Me In + + ) : ( + // Otherwise, ask the user to sign up first + history.push("/signup")} + > + Count Me In + + ) + } + + ) + } + + + ); +} + +export default PremiumContent; \ No newline at end of file diff --git a/src/components/profile.js b/src/components/profile.js new file mode 100644 index 000000000..9b5c54927 --- /dev/null +++ b/src/components/profile.js @@ -0,0 +1,45 @@ +import { useEffect, useContext } from "react"; +import { useHistory } from "react-router-dom"; +import { UserContext } from "../lib/UserContext"; +import Loading from "./loading"; + +const Profile = () => { + const history = useHistory(); + const [user] = useContext(UserContext); + + // Redirect to login page if no [logged-in] user is found + useEffect(() => { + user && !user.loading && !user.issuer && history.push("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/login"); + }, [user, history]); + + return ( + <> + {user?.loading ? ( + + ) : ( + user?.issuer && ( + <> +
Email
+
{user.email}
+ +
User Id
+
{user.issuer}
+ + ) + )} + + + ); +}; + +export default Profile; \ No newline at end of file diff --git a/src/components/signup-form.js b/src/components/signup-form.js new file mode 100644 index 000000000..878c3bb9e --- /dev/null +++ b/src/components/signup-form.js @@ -0,0 +1,53 @@ +import { useState } from "react"; +import { Input, Icon, MonochromeIcons, CallToAction } from "@magiclabs/ui"; + +const SignUpForm = ({ onEmailSubmit, disabled }) => { + const [email, setEmail] = useState(""); + + const handleSubmit = async (e) => { + e.preventDefault(); + onEmailSubmit(email); + }; + + return ( + <> +
+

Sign Up

+
+ setEmail(e.target.value)} + prefix={} + /> +
+
+ + Sign up + +
+
+ + + ); +}; + +export default SignUpForm; \ No newline at end of file diff --git a/src/components/signup.js b/src/components/signup.js new file mode 100644 index 000000000..2dac68461 --- /dev/null +++ b/src/components/signup.js @@ -0,0 +1,72 @@ +import { useState, useContext } from "react"; +import { useHistory } from "react-router-dom"; +import { magic } from "../lib/magic"; +import { UserContext } from "../lib/UserContext"; +import SignUpForm from "./signup-form"; + +const SignUp = () => { + const history = useHistory(); + const [disabled, setDisabled] = useState(false); + const [, setUser] = useContext(UserContext); + + async function handleSignUpWithEmail(email) { + try { + setDisabled(true); // Disable sign up button to prevent multiple emails from being triggered + + // Trigger Magic link to be sent to user + let didToken = await magic.auth.loginWithMagicLink({ + email, + }); + + // Validate didToken with server + const res = await fetch(`${process.env.REACT_APP_SERVER_URL}/login`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + didToken, + }, + }); + + if (res.status === 200) { + // Set the UserContext to the now signed up user + let userMetadata = await magic.user.getMetadata(); + await setUser(userMetadata); + history.push("/payment"); + } + } catch (error) { + setDisabled(false); // Re-enable sign up button - user may have requested to edit their email + console.log(error); + } + } + + return ( + <> +

Sign Up for Lifetime Access Pass 👩🏻‍💻

+

+ YAY! We're excited for you to sign up for a Lifetime Access Pass to + awesomeness. First, please sign in below to register your new account: +

+
+ +
+ + + ); +}; + +export default SignUp; \ No newline at end of file diff --git a/src/package.json b/src/package.json new file mode 100644 index 000000000..ac2dee924 --- /dev/null +++ b/src/package.json @@ -0,0 +1,44 @@ +{ + "name": "sk-club", + "version": "0.1.0", + "private": true, + "dependencies": { + "@magiclabs/ui": "0.8.0", + "@stripe/react-stripe-js": "^1.2.2", + "@stripe/stripe-js": "^1.12.1", + "magic-sdk": "^4.0.2", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-router-dom": "^5.2.0", + "react-scripts": "4.0.1", + "web-vitals": "^0.2.4", + "yarn": "^1.22.10" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "framer-motion": "^2.3.0" + } + } \ No newline at end of file diff --git a/src/server.js b/src/server.js new file mode 100644 index 000000000..f3cbad1da --- /dev/null +++ b/src/server.js @@ -0,0 +1,94 @@ +require("dotenv").config(); // enables loading .env vars +const express = require("express"); +const app = express(); +const path = require("path"); +const cors = require("cors"); +const bodyParser = require("body-parser"); +app.use(bodyParser.json()); + +// Import, then initiate Magic instance for server-side methods +const { Magic } = require("@magic-sdk/admin"); +const magic = new Magic(process.env.MAGIC_SECRET_KEY); + +// Import & initiate Stripe instance +const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); + +// Allow requests from client-side +app.use(cors({ origin: process.env.CLIENT_URL })); + +// Route to validate the user's DID token +app.post("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/login", async (req, res) => { + try { + const didToken = req.headers.authorization.substr(7); + await magic.token.validate(didToken); + res.status(200).json({ authenticated: true }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Add the user to your list of customers +// Then create a PaymentIntent to track the customer's payment cycle +app.post("/create-payment-intent", async (req, res) => { + const { email } = req.body; + + const paymentIntent = await stripe.customers + .create({ + email, + }) + .then((customer) => + stripe.paymentIntents + .create({ + amount: 50000, // Replace this constant with the price of your service + currency: "usd", + customer: customer.id, + }) + .catch((error) => console.log("error: ", error)) + ); + + res.send({ + clientSecret: paymentIntent.client_secret, + customer: paymentIntent.customer, + }); +}); + +// Update the customer's info to reflect that they've +// paid for lifetime access to your Premium Content +app.post("/update-customer", async (req, res) => { + const { customerID } = req.body; + + const customer = await stripe.customers.update(customerID, { + metadata: { lifetimeAccess: true }, + }); + + res.send({ + customer, + }); +}); + +// Collect the customer's information to help validate +// that they've paid for lifetime access +app.post("/validate-customer", async (req, res) => { + const { email } = req.body; + + const customer = await stripe.customers.list({ + limit: 1, + email, + }); + + res.send({ + customer: customer.data, + }); +}); + +/* For heroku deployment */ +if (process.env.NODE_ENV === "production") { + app.use(express.static("client/build")); + app.get("*", (req, res) => { + res.sendFile(path.resolve(__dirname, "client", "build", "index.html")); + }); +} + +const listener = app.listen(process.env.PORT || 8080, function () { + console.log("Listening on port " + listener.address().port); +}); \ No newline at end of file