· 5 years ago · Jul 30, 2020, 03:40 AM
1import React, {useState, useEffect, useReducer} from 'react'
2import {
3 Text,
4 View,
5 ScrollView,
6 Switch,
7 KeyboardAvoidingView,
8 Platform,
9 StyleSheet,
10 SafeAreaView,
11 Alert,
12} from 'react-native'
13import {commonStyle} from '../../../styles/commonStyle'
14import {
15 BottomSelectButton,
16 StoryHeader,
17 PaymentMethod,
18} from '../../../components'
19import {styles as ButtonStyles, FilterButton} from '../../../components/Button'
20import {BookingSummaryItem, Separator} from '../../../components'
21import Section from '../../../components/Section'
22import {NameInput, EmailInput, CodeInput} from '../../../components/Input'
23import {currencyFormat} from '../../../utils/format'
24import {getDiscountApply, getArrivalRestriction} from '../../../utils/calculate'
25import {validateEmail} from '../../../utils/validation'
26import {Fonts, Colors} from '../../../styles/constants'
27import {
28 ScreenLifeCycle,
29 useGlobalActionContext,
30 useGlobalContext,
31} from '../../../store'
32import {
33 PaymentReducer,
34 paymentInitialState,
35} from '../../Purchases/PurchaseOrderDetails/model'
36import {GiftPaymentDetails} from './GiftPaymentDetails'
37import {GiftType, PaymentFlowType, PaymentType} from '../../../api/types'
38import {smallerText} from '../../../styles/typography'
39import {getCreditCardToken} from '../../../api'
40
41const styles = StyleSheet.create({
42 section: {
43 borderBottomColor: Colors.border,
44 borderBottomWidth: 1,
45 marginBottom: 22.2,
46 },
47 switchRow: {
48 marginTop: 16,
49 flexDirection: 'row',
50 alignItems: 'center',
51 },
52 switchTitle: {
53 color: Colors.primaryText,
54 fontFamily: Fonts.primaryFont,
55 fontSize: 18,
56 paddingLeft: 16,
57 },
58 button: {
59 width: '45%',
60 height: 40,
61 },
62 buttonTitle: {
63 fontFamily: Fonts.secondaryFont,
64 ...smallerText,
65 lineHeight: 24,
66 color: Colors.primaryText,
67 },
68 giftButton: {
69 width: '60%',
70 height: 50,
71 backgroundColor: Colors.primary,
72 borderRadius: 13,
73 },
74 row: {
75 flexDirection: 'row',
76 justifyContent: 'space-between',
77 },
78 total: {
79 ...commonStyle.h3,
80 fontFamily: Fonts.primaryFont,
81 color: Colors.primaryText,
82 },
83 sectionTitle: {
84 ...commonStyle.componentSubtitle,
85 fontFamily: Fonts.primaryFont,
86 marginBottom: 4,
87 marginTop: 18,
88 },
89 giftButtonTitle: {
90 color: Colors.background,
91 fontFamily: Fonts.primaryFont,
92 fontSize: 18,
93 },
94 story: {
95 ...ButtonStyles.backButtonText,
96 marginHorizontal: 0,
97 fontWeight: '500',
98 paddingBottom: 15.6,
99 justifyContent: 'flex-start',
100 },
101 container: {
102 paddingVertical: 50,
103 },
104})
105
106export default ({navigation}) => {
107 const state = useGlobalContext()
108 const dispatch = useGlobalActionContext()
109 const {selectedPaymentMethod} = state
110
111 const [purchaseEmail, setEmail] = useState('')
112 const [purchaseName, setName] = useState('')
113 const [err, setErrMsg] = useState(null)
114 const [nameErr, setNameErr] = useState('')
115 const [tokenErr, setTokenErr] = useState('')
116 const [cardNameErr, setCardNameErr] = useState('')
117
118 const [payTotal, setPayTotal] = useState(0)
119 const {newGift, selectedGift} = state
120
121 const [doNotChargeCard, setDoNotChargeCard] = useState(false)
122 const [comment, setComment] = useState('')
123
124 const [localState, localDispatch] = useReducer(
125 PaymentReducer,
126 paymentInitialState,
127 )
128
129 useEffect(() => {
130 fetchCustomer()
131 }, [purchaseEmail])
132
133 const fetchCustomer = async () => {
134 const res = await state.api.customers(state.seller.seller.id, {
135 search: purchaseEmail,
136 })
137 dispatch({
138 type: ScreenLifeCycle.Update,
139 payload: {
140 selectedCustomer: res.data[0],
141 },
142 })
143 }
144
145 const onContinue = async () => {
146 if (!validateEmail(purchaseEmail)) {
147 setErrMsg('Email is invalid')
148 return
149 }
150
151 if (purchaseName === '') {
152 setNameErr('Name is required')
153 return
154 }
155
156 if (!selectedPaymentMethod) {
157 Alert.alert('error', 'Please select payment method')
158 return
159 }
160
161 dispatch({type: ScreenLifeCycle.Loading})
162 const items = newGift.items.map(item => {
163 return {
164 amount: Number(item.amount),
165 baseAmount: Number(item.amount),
166 // code: item.code || '', // FIX ME Giftcode duplicated api issue while can't edit
167 message: item.message,
168 product: {},
169 quantity: 1,
170 recipientEmail: item.recipientEmail,
171 recipientName: item.recipientName,
172 sendToCustomer: item.sendToCustomer,
173 sendToRecipient: item.sendToRecipient,
174 status: 200,
175 }
176 })
177 const postData = {
178 amount: Number(items[0].amount),
179 baseAmount: Number(items[0].amount),
180 currency: 'USD',
181 customerEmail: purchaseEmail,
182 customerName: purchaseName,
183 items,
184 phone: newGift.phone,
185 seller: {id: state.seller.seller.id},
186 source: 'office',
187 }
188
189 let token = {id: '', card: {last4: ''}}
190 let payment = {
191 method: '',
192 }
193
194 payment.method = selectedPaymentMethod.type
195
196 if (selectedPaymentMethod.type === PaymentType.NEW_CARD) {
197 const {name, number, expiry, cvc} = localState.card
198 const [expiryMonth, expiryYear] = expiry.split('/')
199 if (name === '') {
200 return setCardNameErr('Card name is invalid')
201 } else {
202 setCardNameErr('')
203 }
204 const stripeTokenRes = await getCreditCardToken(
205 state.seller.seller.defaultGateway.publicKey,
206 {
207 name,
208 number,
209 expiryMonth,
210 expiryYear,
211 cvc,
212 },
213 )
214 if (stripeTokenRes && stripeTokenRes.status === 200) {
215 setTokenErr('')
216 token = await stripeTokenRes.json()
217 } else {
218 dispatch({type: ScreenLifeCycle.Ready})
219 return setTokenErr('Card number is invalid')
220 }
221 payment = {
222 ...payment,
223 method: PaymentType.CARD,
224 card: {
225 token: token.id,
226 last4: token.card.last4,
227 },
228 }
229 } else if (selectedPaymentMethod.type === PaymentType.CARD) {
230 payment = {
231 ...payment,
232 card: selectedPaymentMethod.extraData.id,
233 method: PaymentType.CARD,
234 }
235 } else if (selectedPaymentMethod.type === PaymentType.CUSTOM_PAYMENT) {
236 payment = {...payment, method: selectedPaymentMethod.key, comment}
237 } else {
238 payment = {...payment, comment}
239 }
240
241 await state.api.createGift({...postData, payment})
242
243 dispatch({type: ScreenLifeCycle.Ready})
244
245 navigation.navigate('Purchases')
246 }
247
248 useEffect(() => {
249 let total = 0
250 newGift.items.map(item => {
251 total += Number(item.totalPrice)
252 })
253 setPayTotal(total)
254 }, [newGift, selectedGift])
255
256 const editGift = gift => {
257 dispatch({
258 type: ScreenLifeCycle.Update,
259 payload: {
260 selectedGift: gift,
261 },
262 })
263 if (gift.giftType === GiftType.DISCOUNT_GIFT) {
264 navigation.navigate({name: 'DiscountGift', key: new Date().getTime()})
265 } else if (gift.giftType === GiftType.CHOOSE_AMOUNT) {
266 navigation.navigate({name: 'ChooseAmount', key: new Date().getTime()})
267 } else {
268 navigation.navigate({name: 'PersonalizeGift', key: new Date().getTime()})
269 }
270 }
271
272 const GiftDetail = ({gift}) => {
273 const exist = newGift.items.find(item => item.tempId === gift.tempId)
274
275 const setGiftCode = giftCode => {
276 if (exist) {
277 const newItems = newGift.items.map(item => {
278 if (item.tempId === gift.tempId) {
279 return {...item, code: giftCode}
280 }
281 return item
282 })
283
284 dispatch({
285 type: ScreenLifeCycle.Update,
286 payload: {
287 newGift: {
288 ...newGift,
289 items: newItems,
290 },
291 },
292 })
293 } else {
294 dispatch({
295 type: ScreenLifeCycle.Update,
296 payload: {
297 selectedGift: {
298 ...selectedGift,
299 code: giftCode,
300 },
301 },
302 })
303 }
304 }
305
306 const deleteGift = () => {
307 dispatch({
308 type: ScreenLifeCycle.Loading,
309 })
310 if (exist) {
311 const newItems = newGift.items.filter(
312 item => item.tempId !== gift.tempId,
313 )
314 dispatch({
315 type: ScreenLifeCycle.Update,
316 payload: {
317 newGift: {
318 ...newGift,
319 items: newItems,
320 },
321 },
322 })
323 } else {
324 dispatch({
325 type: ScreenLifeCycle.Update,
326 payload: {
327 selectedGift: null,
328 },
329 })
330 }
331 }
332
333 const updateGift = (key, val) => {
334 if (exist) {
335 const newItems = newGift.items.map(item => {
336 if (item.tempId === gift.tempId) {
337 return {...item, [key]: val}
338 }
339 return item
340 })
341
342 dispatch({
343 type: ScreenLifeCycle.Update,
344 payload: {
345 newGift: {
346 ...newGift,
347 items: newItems,
348 },
349 },
350 })
351 } else {
352 dispatch({
353 type: ScreenLifeCycle.Update,
354 payload: {
355 selectedGift: {
356 ...selectedGift,
357 [key]: val,
358 },
359 },
360 })
361 }
362 }
363
364 if (!gift) return <View />
365
366 return (
367 <>
368 <View style={{marginHorizontal: 27}}>
369 <View style={{paddingBottom: 12, marginTop: 8}}>
370 <BookingSummaryItem
371 image={{uri: gift.item.image}}
372 detail={gift.item.name}
373 onPress={() => {}}
374 />
375 <Text style={[styles.buttonTitle, {paddingTop: 4}]}>
376 {`to: `}
377 <Text
378 style={{
379 color: Colors.primary,
380 textDecorationLine: 'underline',
381 }}>
382 {gift.recipientEmail}
383 </Text>
384 </Text>
385 </View>
386 {gift.giftType === GiftType.DISCOUNT_GIFT && (
387 <>
388 <View style={{marginBottom: 12}}>
389 <Text
390 style={[
391 commonStyle.componentSubtitle,
392 {
393 fontFamily: Fonts.primaryFont,
394 marginBottom: 4,
395 marginTop: 18,
396 },
397 ]}>
398 Arrival Restriction:
399 </Text>
400 <Text
401 style={{
402 fontFamily: Fonts.secondaryFont,
403 fontSize: 18,
404 color: Colors.primaryText,
405 marginBottom: 4,
406 }}>
407 {getArrivalRestriction(gift.item)}
408 </Text>
409 </View>
410 <View style={{marginBottom: 12}}>
411 <Text
412 style={[
413 commonStyle.componentSubtitle,
414 {
415 fontFamily: Fonts.primaryFont,
416 marginBottom: 4,
417 marginTop: 18,
418 },
419 ]}>
420 Applies to:
421 </Text>
422 <Text
423 style={{
424 fontFamily: Fonts.secondaryFont,
425 fontSize: 18,
426 color: Colors.primaryText,
427 marginBottom: 4,
428 }}>
429 {getDiscountApply(gift.item)}
430 </Text>
431 </View>
432 </>
433 )}
434 {newGift.items.length > 1 && (
435 <View style={[styles.row, {marginTop: 4}]}>
436 <FilterButton
437 style={styles.button}
438 onPress={() => editGift(gift)}>
439 <Text style={styles.buttonTitle}>Edit</Text>
440 </FilterButton>
441 <FilterButton style={styles.button} onPress={deleteGift}>
442 <Text style={styles.buttonTitle}>Delete</Text>
443 </FilterButton>
444 </View>
445 )}
446 <View style={{marginTop: 40}}>
447 <CodeInput
448 label="Custom Gift Code"
449 value={gift.code}
450 onChange={setGiftCode}
451 error=""
452 placeholder="Enter Optional gift code"
453 />
454 </View>
455 <View style={[styles.switchRow, {marginTop: 4}]}>
456 <Switch
457 value={gift.sendToRecipient}
458 onValueChange={val => updateGift('sendToRecipient', val)}
459 />
460 <Text style={styles.switchTitle}>Email gift to recipient</Text>
461 </View>
462 <View style={[styles.switchRow, {marginBottom: 20}]}>
463 <Switch
464 value={gift.sendToCustomer}
465 onValueChange={val => updateGift('sendToCustomer', val)}
466 />
467 <Text style={styles.switchTitle}>Email gift to purchaser</Text>
468 </View>
469 <Separator />
470 <View style={styles.row}>
471 <Text style={styles.buttonTitle}>Gift value</Text>
472 <Text style={styles.buttonTitle}>
473 {currencyFormat(gift.amount)}
474 </Text>
475 </View>
476
477 <GiftPaymentDetails
478 discount={gift.discount}
479 taxFee={gift.taxFee}
480 total={gift.totalPrice}
481 />
482 </View>
483 <Separator />
484 </>
485 )
486 }
487
488 const addAnotherGift = () => {
489 // temporary removed until gift api is ready.
490 // navigation.navigate('GiftList')
491
492 dispatch({
493 type: ScreenLifeCycle.Update,
494 payload: {
495 selectedGift: {
496 item: {
497 id: 'choose_amount',
498 object: GiftType.CHOOSE_AMOUNT,
499 name: 'Choose amount',
500 image: 'https://picsum.photos/66/66',
501 },
502 tempId: new Date().getTime(),
503 },
504 },
505 })
506
507 navigation.navigate({name: 'ChooseAmount', key: new Date().getTime()})
508 }
509
510 return (
511 <View style={{flex: 1}}>
512 <KeyboardAvoidingView
513 behavior={Platform.OS === 'ios' ? 'padding' : null}
514 style={{flex: 1}}>
515 <SafeAreaView style={{flex: 1}}>
516 <ScrollView contentContainerStyle={{paddingBottom: 50}}>
517 <Section>
518 <StoryHeader
519 storyName="New gift purchase"
520 screenName="Payment Details"
521 cart={newGift.items.length}
522 />
523 </Section>
524 {newGift.items.map((item, i) => (
525 <GiftDetail gift={item} key={`${item.tempId}`} />
526 ))}
527 <Section>
528 <Text style={styles.sectionTitle}>From</Text>
529 <View style={styles.section} />
530 <NameInput
531 label="Purchase Name"
532 value={purchaseName}
533 onChange={setName}
534 error={nameErr}
535 placeholder="Purchase name"
536 />
537 <EmailInput
538 label="Purchase Email"
539 value={purchaseEmail}
540 onChange={setEmail}
541 error={err}
542 placeholder="Purchase e-mail"
543 />
544 <Text style={styles.sectionTitle}>Collect payment</Text>
545 <View style={styles.section} />
546 </Section>
547 <PaymentMethod
548 showTag={true}
549 paymentFlow={PaymentFlowType.NEWBOOKING}
550 doNotChargeCard={doNotChargeCard}
551 onDoNotChargeCard={setDoNotChargeCard}
552 onTagChange={setComment}
553 onCustomMethodChange={setComment}
554 onNameChange={value =>
555 localDispatch({
556 type: 'card',
557 payload: {key: 'name', value: value},
558 })
559 }
560 onNumberChange={value =>
561 localDispatch({
562 type: 'card',
563 payload: {key: 'number', value: value},
564 })
565 }
566 onExpiryChange={value =>
567 localDispatch({
568 type: 'card',
569 payload: {key: 'expiry', value: value},
570 })
571 }
572 onCVCChange={value =>
573 localDispatch({
574 type: 'card',
575 payload: {key: 'cvc', value: value},
576 })
577 }
578 tokenErr={tokenErr}
579 cardNameErr={cardNameErr}
580 />
581 <View
582 style={[styles.row, {marginTop: 18, justifyContent: 'center'}]}>
583 <FilterButton style={styles.giftButton} onPress={addAnotherGift}>
584 <Text style={styles.giftButtonTitle}>Add another gift</Text>
585 </FilterButton>
586 </View>
587 </ScrollView>
588 </SafeAreaView>
589 </KeyboardAvoidingView>
590 <BottomSelectButton
591 cancelText="Back"
592 submitText={`Pay ${currencyFormat(payTotal)}`}
593 onCancel={() => navigation.goBack()}
594 onSubmit={() => onContinue()}
595 submitDisabled={!newGift.items.length}
596 submitButtonBackground={Colors.secondaryButtonColor}
597 />
598 </View>
599 )
600}
601