nick's cart
click pay with card, fill email, address
fill card info, pay
transaction processed, receipt page opens
nick's cart is emptied
stripe keeps track payments, click top one
payment method displayed
followed by Metadata that contains sales of items
#powershellpip install stripe
-----------------------------
#django/urls
path('cart_checkout/', apiview.CartCheckout.as_view(), name='CartCheckout'),
-----------------------------------
sign up stripe to obtain keys
#django/apiview
class CartCheckout(APIView):
def post(self, request, format=None):
stripe.api_key = settings.STRIPE_SECRET_KEY
token = request.data.get('token')
cart = request.data.get('cart')
try:
customer=stripe.Customer.create(email=token['email'],source=token['id'])
charge=stripe.Charge.create(
amount=cart['price']*100,
currency='cad',
receipt_email= token['email'],
customer=customer.id,
metadata=cart['items'],
)
return Response(charge, status=status.HTTP_202_ACCEPTED)
except Exception as e:
return Response(e, status=status.HTTP_400_BAD_REQUEST)
-------------------------------
//react/pages/shoppingCart
import React, { Component } from 'react';
import '../App.css';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { getShoppingItems } from '../redux/actions/getShoppingIItems';
import { getAlbums } from '../redux/actions/getAlbums';
import { getUsers } from '../redux/actions/getUser';
import { putShoppingItem } from '../redux/actions/putShoppingItem';
import { deleteShoppingItem } from '../redux/actions/deleteShoppingItem'
import { checkoutCart } from '../redux/actions/checkoutCart'
import { Input, message, Tag } from 'antd';
import { Button } from 'reactstrap';
import { MdExposureNeg1, MdExposurePlus1 } from "react-icons/md";
import Stripecheckout from 'react-stripe-checkout';
class ShoppingCart extends Component {
constructor(props) {
super(props);
this.state = {
qty: [],
qty_input: [],
qty_loaded: false,
};
}
componentDidMount() {
//start fetching database once logged in
if (this.props.loggedin) {
if (!this.props.gotAlbums) {
this.props.dispatch(getAlbums(this.props.token));
}
if (!this.props.gotUsers) {
this.props.dispatch(getUsers(this.props.token));
}
if (!this.props.gotShoppingItems) {
this.props.dispatch(getShoppingItems(this.props.token));
}
//wait for 5sec, check every 0.1s to see if shoppingItems are fetched
let i = 0;
const waitShoppingItem = setInterval(async () => {
if (this.props.gotShoppingItems) {
await this.setState({ qty: [], qty_input: [], qty_loaded: false })
this.props.shoppingItems.map(async (item, index) => {
await this.setState(prevState => {
return {
qty: prevState.qty.concat({ album_id: item.album, qty: item.quantity }),
qty_input: prevState.qty_input.concat({ album_id: item.album, qty: null })
}
})
if (this.state.qty.length === this.props.shoppingItems.length) { this.setState({ qty_loaded: true }) }
})
clearInterval(waitShoppingItem);
}
if (i == 50) {
message.error('fetching shopping items timed out.')
clearInterval(waitShoppingItem);
}
i++;
}, 100)
}
}
add = async (album_id) => {
await this.setState(prevState => { return { qty_input: prevState.qty_input.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: null }) } });
this.setState(prevState => { return { qty: prevState.qty.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: prevState.qty.find(item => item.album_id === album_id).qty + 1 }) } })
}
subtract = async (album_id) => {
await this.setState(prevState => { return { qty_input: prevState.qty_input.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: null }) } });
if (this.state.qty.find(item => item.album_id === album_id).qty > 1) {
this.setState(prevState => { return { qty: prevState.qty.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: prevState.qty.find(item => item.album_id === album_id).qty - 1 }) } })
}
}
inputChange = (e, album_id) => {
if (e.target.value > 0) {
const newQty = parseInt(e.target.value)
this.setState(prevState => { return { qty: prevState.qty.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: newQty }) } })
this.setState(prevState => { return { qty_input: prevState.qty_input.filter(item => item.album_id !== album_id).concat({ album_id: album_id, qty: newQty }) } })
} else {
message.error('entered invalid value')
}
}
deleteItem = (id) => {
this.props.dispatch(deleteShoppingItem(this.props.token, id));
//wait for 5sec, check every sec to see if delete successful
let i = 0;
const waitDelete = setInterval(() => {
if (this.props.shoppingItemDeleted) {
clearInterval(waitDelete);
}
if (i == 50) {
message.error('connection timed out.')
clearInterval(waitDelete);
}
i++;
}, 100)
}
updateCart = () => {
this.state.qty.map((item, index) => {
//only update items with qty changed
const oldItem = this.props.shoppingItems.find(i => i.album === item.album_id)
if (item.qty !== oldItem.quantity) {
const formData = new FormData();
formData.set('shopper', this.props.users.find(user => user.username === this.props.username).id);
formData.set('album', item.album_id);
formData.set('quantity', item.qty);
this.props.dispatch(putShoppingItem(this.props.token, oldItem.id, formData));
//wait for 5sec, check every sec to see if updateShoppingItem successful
let i = 0;
const waitUpdate = setInterval(() => {
const newItem = this.props.shoppingItems.find(i => i.album === item.album_id)
if (newItem.quantity === item.qty) {
message.success('Updated quantity of ' + this.props.albums.filter(album => album.id === item.album_id)[0].album_title + ' to ' + item.qty)
clearInterval(waitUpdate);
}
if (i == 50) {
message.error('connection timed out.')
clearInterval(waitUpdate);
}
i++;
}, 100)
}
})
}
checkout = (token) => {
if (this.state.qty_loaded) {
let total = 0, i = 0, items = {}
this.props.shoppingItems.map((item, index) => {
const album = this.props.albums.find(album => album.id === item.album)
total = total + album.price * item.quantity
i++
items['item' + i.toString()] = album.album_title + ' $' + album.price + ' x ' + item.quantity
})
if (i === this.props.shoppingItems.length) {
const cartName = this.props.username + "'s Cart"
const cart = { name: cartName, price: total, items: items }
this.props.dispatch(checkoutCart(this.props.token, { token, cart }))
}
}
else {
message.error('please wait for cart info been retrieved')
}
//wait for 5sec, check every sec to see if checkout successful
let i = 0;
const waitCheckout = setInterval(() => {
if (this.props.checkedout) {
message.success('payment received')
//display receipt
window.open(this.props.receipt, '_blank')
clearInterval(waitCheckout);
}
if (i == 50) {
message.error('connection timed out.')
clearInterval(waitCheckout);
}
i++;
}, 100)
}
render() {
if (!this.props.loggedin) {
return <Redirect to='/login' />
}
let total = 0
return (
<div style={{ padding: '10px', marginTop: '10px' }}>
<legend>Shopping Cart</legend>
<hr />
<div style={{ color: 'red' }}>{this.props.errorShoppingItem} {this.props.errorAlbum}</div>
{
this.state.qty_loaded ?
this.props.shoppingItems
.sort((a, b) => { return this.props.albums.find(album => album.id === a.album).album_title.toUpperCase().localeCompare(this.props.albums.find(album => album.id === b.album).album_title.toUpperCase()) })
.map((item, index) => {
const album = this.props.albums.find(album => album.id === item.album)
total = total + album.price * item.quantity
return <div key={index}>
<div style={{ width: '90%', display: 'flex', justifyContent: 'space-between' }}>
<div style={{ width: '90%', display: 'flex', justifyContent: 'space-between' }}>
<span style={{ fontStyle: 'italic' }}>{album.album_title} <Tag color='geekblue'>${album.price}</Tag></span>
<Input
type='number'
onChange={(e) => { this.inputChange(e, item.album) }}
style={{ width: '150px' }}
addonBefore={<MdExposureNeg1 onClick={() => this.subtract(item.album)} style={{ cursor: 'pointer', fontSize: '20px' }} />}
addonAfter={<MdExposurePlus1 onClick={() => this.add(item.album)} style={{ cursor: 'pointer', fontSize: '20px' }} />}
value={this.state.qty_input.find(i => i.album_id === item.album).qty || this.state.qty.find(i => i.album_id === item.album).qty}
/>
</div>
<b style={{ textAlign: 'right' }} onClick={() => this.deleteItem(item.id)} style={{ cursor: 'pointer' }}>X</b>
</div>
<hr />
</div>
})
: null
}
<div style={{ width: '90%', display: 'flex', justifyContent: 'space-between' }}>
<span>Total: <Tag color='geekblue'>${total}</Tag></span>
<div>
<Button color='success' size='sm' onClick={() => this.updateCart()} style={{ marginRight: '15px' }}>Update</Button>
<Stripecheckout
stripeKey='pk_test_i8PD1nMcLZvtkCghkFKU1r0d00P9v7L9wg'
token={this.checkout}
billingAddress
shippingAddress
amount={total * 100}
name={this.props.username + "'s Cart"}
currency='CAD'
/>
</div>
</div>
</div>
);
}
}
export default connect(
(store) => {
return {
loggedin: store.login.fetched,
gotShoppingItems: store.shoppingItems.fetched,
shoppingItems: store.shoppingItems.shoppingItems,
errorShoppingItem: store.shoppingItems.error,
shoppingItemUpdated: store.shoppingItems.updated,
shoppingItemDeleted: store.shoppingItems.deleted,
albums: store.albums.albums,
gotAlbums: store.albums.fetched,
errorAlbum: store.albums.error,
users: store.users.users,
gotUsers: store.users.fetched,
errorUser: store.users.error,
username: store.login.username,
token: store.login.token,
loggedin: store.login.fetched,
checkedout: store.shoppingItems.checkedOut,
receipt: store.shoppingItems.receipt,
};
}
)(ShoppingCart);
-----------------------------------
//react/redux/actions/checkoutCart
import axios from 'axios';
export function checkoutCart(token, data) {
return {
type: "checkout_cart",
payload: axios({
method: 'post',
url: 'http://127.0.0.1:8000/api/cart_checkout/',
headers: {
Authorization: 'Token ' + token,
},
data: data,
})
}
}
-------------------------------------------
//react/redux/reducers/shoppingItemsReducer
export default function reducer(
state = {
shoppingItems: [],
fetching: false,
fetched: false,
deleting: false,
deleted: false,
updating: false,
updated: false,
adding: false,
added: false,
checkingOut: false,
checkedOut: false,
receipt: '',
error: ''
},
action
) {
switch (action.type) {
case "fetch_shoppingItems_PENDING": {
return { ...state, fetching: true, fetched: false, error: '' }
}
case "fetch_shoppingItems_FULFILLED": {
return {
...state,
fetching: false,
fetched: true,
shoppingItems: action.payload.data,
error: ''
}
}
case "fetch_shoppingItems_REJECTED": {
return {
...state,
fetching: false,
error: JSON.stringify(action.payload.response.data),
}
}
case "delete_shoppingItem_PENDING": {
return { ...state, deleting: true, deleted: false, error: '' }
}
case "delete_shoppingItem_FULFILLED": {
const deleteId = action.payload.config.data;
return {
...state,
deleting: false,
deleted: true,
shoppingItems: [...state.shoppingItems].filter(item => item.id != deleteId),
error: ''
}
}
case "delete_shoppingItem_REJECTED": {
return {
...state,
deleting: false,
error: JSON.stringify(action.payload.response.data),
}
}
case "add_shoppingItem_PENDING": {
return { ...state, adding: true, added: false, error: '' }
}
case "add_shoppingItem_FULFILLED": {
const data = action.payload.data;
//check if shopper has item in cart
const itemInCart = [...state.shoppingItems].filter(item => item.shopper == data.shopper && item.album == data.album)
//not in cart, create new
if (itemInCart.length === 0) {
return {
...state,
adding: false,
added: true,
shoppingItems: [...state.shoppingItems].concat(data),
error: ''
}
}
//in cart, update
else {
const repurchasedItem = {
shopper: data.shopper,
album: data.album,
quantity: data.quantity,
}
return {
...state,
adding: false,
added: true,
shoppingItems: [...state.shoppingItems].filter(item => item.album != data.album || item.shopper != data.shopper).concat(repurchasedItem),
error: ''
}
}
}
case "add_shoppingItem_REJECTED": {
return {
...state,
adding: false,
error: JSON.stringify(action.payload.response.data),
}
}
case "update_shoppingItem_PENDING": {
return { ...state, updating: true, updated: false, error: '' }
}
case "update_shoppingItem_FULFILLED": {
const data = action.payload.data;
return {
...state,
updating: false,
updated: true,
shoppingItems: [...state.shoppingItems].filter(item => item.album != data.album || item.shopper != data.shopper).concat(data),
error: ''
}
}
case "update_shoppingItem_REJECTED": {
return {
...state,
updating: false,
error: JSON.stringify(action.payload.response.data),
}
}
case "reset": {
return {
...state,
shoppingItems: [],
fetching: false,
fetched: false,
deleting: false,
deleted: false,
updating: false,
updated: false,
error: ''
}
}
case "checkout_cart_PENDING": {
return { ...state, checkingOut: true, checkedOut: false, error: '' }
}
case "checkout_cart_FULFILLED": {
return {
...state,
checkingOut: false,
checkedOut: true,
shoppingItems: [],
receipt: action.payload.data.receipt_url,
error: ''
}
}
case "checkout_cart_REJECTED": {
return {
...state,
checkingOut: false,
error: JSON.stringify(action.payload.response.data),
}
}
default:
break;
}
return state;
}
reference:
https://www.youtube.com/watch?v=lkA4rmo7W6k
https://ultimatedjango.com/learn-django/lessons/create-stripe-processing-code/
https://stackoverflow.com/questions/51050116/recurring-payments-using-stripe-and-django
https://chuanshuoge2.blogspot.com/2018/09/stripe-api-node-express.html
https://stripe.com/docs/api/charges
python dict
https://www.w3schools.com/python/python_dictionaries.asp
No comments:
Post a Comment