(Youtube)Amazon Clone Project(Day2)
금일 진행 사항
[Part4]The Login Page
- Build up login page with CSS
[Part5]User Authentication
- Manage State of Login by redux
[Part6]Deploying our App
- Build and Deploy this App by firebase
- Username / Email in Header
[Part7]The Payment Page
- Set up payment page with CSS
[Part8] Payment Processing
Front-Side
- Set up stripe API for payment function
- Set up axios.js for using 'baseURL' as endpoint of cloud functions
- Make payment logics in Payments.js by using clientSecret
Back-Side
- Set up cloud functions(full back-end) - Serverless Application
[Part5]User Authentication - Manage State of Login and Make register function
firebase.js
import firebase from 'firebase';
const firebaseConfig = {
apiKey: "AIzaSyDof3yqvoaz9dRNwlYfhHHX56EVyprbWS0",
authDomain: "challenge-cc017.firebaseapp.com",
projectId: "challenge-cc017",
storageBucket: "challenge-cc017.appspot.com",
messagingSenderId: "852584789925",
appId: "1:852584789925:web:51c29467aa116ee2599a4c",
measurementId: "G-K56QBW31E7",
};
const firebaseApp = firebase.initializeApp(firebaseConfig);
const db = firebaseApp.firestore();
const auth = firebase.auth();
export { db, auth };
Login.js
import React, { useState } from 'react'
import './Login.css'
import { Link, useHistory } from "react-router-dom"
import { auth } from './firebase'
function Login() {
const history = useHistory();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const signIn = (e) => {
e.preventDefault();
//make firebase login
auth
.signInWithEmailAndPassword(email, password)
.then(auth => {
history.push('/')
})
.catch(error => alert(error.message))
}
const register = (e) => {
e.preventDefault();
//make firebase register
auth
.createUserWithEmailAndPassword(email, password)
.then((auth) => {
//it successfully created a new user with email and password
// console.log(auth)
if(auth) {
history.push('/')
}
})
.catch(error => alert(error.message))
}
return (
<div className="login">
<Link to = '/'>
<img className="login__logo" src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a9/Amazon_logo.svg/1024px-Amazon_logo.svg.png" alt=""/>
</Link>
<div className="login__container">
<h1>Sign-in</h1>
<form action="">
<h5>E-mail</h5>
<input type="text" name="" id="" value={email} onChange={e => setEmail(e.target.value)}/>
<h5>Password</h5>
<input type="password" name="" id="" value={password} onChange={e =>setPassword(e.target.value)}/>
<button type="submit" className='login__signInButton' onClick={signIn}>Sign In</button>
</form>
<p>
By signing-in you agree to the AMAZON FAKE CLONE Conditions of Use & Sale. Please
see our Privacy Notice, our Cookies Notice and our Interest-Based Ads Notice.
</p>
<button className='login__registerButton' onClick={register}>Create your Amazon account</button>
</div>
</div>
)
}
export default Login
로그인 유지
App.js - auth 상태변화 감지하여 user login 상태 관리
import React, { useEffect } from 'react'
import { BrowserRouter as Router, Switch, Route } from "react-router-dom"
import { useStateValue } from './StateProvider'
import { auth } from './firebase'
import "./App.css";
import Header from './Header'
import Home from './Home'
import Checkout from './Checkout'
import Login from './Login'
import Payment from './Payment'
import Orders from './Orders'
import { Elements } from '@stripe/react-stripe-js'
import { loadStripe } from '@stripe/stripe-js';
const promise = loadStripe('pk_test_51Ik2QDKonIC4bazsf3Kn239pwoChdP7MkJgP1xcYhKCSAhjzSqGm1cE0ulRZYKU4DpNsq0QGycpWYfpgnfCFr72l0010ikyYrl')
function App() {
const [, dispatch] = useStateValue();
//Manage State of Log-in by Redux
useEffect(() => {
auth.onAuthStateChanged(authUser => {
if(authUser) {
// the user just logged in / the user was logged in
dispatch({
type: 'SET_USER',
user: authUser
})
} else {
// the user is logged out
dispatch({
type: 'SET_USER',
user: null
})
}
})
}, [dispatch])
return (
//BEM
<Router>
생략
</Router>
);
}
export default App;
Header.js - 로그아웃
import React from 'react';
import './Header.css';
import SearchIcon from '@material-ui/icons/Search';
import ShoppingBasketIcon from '@material-ui/icons/ShoppingBasket';
import { Link } from 'react-router-dom';
import { useStateValue } from './StateProvider'
import { auth } from './firebase';
function Header() {
const [{basket, user}] = useStateValue();
// console.log(basket.length);
const handleAuthentication = () =>{
if(user){
auth.signOut();
}
}
return (
<div className="header">
...생략...
</div>
)
}
export default Header
reducer.js
action.user === null or specific id
export const initialState = {
basket: [],
user: null
}
//Selector
export const getBasketTotal = (basket) =>
basket?.reduce((amount, item) => item.price + amount, 0);
//? function of '?'
const reducer = (state, action) => {
// console.log(action)
// console.log(state)
switch(action.type) {
...생략...
case 'SET_USER':
return {
...state,
user: action.user
}
default:
return state;
}
}
export default reducer;
[Part8] Payment Processing
1. Front-Side
- Set up stripe API for payment function(install stripe-related modules first)
- Set up axios.js for using 'baseURL' as endpoint of cloud functions
- Make payment logics in Payments.js by using clientSecret
stripe.js
payment infrastructure
App.js
import React, { useEffect } from 'react'
import { BrowserRouter as Router, Switch, Route } from "react-router-dom"
import { useStateValue } from './StateProvider'
import { auth } from './firebase'
import "./App.css";
import Header from './Header'
import Home from './Home'
import Checkout from './Checkout'
import Login from './Login'
import Payment from './Payment'
import Orders from './Orders'
import { Elements } from '@stripe/react-stripe-js'
import { loadStripe } from '@stripe/stripe-js';
//publishable key from stripe.com
const promise = loadStripe('pk_test_51Ik2QDKonIC4bazsf3Kn239pwoChdP7MkJgP1xcYhKCSAhjzSqGm1cE0ulRZYKU4DpNsq0QGycpWYfpgnfCFr72l0010ikyYrl')
function App() {
const [, dispatch] = useStateValue();
...생략...
return (
//BEM
<Router>
<div className="app">
<Switch>
...생략...
<Route path="/payment">
<Header />
<Elements stripe={promise}>
<Payment/>
</Elements>
</Route>
...생략...
</Switch>
</div>
</Router>
);
}
export default App;
Payment.js
const handleSubmit = async e => {
e.preventDefault(); //prevent refreshing
setProcessing(true);
//clientSecret: how much money to charge
const payload = await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: elements.getElement(CardElement)
}
}).then(({paymentIntent}) => {
//paymentIntent = payment confirmation
db
.collection('users')
.doc(user?.uid)
.collection('orders')
.doc(paymentIntent.id)
.set({
basket: basket,
amount: paymentIntent.amount,
created: paymentIntent.created
})
setSucceeded(true);
setError(null);
setProcessing(false);
dispatch({
type: 'EMPTY_BASKET'
})
history.replace('/orders') //don't come back to payment page
})
}
1) CardElement
A flexible single-line input that collects all necessary card details.
// Get a reference to a mounted CardElement. Elements knows how
// to find your CardElement because there can only ever be one of
// each type of element.
const cardElement = elements.getElement(CardElement);
2) useStripe
3) useElements
4) clientSecret
The client secret is a unique key returned from Stripe as part of a PaymentIntent. This key lets the client access important fields from the PaymentIntent (e.g., status) while hiding sensitive ones (e.g., amount).
import axios from './axios';
useEffect(() => {
//generate the special stripe secret which allows us to charge a customer
const getClientSecret = async () =>{
const response = await axios({
method:'post',
//Stripe expects the total in a currencies subunits. dollars => * 100
url: `/payments/create?total=${getBasketTotal(basket) * 100}`
});
// console.log(response.data.clientSecret);
setClientSecret(response.data.clientSecret);
}
getClientSecret();
}, [basket]);
axios.js
import axios from 'axios'
const instance = axios.create({
baseURL: "https://us-central1-challenge-cc017.cloudfunctions.net/api" // THE API(cloud function) URL
// baseURL: "http://localhost:5001/challenge-cc017/us-central1/api" // THE API Emulater URL
});
export default instance;
baseURL ? 파이어베이스 클라우드 서버의 URL
functions - index.js (파이어베이스 클라우드 서버)
require("dotenv").config();
const functions = require("firebase-functions");
const express = require("express");
const cors = require("cors");
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
// API
// App config
const app = express();
// Middlewares
app.use(cors({origin: true}));
app.use(express.json());
// API routes
app.get("/", (req, res) => res.status(200).send("hello world"));
app.post("/payments/create", async (req, res) => {
const total = req.query.total;
console.log("Payment Request Received!! >>> ", total);
const paymentIntent = await stripe.paymentIntents.create({
amount: total,
currency: "usd",
});
// Ok - Created
// console.log(paymentIntent);
res.status(201).send({
clientSecret: paymentIntent.client_secret,
});
});
// Listen command
exports.api = functions.https.onRequest(app);
// Example Endpoint
// http://localhost:5001/challenge-cc017/us-central1/api
5) paymentIntent
A PaymentIntent guides you through the process of collecting a payment from your customer. We recommend that you create exactly one PaymentIntent for each order or customer session in your system. You can reference the PaymentIntent later to see the history of payment attempts for a particular session.
paymentIntent Objects
{
"id": "pi_1DoRpv2eZvKYlo2CpdwZmJrT",
"object": "payment_intent",
"amount": 1099,
"amount_capturable": 0,
"amount_received": 0,
"application": null,
"application_fee_amount": null,
"canceled_at": null,
"cancellation_reason": null,
"capture_method": "automatic",
"charges": {
"object": "list",
"data": [],
"has_more": false,
"url": "/v1/charges?payment_intent=pi_1DoRpv2eZvKYlo2CpdwZmJrT"
},
"client_secret": "pi_1DoRpv2eZvKYlo2CpdwZmJrT_secret_yfTgUAdOksvMkOCKQS6wyLy0w",
"confirmation_method": "automatic",
"created": 1546505155,
"currency": "eur",
"customer": null,
"description": null,
"invoice": null,
"last_payment_error": null,
"livemode": false,
"metadata": {},
"next_action": null,
"on_behalf_of": null,
"payment_method": null,
"payment_method_options": {},
"payment_method_types": [
"card"
],
"receipt_email": null,
"review": null,
"setup_future_usage": null,
"shipping": null,
"statement_descriptor": null,
"statement_descriptor_suffix": null,
"status": "requires_payment_method",
"transfer_data": null,
"transfer_group": null
}
CurrencyFormat(module/library)
<CurrencyFormat
renderText={(value) => (
<h3>
Order Total: {value}
</h3>
)}
decimalScale={2}
value={getBasketTotal(basket)}
displayType={"text"}
thousandSeparator={true}
prefix={"$"}
/>
For more detail: www.npmjs.com/package/react-currency-format
2. Back-Side
- Set up cloud functions(full back-end) - Serverless Application
require("dotenv").config();
const functions = require("firebase-functions");
const express = require("express");
const cors = require("cors");
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
// API
// App config
const app = express();
// Middlewares
app.use(cors({origin: true}));
app.use(express.json());
// API routes
app.get("/", (req, res) => res.status(200).send("hello world"));
app.post("/payments/create", async (req, res) => {
const total = req.query.total;
console.log("Payment Request Received!! >>> ", total);
const paymentIntent = await stripe.paymentIntents.create({
amount: total,
currency: "usd",
});
// Ok - Created
// console.log(paymentIntent);
res.status(201).send({
clientSecret: paymentIntent.client_secret,
});
});
// Listen command
exports.api = functions.https.onRequest(app);
// Example Endpoint
// http://localhost:5001/challenge-cc017/us-central1/api
dashboard.stripe.com
payments 기록을 확인할 수 있다.