diff --git a/.DS_Store b/.DS_Store
index d2e004d5e..c7a5f3616 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/src/.DS_Store b/src/.DS_Store
new file mode 100644
index 000000000..415dac817
Binary files /dev/null and b/src/.DS_Store differ
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 = () => (
+
+

+
+ );
+
+ 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 (
+ <>
+
+
+ >
+ )
+}
+
+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 (
+ <>
+
+
+ >
+ );
+}
\ 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 (
+ <>
+
+
+ >
+ );
+};
+
+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