ordering-ui-react-native 0.15.48 → 0.15.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/themes/business/src/components/PreviousOrders/index.tsx +7 -5
- package/themes/original/src/components/AppleLogin/index.tsx +77 -71
- package/themes/original/src/components/LoginForm/index.tsx +73 -2
- package/themes/original/src/components/LoginForm/styles.tsx +6 -1
- package/themes/original/src/components/Promotions/index.tsx +232 -0
- package/themes/original/src/components/Promotions/styles.tsx +80 -0
- package/themes/original/src/components/UserProfile/index.tsx +8 -1
- package/themes/original/src/types/index.tsx +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ordering-ui-react-native",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.51",
|
|
4
4
|
"description": "Reusable components made in react native",
|
|
5
5
|
"main": "src/index.tsx",
|
|
6
6
|
"author": "ordering.inc",
|
|
@@ -99,6 +99,7 @@
|
|
|
99
99
|
"react-native-picker-select": "^8.0.4",
|
|
100
100
|
"react-native-print": "^0.9.0",
|
|
101
101
|
"react-native-reanimated": "^1.13.1",
|
|
102
|
+
"react-native-recaptcha-that-works": "^1.2.0",
|
|
102
103
|
"react-native-restart": "^0.0.22",
|
|
103
104
|
"react-native-safe-area-context": "^3.1.8",
|
|
104
105
|
"react-native-screens": "^2.11.0",
|
|
@@ -102,12 +102,13 @@ export const PreviousOrders = (props: any) => {
|
|
|
102
102
|
const getDelayMinutes = (order: any) => {
|
|
103
103
|
// targetMin = delivery_datetime + eta_time - now()
|
|
104
104
|
const offset = 300
|
|
105
|
-
const cdtToutc =
|
|
105
|
+
const cdtToutc = moment(order?.delivery_datetime).add(offset, 'minutes').format('YYYY-MM-DD HH:mm:ss')
|
|
106
106
|
const _delivery = order?.delivery_datetime_utc
|
|
107
107
|
? parseDate(order?.delivery_datetime_utc)
|
|
108
108
|
: parseDate(cdtToutc)
|
|
109
109
|
const _eta = order?.eta_time
|
|
110
|
-
|
|
110
|
+
const diffTimeAsSeconds = moment(_delivery, 'YYYY-MM-DD hh:mm A').add(_eta, 'minutes').diff(moment().utc(), 'seconds')
|
|
111
|
+
return Math.ceil(diffTimeAsSeconds / 60)
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
const displayDelayedTime = (order: any) => {
|
|
@@ -128,9 +129,10 @@ export const PreviousOrders = (props: any) => {
|
|
|
128
129
|
return finalTaget
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
const getStatusClassName = (minutes:
|
|
132
|
-
if (isNaN(Number(minutes))) return
|
|
133
|
-
|
|
132
|
+
const getStatusClassName = (minutes: number) => {
|
|
133
|
+
if (isNaN(Number(minutes))) return 'in_time'
|
|
134
|
+
const delayTime = configState?.configs?.order_deadlines_delayed_time?.value
|
|
135
|
+
return minutes > 0 ? 'in_time' : Math.abs(minutes) <= delayTime ? 'at_risk' : 'delayed'
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
useEffect(() => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useEffect } from 'react';
|
|
2
2
|
import { Platform, Text, StyleSheet } from 'react-native';
|
|
3
|
-
import { useApi, useSession, useLanguage } from 'ordering-components/native';
|
|
3
|
+
import { useApi, useSession, useLanguage, useConfig } from 'ordering-components/native';
|
|
4
4
|
import { appleAuthAndroid, appleAuth } from '@invertase/react-native-apple-authentication';
|
|
5
5
|
import uuid from 'react-native-uuid';
|
|
6
6
|
import Icon from 'react-native-vector-icons/FontAwesome5';
|
|
@@ -16,12 +16,12 @@ export const AppleLogin = (props: any) => {
|
|
|
16
16
|
} = props
|
|
17
17
|
|
|
18
18
|
const [ordering] = useApi();
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
const [{ auth }] = useSession();
|
|
20
|
+
const [, t] = useLanguage();
|
|
21
|
+
const [{ configs }] = useConfig()
|
|
22
|
+
const buttonText = auth
|
|
23
|
+
? t('CONTINUE_WITH_APPLE', 'Logout with Apple')
|
|
24
|
+
: t('CONTINUE_WITH_FACEBOOK', 'Continue with Apple');
|
|
25
25
|
|
|
26
26
|
const performAppleLogin = async (code: string) => {
|
|
27
27
|
try {
|
|
@@ -32,9 +32,10 @@ export const AppleLogin = (props: any) => {
|
|
|
32
32
|
code: code
|
|
33
33
|
})
|
|
34
34
|
})
|
|
35
|
-
|
|
35
|
+
const { result, error } = await response.json()
|
|
36
|
+
if (!error) {
|
|
36
37
|
if (handleSuccessAppleLogin) {
|
|
37
|
-
handleSuccessAppleLogin(
|
|
38
|
+
handleSuccessAppleLogin(result)
|
|
38
39
|
handleLoading && handleLoading(false)
|
|
39
40
|
}
|
|
40
41
|
} else {
|
|
@@ -52,54 +53,58 @@ export const AppleLogin = (props: any) => {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
const onIOSButtonPress = async () => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
56
|
+
try {
|
|
57
|
+
const appleAuthRequestResponse = await appleAuth.performRequest({
|
|
58
|
+
requestedOperation: appleAuth.Operation.LOGIN,
|
|
59
|
+
requestedScopes: [appleAuth.Scope.EMAIL, appleAuth.Scope.FULL_NAME],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// get current authentication state for user
|
|
63
|
+
// /!\ This method must be tested on a real device. On the iOS simulator it always throws an error.
|
|
64
|
+
const credentialState = await appleAuth.getCredentialStateForUser(appleAuthRequestResponse.user);
|
|
65
|
+
|
|
66
|
+
// use credentialState response to ensure the user is authenticated
|
|
67
|
+
if (credentialState === appleAuth.State.AUTHORIZED) {
|
|
68
|
+
// user is authenticated
|
|
69
|
+
if (appleAuthRequestResponse.authorizationCode) {
|
|
70
|
+
performAppleLogin(appleAuthRequestResponse.authorizationCode)
|
|
71
|
+
}
|
|
70
72
|
}
|
|
73
|
+
} catch (err: any) {
|
|
74
|
+
handleLoading && handleLoading(false)
|
|
75
|
+
handleErrors && handleErrors(err.message)
|
|
71
76
|
}
|
|
72
|
-
|
|
73
77
|
}
|
|
74
|
-
|
|
75
78
|
const onAndroidButtonPress = async () => {
|
|
76
|
-
// Generate secure, random values for state and nonce
|
|
77
|
-
const rawNonce: any = uuid.v4();
|
|
78
|
-
const state: any = uuid.v4();
|
|
79
|
-
|
|
80
|
-
// Configure the request
|
|
81
|
-
appleAuthAndroid.configure({
|
|
82
|
-
clientId: 'com.example.client-android',
|
|
83
|
-
// Return URL added to your Apple dev console. We intercept this redirect, but it must still match
|
|
84
|
-
// the URL you provided to Apple. It can be an empty route on your backend as it's never called.
|
|
85
|
-
redirectUri: 'https://example.com/auth/callback',
|
|
86
|
-
responseType: appleAuthAndroid.ResponseType.ALL,
|
|
87
|
-
scope: appleAuthAndroid.Scope.ALL,
|
|
88
|
-
// Random nonce value that will be SHA256 hashed before sending to Apple.
|
|
89
|
-
nonce: rawNonce,
|
|
90
|
-
state,
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// Open the browser window for user sign in
|
|
94
|
-
const response = await appleAuthAndroid.signIn();
|
|
95
|
-
|
|
96
79
|
try {
|
|
80
|
+
// Generate secure, random values for state and nonce
|
|
81
|
+
const rawNonce: any = uuid.v4();
|
|
82
|
+
const state: any = uuid.v4();
|
|
83
|
+
|
|
84
|
+
// Configure the request
|
|
85
|
+
appleAuthAndroid.configure({
|
|
86
|
+
// The Service ID you registered with Apple
|
|
87
|
+
clientId: configs?.apple_login_client_id?.value,
|
|
88
|
+
// Return URL added to your Apple dev console. We intercept this redirect, but it must still match
|
|
89
|
+
// the URL you provided to Apple. It can be an empty route on your backend as it's never called.
|
|
90
|
+
redirectUri: 'https://example.com/auth/callback',
|
|
91
|
+
responseType: appleAuthAndroid.ResponseType.ALL,
|
|
92
|
+
scope: appleAuthAndroid.Scope.ALL,
|
|
93
|
+
// Random nonce value that will be SHA256 hashed before sending to Apple.
|
|
94
|
+
nonce: rawNonce,
|
|
95
|
+
state,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Open the browser window for user sign in
|
|
99
|
+
const response = await appleAuthAndroid.signIn();
|
|
97
100
|
if (response.code) {
|
|
98
101
|
performAppleLogin(response.code)
|
|
99
102
|
}
|
|
100
103
|
} catch (err: any) {
|
|
101
|
-
|
|
104
|
+
handleLoading && handleLoading(false)
|
|
105
|
+
handleErrors && handleErrors(err.message)
|
|
102
106
|
}
|
|
107
|
+
|
|
103
108
|
}
|
|
104
109
|
|
|
105
110
|
useEffect(() => {
|
|
@@ -115,35 +120,36 @@ export const AppleLogin = (props: any) => {
|
|
|
115
120
|
if (Platform.OS === 'android') return appleAuthAndroid.isSupported;
|
|
116
121
|
return false;
|
|
117
122
|
}
|
|
123
|
+
|
|
118
124
|
return (
|
|
119
125
|
<Container>
|
|
120
|
-
{canShowButton() &&
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
126
|
+
{canShowButton() &&
|
|
127
|
+
<AppleButton
|
|
128
|
+
onPress={() => Platform.OS == 'android' ? onAndroidButtonPress() : onIOSButtonPress()}
|
|
129
|
+
>
|
|
130
|
+
<Icon
|
|
131
|
+
name="apple"
|
|
132
|
+
size={20}
|
|
133
|
+
color={'black'}
|
|
134
|
+
style={style.fbBtn}
|
|
135
|
+
/>
|
|
136
|
+
<Text style={style.textBtn}>
|
|
137
|
+
{buttonText}
|
|
138
|
+
</Text>
|
|
139
|
+
</AppleButton>
|
|
134
140
|
}
|
|
135
141
|
</Container>
|
|
136
142
|
);
|
|
137
143
|
}
|
|
138
144
|
|
|
139
145
|
const style = StyleSheet.create({
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
})
|
|
146
|
+
fbBtn: {
|
|
147
|
+
position: 'absolute',
|
|
148
|
+
left: 0,
|
|
149
|
+
marginHorizontal: 16
|
|
150
|
+
},
|
|
151
|
+
textBtn: {
|
|
152
|
+
fontSize: 14,
|
|
153
|
+
color: '#000000'
|
|
154
|
+
}
|
|
155
|
+
})
|
|
@@ -4,6 +4,7 @@ import Spinner from 'react-native-loading-spinner-overlay';
|
|
|
4
4
|
import { useForm, Controller } from 'react-hook-form';
|
|
5
5
|
import { PhoneInputNumber } from '../PhoneInputNumber';
|
|
6
6
|
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
|
7
|
+
import Recaptcha from 'react-native-recaptcha-that-works'
|
|
7
8
|
|
|
8
9
|
import {
|
|
9
10
|
LoginForm as LoginFormController,
|
|
@@ -18,7 +19,6 @@ import { FacebookLogin } from '../FacebookLogin';
|
|
|
18
19
|
import { VerifyPhone } from '../../../../../src/components/VerifyPhone';
|
|
19
20
|
import { OModal } from '../../../../../src/components/shared';
|
|
20
21
|
|
|
21
|
-
|
|
22
22
|
import {
|
|
23
23
|
Container,
|
|
24
24
|
ButtonsWrapper,
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
LineSeparator,
|
|
33
33
|
SkeletonWrapper,
|
|
34
34
|
TabBtn,
|
|
35
|
+
RecaptchaButton
|
|
35
36
|
} from './styles';
|
|
36
37
|
|
|
37
38
|
import NavBar from '../NavBar';
|
|
@@ -60,7 +61,9 @@ const LoginFormUI = (props: LoginParams) => {
|
|
|
60
61
|
handleSendVerifyCode,
|
|
61
62
|
handleCheckPhoneCode,
|
|
62
63
|
onNavigationRedirect,
|
|
63
|
-
notificationState
|
|
64
|
+
notificationState,
|
|
65
|
+
handleReCaptcha,
|
|
66
|
+
enableReCaptcha
|
|
64
67
|
} = props;
|
|
65
68
|
|
|
66
69
|
const [, { showToast }] = useToast();
|
|
@@ -79,6 +82,8 @@ const LoginFormUI = (props: LoginParams) => {
|
|
|
79
82
|
cellphone: null,
|
|
80
83
|
},
|
|
81
84
|
});
|
|
85
|
+
const [recaptchaConfig, setRecaptchaConfig] = useState<any>({})
|
|
86
|
+
const [recaptchaVerified, setRecaptchaVerified] = useState(false)
|
|
82
87
|
|
|
83
88
|
const theme = useTheme();
|
|
84
89
|
|
|
@@ -100,10 +105,15 @@ const LoginFormUI = (props: LoginParams) => {
|
|
|
100
105
|
flexGrow: 1,
|
|
101
106
|
marginBottom: 7,
|
|
102
107
|
},
|
|
108
|
+
recaptchaIcon: {
|
|
109
|
+
width: 100,
|
|
110
|
+
height: 100,
|
|
111
|
+
}
|
|
103
112
|
});
|
|
104
113
|
|
|
105
114
|
const emailRef = useRef<any>({});
|
|
106
115
|
const passwordRef = useRef<any>({});
|
|
116
|
+
const recaptchaRef = useRef<any>({});
|
|
107
117
|
|
|
108
118
|
const handleChangeTab = (val: string) => {
|
|
109
119
|
props.handleChangeTab(val);
|
|
@@ -156,6 +166,33 @@ const LoginFormUI = (props: LoginParams) => {
|
|
|
156
166
|
onChange(value.toLowerCase().replace(/[&,()%";:ç?<>{}\\[\]\s]/g, ''));
|
|
157
167
|
};
|
|
158
168
|
|
|
169
|
+
const handleOpenRecaptcha = () => {
|
|
170
|
+
setRecaptchaVerified(false)
|
|
171
|
+
if (!recaptchaConfig?.siteKey) {
|
|
172
|
+
showToast(ToastType.Error, t('NO_RECAPTCHA_SITE_KEY', 'The config doesn\'t have recaptcha site key'));
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
if (!recaptchaConfig?.baseUrl) {
|
|
176
|
+
showToast(ToastType.Error, t('NO_RECAPTCHA_BASE_URL', 'The config doesn\'t have recaptcha base url'));
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
recaptchaRef.current.open()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const onRecaptchaVerify = (token: any) => {
|
|
183
|
+
setRecaptchaVerified(true)
|
|
184
|
+
handleReCaptcha(token)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (configs && Object.keys(configs).length > 0 && enableReCaptcha) {
|
|
189
|
+
setRecaptchaConfig({
|
|
190
|
+
siteKey: configs?.security_recaptcha_site_key?.value || null,
|
|
191
|
+
baseUrl: configs?.security_recaptcha_base_url?.value || null
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
}, [configs, enableReCaptcha])
|
|
195
|
+
|
|
159
196
|
useEffect(() => {
|
|
160
197
|
if (!formState.loading && formState.result?.error) {
|
|
161
198
|
formState.result?.result &&
|
|
@@ -399,6 +436,39 @@ const LoginFormUI = (props: LoginParams) => {
|
|
|
399
436
|
</OText>
|
|
400
437
|
</TouchableOpacity>
|
|
401
438
|
)}
|
|
439
|
+
|
|
440
|
+
{enableReCaptcha && (
|
|
441
|
+
<>
|
|
442
|
+
<TouchableOpacity
|
|
443
|
+
onPress={handleOpenRecaptcha}
|
|
444
|
+
>
|
|
445
|
+
<RecaptchaButton>
|
|
446
|
+
{recaptchaVerified ? (
|
|
447
|
+
<MaterialCommunityIcons
|
|
448
|
+
name="checkbox-marked"
|
|
449
|
+
size={26}
|
|
450
|
+
color={theme.colors.primary}
|
|
451
|
+
/>
|
|
452
|
+
) : (
|
|
453
|
+
<MaterialCommunityIcons
|
|
454
|
+
name="checkbox-blank-outline"
|
|
455
|
+
size={26}
|
|
456
|
+
color={theme.colors.mediumGray}
|
|
457
|
+
/>
|
|
458
|
+
)}
|
|
459
|
+
<OText size={14} mLeft={8}>{t('VERIFY_ReCAPTCHA', 'Verify reCAPTCHA')}</OText>
|
|
460
|
+
</RecaptchaButton>
|
|
461
|
+
</TouchableOpacity>
|
|
462
|
+
<Recaptcha
|
|
463
|
+
ref={recaptchaRef}
|
|
464
|
+
siteKey={recaptchaConfig?.siteKey}
|
|
465
|
+
baseUrl={recaptchaConfig?.baseUrl}
|
|
466
|
+
onVerify={onRecaptchaVerify}
|
|
467
|
+
onExpire={() => setRecaptchaVerified(false)}
|
|
468
|
+
/>
|
|
469
|
+
</>
|
|
470
|
+
)}
|
|
471
|
+
|
|
402
472
|
<OButton
|
|
403
473
|
onClick={handleSubmit(onSubmit)}
|
|
404
474
|
text={loginButtonText}
|
|
@@ -559,6 +629,7 @@ const LoginFormUI = (props: LoginParams) => {
|
|
|
559
629
|
export const LoginForm = (props: any) => {
|
|
560
630
|
const loginProps = {
|
|
561
631
|
...props,
|
|
632
|
+
isRecaptchaEnable: true,
|
|
562
633
|
UIComponent: LoginFormUI,
|
|
563
634
|
};
|
|
564
635
|
return <LoginFormController {...loginProps} />;
|
|
@@ -36,7 +36,7 @@ export const LoginWith = styled.View`
|
|
|
36
36
|
width: 100%;
|
|
37
37
|
align-items: flex-start;
|
|
38
38
|
border-bottom-width: 1px;
|
|
39
|
-
border-bottom-color: ${(props: any) => props.theme.colors.border}
|
|
39
|
+
border-bottom-color: ${(props: any) => props.theme.colors.border};
|
|
40
40
|
`;
|
|
41
41
|
|
|
42
42
|
export const FormInput = styled.View`
|
|
@@ -80,3 +80,8 @@ export const LineSeparator = styled.View`
|
|
|
80
80
|
export const SkeletonWrapper = styled.View`
|
|
81
81
|
width: 90%;
|
|
82
82
|
`
|
|
83
|
+
export const RecaptchaButton = styled.View`
|
|
84
|
+
flex-direction: row;
|
|
85
|
+
align-items: center;
|
|
86
|
+
margin-bottom: 10px;
|
|
87
|
+
`
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { PromotionsController, useLanguage, useUtils, useEvent } from 'ordering-components/native'
|
|
3
|
+
import {
|
|
4
|
+
PromotionsContainer,
|
|
5
|
+
SingleOfferContainer,
|
|
6
|
+
OfferInformation,
|
|
7
|
+
SearchBarContainer,
|
|
8
|
+
SingleBusinessOffer,
|
|
9
|
+
AvailableBusinesses,
|
|
10
|
+
OfferData,
|
|
11
|
+
Code,
|
|
12
|
+
BusinessInfo
|
|
13
|
+
} from './styles'
|
|
14
|
+
import { SearchBar } from '../SearchBar'
|
|
15
|
+
import NavBar from '../NavBar'
|
|
16
|
+
import { useTheme } from 'styled-components/native';
|
|
17
|
+
import { OButton, OIcon, OModal, OText } from '../shared'
|
|
18
|
+
import { Placeholder, PlaceholderLine } from 'rn-placeholder'
|
|
19
|
+
import { NotFoundSource } from '../NotFoundSource'
|
|
20
|
+
import { View, StyleSheet, ScrollView } from 'react-native'
|
|
21
|
+
import FastImage from 'react-native-fast-image'
|
|
22
|
+
import { PromotionParams } from '../../types'
|
|
23
|
+
const PromotionsUI = (props : PromotionParams) => {
|
|
24
|
+
const {
|
|
25
|
+
navigation,
|
|
26
|
+
offersState,
|
|
27
|
+
handleSearchValue,
|
|
28
|
+
searchValue,
|
|
29
|
+
offerSelected,
|
|
30
|
+
setOfferSelected
|
|
31
|
+
} = props
|
|
32
|
+
|
|
33
|
+
const theme = useTheme();
|
|
34
|
+
|
|
35
|
+
const styles = StyleSheet.create({
|
|
36
|
+
productStyle: {
|
|
37
|
+
width: 75,
|
|
38
|
+
height: 75,
|
|
39
|
+
borderRadius: 7.6
|
|
40
|
+
},
|
|
41
|
+
buttonStyle: {
|
|
42
|
+
width: 55,
|
|
43
|
+
height: 25,
|
|
44
|
+
paddingLeft: 0,
|
|
45
|
+
paddingRight: 0
|
|
46
|
+
},
|
|
47
|
+
offerTitle: {
|
|
48
|
+
fontSize: 12
|
|
49
|
+
},
|
|
50
|
+
offerDescription: {
|
|
51
|
+
color: '#909BA9',
|
|
52
|
+
fontSize: 10
|
|
53
|
+
},
|
|
54
|
+
offerExtraInfo: {
|
|
55
|
+
fontSize: 10
|
|
56
|
+
},
|
|
57
|
+
modalButtonStyle: {
|
|
58
|
+
width: 100,
|
|
59
|
+
height: 35,
|
|
60
|
+
paddingLeft: 0,
|
|
61
|
+
paddingRight: 0,
|
|
62
|
+
borderRadius: 7.6
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const [, t] = useLanguage()
|
|
67
|
+
const [{ parseDate, parsePrice, optimizeImage }] = useUtils()
|
|
68
|
+
const [events] = useEvent()
|
|
69
|
+
const [openModal, setOpenModal] = useState(false)
|
|
70
|
+
|
|
71
|
+
const handleClickOffer = (offer : any) => {
|
|
72
|
+
setOpenModal(true)
|
|
73
|
+
setOfferSelected(offer)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const handleBusinessClick = (business : any) => {
|
|
77
|
+
events.emit('go_to_page', { page: 'business', params: { store: business.slug } })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const filteredOffers = offersState?.offers?.filter((offer : any) => offer.name.toLowerCase().includes(searchValue.toLowerCase()))
|
|
81
|
+
|
|
82
|
+
const targetString = offerSelected?.target === 1
|
|
83
|
+
? t('SUBTOTAL', 'Subtotal')
|
|
84
|
+
: offerSelected?.target === 2
|
|
85
|
+
? t('DELIVERY_FEE', 'Delivery fee')
|
|
86
|
+
: t('SERVICE_FEE', 'Service fee')
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<PromotionsContainer>
|
|
90
|
+
<NavBar
|
|
91
|
+
onActionLeft={() => navigation.goBack()}
|
|
92
|
+
btnStyle={{ paddingLeft: 0 }}
|
|
93
|
+
paddingTop={20}
|
|
94
|
+
style={{ paddingBottom: 0, flexDirection: 'column', alignItems: 'flex-start' }}
|
|
95
|
+
title={t('PROMOTIONS', 'Promotions')}
|
|
96
|
+
titleAlign={'center'}
|
|
97
|
+
titleStyle={{ fontSize: 16, marginRight: 0, marginLeft: 0, marginBottom: 10 }}
|
|
98
|
+
titleWrapStyle={{ paddingHorizontal: 0 }}
|
|
99
|
+
/>
|
|
100
|
+
<SearchBarContainer>
|
|
101
|
+
<SearchBar
|
|
102
|
+
placeholder={t('SEARCH_OFFERS', 'Search offers')}
|
|
103
|
+
onSearch={handleSearchValue}
|
|
104
|
+
/>
|
|
105
|
+
</SearchBarContainer>
|
|
106
|
+
|
|
107
|
+
{offersState?.loading && (
|
|
108
|
+
<>
|
|
109
|
+
{[...Array(5).keys()].map((key, i) => (
|
|
110
|
+
<Placeholder key={i} style={{ flexDirection: 'row', marginBottom: 20 }}>
|
|
111
|
+
<PlaceholderLine height={10} width={45} />
|
|
112
|
+
<PlaceholderLine height={10} width={60} />
|
|
113
|
+
<PlaceholderLine height={10} width={75} />
|
|
114
|
+
</Placeholder>
|
|
115
|
+
))}
|
|
116
|
+
</>
|
|
117
|
+
)}
|
|
118
|
+
{((!offersState?.loading && filteredOffers?.length === 0) || offersState?.error) && (
|
|
119
|
+
<NotFoundSource
|
|
120
|
+
content={offersState?.error || t('NOT_FOUND_OFFERS', 'Not found offers')}
|
|
121
|
+
/>
|
|
122
|
+
)}
|
|
123
|
+
<ScrollView>
|
|
124
|
+
{!offersState?.loading && offersState.offers?.length > 0 && filteredOffers?.map((offer : any) => (
|
|
125
|
+
<SingleOfferContainer key={offer.id}>
|
|
126
|
+
<OfferInformation>
|
|
127
|
+
<OText style={styles.offerTitle}>{offer?.name}</OText>
|
|
128
|
+
{offer?.description && (
|
|
129
|
+
<OText style={styles.offerDescription}>{offer?.description}</OText>
|
|
130
|
+
)}
|
|
131
|
+
<OText style={styles.offerExtraInfo}>
|
|
132
|
+
{t('EXPIRES', 'Expires')} {parseDate(offer?.end, { outputFormat: 'MMM DD, YYYY' })}
|
|
133
|
+
</OText>
|
|
134
|
+
<AvailableBusinesses>
|
|
135
|
+
<OText style={styles.offerExtraInfo}>{t('APPLY_FOR', 'Apply for')}:</OText>
|
|
136
|
+
{offer.businesses.map((business: any, i: number) => (
|
|
137
|
+
<OText style={styles.offerExtraInfo} key={business?.id}>{' '}{business?.name}{i + 1 < offer.businesses?.length ? ',' : ''}</OText>
|
|
138
|
+
))}
|
|
139
|
+
</AvailableBusinesses>
|
|
140
|
+
</OfferInformation>
|
|
141
|
+
<OButton
|
|
142
|
+
onClick={() => handleClickOffer(offer)}
|
|
143
|
+
text={t('VIEW', 'View')}
|
|
144
|
+
style={styles.buttonStyle}
|
|
145
|
+
textStyle={{ fontSize: 10, color: '#fff', flexWrap: 'nowrap' }}
|
|
146
|
+
/>
|
|
147
|
+
</SingleOfferContainer>
|
|
148
|
+
))}
|
|
149
|
+
</ScrollView>
|
|
150
|
+
<OModal
|
|
151
|
+
open={openModal}
|
|
152
|
+
onClose={() => setOpenModal(false)}
|
|
153
|
+
entireModal
|
|
154
|
+
|
|
155
|
+
title={``}
|
|
156
|
+
>
|
|
157
|
+
<View style={{ padding: 20 }}>
|
|
158
|
+
<OText style={{ alignSelf: 'center', fontWeight: '700' }} mBottom={20}>
|
|
159
|
+
{offerSelected?.name} / {t('VALUE_OF_OFFER', 'Value of offer')}: {offerSelected?.rate_type === 1 ? `${offerSelected?.rate}%` : `${parsePrice(offerSelected?.rate)}`}
|
|
160
|
+
</OText>
|
|
161
|
+
<OfferData>
|
|
162
|
+
{offerSelected?.type === 2 && (
|
|
163
|
+
<Code>
|
|
164
|
+
<OText>{t('YOUR_CODE', 'Your code')}</OText>
|
|
165
|
+
<OText color={theme.colors.primary}>{offerSelected.coupon}</OText>
|
|
166
|
+
</Code>
|
|
167
|
+
)}
|
|
168
|
+
<OText>{t('APPLIES_TO', 'Applies to')}: {targetString}</OText>
|
|
169
|
+
{offerSelected?.auto && (
|
|
170
|
+
<OText>{t('OFFER_AUTOMATIC', 'This offer applies automatic')}</OText>
|
|
171
|
+
)}
|
|
172
|
+
{offerSelected?.minimum && (
|
|
173
|
+
<OText>{t('MINIMUM_PURCHASE_FOR_OFFER', 'Minimum purshase for use this offer')}: {parsePrice(offerSelected?.minimum)}</OText>
|
|
174
|
+
)}
|
|
175
|
+
{offerSelected?.max_discount && (
|
|
176
|
+
<OText>{t('MAX_DISCOUNT_ALLOWED', 'Max discount allowed')}: {parsePrice(offerSelected?.max_discount)}</OText>
|
|
177
|
+
)}
|
|
178
|
+
{offerSelected?.description && (
|
|
179
|
+
<OText>{offerSelected?.description}</OText>
|
|
180
|
+
)}
|
|
181
|
+
</OfferData>
|
|
182
|
+
<OText style={{ marginTop: 10, marginBottom: 10 }}>
|
|
183
|
+
{t('AVAILABLE_BUSINESSES_FOR_OFFER', 'Available businesses for this offer')}:
|
|
184
|
+
</OText>
|
|
185
|
+
<ScrollView style={{height: '75%'}}>
|
|
186
|
+
{offerSelected?.businesses?.map((business : any) => {
|
|
187
|
+
return (
|
|
188
|
+
<SingleBusinessOffer key={business.id}>
|
|
189
|
+
{business?.logo ? (
|
|
190
|
+
<FastImage
|
|
191
|
+
style={styles.productStyle}
|
|
192
|
+
source={{
|
|
193
|
+
uri: optimizeImage(business?.logo, 'h_250,c_limit'),
|
|
194
|
+
priority: FastImage.priority.normal,
|
|
195
|
+
}}
|
|
196
|
+
resizeMode={FastImage.resizeMode.cover}
|
|
197
|
+
/>
|
|
198
|
+
) : (
|
|
199
|
+
<OIcon
|
|
200
|
+
src={theme?.images?.dummies?.product}
|
|
201
|
+
style={styles.productStyle}
|
|
202
|
+
/>
|
|
203
|
+
)}
|
|
204
|
+
<BusinessInfo>
|
|
205
|
+
<OText>{business.name}</OText>
|
|
206
|
+
<OButton
|
|
207
|
+
onClick={() => handleBusinessClick(business)}
|
|
208
|
+
text={t('GO_TO_BUSINESSS', 'Go to business')}
|
|
209
|
+
style={styles.modalButtonStyle}
|
|
210
|
+
textStyle={{ fontSize: 10, color: '#fff' }}
|
|
211
|
+
/>
|
|
212
|
+
</BusinessInfo>
|
|
213
|
+
</SingleBusinessOffer>
|
|
214
|
+
)
|
|
215
|
+
})}
|
|
216
|
+
</ScrollView>
|
|
217
|
+
</View>
|
|
218
|
+
</OModal>
|
|
219
|
+
</PromotionsContainer>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export const Promotions = (props : PromotionParams) => {
|
|
224
|
+
const PromotionsProps = {
|
|
225
|
+
...props,
|
|
226
|
+
UIComponent: PromotionsUI
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<PromotionsController {...PromotionsProps} />
|
|
231
|
+
)
|
|
232
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import styled, { css } from 'styled-components/native'
|
|
2
|
+
|
|
3
|
+
export const PromotionsContainer = styled.View`
|
|
4
|
+
width: 100%;
|
|
5
|
+
`
|
|
6
|
+
|
|
7
|
+
export const SingleOfferContainer = styled.View`
|
|
8
|
+
flex-direction: row;
|
|
9
|
+
width: 100%;
|
|
10
|
+
height: 80px;
|
|
11
|
+
justify-content: space-between;
|
|
12
|
+
align-items: center;
|
|
13
|
+
margin-bottom: 20px;
|
|
14
|
+
|
|
15
|
+
`
|
|
16
|
+
|
|
17
|
+
export const OfferInformation = styled.View`
|
|
18
|
+
justify-content: space-between;
|
|
19
|
+
max-width: 75%;
|
|
20
|
+
`
|
|
21
|
+
|
|
22
|
+
export const SearchBarContainer = styled.View`
|
|
23
|
+
display: flex;
|
|
24
|
+
width: 100%;
|
|
25
|
+
justify-content: flex-start;
|
|
26
|
+
margin-bottom: 20px;
|
|
27
|
+
.search-bar {
|
|
28
|
+
justify-content: flex-start;
|
|
29
|
+
input {
|
|
30
|
+
width: 100%;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
.clear {
|
|
34
|
+
right: 0;
|
|
35
|
+
}
|
|
36
|
+
`
|
|
37
|
+
|
|
38
|
+
export const SingleBusinessOffer = styled.View`
|
|
39
|
+
flex-direction: row;
|
|
40
|
+
`
|
|
41
|
+
|
|
42
|
+
export const AvailableBusinesses = styled.View`
|
|
43
|
+
flex-direction: row;
|
|
44
|
+
overflow: hidden;
|
|
45
|
+
`
|
|
46
|
+
|
|
47
|
+
export const OfferData = styled.View`
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
p{
|
|
52
|
+
color: #909BA9;
|
|
53
|
+
margin: 3px;
|
|
54
|
+
font-size: 14px;
|
|
55
|
+
}
|
|
56
|
+
`
|
|
57
|
+
|
|
58
|
+
export const Code = styled.View`
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-direction: column;
|
|
61
|
+
align-items: center;
|
|
62
|
+
margin-bottom: 10px;
|
|
63
|
+
`
|
|
64
|
+
|
|
65
|
+
export const ValueOfOffer = styled.View`
|
|
66
|
+
p{
|
|
67
|
+
font-size: 16px;
|
|
68
|
+
}
|
|
69
|
+
span{
|
|
70
|
+
font-size: 20px;
|
|
71
|
+
}
|
|
72
|
+
`
|
|
73
|
+
|
|
74
|
+
export const BusinessInfo = styled.View`
|
|
75
|
+
flex: 1;
|
|
76
|
+
flex-direction: row;
|
|
77
|
+
justify-content: space-between;
|
|
78
|
+
align-items: center;
|
|
79
|
+
margin-left: 10px;
|
|
80
|
+
`
|
|
@@ -16,6 +16,7 @@ import { LogoutButton } from '../LogoutButton'
|
|
|
16
16
|
import { LanguageSelector } from '../LanguageSelector'
|
|
17
17
|
import MessageCircle from 'react-native-vector-icons/AntDesign'
|
|
18
18
|
import Ionicons from 'react-native-vector-icons/Ionicons'
|
|
19
|
+
import MaterialIcons from 'react-native-vector-icons/MaterialIcons'
|
|
19
20
|
import FastImage from 'react-native-fast-image'
|
|
20
21
|
|
|
21
22
|
import {
|
|
@@ -101,7 +102,7 @@ const ProfileListUI = (props: ProfileParams) => {
|
|
|
101
102
|
const { top, bottom } = useSafeAreaInsets();
|
|
102
103
|
|
|
103
104
|
const isWalletEnabled = configs?.wallet_enabled?.value === '1' && (configs?.wallet_cash_enabled?.value === '1' || configs?.wallet_credit_point_enabled?.value === '1')
|
|
104
|
-
|
|
105
|
+
const IsPromotionsEnabled = configs?.advanced_offers_module === '1' || configs?.advanced_offers_module === 'true'
|
|
105
106
|
const onRedirect = (route: string, params?: any) => {
|
|
106
107
|
navigation.navigate(route, params)
|
|
107
108
|
}
|
|
@@ -178,6 +179,12 @@ const ProfileListUI = (props: ProfileParams) => {
|
|
|
178
179
|
<OText size={14} lineHeight={24} weight={'400'} color={theme.colors.textNormal}>{t('WALLETS', 'Wallets')}</OText>
|
|
179
180
|
</ListItem>
|
|
180
181
|
)}
|
|
182
|
+
{IsPromotionsEnabled && (
|
|
183
|
+
<ListItem onPress={() => onRedirect('Promotions', { isFromProfile: true, isGoBack: true })} activeOpacity={0.7}>
|
|
184
|
+
<MaterialIcons name='local-offer' style={styles.messageIconStyle} color={theme.colors.textNormal} />
|
|
185
|
+
<OText size={14} lineHeight={24} weight={'400'} color={theme.colors.textNormal}>{t('PROMOTIONS', 'Promotions')}</OText>
|
|
186
|
+
</ListItem>
|
|
187
|
+
)}
|
|
181
188
|
<ListItem onPress={() => navigation.navigate('Help', {})} activeOpacity={0.7}>
|
|
182
189
|
<OIcon src={theme.images.general.ic_help} width={16} color={theme.colors.textNormal} style={{ marginEnd: 14 }} />
|
|
183
190
|
<OText size={14} lineHeight={24} weight={'400'} color={theme.colors.textNormal}>{t('HELP', 'Help')}</OText>
|
|
@@ -17,6 +17,8 @@ export interface LoginParams {
|
|
|
17
17
|
handleSendVerifyCode?: any;
|
|
18
18
|
handleCheckPhoneCode?: any;
|
|
19
19
|
notificationState?: any;
|
|
20
|
+
handleReCaptcha?: any;
|
|
21
|
+
enableReCaptcha?: any;
|
|
20
22
|
}
|
|
21
23
|
export interface ProfileParams {
|
|
22
24
|
navigation?: any;
|
|
@@ -566,3 +568,12 @@ export interface PlaceSpotParams {
|
|
|
566
568
|
getPlacesList?: any,
|
|
567
569
|
setOpenPlaceModal?: any
|
|
568
570
|
}
|
|
571
|
+
|
|
572
|
+
export interface PromotionParams {
|
|
573
|
+
navigation: any,
|
|
574
|
+
offersState: any,
|
|
575
|
+
handleSearchValue: any,
|
|
576
|
+
searchValue: string,
|
|
577
|
+
offerSelected: any,
|
|
578
|
+
setOfferSelected: any,
|
|
579
|
+
}
|