Skip to content

Commit fa9055f

Browse files
committedApr 16, 2022
examples/shopping-cart using React Native
1 parent 6d28c1a commit fa9055f

28 files changed

+783
-312
lines changed
 

‎.circleci/config.yml

-18
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,6 @@ jobs:
6161
command: |
6262
yarn test:unit
6363
64-
test-e2e:
65-
<<: *defaults
66-
steps:
67-
- checkout
68-
- run:
69-
name: Installing Dependencies
70-
command: yarn
71-
- run:
72-
name: Installing peerDependencies explicitly
73-
command: yarn add vue --peer
74-
- run:
75-
name: Running End-to-end Tests
76-
command: |
77-
yarn test:e2e
78-
7964
test-ssr:
8065
<<: *defaults
8166
steps:
@@ -102,9 +87,6 @@ workflows:
10287
- test-unit:
10388
requires:
10489
- install
105-
- test-e2e:
106-
requires:
107-
- install
10890
- test-ssr:
10991
requires:
11092
- install

‎examples/README.md

+8-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ Simple working example using Vuex within a [React Native](https://reactnative.de
1010

1111
## Chat
1212

13-
Simple working example using Vuex within a [React Native](https://reactnative.dev/) project built on top of [Expo](https://docs.expo.dev/)
13+
Slightly complex but working example using Vuex within a [React Native](https://reactnative.dev/) project built on top of [Expo](https://docs.expo.dev/)
14+
15+
![img](public/chat-example.gif)
16+
17+
## Shopping Cart
18+
19+
A complete, feature-rich example using modules in Vuex within a [React Native](https://reactnative.dev/) project built on top of [Expo](https://docs.expo.dev/)
1420

15-
![img](public/chat-example.gif)
21+
![img](public/shopping-cart-example.gif)
1.04 MB
Loading

‎examples/shopping-cart/.gitignore

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
node_modules/
2+
.expo/
3+
dist/
4+
npm-debug.*
5+
*.jks
6+
*.p8
7+
*.p12
8+
*.key
9+
*.mobileprovision
10+
*.orig.*
11+
web-build/
12+
13+
# macOS
14+
.DS_Store

‎examples/shopping-cart/.npmrc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@visitsb:registry=https://npm.pkg.github.com

‎examples/shopping-cart/App.tsx

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import {NavigationContainer} from '@react-navigation/native';
2+
import {SafeAreaProvider} from 'react-native-safe-area-context';
3+
import {createNativeStackNavigator} from '@react-navigation/native-stack';
4+
import {CartScreen, HomeScreen} from "./screens";
5+
import {Button, Pressable, StyleSheet, Text, View} from 'react-native';
6+
import useStore, {StateContext, StoreContext} from './store';
7+
import {RootStackScreenProps} from './types'
8+
import {FontAwesome} from '@expo/vector-icons';
9+
import {mapGetters} from '@visitsb/vuex';
10+
11+
const Stack = createNativeStackNavigator();
12+
13+
export default function App() {
14+
const {store, state} = useStore();
15+
16+
const {items} = mapGetters('cart', {
17+
items: 'cartTotalItems'
18+
});
19+
20+
return (
21+
<SafeAreaProvider>
22+
<StoreContext.Provider value={store}>
23+
<StateContext.Provider value={state}>
24+
<NavigationContainer>
25+
<Stack.Navigator initialRouteName="Home">
26+
<Stack.Screen
27+
name="Home"
28+
component={HomeScreen}
29+
options={({navigation}: RootStackScreenProps<'Home'>) => ({
30+
title: 'Products',
31+
headerRight: () => (
32+
<Pressable
33+
onPress={() => navigation.navigate('Cart')}
34+
style={({pressed}) => ({
35+
opacity: pressed ? 0.5 : 1,
36+
})}>
37+
<View style={[styles.cartContainer, styles.row]}>
38+
<FontAwesome
39+
name="shopping-cart"
40+
size={25}
41+
style={{marginRight: 15}}
42+
/>
43+
<Text style={styles.cartCount}>{items()}</Text>
44+
</View>
45+
</Pressable>
46+
),
47+
})}/>
48+
<Stack.Group screenOptions={{presentation: 'modal'}}>
49+
<Stack.Screen name="Cart"
50+
component={CartScreen}
51+
options={({navigation}: RootStackScreenProps<'Cart'>) => ({
52+
title: 'Cart',
53+
headerRight: () => <Button
54+
title="Done"
55+
onPress={() => navigation.goBack()}/>,
56+
})}/>
57+
</Stack.Group>
58+
</Stack.Navigator>
59+
</NavigationContainer>
60+
</StateContext.Provider>
61+
</StoreContext.Provider>
62+
</SafeAreaProvider>
63+
);
64+
}
65+
66+
const styles = StyleSheet.create({
67+
cartContainer: {
68+
flex: 1,
69+
justifyContent: "center",
70+
alignItems: "center"
71+
},
72+
row: {
73+
flexDirection: "row"
74+
},
75+
column: {
76+
flexDirection: "column"
77+
},
78+
cartCount: {
79+
color: "green"
80+
}
81+
});

‎examples/shopping-cart/api/shop.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {Product} from "../types";
2+
3+
/**
4+
* Mocking client-server processing
5+
*/
6+
const _products: Product[] = [
7+
{'id': 1, 'title': 'iPad 4 Mini', 'price': 500.01, 'inventory': 2},
8+
{'id': 2, 'title': 'H&M T-Shirt White', 'price': 10.99, 'inventory': 10},
9+
{'id': 3, 'title': 'Charli XCX - Sucker CD', 'price': 19.99, 'inventory': 5}
10+
]
11+
12+
export default {
13+
async getProducts(): Promise<Product[]> {
14+
await wait(100)
15+
return _products
16+
},
17+
18+
async buyProducts(products: Product[]): Promise<void> {
19+
await wait(100)
20+
if (Math.random() > 0.5) { // simulate random checkout failure.
21+
return
22+
} else {
23+
throw new Error('Checkout error')
24+
}
25+
}
26+
}
27+
28+
function wait(ms: number): Promise<void> {
29+
return new Promise(resolve => {
30+
setTimeout(resolve, ms)
31+
})
32+
}

‎examples/shopping-cart/app.json

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"expo": {
3+
"name": "shopping-cart",
4+
"slug": "shopping-cart",
5+
"version": "1.0.0",
6+
"orientation": "portrait",
7+
"icon": "./assets/icon.png",
8+
"splash": {
9+
"image": "./assets/splash.png",
10+
"resizeMode": "contain",
11+
"backgroundColor": "#ffffff"
12+
},
13+
"jsEngine": "hermes",
14+
"updates": {
15+
"fallbackToCacheTimeout": 0
16+
},
17+
"assetBundlePatterns": [
18+
"**/*"
19+
],
20+
"ios": {
21+
"supportsTablet": true
22+
},
23+
"android": {
24+
"adaptiveIcon": {
25+
"foregroundImage": "./assets/adaptive-icon.png",
26+
"backgroundColor": "#FFFFFF"
27+
}
28+
},
29+
"web": {
30+
"favicon": "./assets/favicon.png"
31+
}
32+
}
33+
}
17.1 KB
Loading
1.43 KB
Loading
21.9 KB
Loading
46.2 KB
Loading
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = function(api) {
2+
api.cache(true);
3+
return {
4+
presets: ['babel-preset-expo'],
5+
};
6+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {Alert, Pressable, StyleSheet, Text, View, ViewStyle} from 'react-native';
2+
import {CartProduct} from '../types';
3+
4+
export default function CartProductComponent({product, style}: { product: CartProduct, style: ViewStyle }) {
5+
const title: string = product.title
6+
const price: string = (new Intl.NumberFormat('en-US', {
7+
style: 'currency',
8+
currency: 'USD'
9+
}).format(product.price));
10+
const quantity: number = product.quantity
11+
12+
const tbd = (): void => Alert.alert("Todo", "Not implemented yet");
13+
14+
return <View style={style}>
15+
<View style={[styles.row]}>
16+
<View style={[styles.column, {alignItems: "flex-start", justifyContent: "space-between"}]}>
17+
<Text style={styles.productTitle}>{title}</Text>
18+
<Text style={[styles.linkText, {color: "darkslategrey"}]}>Quantity: {quantity}</Text>
19+
</View>
20+
<View style={[styles.spacer]}/>
21+
<View style={[styles.column, styles.productTag]}>
22+
<Text style={styles.productPrice}>{price}</Text>
23+
<Pressable onPress={() => tbd()} style={({pressed}) => ({opacity: pressed ? 0.5 : 1,})}>
24+
<Text style={[styles.link, styles.linkText]}>Remove</Text>
25+
</Pressable>
26+
</View>
27+
</View>
28+
</View>
29+
}
30+
31+
const styles = StyleSheet.create({
32+
row: {
33+
flex: 1,
34+
flexDirection: "row"
35+
},
36+
column: {
37+
flex: 1,
38+
flexDirection: "column"
39+
},
40+
productTitle: {
41+
fontSize: 15
42+
},
43+
productPrice: {
44+
fontSize: 15
45+
},
46+
productTag: {
47+
justifyContent: "space-between",
48+
alignItems: "flex-end"
49+
},
50+
link: {},
51+
linkText: {
52+
fontSize: 15,
53+
color: '#2e78b7'
54+
},
55+
spacer: {
56+
flex: 1
57+
}
58+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import {mapActions} from '@visitsb/vuex';
2+
import {Pressable, StyleSheet, Text, View, ViewStyle} from 'react-native';
3+
import {Product} from '../types';
4+
5+
export default function ProductComponent({product, style}: { product: Product, style: ViewStyle }) {
6+
const {addProductToCart} = mapActions('cart', ['addProductToCart'])
7+
8+
const title: string = product.title
9+
const price: string = (new Intl.NumberFormat('en-US', {
10+
style: 'currency',
11+
currency: 'USD'
12+
}).format(product.price));
13+
14+
const inventory: number = product.inventory
15+
const isInStock: boolean = (product.inventory > 0)
16+
17+
return <View style={style}>
18+
<View style={[styles.row]}>
19+
<View style={[styles.column, {alignItems: "flex-start", justifyContent: "space-between"}]}>
20+
<Text style={styles.productTitle}>{title}</Text>
21+
{isInStock ?
22+
<Text style={[styles.linkText, {color: "darkslategrey"}]}>In
23+
stock: {inventory} left</Text> : <></>}
24+
</View>
25+
<View style={[styles.spacer]}/>
26+
<View style={[styles.column, styles.productTag]}>
27+
<Text style={styles.productPrice}>{price}</Text>
28+
{isInStock ?
29+
<Pressable onPress={() => addProductToCart(product)} style={({pressed}) => ({
30+
opacity: pressed ? 0.5 : 1,
31+
})}>
32+
<Text style={[styles.link, styles.linkText]}>Buy</Text>
33+
</Pressable> :
34+
<Text style={[styles.linkText, {color: "darkslategrey"}]}>Out of stock</Text>
35+
}
36+
</View>
37+
</View>
38+
</View>
39+
}
40+
41+
const styles = StyleSheet.create({
42+
row: {
43+
flex: 1,
44+
flexDirection: "row"
45+
},
46+
column: {
47+
flex: 1,
48+
flexDirection: "column"
49+
},
50+
product: {
51+
flex: 1,
52+
justifyContent: "flex-start",
53+
margin: 10,
54+
padding: 10,
55+
backgroundColor: "snow"
56+
},
57+
productTitle: {
58+
fontSize: 15
59+
},
60+
productPrice: {
61+
fontSize: 15
62+
},
63+
productTag: {
64+
justifyContent: "space-between",
65+
alignItems: "flex-end"
66+
},
67+
link: {},
68+
linkText: {
69+
fontSize: 15,
70+
color: '#2e78b7'
71+
},
72+
spacer: {
73+
flex: 1
74+
}
75+
});

‎examples/shopping-cart/package.json

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "shopping-cart",
3+
"version": "1.0.0",
4+
"main": "node_modules/expo/AppEntry.js",
5+
"scripts": {
6+
"start": "expo start --offline --lan --no-dev",
7+
"android": "expo start --android",
8+
"ios": "expo start --ios",
9+
"web": "expo start --web",
10+
"eject": "expo eject"
11+
},
12+
"dependencies": {
13+
"@react-navigation/native": "^6.0.10",
14+
"@react-navigation/native-stack": "^6.6.1",
15+
"@visitsb/vuex": "^4.0.2",
16+
"expo": "~44.0.0",
17+
"expo-status-bar": "~1.2.0",
18+
"react": "17.0.1",
19+
"react-dom": "17.0.1",
20+
"react-native": "0.64.3",
21+
"react-native-safe-area-context": "3.3.2",
22+
"react-native-screens": "~3.10.1",
23+
"react-native-web": "0.17.1",
24+
"@expo/vector-icons": "^12.0.0"
25+
},
26+
"devDependencies": {
27+
"@babel/core": "^7.12.9",
28+
"@types/react": "~17.0.21",
29+
"@types/react-native": "~0.64.12",
30+
"typescript": "~4.3.5"
31+
},
32+
"private": true,
33+
"optionalDependencies": {
34+
"expo-cli": "^5.3.1"
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import {mapGetters, mapState} from '@visitsb/vuex';
2+
import {useContext} from 'react';
3+
import {Alert, Button, StyleSheet, Text, View} from 'react-native';
4+
import {StateContext, StoreContext} from '../store';
5+
import {CartProduct, CheckoutStatus, RootStackScreenProps, State} from '../types';
6+
import CartProductComponent from "../components/CartProductComponent"
7+
8+
export default function CartScreen({navigation}: RootStackScreenProps<"Home">) {
9+
const {products} = mapGetters('cart', {
10+
products: 'cartProducts'
11+
});
12+
13+
return <View style={[styles.container, styles.column]}>
14+
<StateContext.Consumer>{(state: State) => {
15+
const isCartEmpty: boolean = (products().length === 0)
16+
if (isCartEmpty) return <EmptyCartView/>
17+
18+
return <>
19+
<CartItemsView products={products()}/>
20+
<CartCheckoutView/>
21+
<View style={[styles.spacer, {flex: 8}]}/>
22+
</>
23+
}}</StateContext.Consumer>
24+
</View>
25+
}
26+
27+
const EmptyCartView = () => (
28+
<View style={[styles.container, styles.column, {justifyContent: "center", alignItems: "center"}]}><Text
29+
style={[styles.linkText, {color: "darkslategrey"}]}>Your cart is empty</Text></View>)
30+
31+
const CartItemsView = ({products}: { products: CartProduct[] }) => products.map((product: CartProduct) =>
32+
<CartProductComponent product={product} key={product.id} style={styles.product}/>)
33+
34+
const CartCheckoutView = () => {
35+
const {dispatch} = useContext(StoreContext)
36+
37+
const {products, total} = mapGetters('cart', {
38+
products: 'cartProducts',
39+
total: 'cartTotalPrice'
40+
})
41+
const {checkoutStatus} = mapState({
42+
checkoutStatus: (state: State) => state.cart.checkoutStatus
43+
})
44+
45+
const showCheckoutAlert = () =>
46+
Alert.alert(
47+
"Uh oh",
48+
"Something went wrong. Try again later.",
49+
//[{text: "OK", onPress: () => console.debug("Try again later")}]
50+
);
51+
52+
return <View style={styles.priceContainer}>
53+
<Text style={styles.price}>Total: {(new Intl.NumberFormat('en-US', {
54+
style: 'currency',
55+
currency: 'USD'
56+
}).format(total()))}</Text>
57+
<Button title="Checkout" onPress={() => (async () => {
58+
await dispatch('cart/checkout', products)
59+
if (checkoutStatus() == CheckoutStatus.FAILED) showCheckoutAlert()
60+
})()}/>
61+
<Text style={[styles.linkText, {color: "darkslategrey"}]}>{checkoutStatus()}</Text>
62+
</View>
63+
}
64+
65+
const styles = StyleSheet.create({
66+
container: {
67+
backgroundColor: '#fff',
68+
justifyContent: 'flex-start',
69+
},
70+
row: {
71+
flex: 1,
72+
flexDirection: "row"
73+
},
74+
column: {
75+
flex: 1,
76+
flexDirection: "column"
77+
},
78+
product: {
79+
flex: 1,
80+
justifyContent: "flex-start",
81+
margin: 10,
82+
padding: 10,
83+
backgroundColor: "snow"
84+
},
85+
priceContainer: {
86+
justifyContent: "center",
87+
alignItems: "flex-end",
88+
padding: 20
89+
},
90+
price: {
91+
fontSize: 20,
92+
color: "green"
93+
},
94+
link: {},
95+
linkText: {
96+
fontSize: 15,
97+
color: 'orange'
98+
},
99+
spacer: {
100+
flex: 1
101+
}
102+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {mapActions, mapState} from '@visitsb/vuex';
2+
import {useEffect} from 'react';
3+
import {StyleSheet, View} from 'react-native';
4+
import {StateContext} from '../store';
5+
import {Product, RootStackScreenProps, State} from '../types';
6+
import ProductComponent from "../components/ProductComponent"
7+
8+
export default function HomeScreen({navigation}: RootStackScreenProps<"Home">) {
9+
const {products} = mapState({
10+
products: (state: State): Product[] => state.products.all
11+
});
12+
const {getAllProducts} = mapActions('products', ['getAllProducts'])
13+
14+
useEffect(() => {
15+
(async () => await getAllProducts())()
16+
}, [])
17+
18+
return (
19+
<View style={[styles.container, styles.column]}>
20+
<StateContext.Consumer>{(state: State) => <>{
21+
products().map((product: Product) => <ProductComponent product={product} key={product.id}
22+
style={styles.product}/>)}
23+
</>}</StateContext.Consumer>
24+
<View style={[styles.spacer, {flex: 8}]}/>
25+
</View>
26+
);
27+
}
28+
29+
const styles = StyleSheet.create({
30+
container: {
31+
backgroundColor: '#fff',
32+
justifyContent: 'flex-start',
33+
},
34+
row: {
35+
flex: 1,
36+
flexDirection: "row"
37+
},
38+
column: {
39+
flex: 1,
40+
flexDirection: "column"
41+
},
42+
product: {
43+
flex: 1,
44+
justifyContent: "flex-start",
45+
margin: 10,
46+
padding: 10,
47+
backgroundColor: "snow"
48+
},
49+
spacer: {
50+
flex: 1
51+
}
52+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { StyleSheet, TouchableOpacity, Text, View } from 'react-native';
2+
import { RootStackScreenProps } from '../types';
3+
4+
export default function NotFoundScreen({ navigation }: RootStackScreenProps<'NotFound'>) {
5+
return (
6+
<View style={styles.container}>
7+
<Text style={styles.title}>This screen doesn't exist.</Text>
8+
<TouchableOpacity onPress={() => navigation.replace('Home')} style={styles.link}>
9+
<Text style={styles.linkText}>Go to home screen!</Text>
10+
</TouchableOpacity>
11+
</View>
12+
);
13+
}
14+
15+
const styles = StyleSheet.create({
16+
container: {
17+
flex: 1,
18+
alignItems: 'center',
19+
justifyContent: 'center',
20+
padding: 20,
21+
},
22+
title: {
23+
fontSize: 20,
24+
fontWeight: 'bold',
25+
},
26+
link: {
27+
marginTop: 15,
28+
paddingVertical: 15,
29+
},
30+
linkText: {
31+
fontSize: 14,
32+
color: '#2e78b7',
33+
},
34+
});
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import HomeScreen from './HomeScreen'
2+
import CartScreen from './CartScreen'
3+
import NotFoundScreen from './NotFoundScreen'
4+
5+
export {
6+
HomeScreen, CartScreen, NotFoundScreen
7+
}

‎examples/shopping-cart/store/index.ts

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import {Commit, createLogger, createStore, Store} from "@visitsb/vuex";
2+
import {Context, createContext, useContext, useEffect, useState} from "react";
3+
import {CartItem, CartProduct, CartState, CheckoutStatus, Product, ProductsState, State} from "./../types";
4+
import shop from '../api/shop'
5+
6+
const debug = process.env.NODE_ENV !== 'production'
7+
8+
export const store: Store<State> = createStore({
9+
strict: debug,
10+
state: (): State => ({}),
11+
getters: {},
12+
mutations: {},
13+
actions: {},
14+
modules: {
15+
products: {
16+
namespaced: true,
17+
state: (): ProductsState => ({
18+
all: []
19+
}),
20+
getters: {},
21+
mutations: {
22+
setProducts(state: ProductsState, products: Product[]) {
23+
state.all = products
24+
},
25+
decrementProductInventory(state: ProductsState, {id}: Product) {
26+
const product = state.all.find(product => product.id === id)
27+
product!.inventory--
28+
}
29+
},
30+
actions: {
31+
async getAllProducts({commit}: { commit: Commit }) {
32+
const products: Product[] = await shop.getProducts()
33+
commit('setProducts', products)
34+
}
35+
}
36+
},
37+
cart: {
38+
namespaced: true,
39+
state: (): CartState => ({
40+
items: [],
41+
checkoutStatus: CheckoutStatus.EMPTY
42+
}),
43+
getters: {
44+
cartProducts: (state: CartState, getters: any, rootState: State): CartProduct[] => {
45+
return state.items.map(({id, quantity}): CartProduct => {
46+
const product: Product = rootState.products.all.find((product: Product) => (product.id === id))
47+
return <CartProduct>{
48+
id: product.id,
49+
title: product.title,
50+
price: product.price,
51+
quantity
52+
}
53+
})
54+
},
55+
cartTotalItems: (state: CartState, getters: any): number => {
56+
return getters.cartProducts.reduce((total: number, product: CartProduct) => {
57+
return total + product.quantity
58+
}, 0)
59+
},
60+
cartTotalPrice: (state: CartState, getters: any): number => {
61+
return getters.cartProducts.reduce((total: number, product: CartProduct) => {
62+
return total + product.price * product.quantity
63+
}, 0)
64+
}
65+
},
66+
mutations: {
67+
pushProductToCart(state: CartState, {id}: Partial<Product> & { id: number }) {
68+
state.items.push({id, quantity: 1})
69+
},
70+
71+
incrementItemQuantity(state: CartState, {id}: Partial<Product> & { id: number }) {
72+
const cartItem: CartItem = state.items.find(item => item.id === id)!
73+
cartItem.quantity++
74+
},
75+
76+
setCartItems(state: CartState, {items}: { items: CartItem[] }): void {
77+
state.items = items
78+
},
79+
80+
setCheckoutStatus(state: CartState, status: CheckoutStatus = CheckoutStatus.EMPTY): void {
81+
state.checkoutStatus = status
82+
}
83+
},
84+
actions: {
85+
async checkout({
86+
commit,
87+
state
88+
}: { commit: Commit, state: CartState }, products: Product[]): Promise<void> {
89+
commit('setCheckoutStatus')
90+
try {
91+
await shop.buyProducts(products)
92+
// empty cart
93+
commit('setCartItems', {items: []})
94+
commit('setCheckoutStatus', CheckoutStatus.SUCCESSFUL)
95+
} catch (e) {
96+
// Log error somewhere
97+
commit('setCheckoutStatus', CheckoutStatus.FAILED)
98+
}
99+
},
100+
101+
async addProductToCart({
102+
state,
103+
commit
104+
}: { state: CartState, commit: Commit }, product: Product): Promise<void> {
105+
commit('setCheckoutStatus')
106+
107+
if (product.inventory > 0) {
108+
const cartItem: CartItem = state.items.find(item => item.id === product.id)!
109+
110+
if (!cartItem) {
111+
commit('pushProductToCart', {id: product.id})
112+
} else {
113+
commit('incrementItemQuantity', cartItem)
114+
}
115+
116+
// remove 1 item from stock
117+
commit('products/decrementProductInventory', {id: product.id}, {root: true})
118+
}
119+
}
120+
}
121+
}
122+
},
123+
plugins: debug ? [
124+
createLogger<State>({
125+
collapsed: true,
126+
transformer: () => '...', // Skip log for state
127+
actionTransformer: JSON.stringify,
128+
mutationTransformer: JSON.stringify
129+
})
130+
] : [/*no plugins*/]
131+
})
132+
133+
export const StoreContext: Context<Store<State>> = createContext(store);
134+
export const StateContext: Context<State> = createContext(store.state);
135+
136+
export default function useStore() {
137+
const store = useContext<Store<State>>(StoreContext);
138+
const state = useContext<State>(StateContext);
139+
140+
// Provider can expose a global variable
141+
// but it needs to be reactive in order to cause a re-render
142+
// which is different to Vuex - hence subscribe to state changes
143+
// and refresh a `react`-ive state which Provider understands
144+
let [watchedState, setWatchedState] = useState(state);
145+
146+
useEffect((/*didUpdate*/) => {
147+
const unsubscribe = store.subscribe((mutation, newState) => setWatchedState((prevState: State) => ({...prevState, ...newState})));
148+
149+
return (/*cleanup*/) => unsubscribe()
150+
}, []);
151+
152+
const _globalThis = (globalThis || self || window || global || {});
153+
if (typeof _globalThis.$store === 'undefined') {
154+
_globalThis.$store = store;
155+
}
156+
157+
return {store, state: watchedState};
158+
}

‎examples/shopping-cart/tsconfig.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"extends": "expo/tsconfig.base",
3+
"compilerOptions": {
4+
"strict": true
5+
}
6+
}

‎examples/shopping-cart/types.tsx

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {Store} from "@visitsb/vuex";
2+
3+
import {CompositeScreenProps} from '@react-navigation/native';
4+
import {NativeStackScreenProps} from '@react-navigation/native-stack';
5+
6+
/**
7+
* Learn more about using TypeScript with React Navigation:
8+
* https://reactnavigation.org/docs/typescript/
9+
*/
10+
11+
declare global {
12+
namespace ReactNavigation {
13+
interface RootParamList extends RootStackParamList {
14+
}
15+
}
16+
17+
type Nullable<T> = T | null;
18+
type ObjectKeys<T> = T extends object ? (keyof T)[] : T extends number ? [] : T extends Array<any> | string ? string[] : never;
19+
20+
interface ObjectConstructor {
21+
keys<T>(o: T): ObjectKeys<T>
22+
}
23+
24+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis
25+
// `globalThis` property provides a standard way of accessing the global this value (and hence the
26+
// global object itself) across environments.
27+
// https://vuex.vuejs.org/api/#component-binding-helpers
28+
// mapXXX, createNamespacedHelpers presume `this.$store` is setup
29+
var $store: Store<State>;
30+
}
31+
32+
export type RootStackParamList = {
33+
Home: undefined;
34+
Cart: undefined;
35+
Modal: undefined;
36+
NotFound: undefined;
37+
};
38+
39+
export type RootStackScreenProps<Screen extends keyof RootStackParamList> = CompositeScreenProps<NativeStackScreenProps<RootStackParamList>, NativeStackScreenProps<RootStackParamList, Screen>>;
40+
41+
// Custom shopping-cart app types
42+
type ProductLabel = {
43+
id: number,
44+
title: string,
45+
price: number
46+
}
47+
48+
export type Product = ProductLabel & {
49+
inventory: number
50+
}
51+
52+
export type CartItem = {
53+
id: number,
54+
quantity: number
55+
}
56+
57+
export type CartProduct = ProductLabel & {
58+
quantity: number
59+
}
60+
61+
export type State = {}
62+
63+
export type ProductsState = {
64+
all: Product[]
65+
}
66+
67+
export enum CheckoutStatus {
68+
EMPTY = "",
69+
SUCCESSFUL = "successful",
70+
FAILED = "failed"
71+
}
72+
73+
export type CartState = {
74+
items: CartItem[],
75+
checkoutStatus: CheckoutStatus
76+
}

‎package.json

+4-5
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,12 @@
2929
"dev": "node examples/server.js",
3030
"build": "node scripts/build.js",
3131
"lint": "eslint src test",
32-
"test": "npm run lint && npm run build && npm run test:types && npm run test:unit && npm run test:ssr && npm run test:e2e && npm run test:esm",
33-
"test:unit": "jest --testPathIgnorePatterns test/e2e",
34-
"test:e2e": "start-server-and-test dev http://localhost:8080 \"jest --testPathIgnorePatterns test/unit\"",
35-
"test:ssr": "cross-env VUE_ENV=server jest --testPathIgnorePatterns test/e2e",
32+
"test": "npm run lint && npm run build && npm run test:types && npm run test:unit && npm run test:ssr && npm run test:esm",
33+
"test:unit": "jest",
34+
"test:ssr": "cross-env VUE_ENV=server jest",
3635
"test:types": "tsc -p types/test",
3736
"test:esm": "node test/esm/esm-test.js",
38-
"coverage": "jest --testPathIgnorePatterns test/e2e --coverage",
37+
"coverage": "jest --coverage",
3938
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
4039
"release": "node scripts/release.js",
4140
"docs": "vitepress dev docs",

‎test/e2e/cart.spec.js

-45
This file was deleted.

‎test/e2e/chat.spec.js

-42
This file was deleted.

‎test/e2e/counter.spec.js

-38
This file was deleted.

‎test/e2e/todomvc.spec.js

-162
This file was deleted.

0 commit comments

Comments
 (0)
Please sign in to comment.